苍穹外卖-day02

课程内容

  • 新增员工
  • 员工分页查询
  • 启用禁用员工账号
  • 编辑员工
  • 导入分类模块功能代码

功能实现:员工管理、菜品分类管理。

单表

员工管理效果:

在这里插入图片描述

菜品分类管理效果:

在这里插入图片描述

1. 新增员工(员工管理)

1.1 需求分析和设计

1.1.1 产品原型

一般在做需求分析时,往往都是对照着产品原型进行分析,因为产品原型比较直观,便于我们理解业务。

后台系统中可以管理员工信息,通过新增员工来添加后台系统用户。

新增员工原型:

在这里插入图片描述

当填写完表单信息, 点击"保存"按钮后, 会提交该表单的数据到服务端, 在服务端中需要接受数据, 然后将数据保存至数据库中。

注意事项:

  1. 账号必须是唯一的
  2. 手机号为合法的11位手机号码
  3. 身份证号为合法的18位身份证号码
  4. 密码默认为123456
    • 录入完员工信息后,这个员工就可以拿到这个账号来登录我们的系统了,登录就需要有登录的密码而我们这个表单里好像并没有密码,这是为什么呢???
    • 这个地方约定的是新增出来的员工,它的密码都是默认的密码123456,当然它登录进来之后可以去修改密码,因为这个系统里面也有修改密码的功能。所以说在新增的时候这个密码我们都给它设置为默认的密码123456。
1.1.2 接口设计(统一前缀,必须非必须)

找到资料–>项目接口文档–>苍穹外卖-管理端接口.html

在这里插入图片描述
在这里插入图片描述

明确新增员工接口的请求路径、请求方式、请求参数、返回数据

  • 请求路径:为什么不直接使用/employee,而是在前面还要加上这个前缀admin呢??
    • 此项目分为了2个端(管理端和用户端),管理端提供给商家使用,用户端主要是给用户点餐使用的,这2个端都会请求到我们的后端,这个时候想要区分一下具体这个请求是那个端发过来的,约定管理端发出的请求,统一使用/admin作为前缀,约定用户端发出的请求,统一使用/user作为前缀.
    • 好处:通过前缀来区分不同的端方便我们做一些处理的工作,比如统一拦截用户端发过来的请求来看一下用户的状态,这样的话很容易就拦截到。如果没有这样的约定,那么这个请求发送过来的也没有什么规范,此时我们后端就很难区分这个请求到底是哪个端来发送的。
    • 所以后期我们所有的接口都需要遵守这个约定,只要是管理端都以…前缀为开头,只要是用户端都以…前缀为开头。
  • 请求方式:新增员工,所以post请求比较合适
  • 请求参数:通过json格式来提交页面上看到的表单项,其中id是非必须的,也就是说前端给我们提交或者是不提交都可以
  • 返回数据:返回的也是json格式并且会封装成一个result统一响应结果给前端页面
    • code:状态码是必须的,约定返回1表示业务操作成功,返回0代表业务操作失败。
    • data:返回的数据非必须,一般查询返回数据,这个地方是新增所以不需要返回
    • message:提示信息非必须,一般成功不设置,失败设置一个提示信息

本项目约定:

  • 管理端发出的请求,统一使用/admin作为前缀。
  • 用户端发出的请求,统一使用/user作为前缀。
1.1.3 表设计

新增员工,其实就是将我们新增页面录入的员工数据插入到employee表。

employee表结构:

  • id:设置了主键自增所以不用我们去维护它的值,而是由数据库维护
  • username:用户名其实就是上面产品原型上的账号,它是登录的依据所以必须是唯一的
  • status:账号的状态,1代表账号正常允许登录, 0锁定代表账号异常不允许登录
字段名数据类型说明备注
idbigint主键自增
namevarchar(32)姓名
usernamevarchar(32)用户名唯一
passwordvarchar(64)密码
phonevarchar(11)手机号
sexvarchar(2)性别
id_numbervarchar(18)身份证号
statusInt账号状态1正常 0锁定
create_timeDatetime创建时间
update_timedatetime最后修改时间
create_userbigint创建人id
update_userbigint最后修改人id

其中,employee表中的status字段已经设置了默认值1,表示状态正常。

在这里插入图片描述

1.2 代码开发

1.2.1 设计DTO类(直接使用实体类接收请求参数的缺点)

根据新增员工接口设计对应的DTO

前端传递参数列表:

在这里插入图片描述

思考:是否可以使用对应的实体类来接收呢?

在这里插入图片描述

注意:当前端提交的数据和实体类中对应的属性差别比较大时,建议使用DTO来封装数据

即:实体类当中包含很多不需要提交的属性时,此时直接使用实体类来接收请求参数,可读性差、浪费性能。如果使用DTO精确封装,可读性好并且不会造成性能浪费。

由于上述传入参数和实体类有较大差别,所以自定义DTO类。

进入sky-pojo模块,在com.sky.dto包下,已定义EmployeeDTO
在这里插入图片描述

package com.sky.dto;

import lombok.Data;

import java.io.Serializable;

@Data
public class EmployeeDTO implements Serializable {

    private Long id;

    private String username;

    private String name;

    private String phone;

    private String sex;

    private String idNumber;

}
1.2.2 Controller层(log占位符)

EmployeeController中创建新增员工方法

进入到sky-server模块中,在com.sky.controller.admin包下,在EmployeeController中创建新增员工方法,接收前端提交的参数。

在这里插入图片描述

    /**
     * 新增员工
     * @param employeeDTO
     * @return
     */
    @PostMapping
    @ApiOperation("新增员工")
    public Result save(@RequestBody EmployeeDTO employeeDTO){
        log.info("新增员工:{}",employeeDTO);//{}占位符:表示当程序运行的时候,它就会把后面的这个参数动态的拼接到占位符的位置
        employeeService.save(employeeDTO);//该方法后续步骤会定义
        return Result.success();
    }

:Result类定义了后端统一返回结果格式。

进入sky-common模块,在com.sky.result包下定义了Result.java

在这里插入图片描述

package com.sky.result;

import lombok.Data;

import java.io.Serializable;

/**
 * 后端统一返回结果
 * @param <T>
 */
@Data
public class Result<T> implements Serializable {

    private Integer code; //编码:1成功,0和其它数字为失败
    private String msg; //错误信息
    private T data; //数据

    public static <T> Result<T> success() {
        Result<T> result = new Result<T>();
        result.code = 1;
        return result;
    }

    public static <T> Result<T> success(T object) {
        Result<T> result = new Result<T>();
        result.data = object;
        result.code = 1;
        return result;
    }

    public static <T> Result<T> error(String msg) {
        Result result = new Result();
        result.msg = msg;
        result.code = 0;
        return result;
    }

}
1.2.3 Service层接口

在EmployeeService接口中声明新增员工方法

进入到sky-server模块中,com.sky.server.EmployeeService

	/**
     * 新增员工
     * @param employeeDTO
     */
    void save(EmployeeDTO employeeDTO);
1.2.4 Service层实现类(属性拷贝,TODO,常量类)

在EmployeeServiceImpl中实现新增员工方法

com.sky.server.impl.EmployeeServiceImpl中创建方法
在这里插入图片描述

    /**
     * 新增员工
     * @param employeeDTO
     *  传递过来的是DTO类,这是为了方便封装前端传递过来的数据,但是最终
     *  传递到持久层建议还是使用我们的实体类。所以这个地方需要做一个类型转换
     *  将DTO转化为我们的实体Employee。
     *
     */
    @Override //此注解不是必须的可以去掉
    public void save(EmployeeDTO employeeDTO) {
        Employee employee = new Employee();

        //1.直接使用set,get一个个的转化太麻烦,可以使用对象属性拷贝,
        //一次性的将这些属性给它拷过来,前提是2个类之间的属性要一一对应(属性名一致)
        //把employeeDTO的属性拷贝给employee中对应的属性上
        BeanUtils.copyProperties(employeeDTO, employee); //Spring提供的工具类

        //2.实体类employee中的属性更多一些,DTO只是把前面几个属性拷贝过来,所以employee剩余的属性需要自己去设置
        //设置账号的状态,默认正常状态 1表示正常 0表示锁定
        employee.setStatus(StatusConstant.ENABLE); //常量类的方式,比硬编码更灵活

        //设置密码,默认密码123456  md5加密,并且是常量类的方式调用123456
        employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));

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

        //TODO 设置当前记录创建人id和修改人id
        //  其实指的就是我们当前这个登录用户的id,通过这些参数获取不到,所以暂时固定写死一个值,
        //  后面会学习一个新的技术来解决这个问题。
        employee.setCreateUser(10L);//目前写个假数据,后期修改
        employee.setUpdateUser(10L);

        employeeMapper.insert(employee);//后续步骤定义

    }

在这里插入图片描述

在sky-common模块com.sky.constants包下已定义StatusConstant.java
在这里插入图片描述

package com.sky.constant;

/**
 * 状态常量,启用或者禁用
 */
public class StatusConstant {

    //启用
    public static final Integer ENABLE = 1;

    //禁用
    public static final Integer DISABLE = 0;
}

1.2.5 Mapper层

在EmployeeMapper中声明insert方法

com.sky.EmployeeMapper中添加方法
在这里插入图片描述

	/**
     * 插入员工数据
     * @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);

在application.yml中已开启驼峰命名,故id_number和idNumber可对应。
在这里插入图片描述

mybatis:
  configuration:
    #开启驼峰命名
    map-underscore-to-camel-case: true

1.3 功能测试

代码已经发开发完毕,对新增员工功能进行测试。

功能测试实现方式:

  • 通过接口文档测试
  • 通前后端联调测试

接下来我们使用上述两种方式分别测试。

1.3.1 接口文档测试(常用,全局参数设置)

启动服务:访问http://localhost:8080/doc.html,进入新增员工接口

在这里插入图片描述

json数据:

{
  "id": 0,
  "idNumber": "111222333444555666",
  "name": "xiaozhi",
  "phone": "13812344321",
  "sex": "1",
  "username": "小智"
}

添加断点,debug运行

在这里插入图片描述

响应码:401 报错,发现请求并没有停留在断点位置

原因:项目初始代码有一个拦截器JwtTokenAdminInterceptor,它是jwt令牌校验的拦截器,从请求头里面获取出令牌,之后对令牌进行一个校验。显然这一次请求没校验通过,因为刚才执行这个接口的时候 并没有把这个令牌给它提交过来

在这里插入图片描述
通过断点调试:进入到JwtTokenAdminInterceptor拦截器

添加断点此时token为null,所以它在校验的时候会抛出异常 ,被下面catch捕获设置状态码为401,之后返回
在这里插入图片描述
在这里插入图片描述

 	/**
     * 校验jwt
     *
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断当前拦截到的是Controller的方法还是其他资源
        if (!(handler instanceof HandlerMethod)) {
            //当前拦截到的不是动态方法,直接放行
            return true;
        }

        //1、从请求头中获取令牌 jwtProperties.getAdminTokenName()获取为token
        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);
            //3、通过,放行
            return true;
        } catch (Exception ex) {
            //4、不通过,响应401状态码
            response.setStatus(401);
            return false;
        }
    }

报错原因:由于JWT令牌校验失败,导致EmployeeController的save方法没有被调用

解决方法:调用员工登录接口获得一个合法的JWT令牌

使用admin用户登录获取令牌

在这里插入图片描述

添加令牌:

将合法的JWT令牌添加到全局参数中

文档管理–>全局参数设置–>添加参数

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

接口测试:

在这里插入图片描述

其中,请求头部含有JWT令牌

在这里插入图片描述

查看employee表:

在这里插入图片描述

测试成功。

1.3.2 前后端联调测试

启动nginx,访问 http://localhost

登录–>员工管理–>添加员工
在这里插入图片描述

在这里插入图片描述

保存后,查看employee表

在这里插入图片描述

测试成功。

注意:由于开发阶段前端和后端是并行开发的,后端完成某个功能后,此时前端对应的功能可能还没有开发完成,导致无法进行前后端联调测试。所以在开发阶段,后端测试主要以接口文档测试为主。

1.4 代码完善(ThreadLocal)

目前,程序存在的问题主要有两个:

  • 录入的用户名已存,抛出的异常后没有处理
    • 用户名在数据库存储时设置了唯一约束
  • 新增员工时,创建人id和修改人id设置为固定值

接下来,我们对上述两个问题依次进行分析和解决。

1.4.1 问题一:用户名重复,需要处理异常

描述:录入的用户名已存,抛出的异常后没有处理

分析

新增username=zhangsan的用户,若employee表中之前已存在。

在这里插入图片描述

后台报错信息:

在这里插入图片描述

查看employee表结构:

在这里插入图片描述

发现,username已经添加了唯一约束,不能重复。

解决:

通过全局异常处理器来处理。

进入到sky-server模块,com.sky.hander包下,GlobalExceptionHandler.java添加方法
在这里插入图片描述

    /**
     * 处理SQL异常
     * @param ex
     * @return
     */
    @ExceptionHandler
    public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
        //Duplicate entry 'zhangsan' for key 'employee.idx_username' 出现异常后控制台输出的异常信息
        //捕获到异常后具体如何处理呢???
        //思路:先获取异常信息,之后判断异常信息是否包含Duplicate entry(重复的键值对)这个关键字,
        //     如果包含就可以确认输出的日志确实是上面报错输出的异常信息,接下来希望给前端返回一个
        //     zhangsan已存在,所以需要把zhangsan这个字符串动态的提取出来,之后在拼接常量类中设置的属性 String ALREADY_EXISTS = "已存在";
        String message = ex.getMessage();
        if(message.contains("Duplicate entry")){
            String[] split = message.split(" ");//通过空格进行分割,得到一个数组对象
            String username = split[2]; //zhangsan 在第3个位子,也就是数组下标为2的位置
            String msg = username + MessageConstant.ALREADY_EXISTS; //之后进行拼接
            return Result.error(msg); //返回统一响应结果   xxx已存在
        }else{
            return Result.error(MessageConstant.UNKNOWN_ERROR);  //未知错误
        }
    }

进入到sky-common模块,在MessageConstant.java添加
在这里插入图片描述

public static final String ALREADY_EXISTS = "已存在";

再次,接口测试:

在这里插入图片描述

1.4.2 问题二:新增员工,创建人id修改为动态获取

描述:新增员工时,创建人id和修改人id设置为固定值

分析:

在这里插入图片描述

	/**
     * 新增员工
     *
     * @param employeeDTO
     */
    public void save(EmployeeDTO employeeDTO) {
        Employee employee = new Employee();
        //................
        //当前设置的id为固定值10//
        employee.setCreateUser(10L);
        employee.setUpdateUser(10L);
        //
        //.................................

        employeeMapper.insert(employee);//后续步骤定义
    }

解决:

需要先看一下基于jwt的这种认证的方式,只有把这个流程搞明白之后才能够想办法来获取当前登录用户的id。

通过某种方式动态获取当前登录员工的id。

在拦截器这个位置可以把请求拦截下来,然后把用户携带过来的token进行一个校验,如果校验通过了说明登录成功,此时token里面就存在登录用户的id,因为我们登录成功之后在生成token的时候已经把这个id给它放进去了,所以反向解析也可以吧这个id解析出来。

在这里插入图片描述

员工登录成功后会生成JWT令牌,存入用户id,之后响应给前端:

在sky-server模块
在这里插入图片描述

package com.sky.controller.admin;
/**
 * 员工管理
 */
@RestController
@RequestMapping("/admin/employee")
@Slf4j
@Api(tags = "员工相关接口")
public class EmployeeController {

    @Autowired
    private EmployeeService employeeService;
    @Autowired
    private JwtProperties jwtProperties;

    /**
     * 登录
     *
     * @param employeeLoginDTO
     * @return
     */
    @PostMapping("/login")
    @ApiOperation(value = "员工登录")
    public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {
        //.........

        //登录成功后,生成jwt令牌
        Map<String, Object> claims = new HashMap<>();
        claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
        String token = JwtUtil.createJWT(
                jwtProperties.getAdminSecretKey(),
                jwtProperties.getAdminTtl(),
                claims);

        //............

        return Result.success(employeeLoginVO);
    }

}

后续请求中,前端会携带JWT令牌,通过JWT令牌可以解析出当前登录员工id:

拦截器把这个token拦截下来之后,如果校验通过就可以把这个empid给它取出来,因为当时生成的时候就放进去了,所以现在肯定可以解析出来,所以我们可以在拦截器这个位置把id取到。

JwtTokenAdminInterceptor.java

在这里插入图片描述

package com.sky.interceptor;

/**
 * jwt令牌校验的拦截器
 */
@Component
@Slf4j
public class JwtTokenAdminInterceptor implements HandlerInterceptor {

    @Autowired
    private JwtProperties jwtProperties;

    /**
     * 校验jwt
     *
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
       
		//..............
        
        //1、从请求头中获取令牌
        String token = request.getHeader(jwtProperties.getAdminTokenName());

        //2、校验令牌
        try {
            log.info("jwt校验:{}", token);
            //解析后的结果是一个Map集合
            Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
            Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
            log.info("当前员工id:", empId);
            //3、通过,放行
            return true;
        } catch (Exception ex) {
            //4、不通过,响应401状态码
            response.setStatus(401);
            return false;
        }
    }
}

思考:在拦截器位置解析出登录员工id后,如何传递给Service的save方法?

通过ThreadLocal进行传递。

1.4.3 ThreadLocal

介绍:

ThreadLocal 并不是一个Thread线程,而是Thread的局部变量。

ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。只要在这个线程整个生命周期之内,就可以共享这一份存储空间。

客户端每次发起的请求,tomact服务器都会给我们分配一个单独的线程,然后在这个线程上可能要执行不同的代码,比如拦截器的代码、controller层的代码、service层的代码,它们都属于同一个线程 此时就可以使用ThreadLocal,将对应的数据存进去 然后在对应的地方给它取出来

常用方法:

  • public void set(T value) 设置当前线程的线程局部变量的值
  • public T get() 返回当前线程所对应的线程局部变量的值
  • public void remove() 移除当前线程的线程局部变量

对ThreadLocal有了一定认识后,接下来继续解决问题二

在这里插入图片描述

初始工程中已经封装了 ThreadLocal 操作的工具类:

在sky-common模块
在这里插入图片描述

package com.sky.context;

public class BaseContext {

    public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    public static void setCurrentId(Long id) {
        threadLocal.set(id);
    }

    public static Long getCurrentId() {
        return threadLocal.get();
    }

    public static void removeCurrentId() {
        threadLocal.remove();
    }

}

在拦截器中解析出当前登录员工id,并放入线程局部变量中:

在sky-server模块中,拦截器:
在这里插入图片描述

package com.sky.interceptor;

/**
 * jwt令牌校验的拦截器
 */
@Component
@Slf4j
public class JwtTokenAdminInterceptor implements HandlerInterceptor {

    @Autowired
    private JwtProperties jwtProperties;

    /**
     * 校验jwt
     *
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        
		//.............................
       
        //2、校验令牌
        try {
            //.................
            Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
            Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
            log.info("当前员工id:", empId);
            /将用户id存储到ThreadLocal
            BaseContext.setCurrentId(empId);
            
            //3、通过,放行
            return true;
        } catch (Exception ex) {
            //......................
        }
    }
}

在Service中获取线程局部变量中的值:

在这里插入图片描述

	/**
     * 新增员工
     *
     * @param employeeDTO
     */
    public void save(EmployeeDTO employeeDTO) {
        //.............................

        //设置当前记录创建人id和修改人id
        employee.setCreateUser(BaseContext.getCurrentId());//目前写个假数据,后期修改
        employee.setUpdateUser(BaseContext.getCurrentId());

        employeeMapper.insert(employee);
    }

测试:使用admin(id=1)用户登录后添加一条记录

在这里插入图片描述

查看employee表记录

在这里插入图片描述

1.5 代码提交

点击提交:

在这里插入图片描述

提交过程中,出现提示:

在这里插入图片描述

继续push:

在这里插入图片描述

推送成功:

在这里插入图片描述

2. 员工分页查询(员工管理)

2.1 需求分析和设计

2.1.1 产品原型

系统中的员工很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。而在我们的分页查询页面中, 除了分页条件以外,还有一个查询条件 “员工姓名”。

查询员工原型:

在这里插入图片描述

业务规则

  • 根据页码展示员工信息
  • 每页展示10条数据
  • 分页查询时可以根据需要,输入员工姓名进行查询
2.1.2 接口设计

找到资料–>项目接口文档–>苍穹外卖-管理端接口.html

  • 请求方式:查询所以为get请求
  • 提交的参数:页码,每页的记录数,员工的姓名
  • 响应的数据:总记录数total,当前页展示的数据集合records

在这里插入图片描述

在这里插入图片描述

注意事项:

  • 请求参数类型为Query,不是json格式提交,在路径后直接拼接。/admin/employee/page?name=zhangsan
  • 返回数据中records数组中使用Employee实体类对属性进行封装。

2.2 代码开发

2.2.1 设计DTO类

根据请求参数进行封装,在sky-pojo模块中
在这里插入图片描述

package com.sky.dto;

import lombok.Data;

import java.io.Serializable;

@Data
public class EmployeePageQueryDTO implements Serializable {

    //员工姓名
    private String name;

    //页码
    private int page;

    //每页显示记录数
    private int pageSize;

}

2.2.2 封装PageResult

后面所有的分页查询,统一都封装为PageResult对象。

在sky-common模块
在这里插入图片描述

package com.sky.result;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.List;

/**
 * 封装分页查询结果
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult implements Serializable {

    private long total; //总记录数

    private List records; //当前页数据集合

}

员工信息分页查询后端返回的对象类型为: Result

在这里插入图片描述
在这里插入图片描述

package com.sky.result;

import lombok.Data;

import java.io.Serializable;

/**
 * 后端统一返回结果
 * @param <T>
 */
@Data
public class Result<T> implements Serializable {

    private Integer code; //编码:1成功,0和其它数字为失败
    private String msg; //错误信息
    private T data; //数据

    public static <T> Result<T> success() {
        Result<T> result = new Result<T>();
        result.code = 1;
        return result;
    }

    public static <T> Result<T> success(T object) {
        Result<T> result = new Result<T>();
        result.data = object;
        result.code = 1;
        return result;
    }

    public static <T> Result<T> error(String msg) {
        Result result = new Result();
        result.msg = msg;
        result.code = 0;
        return result;
    }

}
2.2.3 Controller层

在sky-server模块中,com.sky.controller.admin.EmployeeController中添加分页查询方法。
在这里插入图片描述

	/**
     * 员工分页查询
     * @param employeePageQueryDTO
     * @return
     */
    @GetMapping("/page")
    @ApiOperation("员工分页查询")
    public Result<PageResult> page(EmployeePageQueryDTO employeePageQueryDTO){
        log.info("员工分页查询,参数为:{}", employeePageQueryDTO);
        PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO);//后续定义
        return Result.success(pageResult);
    }
2.2.4 Service层接口

在EmployeeService接口中声明pageQuery方法:

	/**
     * 分页查询
     * @param employeePageQueryDTO
     * @return
     */
    PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
2.2.5 Service层实现类(mybatis 分页插件)

在EmployeeServiceImpl中实现pageQuery方法:

    /**
     * 分页查询
     *
     * @param employeePageQueryDTO
     * @return
     */
    @Override
    public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
        // select * from employee limit 0,10
        //需要在查询功能之前开启分页功能:当前页的页码   每页显示的条数
        PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize());

        //这个方法有返回值为Page对象,里面保存的是分页之后的相关数据
        Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);//后续定义

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

        return new PageResult(total, records); //封装到PageResult中
    }

注意:此处使用 mybatis 的分页插件 PageHelper 来简化分页代码的开发。底层基于 mybatis 的拦截器实现

原理:它会把后面的一条sql进行一个动态的拼接,类似于mybatis的动态sql,他会动态的把这个limit关键字给我们拼进去,并且把这2个参数动态的去计算,因为我们已经把页码和每页的记录数传给它了,动态的去算之后拼接到sql语句中,最终实现了分页查询。

好处:这个字符串拼接不需要自己去做了,而是让这个插件去做。

故在pom.xml文中添加依赖(初始工程已添加)

在这里插入图片描述

<dependency>
   <groupId>com.github.pagehelper</groupId>
   <artifactId>pagehelper-spring-boot-starter</artifactId>
   <version>${pagehelper}</version>
</dependency>
2.2.6 Mapper层

在 EmployeeMapper 中声明 pageQuery 方法:

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

在 src/main/resources/mapper/EmployeeMapper.xml 中编写SQL:

  • name是员工的姓名,姓名作为条件我们用等值查询好像并不合适,应该使用模糊查询更合理一些,使用concat字符串函数动态拼接为字符串。
  • 不需要自己手动写Limit关键字了,因为使用PageHelper 这个插件可以动态的给我们去追加Limit之后的内容。

在这里插入图片描述

<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>

2.3 功能测试

可以通过接口文档进行测试,也可以进行前后端联调测试。

接下来使用两种方式分别测试:

2.3.1 接口文档测试(token失效,重新设置全局参数)

重启服务:访问http://localhost:8080/doc.html,进入员工分页查询

在这里插入图片描述

响应结果:返回401报错,JWT令牌在校验时出现了问题

在这里插入图片描述
明明之前已经配置过全局参数了为什么还是没有校验通过呢???
原因:在生成jwt令牌时设置了token的有效期为2个小时

在这里插入图片描述

解决:失效之后重新进行登录,生成一个新的令牌,之后把这个新的令牌复制到全局参数里面。

在这里插入图片描述
在这里插入图片描述

把原先的导航栏叉掉,点击一个新的员工分页查询窗口发送请求,可以看到此时正确响应结果

在这里插入图片描述

在这里插入图片描述

2.3.2 前后端联调测试

点击员工管理

在这里插入图片描述

输入员工姓名为zhangsan

在这里插入图片描述

不难发现,最后操作时间格式不清晰,在代码完善中解决。

在这里插入图片描述

2.4 代码完善(日期时间格式化返回)

问题描述:操作时间字段显示有问题。

在这里插入图片描述

解决方式:

1). 方式一

在属性上加上注解,对日期进行格式化

在这里插入图片描述

但这种方式,需要在每个时间属性上都要加上该注解,使用较麻烦,不能全局处理。

测试:

  • 实体类上一个加一个不加
    在这里插入图片描述
  • 重启服务,查看页面效果,添加注解的是格式化显示时间
    在这里插入图片描述

2). 方式二(推荐 )

在WebMvcConfiguration中扩展SpringMVC的消息转换器,统一对日期类型进行格式处理

在这里插入图片描述

    /**
     * 扩展Spring MVC框架的消息转化器
     *
     * 程序启动时会自动加载这个方法
     * @param converters
     */
    @Override
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        log.info("扩展消息转换器...");
        //创建一个消息转换器对象
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        //需要为消息转换器设置一个对象转换器,对象转换器可以将Java对象序列化为json数据
        converter.setObjectMapper(new JacksonObjectMapper());
        //创建出来的消息转换器并没有交给框架,框架也不会使用消息转换器,所以需要
        //   将自己的消息转化器converter加入容器converters中,converters存放的是整个
        //   SpringMVC框架所使用到的消息转换器,是一个集合。
        //converters.add(converter):容器中自带的有一些消息转换器,这样写相当于自己的消息转换器排在最后面,默认使用不到
        //converters.add(0,converter):优先使用自己的消息转换器,0表示索引,自己的消息转换器放在第一位 优先使用
        converters.add(0,converter);
    }

对象转换器类:JacksonObjectMapper,这个类不需要自己编写,只需要知道这个类起一个大概的作用即可。
在这里插入图片描述

package com.sky.json;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;

/**
 * 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
 * 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
 * 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
 */
public class JacksonObjectMapper extends ObjectMapper {

    public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
    //public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
    public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm";
    public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";

    public JacksonObjectMapper() {
        super();
        //收到未知属性时不报异常
        this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);

        //反序列化时,属性不存在的兼容处理
        this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

        SimpleModule simpleModule = new SimpleModule()
                .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
                .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));

        //注册功能模块 例如,可以添加自定义序列化器和反序列化器
        this.registerModule(simpleModule);
    }
}

添加后,再次测试

在这里插入图片描述

时间格式定义,sky-common模块中

package com.sky.json;

public class JacksonObjectMapper extends ObjectMapper {

	//.......
    public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm";
    //.......

    }
}

2.5 代码提交

在这里插入图片描述

后续步骤和新增员工代码提交一致,不再赘述。

3. 启用禁用员工账号(员工管理)

3.1 需求分析与设计

3.1.1 产品原型

在员工管理列表页面,可以对某个员工账号进行启用或者禁用操作。账号禁用的员工不能登录系统,启用后的员工可以正常登录。如果某个员工账号状态为正常,则按钮显示为 “禁用”,如果员工账号状态为已禁用,则按钮显示为"启用"。

启禁用员工原型:

在这里插入图片描述

业务规则:

  • 可以对状态为“启用” 的员工账号进行“禁用”操作
  • 可以对状态为“禁用”的员工账号进行“启用”操作
  • 状态为“禁用”的员工账号不能登录系统
3.1.2 接口设计

在这里插入图片描述
在这里插入图片描述

1). 路径参数携带状态值。

2). 同时,把id传递过去,明确对哪个用户进行操作。
只传一个路径参数不够,那你要对哪个账号进行状态的修改呢???所以需要把这个员工的id给提交过去。

3). 返回数据code状态是必须,其它是非必须。

3.2 代码开发

3.2.1 Controller层

在sky-server模块中,根据接口设计中的请求参数形式对应的在 EmployeeController 中创建启用禁用员工账号的方法:

在这里插入图片描述

    /**
     * 启用禁用员工账号
     * 返回值类型Result的泛型可加可不加,查询操作需要返回data数据 建议加上泛型
     *    对于非查询类的泛型就不需要了,因为最终只要给它返回一个code即可,data往往是空的
     * @param status   路径参数,RESTful风格接收
     * @param id    普通参数,普通方式接收
     * @return
     */
    @PostMapping("/status/{status}")
    @ApiOperation("启用禁用员工账号")
    public Result startOrStop(@PathVariable Integer status,Long id){
        log.info("启用禁用员工账号:{},{}",status,id);
        employeeService.startOrStop(status,id);//后绪步骤定义 (状态,员工的id)
        return Result.success();
    }
3.2.2 Service层接口

在 EmployeeService 接口中声明启用禁用员工账号的业务方法:

	/**
     * 启用禁用员工账号
     * @param status
     * @param id
     */
    void startOrStop(Integer status, Long id);
3.2.3 Service层实现类(更新写为动态sql,根据传递的参数更新多个字段)

在 EmployeeServiceImpl 中实现启用禁用员工账号的业务方法:

    /**
     * 启用禁用员工账号:本质上是根据员工的id修改员工表中的status状态字段
     *
     * @param status
     * @param id
     */
    @Override
    public void startOrStop(Integer status, Long id) {
        //update employee set status = ? where id = ?
        //为了修改的通用性,建议把这个update的语句写成是一个动态的,也就是说不单单是
        //   修改这个status,根据你传递过来的参数的不同可以修改多个字段,这样更新语句的sql通用性更好。
        //动态更新直接传递这2个参数不合适,传递的应该是一个实体类,通过builder方式构建(前面讲过)
        //   创建一个实体类对象,并且设置这2个属性值
        Employee employee = Employee.builder()
                .status(status)
                .id(id)
                .build();

        employeeMapper.update(employee);
    }
3.2.4 Mapper层

在 EmployeeMapper 接口中声明 update 方法:

	/**
     * 根据主键动态修改属性
     * @param employee
     */
    void update(Employee employee);

在 EmployeeMapper.xml 中编写SQL:

把可能修改的字段都列到这了,这样就编写了一个动态的sql,所以后期只要是修改类的操作 都可以调用这条sql。

在这里插入图片描述

<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.3 功能测试

3.3.1 接口文档测试

测试前,查询employee表中员工账号状态

在这里插入图片描述

开始测试

在这里插入图片描述

测试完毕后,再次查询员工账号状态

在这里插入图片描述

3.3.2 前后端联调测试

测试前:

在这里插入图片描述

点击启用:

在这里插入图片描述

3.4 代码提交

在这里插入图片描述

后续步骤和上述功能代码提交一致,不再赘述。

4. 编辑员工(员工管理)

4.1 需求分析与设计

4.1.1 产品原型

在员工管理列表页面点击 “编辑” 按钮,跳转到编辑页面,在编辑页面回显员工信息并进行修改,最后点击 “保存” 按钮完成编辑操作。

员工列表原型:

在这里插入图片描述

修改页面原型

注:点击修改时,数据应该正常回显到修改页面。

在这里插入图片描述

4.1.2 接口设计

根据上述原型图分析,编辑员工功能涉及到两个接口:

  • 根据id查询员工信息
  • 编辑员工信息

1). 根据id查询员工信息
在这里插入图片描述

在这里插入图片描述

2). 编辑员工信息

在这里插入图片描述
在这里插入图片描述

注:因为是修改功能,请求方式可设置为PUT。

4.2 代码开发

4.2.1 回显员工信息功能

1). Controller层

在 EmployeeController 中创建 getById 方法:

在这里插入图片描述

	/**
     * 根据id查询员工信息
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    @ApiOperation("根据id查询员工信息")
    public Result<Employee> getById(@PathVariable Long id){
        Employee employee = employeeService.getById(id);
        return Result.success(employee);
    }

2). Service层接口

在 EmployeeService 接口中声明 getById 方法:

    /**
     * 根据id查询员工
     * @param id
     * @return
     */
    Employee getById(Long id);

3). Service层实现类

在 EmployeeServiceImpl 中实现 getById 方法:

    /**
     * 根据id查询员工
     *
     * @param id
     * @return
     */
    @Override
    public Employee getById(Long id) {
        Employee employee = employeeMapper.getById(id);
        //查询出来的信息包含密码,这个密码也要传给前端,虽然这个后端的密码已经加密了,
        //    但是现在连这个加密的密码都不想让你看,查出来密码后给它修改为****穿给前端,
        //    进一步加强这个安全性。
        employee.setPassword("****");
        return employee;
    }

4). Mapper层

在 EmployeeMapper 接口中声明 getById 方法:

	/**
     * 根据id查询员工信息
     * @param id
     * @return
     */
    @Select("select * from employee where id = #{id}")
    Employee getById(Long id);
4.2.2 修改员工信息功能

1). Controller层

在 EmployeeController 中创建 update 方法:

在这里插入图片描述

	/**
     * 编辑员工信息
     * @param employeeDTO
     * @return
     */
    @PutMapping
    @ApiOperation("编辑员工信息")
    public Result update(@RequestBody EmployeeDTO employeeDTO){
        log.info("编辑员工信息:{}", employeeDTO);
        employeeService.update(employeeDTO);
        return Result.success();
    }

2). Service层接口

在 EmployeeService 接口中声明 update 方法:

    /**
     * 编辑员工信息
     * @param employeeDTO
     */
    void update(EmployeeDTO employeeDTO);

3). Service层实现类

在 EmployeeServiceImpl 中实现 update 方法:

    /**
     * 编辑员工信息
     *
     * @param employeeDTO
     */
    @Override
    public void update(EmployeeDTO employeeDTO) {
        //在上面写启用禁用功能时,已经写了一个通用的update方法,这里直接使用
        //   这个通用的update方法即可,这个方法需要的是Employee类型而这里
        //   传递的是EmployeeDTO类型,所以需要进行一个数据转换。
        Employee employee = new Employee();
        //employeeDTO的属性拷贝到employee对应的属性上
        BeanUtils.copyProperties(employeeDTO, employee); //对象属性拷贝 (新增员工时讲过)


        //设置employee剩余的属性
        employee.setUpdateTime(LocalDateTime.now());//更新时间
        employee.setUpdateUser(BaseContext.getCurrentId());//修改人的id(新增员工时讲过 通过ThreadLocal获得)

        employeeMapper.update(employee);
    }

在实现启用禁用员工账号功能时,已实现employeeMapper.update(employee),在此不需写Mapper层代码。

在这里插入图片描述

4.3 功能测试

4.3.1 接口文档测试

分别测试根据id查询员工信息编辑员工信息两个接口

1). 根据id查询员工信息

查询employee表中的数据,以id=4的记录为例

在这里插入图片描述

开始测试

在这里插入图片描述

获取到了id=4的相关员工信息

2). 编辑员工信息

修改id=4的员工信息,namezhangsan改为张三丰username张三改为zhangsanfeng

在这里插入图片描述

查看employee表数据

在这里插入图片描述

4.3.2 前后端联调测试

进入到员工列表查询

在这里插入图片描述

对员工姓名为杰克的员工数据修改,点击修改,数据已回显

在这里插入图片描述

修改后,点击保存

在这里插入图片描述

4.4 代码提交

在这里插入图片描述

后续步骤和上述功能代码提交一致,不再赘述。

5. 导入分类模块功能代码(分类管理)

说明从技术层面来说,这个分类管理模块跟这个员工管理模块是非常类似的,无非是操作的表不一样,实际上都属于单表的增删改查,技术难度也是非常类似的,所以最后这个分类管理直接提供现成的代码,那它导入到我们的项目中直接使用就行了。

5.1 需求分析与设计

5.1.1 产品原型

后台系统中可以管理分类信息,分类包括两种类型,分别是 菜品分类套餐分类

先来分析菜品分类相关功能。

新增菜品分类:当我们在后台系统中添加菜品时需要选择一个菜品分类,在移动端也会按照菜品分类来展示对应的菜品。

菜品分类分页查询:系统中的分类很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。

根据id删除菜品分类:在分类管理列表页面,可以对某个分类进行删除操作。需要注意的是当分类关联了菜品或者套餐时,此分类不允许删除。

修改菜品分类:在分类管理列表页面点击修改按钮,弹出修改窗口,在修改窗口回显分类信息并进行修改,最后点击确定按钮完成修改操作。

启用禁用菜品分类:在分类管理列表页面,可以对某个分类进行启用或者禁用操作。

分类类型查询:当点击分类类型下拉框时,从数据库中查询所有的菜品分类数据进行展示。

分类管理原型:

在这里插入图片描述

业务规则:

  • 分类名称必须是唯一的
  • 分类按照类型可以分为菜品分类和套餐分类
  • 新添加的分类状态默认为“禁用”
    • 为什么是禁用而不是启用呢???
    • eg:新添加的一个分类,如果这个分类默认是启用状态 ,它就会展示在我们的移动端,因为当前是一个新的分类 这个分类下面肯定是没有菜品的,所以此时只是在移动端展示一个分类的话是没有任何意义的。所以设置新添加的分类默认是禁用状态,禁用状态其实是不会展示在我们这个移动端,防止了这种情况的出现。
5.1.2 接口设计

根据上述原型图分析,菜品分类模块共涉及6个接口。

  • 新增分类
  • 分类分页查询
  • 根据id删除分类
  • 修改分类
  • 启用禁用分类
  • 根据类型查询分类

接下来,详细地分析每个接口。

找到资料–>项目接口文档–>苍穹外卖-管理端接口.html

1). 新增分类

在这里插入图片描述
在这里插入图片描述

2). 分类分页查询

在这里插入图片描述
在这里插入图片描述

3). 根据id删除分类

在这里插入图片描述

4). 修改分类

在这里插入图片描述
在这里插入图片描述

5). 启用禁用分类

在这里插入图片描述
在这里插入图片描述

6). 根据类型查询分类

在这里插入图片描述
在这里插入图片描述

5.1.3 表设计

category表结构:

字段名数据类型说明备注
idbigint主键自增
namevarchar(32)分类名称唯一
typeint分类类型1菜品分类 2套餐分类
sortint排序字段用于分类数据的排序
statusint状态1启用 0禁用
create_timedatetime创建时间
update_timedatetime最后修改时间
create_userbigint创建人id
update_userbigint最后修改人id

5.2 代码导入(删除分类前提,分类下面没有数据)

导入资料中的分类管理模块功能代码即可

在这里插入图片描述

可按照mapper–>service–>controller依次导入,这样代码不会显示相应的报错。

进入到sky-server模块中

5.2.1 Mapper层

在这里插入图片描述

  • 问题:当前这个模块不是我们的分类模块吗,除了分类的Mapper为什么还有菜品的菜品的Mapper、套餐的Mapper呢???

  • 因为:这个地方有一个业务的限定,要删除分类的时候并不是直接删除,而是需要判断一下这个分类下面有没有挂菜品、套餐,也就是说我们需要去查询菜单表、套餐表,看看这个菜品还有套餐是不是有属于当前这个分类的,所以这个地方会使用到菜品和套餐的mapper。

DishMapper.java:菜品的Mapper

package com.sky.mapper;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface DishMapper {

    /**
     * 根据分类id查询菜品数量
     * @param categoryId
     * @return
     */
    @Select("select count(id) from dish where category_id = #{categoryId}")
    Integer countByCategoryId(Long categoryId);

}

SetmealMapper.java:套餐的Mapper

package com.sky.mapper;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface SetmealMapper {

    /**
     * 根据分类id查询套餐的数量
     * @param id
     * @return
     */
    @Select("select count(id) from setmeal where category_id = #{categoryId}")
    Integer countByCategoryId(Long id);

}

CategoryMapper.java:分类的Mapper

package com.sky.mapper;

import com.github.pagehelper.Page;
import com.sky.dto.CategoryPageQueryDTO;
import com.sky.entity.Category;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface 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})")
    void insert(Category category);

    /**
     * 分页查询
     * @param categoryPageQueryDTO
     * @return
     */
    Page<Category> pageQuery(CategoryPageQueryDTO categoryPageQueryDTO);

    /**
     * 根据id删除分类
     * @param id
     */
    @Delete("delete from category where id = #{id}")
    void deleteById(Long id);

    /**
     * 根据id修改分类
     * @param category
     */
    void update(Category category);

    /**
     * 根据类型查询分类
     * @param type
     * @return
     */
    List<Category> list(Integer type);
}

CategoryMapper.xml,进入到resources/mapper目录下
在这里插入图片描述

<?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.CategoryMapper">

    <select id="pageQuery" resultType="com.sky.entity.Category">
        select * from category
        <where>
            <if test="name != null and name != ''">
                and name like concat('%',#{name},'%')
            </if>
            <if test="type != null">
                and type = #{type}
            </if>
        </where>
        order by sort asc , create_time desc
    </select>

    <update id="update" parameterType="Category">
        update category
        <set>
            <if test="type != null">
                type = #{type},
            </if>
            <if test="name != null">
                name = #{name},
            </if>
            <if test="sort != null">
                sort = #{sort},
            </if>
            <if test="status != null">
                status = #{status},
            </if>
            <if test="updateTime != null">
                update_time = #{updateTime},
            </if>
            <if test="updateUser != null">
                update_user = #{updateUser}
            </if>
        </set>
        where id = #{id}
    </update>

    <select id="list" resultType="Category">
        select * from category
        where status = 1
        <if test="type != null">
            and type = #{type}
        </if>
        order by sort asc,create_time desc
    </select>
</mapper>

5.2.2 Service层

在这里插入图片描述

CategoryService.java

package com.sky.service;

import com.sky.dto.CategoryDTO;
import com.sky.dto.CategoryPageQueryDTO;
import com.sky.entity.Category;
import com.sky.result.PageResult;
import java.util.List;

public interface CategoryService {

    /**
     * 新增分类
     * @param categoryDTO
     */
    void save(CategoryDTO categoryDTO);

    /**
     * 分页查询
     * @param categoryPageQueryDTO
     * @return
     */
    PageResult pageQuery(CategoryPageQueryDTO categoryPageQueryDTO);

    /**
     * 根据id删除分类
     * @param id
     */
    void deleteById(Long id);

    /**
     * 修改分类
     * @param categoryDTO
     */
    void update(CategoryDTO categoryDTO);

    /**
     * 启用、禁用分类
     * @param status
     * @param id
     */
    void startOrStop(Integer status, Long id);

    /**
     * 根据类型查询分类
     * @param type
     * @return
     */
    List<Category> list(Integer type);
}

CategoryServiceImpl.java

package com.sky.service.impl;

import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.sky.constant.MessageConstant;
import com.sky.constant.StatusConstant;
import com.sky.context.BaseContext;
import com.sky.dto.CategoryDTO;
import com.sky.dto.CategoryPageQueryDTO;
import com.sky.entity.Category;
import com.sky.exception.DeletionNotAllowedException;
import com.sky.mapper.CategoryMapper;
import com.sky.mapper.DishMapper;
import com.sky.mapper.SetmealMapper;
import com.sky.result.PageResult;
import com.sky.service.CategoryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;

/**
 * 分类业务层
 */
@Service
@Slf4j
public class CategoryServiceImpl implements CategoryService {

    @Autowired
    private CategoryMapper categoryMapper;
    @Autowired
    private DishMapper dishMapper;
    @Autowired
    private SetmealMapper setmealMapper;

    /**
     * 新增分类
     * @param categoryDTO
     */
    @Override
    public void save(CategoryDTO categoryDTO) {
        Category category = new Category();
        //属性拷贝
        BeanUtils.copyProperties(categoryDTO, category);

        //分类状态默认为禁用状态0
        category.setStatus(StatusConstant.DISABLE);

        //设置创建时间、修改时间、创建人、修改人
        category.setCreateTime(LocalDateTime.now());
        category.setUpdateTime(LocalDateTime.now());
        category.setCreateUser(BaseContext.getCurrentId());
        category.setUpdateUser(BaseContext.getCurrentId());

        categoryMapper.insert(category);
    }

    /**
     * 分页查询
     * @param categoryPageQueryDTO
     * @return
     */
    @Override
    public PageResult pageQuery(CategoryPageQueryDTO categoryPageQueryDTO) {
        PageHelper.startPage(categoryPageQueryDTO.getPage(),categoryPageQueryDTO.getPageSize());
        //下一条sql进行分页,自动加入limit关键字分页
        Page<Category> page = categoryMapper.pageQuery(categoryPageQueryDTO);
        return new PageResult(page.getTotal(), page.getResult());
    }

    /**
     * 根据id删除分类
     *
     * 思路:
     *      分类的删除是根据传入的参数分类id进行删的
     *      首先根据这个分类id查询菜品表,如果查询的结果有记录数,说明当前分类下面有菜品,所以不能直接删除
     *      其次根据这个分类id查询套餐表,如果查询的结果有记录数,说明当前分类下面有套餐,所以不能直接删除
     *      最终分类下面没有数据了,此时就可以删除分类了。
     * @param id
     */
    @Override
    public void deleteById(Long id) {
        //查询当前分类是否关联了菜品,如果关联了就抛出业务异常
        Integer count = dishMapper.countByCategoryId(id);
        if(count > 0){
            //当前分类下有菜品,不能删除
            throw new DeletionNotAllowedException(MessageConstant.CATEGORY_BE_RELATED_BY_DISH);
        }

        //查询当前分类是否关联了套餐,如果关联了就抛出业务异常
        count = setmealMapper.countByCategoryId(id);
        if(count > 0){
            //当前分类下有菜品,不能删除
            throw new DeletionNotAllowedException(MessageConstant.CATEGORY_BE_RELATED_BY_SETMEAL);
        }

        //删除分类数据
        categoryMapper.deleteById(id);
    }

    /**
     * 修改分类
     * @param categoryDTO
     */
    @Override
    public void update(CategoryDTO categoryDTO) {
        Category category = new Category();
        BeanUtils.copyProperties(categoryDTO,category);

        //设置修改时间、修改人
        category.setUpdateTime(LocalDateTime.now());
        category.setUpdateUser(BaseContext.getCurrentId());

        categoryMapper.update(category);
    }

    /**
     * 启用、禁用分类
     * @param status
     * @param id
     */
    @Override
    public void startOrStop(Integer status, Long id) {
        Category category = Category.builder()
                .id(id)
                .status(status)
                .updateTime(LocalDateTime.now())
                .updateUser(BaseContext.getCurrentId())
                .build();
        categoryMapper.update(category);
    }

    /**
     * 根据类型查询分类
     * @param type
     * @return
     */
    @Override
    public List<Category> list(Integer type) {
        return categoryMapper.list(type);
    }
}

5.2.3 Controller层

在这里插入图片描述

CategoryController.java

package com.sky.controller.admin;

import com.sky.dto.CategoryDTO;
import com.sky.dto.CategoryPageQueryDTO;
import com.sky.entity.Category;
import com.sky.result.PageResult;
import com.sky.result.Result;
import com.sky.service.CategoryService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;

/**
 * 分类管理
 */
@RestController
@RequestMapping("/admin/category")
@Api(tags = "分类相关接口")
@Slf4j
public class CategoryController {

    @Autowired
    private CategoryService categoryService;

    /**
     * 新增分类
     * @param categoryDTO
     * @return
     */
    @PostMapping
    @ApiOperation("新增分类")
    public Result<String> save(@RequestBody CategoryDTO categoryDTO){
        log.info("新增分类:{}", categoryDTO);
        categoryService.save(categoryDTO);
        return Result.success();
    }

    /**
     * 分类分页查询
     * @param categoryPageQueryDTO
     * @return
     */
    @GetMapping("/page")
    @ApiOperation("分类分页查询")
    public Result<PageResult> page(CategoryPageQueryDTO categoryPageQueryDTO){
        log.info("分页查询:{}", categoryPageQueryDTO);
        PageResult pageResult = categoryService.pageQuery(categoryPageQueryDTO);
        return Result.success(pageResult);
    }

    /**
     * 删除分类
     * @param id
     * @return
     */
    @DeleteMapping
    @ApiOperation("删除分类")
    public Result<String> deleteById(Long id){
        log.info("删除分类:{}", id);
        categoryService.deleteById(id);
        return Result.success();
    }

    /**
     * 修改分类
     * @param categoryDTO
     * @return
     */
    @PutMapping
    @ApiOperation("修改分类")
    public Result<String> update(@RequestBody CategoryDTO categoryDTO){
        categoryService.update(categoryDTO);
        return Result.success();
    }

    /**
     * 启用、禁用分类
     * @param status
     * @param id
     * @return
     */
    @PostMapping("/status/{status}")
    @ApiOperation("启用禁用分类")
    public Result<String> startOrStop(@PathVariable("status") Integer status, Long id){
        categoryService.startOrStop(status,id);
        return Result.success();
    }

    /**
     * 根据类型查询分类
     * @param type
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("根据类型查询分类")
    public Result<List<Category>> list(Integer type){
        List<Category> list = categoryService.list(type);
        return Result.success(list);
    }
}

全部导入完毕后,进行编译

在这里插入图片描述

5.3 功能测试

重启服务,访问http://localhost:80,进入分类管理

分页查询:

在这里插入图片描述

分类类型:

在这里插入图片描述

启用禁用:

在这里插入图片描述

点击禁用

在这里插入图片描述

修改:

回显

在这里插入图片描述

修改后

在这里插入图片描述

新增:

在这里插入图片描述

点击确定,查询列表

在这里插入图片描述

删除:

在这里插入图片描述

删除后,查询分类列表

在这里插入图片描述

删除成功

5.4 代码提交

在这里插入图片描述

后续步骤和上述功能代码提交一致,不再赘述。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐