黑马程序员—苍穹外卖笔记(超级详细版更新中)

目录

开发前言

一:前端环境搭建

二:创建远程仓库

三.数据库环境搭建

四.前后端联调

五.代码开发开始!

1.nginx反向代理

本项目nginx.conf配置

2.登录功能完善

3.开发文档的导入

4.swagger接口测试

5.新增员工

6.员工分页查询

7.员工的启用和禁用

8.编辑员工

9.菜单分类功能

10.公共字段自动填充

11.菜品管理

12.菜品其它接口

13.Redis技术

14.营业状态设置


开发前言

写文章的初衷是为了辅助自己学习因为我学完总爱忘,所以有很多不足的地方,但是我认为不开始不尝试,就不会有错误,也就跟不会有成长。希望也能帮到有需要的人吧~(还在更新中预计劳动节后完成大部分)

一:前端环境搭建

运行前端

点击nginx.exe

我修改了前端访问的端口http://localhost:8081/

二:创建远程仓库

  • 本地仓库提交

在此中可定义不用上传到仓库的文件

  • 创建一个远程仓库

复制地址为https://gitee.com/cao-ruiwhisper/sky-take-out.git

点击推送后远程仓库网址出现内容

三.数据库环境搭建

mysql

四.前后端联调

对项目进行编译的时候我出现了jdk版本不适配的情况,把jdk修改为17就好了。

后端启动,前端登录成功

接下来我们结束当前执行进程,前端退出登录。

在idea中点击小虫子调试在login函数前打断点,前端执行登录操作就可以看到返回数据

单步调试

点击login方法快捷键ctrl+alt+b来查看方法的具体实现过程

同样打断点,逐步放行调试

(w)

五.代码开发开始!

  • 目录结构

本项目基于maven开发

sky-common主要存放一些其他模块需要的公共类:工具类,常量类,异常类

constant:常量类

context:项目上下文相关

enumeration:枚举类

exception:自定义异常类

json:处理json转换的类

properties:springboot中的一些配置属性类

result:后端返回结果类

utils:工具类

sky-pojo存放实体类:VO,DTO等

pojo:java对象,包含属性和get,set方法

以下都属于pojo对象

entity:和数据库表对应的实体

DTO:程序各层之间的数据传输对象

VO:为前端展示数据的视图对象

sky-server主要存放后端服务的逻辑,配置文件:controller,service,mapper

  • 1.nginx反向代理

 思考:前端发出的请求是如何传到后端的? 

前端发起请求的后端地址为

后端实际地址为

我们会发现前端与后端基于tomcat地址并不一致,因为我们使用了nginx反向代理

配置方法:识别到满足条件前端请求地址nginx就对地址进行拼接请求到tomcat

负载均衡配置:不在只单一的写服务器,而是写一组可用服务器,由nginx分配

  • 本项目nginx.conf配置
//nginx封装的前端代码nginx.conf
upstream webservers{
      server 127.0.0.1:8081 weight=90 ;//因为没有多台服务器所以配置weight只是展示策略
      #server 127.0.0.1:8081 weight=10 ;
    }

        # 反向代理,处理管理端发送的请求
        location /api/ {
            proxy_pass   http://localhost:8080/admin/;
            #proxy_pass   http://webservers/admin/;
        }
        
        # 反向代理,处理用户端发送的请求
        location /user/ {
            proxy_pass   http://webservers/user/;
        }
        

2.登录功能完善

EmploveeServiceImpl.java


        // 为了数据的安全我们将前端传过来的密码进行MD5加密
        password=DigestUtils.md5DigestAsHex(password.getBytes());
       

修改代码后,把数据库密码修改为加密密码

MD5加密:只可以单向加密如123456加密为e10adc3949ba59abbe56e057f20f883e

但是我们知道加密后的密码无法直接翻译出密码

为了保护用户的信息安全如果数据库泄露,不会直接让对方知道用户的密码。

3.开发文档的导入

我使用yapi中总是非常卡顿所以暂时只使用swagger,后续会使用其他代替。
打开yapi官网创建两个项目分别是用户端和管理端
导入资料中给的接口,数据管理选json。


4.swagger接口测试


我们可以通过swagger定义接口信息,生成接口文档,进行在线接口测试
1.引入knife4j依赖框架集成swagger
pom.xml

<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-spring-boot-starter</artifactId>
    <version>3.0.2</version>
</dependency>


2.配置类中加入配置
WebMvcConiguration指定文档信息和swagger版本和接口扫描包

/**
     * 通过knife4j生成接口文档
     * @return
     */
    @Bean
    public Docket docket() {
        ApiInfo apiInfo = new ApiInfoBuilder()
                .title("苍穹外卖项目接口文档")
                .version("2.0")
                .description("苍穹外卖项目接口文档")
                .build();
        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.sky.controller"))
                .paths(PathSelectors.any())
                .build();
        return docket;
    }


3.设置静态资源映射
WebMvcConiguration设置生成接口文档存放位置

/**
     * 设置静态资源映射
     * @param registry
     */
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
    }


运行设置完毕访问http://localhost:8080/doc.html
则可以看到生成的接口


swagger常用注解控制接口文档生成

添加注解(添加前后可读性对比)

/**
 * 员工管理
 */
@RestController
@RequestMapping("/admin/employee")
@Slf4j
@Api(tags="员工相关接口")

 /**
     * 登录
     *
     * @param employeeLoginDTO
     * @return
     */
    @PostMapping("/login")
    @ApiOperation("员工登录")

/**
     * 退出
     *
     * @return
     */
    @PostMapping("/logout")
    @ApiOperation("员工退出")

@ApiModel(description = "员工登录返回的数据格式")
public class EmployeeLoginVO implements Serializable {

    @ApiModelProperty("主键值")
//等等

添加注解前

添加注解后

5.新增员工


第一步:需求分析和设计
产品原型:从图中可以看出前端输入内容是有限制的,例如唯一性数字长度等


接口设计:非常重要我们开发过程中要严格按照文档开发


数据库设计:


第二步:代码编写


设计DTO:根据接口设计中body封装前端传入同时后端需要的数据

EmployeeDTO.java
@Data
public class EmployeeDTO implements Serializable {
    private Long id;
    private String username;
    private String name;
    private String phone;
    private String sex;
    private String idNumber;
}


Controller层:依据接口文档定义请求方式,swagger注解,定义方法函数把数据传入service层

/**
     * 新增员工
     * @param employeeDTO
     * @return
     */
    @PostMapping//表示这是一个处理 HTTP POST 请求的接口,路径默认为类上的父路径
    @ApiOperation("新增员工")
    public Result save(@RequestBody EmployeeDTO employeeDTO) {//@RequestBody将请求的 JSON 数据自动反序列化为 EmployeeDTO 对象
        log.info("新增员工:{}", employeeDTO);
        employeeService.save(employeeDTO);//调用服务层方法保存数据
        return Result.success() ;
    }


Service接口:

EmployeeService.java

/**
 * 新增员工
 * @param employeeDTO 
 */
void save(EmployeeDTO employeeDTO);


常量类:在开发过程中出现固定的数据我们最好创建一个常量类进行引用,便于我们后续对数据的修改。
实体类创建:我们之前定义了DTO数据是用来存储前端传给后端的数据,但是在实际开发中我们还需要保存其他数据,例如我们创建一个员工不仅仅要保存员工信息,还要保存是谁创建的此员工,什么时候创建的员工,所以我们在设计实现Service实现类时还要额外定义这些数据。

以上内容在初始工程中都已经存在。


Service实现类:调用Mapper层进行数据库的增删改查

EmployeeServiceImpl.java

/**
 * 新增员工
 * @param employeeDTO 
 */
public void save(EmployeeDTO employeeDTO) {//实现对service层的重写
    Employee employee = new Employee();//创建员工实体类
    
    //将 EmployeeDTO 的属性值复制到 Employee 实体类中(需确保两者字段名一致)
    //依赖:Spring 的 org.springframework.beans.BeanUtils
    BeanUtils.copyProperties(employeeDTO, employee);

    // 设置账号状态,默认正常状态(1正常,0锁定)
    employee.setStatus(StatusConstant.ENABLE);

    // 设置密码,默认密码123456(MD5加密)
    employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));

    // 设置当前记录的创建时间和修改时间
    employee.setCreateTime(LocalDateTime.now());
    employee.setUpdateTime(LocalDateTime.now());

    // 设置创建人和修改人ID(TODO:需改为当前登录用户的ID)
    employee.setCreateUser(10L);
    employee.setUpdateUser(10L);

    employeeMapper.insert(employee);//调用 MyBatis 或 JPA 的 Mapper 接口,将数据插入数据库
}


Mapper:SQL编写

EmployeeMapper.java

@Mapper
public interface EmployeeMapper {

    /**
     * 根据用户名查询员工
     * @param username
     * @return
     */
    @Select("select * from employee where username = #{username}")
    Employee getByUsername(String username);

    /**
     * 插入员工数据
     * @param employee
     */
    @Insert("insert into employee (name, username, password, phone, sex, id_number, create_time, update_time, create_user, update_user, status) " +
            "values (#{name}, #{username}, #{password}, #{phone}, #{sex}, #{idNumber}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser}, #{status})")
    void insert(Employee employee);
}


第三步:测试

我们在进行测试的时候因为后续需要用到拦截器拦截jwt后续使用所以首先要在登录功能响应内容中获取jwt令牌,并设置为全局参数

jwt是会过期的所以我们要延长jwt过期时间,要还是过期我们就重新获取

注意不要把时间设置太长,我就因为设置太长报错了

设置全局参数


测试成功我们可以看到响应码和控制台日志输出


前后端联调测试,前端正常输入,控制台输出成功
查看数据库:

yes!我们第一个接口开发成功啦!

代码功能完善


当录入的用户已经存在,因为我们在数据库中设计username唯一性,会出现异常,应该捕获并且在返回体中显示报错原因

//控制台获取异常处理类名,添加异常处理方法

GlobalExceptionHandler
/**
     * 处理SQL异常
     * @param ex
     * @return
     */
    @ExceptionHandler//识别到异常处理对象
    public Result exceptionHandler(SQLIntegrityConstraintViolationException ex) {
        String message = ex.getMessage();//获取异常处理信息

        if (message.contains("Duplicate entry")) {
            String[] split = message.split(" ");
            String username = split[2];//保存异常处理信息中的第三个数组就是用户名
            String msg = username + MessageConstant.ALREADY_EXISTS;//拼接返回报错语句
            return Result.error(msg);
        } else {
            return Result.error(MessageConstant.UNKNOWN_ERROR);
        }
    }

//多说一句我的常量类里面没有账号存在字段自己加的

创建人id改为当前登录人id
我们需要动态获取当前员工id,因为我们登录时生成了jwt,可解析出jwt中员工id

我们要如何把信息传入给service的save方法呢?
ThreadLocal:为每一个线程提供单独的存储空间具有隔离效果,只有在线程内可以获取相应的值。我们可以使用这种方法来设置当前用户的id.

我们通常把这些方法设置成一个工具类(截图的代码都是项目基本工程里面有的,可复制代码段是需要我们自己放入工程的)

在拦截其中调用方法存储当前解析出的用户id

JwtTokenAdminInterceptor.java
//1、从请求头中获取令牌
        String token = request.getHeader(jwtProperties.getAdminTokenName());

        //2、校验令牌
        try {
            log.info("jwt校验:{}", token);
            Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
            Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
            log.info("当前员工id:", empId);


// 将员工ID存入线程上下文
    BaseContext.setCurrentId(empId);

在实现类中修改id的定义

EmployeeServiceImpl.java
 /**
     * 新增员工
     * @param employeeDTO
     */
    public void save(EmployeeDTO employeeDTO) {//实现对service层的重写
        Employee employee = new Employee();//创建员工实体类

        //将 EmployeeDTO 的属性值复制到 Employee 实体类中(需确保两者字段名一致)
        //依赖:Spring 的 org.springframework.beans.BeanUtils
        BeanUtils.copyProperties(employeeDTO, employee);

        // 设置账号状态,默认正常状态(1正常,0锁定)
        employee.setStatus(StatusConstant.ENABLE);

        // 设置密码,默认密码123456(MD5加密)
        employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));

        // 设置当前记录的创建时间和修改时间
        employee.setCreateTime(LocalDateTime.now());
        employee.setUpdateTime(LocalDateTime.now());

        // 设置当前记录创建人id和修改人id
// TODO 后期需要改为当前登录用户的id
        employee.setCreateUser(BaseContext.getCurrentId());
        employee.setUpdateUser(BaseContext.getCurrentId());

        employeeMapper.insert(employee);//调用 MyBatis 或 JPA 的 Mapper 接口,将数据插入数据库
    }

测试

新加一条数据,数据库的操作人id变化修改成功

提交推送新开发的代码

6.员工分页查询

1.需求分析和设计

2.代码开发

DTO:根据接口可以看出前端需要传输给后端,员工姓名非必须后续用来搜索,当前页码和每页要显示几条记录数。

pageresult:后端需要返回给前端的数据封装,总共有多少条员工记录,当前页面要展示的数据集合。

EmployeeController.java
/**
 * 员工分页查询
 * @param employeePageQueryDTO 分页查询参数
 * @return 分页结果
 */
@GetMapping("/page")//依据接口路径编写
@ApiOperation("员工分页查询")
public Result<PageResult> page(EmployeePageQueryDTO employeePageQueryDTO) {
    log.info("员工分页查询,参数为:{}", employeePageQueryDTO);//打印查询参数
    PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO);//将参数传递给 employeeService.pageQuery() 执行实际查询。
    return Result.success(pageResult);//响应格式定义
}
EmployeeService.java
  /**
     * 分页查询
     * @param employeePageQueryDTO
     * @return//这个打出/**回车即可快速生成
     */

    PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
EmployeeServiceImpl

//为了简化我们分页代码的编写我们需要引入一个依赖,后续方法才可使用

/**
 * 分页查询
 * @param employeePageQueryDTO 分页参数
 * @return 分页结果
 */
public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
    // 1. 启动分页(自动拦截后续查询)此方法要传入当前页码和当前总页数
    PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize());
    
    // 2. 执行分页查询返回值需要按照上面方法规则定义Page<Employee> page
    Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);

//我们需要的是一个PageResult返回值而上面我们定义的是page,我们可以查看PageResult所需要的对象在下面进行定义

 long total = page.getTotal();        // 获取总记录数
List<Employee> records = page.getResult(); // 获取当前页数据列表

return new PageResult(total, records); // 封装为自定义分页返回对象
}
EmployeeMapper

//因为是动态查询所以使用xml文件

/**
     * 分页查询
     * @param employeePageQueryDTO
     * @return
     */
    Page<Employee> pageQuery(EmployeePageQueryDTO employeePageQueryDTO);

配置扫描映射文件

映射文件

EmployeeMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.EmployeeMapper">
<select id="pageQuery" resultType="com.sky.entity.Employee">
    SELECT * FROM employee
    <where>
        <if test="name != null and name != ''">
            AND name LIKE CONCAT('%', #{name}, '%')//需要根据名字进行查询拼接,分页由前面组插件完成
        </if>
    </where>
    ORDER BY create_time DESC//顺序
</select>
</mapper>

3.测试

4.代码完善

对日期进行格式化处理

在开发项目时我们要对全局考虑,所以我们采用第二种方式

扩展SpringMVC消息转换器

WebMvcConfiguration
/**
 * 扩展MVC框架的消息转换器
 * @param converters 默认的消息转换器列表
 */
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    log.info("开始扩展消息转换器...");
    
    // 1. 创建自定义消息转换器
    MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
    
    // 2. 设置自定义的对象映射器(处理Java对象与JSON的转换规则)
    converter.setObjectMapper(new JacksonObjectMapper());
    
    // 3. 将自定义转换器添加到转换器列表的首位
    converters.add(0, converter);
}

前后端联调

又完成一个接口开发啦!推送!

7.员工的启用和禁用

1.需求分析和设计

2.代码开发

EmployeeController

//前端进行操作传输给后端当前要进行操作的员工id和路径参数的状态值

/**
 * 启用禁用员工账号
 * @param status 状态(1启用,0禁用)
 * @param id     员工ID
 * @return 操作结果
 */
@PostMapping("/status/{status}")//通过路径从前端传输给后端时启用还是禁用
@ApiOperation("启用禁用员工账号")
public Result startOrStop(//这里不需要返回值所以不不用设置返回值类型
        @PathVariable Integer status, //利用路径传输参数
        @RequestParam Long id) {//还要传输一个用户id
    
    log.info("启用禁用员工账号:{}, {}", status, id);
    employeeService.startOrStop(status, id);
    return Result.success();
}
EmployeeServiceImpl

//实际的操作就是对数据库中的员工信息进行更改

/**
 * 启用禁用员工账号
 * @param status 状态(1启用,0禁用)
 * @param id     员工ID
 */
public void startOrStop(Integer status, Long id) {
    // 使用建造者模式创建Employee对象
    Employee employee = Employee.builder()
                              .status(status)
                              .id(id)
                              .build();
    
    // 执行数据库更新
    employeeMapper.update(employee);
}

xml

//编写一个update这样以后所有的对员工信息的更改操作就都可以调用了

<update id="update" parameterType="Employee">
    UPDATE employee
    <set>
        <if test="name != null">name = #{name},</if>
        <if test="username != null">username = #{username},</if>
        <if test="password != null">password = #{password},</if>
        <if test="phone != null">phone = #{phone},</if>
        <if test="sex != null">sex = #{sex},</if>
        <if test="idNumber != null">id_Number = #{idNumber},</if>
        <if test="updateTime != null">update_Time = #{updateTime},</if>
        <if test="updateUser != null">update_User = #{updateUser},</if>
        <if test="status != null">status = #{status},</if>
    </set>
    WHERE id = #{id}
</update>

3.测试

更改成功!

前后端联调成功后,提交推送代码

8.编辑员工

1.需求分析和设计

原型

一个查询的接口

一个提交的接口

2.代码开发

EmployeeController
/**
 * 根据id查询员工信息
 * @param id 员工ID
 * @return 员工详细信息
 */
@GetMapping("/{id}")
@ApiOperation("根据id查询员工信息")
public Result<Employee> getById(@PathVariable Long id) {//路径参数,返回值类型
    Employee employee = employeeService.getById(id);//调用service层
    return Result.success(employee);//返回值对象
}
EmployeeServiceImpl
/**
 * 根据id查询员工
 * @param id 员工ID
 * @return 脱敏后的员工对象
 */
public Employee getById(Long id) {
    Employee employee = employeeMapper.getById(id);
    employee.setPassword("****");//为了保密隐密码
    return employee;
}
EmployeeMapper
/**
 * 根据id查询员工信息
 * @param id 员工ID
 * @return 员工实体对象
 */
@Select("SELECT * FROM employee WHERE id = #{id}")//语句实现
Employee getById(long id);//方法定义

编辑员工信息

点击修改按钮后根据id查询出用户的信息后,在界面修改用户的id 

EmployeeController

/**
 * 编辑员工信息
 * @param employeeDTO 员工数据传输对象
 * @return 操作结果
 */
@PutMapping
@ApiOperation("编辑员工信息")
public Result update(@RequestBody EmployeeDTO employeeDTO) {//无返回值
    log.info("编辑员工信息: {}", employeeDTO);
    employeeService.update(employeeDTO);
    return Result.success();
}

EmployeeServiceImpl

/**
 * 编辑员工信息
 * @param employeeDTO 员工数据传输对象
 */
public void update(EmployeeDTO employeeDTO) {//定义方法传入dto对象
    // 1. DTO转Entity
    Employee employee = new Employee();//同样创建实体对象
    BeanUtils.copyProperties(employeeDTO, employee);//复制dto信息到实体对象
    
    // 2. 设置审计字段
    employee.setUpdateTime(LocalDateTime.now());//设置实体对象多出的信息
    employee.setUpdateUser(BaseContext.getCurrentId());
    
    // 3. 执行数据库更新
    employeeMapper.update(employee);//调用mapper层传入实体对象
//之前我们已经在修改员工状态的时候已经创建update员工修改方法
}

3.测试

前端测试成功!

9.菜单分类功能

//因为这个功能及接口和以上员工相关类似所以都是直接导入

1.需求分析和设计

这一步把资料中第二天代码导入即可,后续要自己敲一遍哦

10.公共字段自动填充

在进行菜品管理模块开发前我们先学习公共字段自动填充相关内容

1.问题分析

在开发时我们经常初始化一些字段,代码冗余不利于后续维护

2.实现思路

3.代码实现

创建包annotation

在包中自定义注解AutoFill

/**
 * 自定义注解,用于标识某个方法需要进行功能字段自动填充
 */
@Target(ElementType.METHOD)//表示该注解只能用于标记方法
@Retention(RetentionPolicy.RUNTIME)//固定写法表示注解在运行时可通过反射读取,这是实现自动填充的关键。


public @interface AutoFill {//定义了一个名为 AutoFill 的自定义注解
    /**
     * 数据库操作类型
     */
    OperationType value();//定义一个名为 value 的属性,其类型为 OperationType
}

定义好的对数据库操作的枚举类型

创建aspect包,切面类AutoFillAspect

/**
 * 自定义切面,实现公共字段自动填充处理逻辑
 */
@Aspect//注解为切面类
@Component//由Spring容器管理该Bean
@Slf4j
public class AutoFillAspect {

    /**
     * 切入点:拦截Mapper层带有@AutoFill注解的方法
     */
    @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
    //定义切入点匹配com.sky.mapper包下所有类的所有方法  //定义方法上标记了@AutoFill注解就是目的点
//效果:仅拦截需要自动填充字段的Mapper方法(如insert/update)
    public void autoFillPointCut() {}//定义标记方法
            

            /**
             * 前置通知:在目标方法执行前自动填充公共字段
            */
            @Before("autoFillPointCut()")//在标记方法(被@AutoFill标记的Mapper方法)执行前调用
            public void autoFill(JoinPoint joinPoint) {//调用方法传入JoinPoint对象可获取方法签名、参数等信息
        log.info("开始进行公共字段自动填充...");

    }
EmployeeMapper

//在mapper方法上加入AutoFill注解

/**
     * 插入员工数据
     * @param employee
     */
    @Insert("insert into employee (name, username, password, phone, sex, id_number, create_time, update_time, create_user, update_user, status) " +
            "values (#{name}, #{username}, #{password}, #{phone}, #{sex}, #{idNumber}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser}, #{status})")
    @AutoFill(value = OperationType.INSERT)//参数为之前定义的枚举类型
    void insert(Employee employee);

/**
     * 启用禁用员工账号
     * @param employee
     */
    @AutoFill(value = OperationType.UPDATE)
    void update(Employee employee);

完善前置通知,在通知中给公共字段赋值

不同的操作要修改的字段也不同,如创建和更新操作内容是不同的

AutoFillAspect

/**
 * 自定义切面,实现公共字段自动填充处理逻辑
 */
@Aspect//注解为切面类
@Component//由Spring容器管理该Bean
@Slf4j
public class AutoFillAspect {

    /**
     * 切入点:拦截Mapper层带有@AutoFill注解的方法
     */
    @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
    //定义切入点匹配com.sky.mapper包下所有类的所有方法  //定义方法上标记了@AutoFill注解就是目的点
    public void autoFillPointCut() {}//定义标记方法


            /**
             * 前置通知:在目标方法执行前自动填充公共字段
            */
            @Before("autoFillPointCut()")//在标记方法(被@AutoFill标记的Mapper方法)执行前调用
            public void autoFill(JoinPoint joinPoint) {//调用方法传入JoinPoint对象可获取方法签名、参数等信息
        log.info("开始进行公共字段自动填充...");

                // 1. 获取数据库操作类型和实体对象
                MethodSignature signature = (MethodSignature) joinPoint.getSignature();//通过joinPoint连接点对象获得签名,把接口转型为子接口
                AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);//signature对象获得方法AutoFill注解对象
                OperationType operationType = autoFill.value();//获得数据库操作类型operationType对象
//防止当前方法没有参数则不执行,健壮性
                Object[] args = joinPoint.getArgs();
                if (args == null || args.length == 0) return;
                Object entity = args[0];//获取实体对象从第一位开始

// 2. 准备填充数据
                LocalDateTime now = LocalDateTime.now();//获得当前操作时间
                Long currentId = BaseContext.getCurrentId(); // 从线程上下文获取操作人ID

// 3. 根据操作类型填充字段
                try {
                    if (operationType == OperationType.INSERT) {
                        // 插入操作:填充4个字段
                        Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
                        Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
                        Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                        Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);

                        setCreateTime.invoke(entity, now);
                        setCreateUser.invoke(entity, currentId);
                        setUpdateTime.invoke(entity, now);
                        setUpdateUser.invoke(entity, currentId);
                    }
                    else if (operationType == OperationType.UPDATE) {
                        // 更新操作:填充2个字段
                        Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                        Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);

                        setUpdateTime.invoke(entity, now);
                        setUpdateUser.invoke(entity, currentId);
                    }
                } catch (Exception e) {
                   e.printStackTrace();
                }

    }
}
CategoryMapper
 /**
     * 插入数据
     * @param category
     */
    @Insert("insert into category(type, name, sort, status, create_time, update_time, create_user, update_user)" +
            " VALUES" +
            " (#{type}, #{name}, #{sort}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})")
    @AutoFill(value = OperationType.INSERT)
    void insert(Category category);
 /**
     * 根据id修改分类
     * @param category
     */
    @AutoFill(value = OperationType.UPDATE)
    void update(Category category);

//我们现在已经实现了公共属性赋值操作,则可以删除impl中对应赋值部分

测试

提交代码

11.菜品管理

 1.需求分析和设计

2.代码开发

文件上传接口开发

浏览器如何访问文件呢

想要使用阿里云我们要配置oss

我们要有自己的阿里云然后从自己的阿里云获取以上内容

详细如何申请可以看黑马javaweb课程有详细步骤

application-dev.yml

  alias:
    endpoint: oss-cn-beijing.aliyuncs.com
    access-key-id: 
    access-key-secret: 
    bucket-name: 

application.yml

spring:
  profiles:
    active: dev
alias:
  endpoint: ${sky.alias.endpoint}
  access-key-id: ${sky.alias.access-key-id}
  access-key-secret: ${sky.alias.access-key-secret}
  bucket-name: ${sky.alias.bucket-name}

以上配置项可以通过配置属性类加载

AliOssProperties.java

package com.sky.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "sky.alias")
@Data
public class AliOssProperties {//最终加载配置信息为一个AliOssProperties对象
    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;
}

OssConfiguration.java

package com.sky.config;

import com.sky.properties.AliOssProperties;
import com.sky.utils.AliOssUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 配置类,用于创建AliOssUtil对象
 */
@Configuration
@Slf4j
public class OssConfiguration {

    @Bean
   @ConditionalOnMissingBean
    public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties) {
        log.info("开始创建阿里云文件上传工具类对象:{}", aliOssProperties);
        return new AliOssUtil(
            aliOssProperties.getEndpoint(),
            aliOssProperties.getAccessKeyId(),
            aliOssProperties.getAccessKeySecret(),
            aliOssProperties.getBucketName()
        );
    }
}

Controller

/**
 * 通用接口(文件上传)
 */
@RestController
@RequestMapping("/admin/common")
@Api(tags = "通用接口")
@Slf4j
public class CommonController {
    @Autowired
    private AliOssUtil aliOssUtil;  // 阿里云OSS工具类

    /**
     * 文件上传
     * @param file 前端上传的文件
     * @return 文件访问路径
     */
    @PostMapping("/upload")
    @ApiOperation("文件上传")
    public Result<String> upload(MultipartFile file) {
        log.info("文件上传: {}", file.getOriginalFilename());
        
        try {
            // 1. 生成随机文件名(保留原始后缀)
//原始文件名获取
            String originalFilename = file.getOriginalFilename();
//截取原始文件后缀
            String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
//构架新文件名
            String objectName = UUID.randomUUID() + extension;

            // 2. 上传文件到OSS
            String filePath = aliOssUtil.upload(file.getBytes(), objectName);
            return Result.success(filePath);

        } catch (IOException e) {
            log.error("文件上传失败: {}", e);
}
            return Result.error(MessageConstant.UPLOAD_FAILED);
    }
}

前后端联调测试

新增菜品

DishController

import com.sky.result.Result;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 菜品管理
 */
@RestController
@RequestMapping("/admin/dish")
@Api(tags = "菜品相关接口")
@Slf4j
public class DishController {

    @Autowired
    private DishService dishService;

    /**
     * 新增菜品
     * @param dishDTO 菜品数据传输对象(包含菜品基本信息和口味列表)
     * @return 统一响应结果
     */
    @PostMapping
    @ApiOperation("新增菜品")
    public Result save(@RequestBody DishDTO dishDTO) {
        log.info("新增菜品: {}", dishDTO);
        dishService.saveWithFlavor(dishDTO);
        return Result.success();
    }
}

serviceimpl

package com.sky.service.impl;

@Service
@Slf4j
public class DishServiceImpl implements DishService {

    @Autowired
    private DishMapper dishMapper;//注入菜品对象
    @Autowired
    private DishFlavorMapper dishFlavorMapper;//注入菜品口味对象

    /**
     * 新增菜品和对应的口味
     * @param dishDTO 菜品数据传输对象(包含菜品和口味信息)
     */
    @Transactional
    public void saveWithFlavor(DishDTO dishDTO) {//传入前端参数
        Dish dish = new Dish();
        BeanUtils.copyProperties(dishDTO, dish);//拷贝dto到实体对象
        
        // 向菜品表插入1条数据
        dishMapper.insert(dish);//插入菜品对象

       //我们还要把添加的菜品加入到其对应的口味表中
//获取insert语句生成的主键值
        Long dishId = dish.getId();//我们现在还没有创建成功一个菜品,但是后续我们需要在口味表中插入id,所以这里要提前获取dishmapper中已经生成的菜品id,这里还要提前在dishmapper中返回主键值

        List<DishFlavor> flavors = dishDTO.getFlavors();//得到当前菜品的口味口味列表
        if (flavors != null && flavors.size() > 0) {//如果口味不为空的话
            flavors.forEach(dishFlavor -> {//遍历所有口味对象
                dishFlavor.setDishId(dishId); // 将它们的dishId字段设置为当前菜品的ID,建立菜品与口味的一对多关系
            });
            // 向口味表批量插入n条数据
            dishFlavorMapper.insertBatch(flavors);
        }
    }
}

口味数据表是这样的插入对象是这样的

dishmapper

/**
 * 插入菜品数据
 * @param dish 菜品实体对象
 */
@AutoFill(value = OperationType.INSERT)//注意这里我们自动填充使用我们之前写好的自动填充类
void insert(Dish dish);

DishMapper.xml

//useGeneratedKeys="true这里定义我们需要生成的主键值,后面设置keyProperty="id表示获取的主键值是这个id,因为这个id是自增的,不需要输入只有生成完菜品才可以生成所以我们这里才需要提前获取
<mapper namespace="com.sky.mapper.DishMapper">
    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO dish 
        (name, category_id, price, image, description, 
         create_time, update_time, create_user, update_user, status) 
        VALUES 
        (#{name}, #{categoryId}, #{price}, #{image}, #{description}, 
         #{createTime}, #{updateTime}, #{createUser}, #{updateUser}, #{status})
    </insert>
</mapper>

DishFlavorMapper.xml

<mapper namespace="com.sky.mapper.DishFlavorMapper">
    <insert id="insertBatch">
        INSERT INTO dish_flavor (dish_id, name, value) 
        VALUES
        <foreach collection="flavors" item="df" separator=",">
            (#{df.dishId}, #{df.name}, #{df.value})
        </foreach>
    </insert>
</mapper>

测试

提交推送

12.菜品其它接口

 其它关于菜品的接口因为和前面员工相似,后续再补。先学后面的技术。

13.Redis技术

Redis是对mysql的补充

redis安装配置

Releases · microsoftarchive/redis · GitHub

先启动redis

第五天目录点击安装红色软件

解压到文件夹后打开,我发现我的加载出来是白板经查询发现是渲染问题

解决方法:

创建快捷方式右击找到属性目标栏后面直接追加例"D:\APP\redisdesktop\Another Redis Desktop Manager.exe" --disable-gpu --disable-software-rasterizer,注意这里的目录是否正确,你是否解压到还有小文件夹里否则会追加不成功

打开成功后

创建连接

redis数据类型和常用命令

我们可以在Another Redis Desktop Manager.exe客户端工具中熟悉语法

在开发环境中我们使用Spring Data Redis来在java 环境中使用redis,课程中有写测试类熟悉语法内容我这里不在赘述,直接敲项目相关代码

第一步:导入maven坐标

pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置redis数据源

dev


  redis:
    host: localhost
    port: 6379
    password: 123456
    database: 10

application.yml

redis:
  host: ${sky.redis.host}
  port: ${sky.redis.port}
  password: ${sky.redis.password}
  database: ${sky.redis.database}

编写配置类,创建Redis Template对象

RedisConfiguration.java

@Configuration
@Slf4j
public class RedisConfiguration {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        log.info("开始创建Redis模板类...");
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        // 设置Key的序列化器(默认使用JDK序列化,这里改为String序列化)
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        // 设置连接工厂
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        return redisTemplate;
    }
}

然后就可以通过Redis Template对象操作Redis

14.营业状态设置

1.需求分析和设计

出于新能考虑我们使用redis来存储营业状态

2.代码开发

管理端

ShopController

@RestController("adminShopController")
@RequestMapping("/admin/shop")
@Slf4j
@Api(tags = "店铺相关接口")
public class ShopController {

public static final String KEY = "SHOP_STATUS";
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 设置店铺的营业状态
     * @param status
     * @return
     */
    @PutMapping("/{status}")//用于映射 HTTP 的 PUT 请求,这里路径为 /admin/shop/{status} ,其中 {status} 是一个路径变量。
    @ApiOperation("设置店铺的营业状态")
    public Result setStatus(@PathVariable Integer status){//@PathVariable Integer status 表示从请求路径中获取名为 status 的整数类型参数。
        log.info("设置店铺的营业状态为: {}",status == 1? "营业中" : "打烊中");
        redisTemplate.opsForValue().set("SHOP_STATUS",status);
        return Result.success();
    }
/**
 * 获取店铺的营业状态
 * @return
 */
@GetMapping("/status")
@ApiOperation("获取店铺的营业状态")
public Result<Integer> getStatus(){
    Integer status = (Integer) redisTemplate.opsForValue().get("SHOP_STATUS");
    log.info("获取到店铺的营业状态为: {}",status == 1? "营业中" : "打烊中");
    return Result.success(status);
}
}

创建客户端包

ShopController

@RestController("userController")
@RequestMapping("/user/shop")//只修改接口路径其他照搬
@Slf4j
@Api(tags = "店铺相关接口")
public class ShopController {

public static final String KEY = "SHOP_STATUS";
    @Autowired
    private RedisTemplate redisTemplate;

/**
 * 获取店铺的营业状态
 * @return
 */
@GetMapping("/status")
@ApiOperation("获取店铺的营业状态")
public Result<Integer> getStatus(){
    Integer status = (Integer) redisTemplate.opsForValue().get("SHOP_STATUS");
    log.info("获取到店铺的营业状态为: {}",status == 1? "营业中" : "打烊中");
    return Result.success(status);
}
}

当类名相同时我们可以通过修改@RestController的注解来区分,否则生成bean对象会产生冲突。

客户端管理端接口文档设置

 @Bean
    public Docket docket1() {
        ApiInfo apiInfo = new ApiInfoBuilder()
                .title("苍穹外卖项目接口文档测试")
                .version("2.0")
                .description("苍穹外卖项目接口文档")
                .build();
        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .groupName("管理端接口")
                .apiInfo(apiInfo)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.sky.controller.admin"))
                .paths(PathSelectors.any())
                .build();
        return docket;
    }

    @Bean
    public Docket docket() {
        ApiInfo apiInfo = new ApiInfoBuilder()
                .title("苍穹外卖项目接口文档测试")
                .version("2.0")
                .description("苍穹外卖项目接口文档")
                .build();
        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .groupName("用户端接口")
                .apiInfo(apiInfo)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.sky.controller.user"))
                .paths(PathSelectors.any())
                .build();
        return docket;
    }

3.测试

4.推送

//注意运行前要对应目录启动redis-server.exe,否则无法设置

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值