小柒味来 - 员工管理与分类管理功能实现

  1. 员工与分类模块功能实现笔记
    1. 功能效果展示
      1. 员工管理界面效果
      2. 分类管理界面效果
    2. 一、新增员工
      1. 1.1 需求分析
        1. 1.1.1 产品原型
        2. 1.1.2 接口设计
        3. 1.1.3 表结构设计
      2. 1.2 功能实现
        1. 1.2.1 设计 DTO
        2. 1.2.2 Controller 层
        3. 1.2.3 返回封装使用统一响应类 Result<T>
        4. 1.2.4 Service 层
        5. 1.2.5 Service 实现类
        6. 1.2.6 Mapper 层
      3. 1.3 接口测试
    3. 二、全局异常捕获
      1. 2.1 异常处理逻辑实现
      2. 2.2 常量定义
      3. 2.3 异常测试
    4. 三、JWT令牌
      1. 什么是 ThreadLocal?
  2. 四、 分页查询
    1. 4.1 需求分析
      1. 4.1.1 产品原型
      2. 4.1.2 接口设计
    2. 4.2 代码实现
      1. 4.2.1 设计 DTO
      2. 4.2.2 分页结果封装类 PageResult
      3. 4.2.4 Controller 层
      4. 4.2.5 Service 层
      5. 4.2.6 Service 实现类
      6. 4.2.7 Mapper 层
    3. 4.3 功能测试
      1. 4.3.1 接口文档测试
      2. 4.3.2 前后端联调演示
  3. 五、日期格式化处理
    1. 5.1 方式一:注解式格式化(不推荐)
      1. 缺点:
    2. 5.2 方式二(推荐):全局配置统一格式
      1. 5.2.1 扩展消息转换器
      2. 5.2.2 定义 JacksonObjectMapper
      3. 5.2.3 测试
  4. 六、员工账号启用与禁用
    1. 6.1 需求分析
      1. 6.1.1 产品原型
      2. 6.1.2 接口设计
    2. 6.2 功能实现
      1. 6.2.1 Controller 层
      2. 6.2.2 Service 层
      3. 6.2.3 Service 实现类
      4. 6.2.4 Mapper 层
    3. 6.3 联调测试
  5. 七、编辑员工功能
    1. 7.1 需求分析
      1. 7.1.1 产品原型
      2. 7.1.2 接口设计
    2. 7.2 功能实现
      1. 7.2.1 根据 ID 查询员工信息
      2. 7.2.2 修改员工信息
    3. 7.3 联调测试
  6. 八、分类模块
    1. 8.1 需求分析
      1. 8.1.1 产品原型
      2. 8.1.2 接口设计
        1. 1)新增分类
        2. 2)分页查询分类
        3. 3)删除分类
        4. 4)修改分类
        5. 5)启用/禁用分类
        6. 6)根据类型查询分类
    2. 8.2 功能实现
      1. 8.2.1 Mapper 层
      2. 8.2.2 Service 层
      3. 8.2.3 Service 实现类
      4. 8.2.4 Controller 层

员工与分类模块功能实现笔记

本章节起,原型设计与需求分析将合并编写,建议结合网站侧边栏定位功能阅读,以获得更佳阅读体验。

本模块包含两个主要功能:

  • 员工管理(CRUD)
  • 菜品分类管理

功能效果展示

员工管理界面效果

分类管理界面效果


一、新增员工

1.1 需求分析

1.1.1 产品原型

后台系统支持管理员工信息,点击“新增员工”按钮,填写表单后提交,即可将员工信息存入数据库。

新增员工界面原型:

实现要点:

  • 账号唯一
  • 手机号为合法的11位数字
  • 身份证号为合法的18位
  • 默认密码为 123456

1.1.2 接口设计

接口地址:/admin/employee

请求方式:POST

请求数据类型:application/json

请求示例:

{
  "id": 0,
  "idNumber": "",
  "name": "",
  "phone": "",
  "sex": "",
  "username": ""
}

请求参数:

请求参数:

参数名称 参数说明 请求类型 是否必须 数据类型 schema
employeeDTO employeeDTO body true EmployeeDTO EmployeeDTO
id false integer(int64)
idNumber false string
name false string
phone false string
sex false string
username false string

响应参数:

参数名称 参数说明 类型 schema
code integer(int32) integer(int32)
data object
msg string

响应示例:

{
    "code": 0,
    "data": {},
    "msg": ""
}

1.1.3 表结构设计

员工信息表 employee

字段名 类型 描述 备注
id bigint 主键 自增
name varchar(32) 姓名
username varchar(32) 用户名 唯一
password varchar(64) 密码
phone varchar(11) 手机号
sex varchar(2) 性别
id_number varchar(18) 身份证号
status int 账号状态 1 正常,0 锁定
create_time datetime 创建时间
update_time datetime 最后修改时间
create_user bigint 创建人 ID
update_user bigint 修改人 ID

1.2 功能实现

1.2.1 设计 DTO

由于表单参数与实体类字段差异较大,使用 EmployeeDTO 作为数据传输对象。

com.qi.dto.EmployeeDTO

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

1.2.2 Controller 层

EmployeeController 中添加新增员工接口:

@PostMapping
    @ApiOperation("新增员工")
    public Result save(@RequestBody EmployeeDTO employeeDTO){
        log.info("新增员工:{}", employeeDTO);
        employeeService.save(employeeDTO);
        return Result.success();
    }

1.2.3 返回封装使用统一响应类 Result<T>

@Data
public class Result<T> implements Serializable {
    private Integer code;
    private String msg;
    private T data;
    public static <T> Result<T> success() {
        Result<T> result = new Result<>();
        result.code = 1;
        return result;
    }
    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
        result.code = 1;
        result.data = data;
        return result;
    }
    public static <T> Result<T> error(String msg) {
        Result<T> result = new Result<>();
        result.code = 0;
        result.msg = msg;
        return result;
    }
}

1.2.4 Service 层

EmployeeService 中声明新增方法:

void save(EmployeeDTO employeeDTO);

1.2.5 Service 实现类

EmployeeServiceImpl 中实现 save 方法:

@Override
    public void save(EmployeeDTO employeeDTO) {
        Employee employee = new Employee();
        BeanUtils.copyProperties(employeeDTO, employee);

        //密码加密
        String password = DigestUtils.md5DigestAsHex(DEFAULT_PASSWORD.getBytes());
        employee.setPassword(password);

        employee.setStatus(ENABLE);

        employee.setCreateTime(LocalDateTime.now());
        employee.setUpdateTime(LocalDateTime.now());

        //记录操作人与更新人id
        // 记录操作人功能
        employee.setCreateUser(BaseContext.getCurrentId());
        employee.setUpdateUser(BaseContext.getCurrentId());
        BaseContext.removeCurrentId();
        employeeMapper.insert(employee);
    }

状态常量定义于 StatusConstant.java

public class StatusConstant {
    public static final Integer ENABLE = 1;
    public static final Integer DISABLE = 0;
}

1.2.6 Mapper 层

EmployeeMapper 中定义插入方法:

@Insert("insert into employee values (default,#{name},#{username},#{password},#{phone},#{sex},#{idNumber},#{createTime},#{updateTime},#{createUser},#{updateUser},#{status})")
void insert(Employee employee);

需在 application.yml 中开启驼峰命名映射:

mybatis:
  configuration:
    map-underscore-to-camel-case: true

1.3 接口测试

提示“新增失败”,因为未传入 Token:

使用员工登录接口获取合法 Token:

将 JWT 添加到全局请求头中:


二、全局异常捕获

当尝试新增 username=zhumama7 的员工,而数据库中已存在该用户名时,系统将抛出如下异常:

我们这里先完善全局异常捕获功能

2.1 异常处理逻辑实现

进入 qi-server 模块的 com.qi.handler 包,编辑 GlobalExceptionHandler.java

/*
     * 捕获SQL异常
     * */
    @ExceptionHandler
    public Result exceptionHandler(SQLIntegrityConstraintViolationException ex) {
        String message = ex.getMessage();
        log.error("异常信息:{}", message);
        if (message.contains("Duplicate entry")) {
            String[] split = message.split(" ");
            String msg = split[2] + MessageConstant.ALREADY_EXISTS;
            return Result.error(msg);
        } else {
            return Result.error(MessageConstant.UNKNOWN_ERROR);
        }
    }

2.2 常量定义

进入 qi-common 模块,在 MessageConstant.java 添加常量:

public class MessageConstant {
    public static final String ALREADY_EXISTS = "已存在";
    public static final String UNKNOWN_ERROR = "发生未知错误";
}

2.3 异常测试

再次提交用户名已存在的数据:


三、JWT令牌

员工登录成功后,系统会生成包含用户 ID 的 JWT 令牌,并将其返回给前端。
为了获取当前登录用户的 ID,所以我们要完成登录功能中完成jwt令牌的传递

/**
     * 登录
     *
     * @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:

/**
 * 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);
            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;
        }
    }
}

什么是 ThreadLocal?

在新增员工时,需要设置该记录的:

  • createUser:创建人 ID
  • updateUser:修改人 ID

而请求对象只在 Controller 层可访问,如果要在 Service 层使用,就必须借助中间机制传递。

那么就使用到了ThreadLocalThreadLocal 不是线程,而是线程的“局部变量存储工具”。它为每个线程提供了一个独立的变量副本,实现了线程隔离。

常用方法:

// 设置值
ThreadLocal<T>.set(T value)
// 获取值
ThreadLocal<T>.get()
// 移除值(防止内存泄漏)
ThreadLocal<T>.remove()

ThreadLocal流程图:

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

qi-common模块

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,并放入线程局部变量中:

@Component
@Slf4j
public class JwtTokenAdminInterceptor implements HandlerInterceptor {

    @Autowired
    private JwtProperties jwtProperties;

    /**
     * 校验jwt
     */
    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中获取线程局部变量中的值:

public void save(EmployeeDTO employeeDTO) {
        //记录操作人与更新人id
        employee.setCreateUser(BaseContext.getCurrentId());
        employee.setUpdateUser(BaseContext.getCurrentId());

        employeeMapper.insert(employee);
    }

四、 分页查询

4.1 需求分析

4.1.1 产品原型

当系统中员工数量较多时,如果在一个页面中全部展示会导致页面混乱、加载缓慢,不便于用户操作。因此,采用分页展示是更合理的做法。本系统在分页查询的基础上,支持按“员工姓名”进行模糊查询。

功能设计要求如下:

  • 按页码分页展示员工信息;
  • 每页显示 10 条记录;
  • 支持根据员工姓名进行模糊查询。

4.1.2 接口设计

接口地址:/admin/employee/page

请求方式:GET

请求数据类型:application/x-www-form-urlencoded

请求参数:

参数名称 参数说明 请求类型 是否必须 数据类型 schema
name query false string
page query false integer(int32)
pageSize query false integer(int32)

响应参数:

参数名称 参数说明 类型 schema
code integer(int32) integer(int32)
data PageResult PageResult
records array object
total integer(int64)
msg string

响应示例:

{
    "code": 0,
    "data": {
        "records": [],
        "total": 0
    },
    "msg": ""
}

4.2 代码实现

4.2.1 设计 DTO

qi-pojo 模块中定义 EmployeePageQueryDTO

@Data
public class EmployeePageQueryDTO implements Serializable {
    private String name;  // 员工姓名
    private int page;     // 页码
    private int pageSize; // 每页记录数
}

4.2.2 分页结果封装类 PageResult

分页返回结构统一封装为 PageResult,定义于 sky-common 模块:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult implements Serializable {
    private long total;       // 总记录数
    private List records;     // 当前页数据
}

4.2.4 Controller 层

@GetMapping("/page")
@ApiOperation("员工分页查询")
public Result<PageResult> page(EmployeePageQueryDTO employeePageQueryDTO){
    log.info("员工分页查询,参数为:{}", employeePageQueryDTO);
    PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO);
    return Result.success(pageResult);
}

4.2.5 Service 层

PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO);

4.2.6 Service 实现类

@Override
public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
    // 开始分页
    PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize());

    Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);

    long total = page.getTotal();
    List<Employee> records = page.getResult();

    return new PageResult(total, records);
}

⚠️ 注意:分页功能依赖 MyBatis 插件 PageHelper,其底层基于拦截器机制实现。

Maven 依赖(已在初始工程中添加):

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

4.2.7 Mapper 层

Page<Employee> pageQuery(EmployeePageQueryDTO employeePageQueryDTO);

SQL映射:EmployeeMapper.xml

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

4.3 功能测试

4.3.1 接口文档测试

4.3.2 前后端联调演示

五、日期格式化处理

在接口开发中,后端返回的时间字段通常会以时间戳格式展示,为了美化时间格式,我们需要对返回的时间字段进行统一格式化处理。


5.1 方式一:注解式格式化(不推荐)

通过在时间字段上添加注解 @JsonFormat 实现格式化:

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;

缺点:

  • 每个时间字段都需要单独添加注解;
  • 代码重复,维护成本高;
  • 无法统一全局生效。

5.2 方式二(推荐):全局配置统一格式

通过扩展 Spring MVC 的消息转换器,可以实现对所有时间字段的统一格式化处理,在配置类中进行统一设置。

5.2.1 扩展消息转换器

WebMvcConfiguration 类中重写 extendMessageConverters 方法,设置自定义的对象映射器 JacksonObjectMapper

/**
 * 扩展 Spring MVC 框架的消息转换器
 */
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    log.info("扩展消息转换器...");
    // 创建消息转换器
    MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
    
    // 设置对象转换器(Java对象转JSON)
    converter.setObjectMapper(new JacksonObjectMapper());
    // 优先使用自定义的消息转换器
    converters.add(0, converter);
}

5.2.2 定义 JacksonObjectMapper

qi-common 模块中定义 JacksonObjectMapper 类,实现时间字段的统一格式设置:

/**
 * 对象映射器:基于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);
    }
}

5.2.3 测试

原效果:

统一格式化配置完成后,调用接口返回的时间字段格式将变为:

这样就无需对每个字段添加注解,格式清晰统一,便于前端展示


记得多提交git哦,有助于后续复盘和备份

六、员工账号启用与禁用

6.1 需求分析

在员工管理页面中,可以对员工账号进行启用或禁用操作:

  • 账号禁用:无法登录系统;
  • 账号启用:可以正常登录;

业务规则:

  • 仅允许对“启用”状态的员工账号执行“禁用”操作;
  • 仅允许对“禁用”状态的员工账号执行“启用”操作;
  • 被禁用的账号不能登录系统;
  • 本质上是一次更新操作

6.1.1 产品原型

6.1.2 接口设计

接口地址:/admin/employee/status/{status}

请求方式:POST

请求数据类型:application/json

请求参数:

参数名称 参数说明 请求类型 是否必须 数据类型 schema
status status path true integer(int32)
id id query false integer(int64)

响应参数:

参数名称 参数说明 类型 schema
code integer(int32) integer(int32)
data object
msg string

响应示例:

{
    "code": 0,
    "data": {},
    "msg": ""
}

6.2 功能实现

6.2.1 Controller 层

@PostMapping("/status/{status}")
    @ApiOperation("修改员工状态")
    public Result startOrStop(@PathVariable Integer status, Long id){
        log.info("启用禁用员工参数:status:{},id:{}", status, id);
        employeeService.alterStatus(status,id);
        return Result.success();
    }

6.2.2 Service 层

void alterStatus(Integer status, Long id);

6.2.3 Service 实现类

@Override
    public void alterStatus(Integer status, Long id) {
        
        Employee employee = new Employee();
        employee.setStatus(status);
        employee.setId(id);
    }

6.2.4 Mapper 层

void update(Employee employee);
<update id="update">
    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>

6.3 联调测试

  • 启用/禁用员工账号的交互流程演示:


七、编辑员工功能

7.1 需求分析

在员工列表页面点击“编辑”按钮,可跳转至员工信息编辑页。页面将回显原始员工数据,用户修改后点击“保存”即可完成编辑。

7.1.1 产品原型

7.1.2 接口设计

接口地址:/admin/employee

请求方式:PUT

请求数据类型:application/json

请求示例:

{
  "id": 0,
  "idNumber": "",
  "name": "",
  "phone": "",
  "sex": "",
  "username": ""
}

请求参数:

参数名称 参数说明 请求类型 是否必须 数据类型 schema
employeeDTO employeeDTO body true EmployeeDTO EmployeeDTO
id false integer(int64)
idNumber false string
name false string
phone false string
sex false string
username false string

响应参数:

参数名称 参数说明 类型 schema
code integer(int32) integer(int32)
data object
msg string

响应示例:

{
    "code": 0,
    "data": {},
    "msg": ""
}

7.2 功能实现

该功能分为两个步骤:

  1. 根据员工 ID 查询信息;
  2. 修改并提交员工信息。

7.2.1 根据 ID 查询员工信息

1)Controller 层

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

2)Service 层

Employee getById(Long id);

3)Service 实现类

@Override
    public Employee getById(Long id) {
        Employee employee = employeeMapper.getById(id);
        return employee;
    }

4)Mapper 层

@Select("select * from employee where id = #{id}")
Employee getById(Long id);

7.2.2 修改员工信息

1)Controller 层

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

2)Service 层

void update(EmployeeDTO employeeDTO);

3)Service 实现类

@Override
public void update(EmployeeDTO employeeDTO) {
    Employee employee = new Employee();
    BeanUtils.copyProperties(employeeDTO, employee);

    employee.setUpdateTime(LocalDateTime.now());
    employee.setUpdateUser(BaseContext.getCurrentId());

    employeeMapper.update(employee);
}

注:之前已实现 employeeMapper.update(employee) 方法,直接复用


7.3 联调测试

查询回写


八、分类模块

8.1 需求分析

后台管理系统中支持对分类信息进行管理,主要分为两类:

  • 菜品分类
  • 套餐分类

主要功能如下:

  • 新增菜品分类:在新增菜品时需选择所属分类,移动端展示也基于分类进行。
  • 分页查询:当分类数量较多时,采用分页方式展示,提高可读性与管理效率。
  • 删除分类:支持根据 ID 删除分类。但若分类已被菜品或套餐引用,则不允许删除。
  • 修改分类:可在管理列表中点击“修改”,弹窗回显分类信息并编辑。
  • 启用/禁用分类:通过按钮切换分类状态。
  • 根据类型查询分类:支持通过下拉选择菜品分类或套餐分类,从数据库动态加载数据。

8.1.1 产品原型

业务规则:

  • 分类名称必须唯一
  • 分类类型包括:1 表示菜品分类,2 表示套餐分类
  • 新增分类的状态默认为“禁用”

8.1.2 接口设计

分类模块涉及 6 个接口:

功能 接口说明
新增分类 新增一条分类记录
分类分页查询 支持按名称和类型分页查询
根据 ID 删除分类 删除前需判断是否关联数据
修改分类 支持根据 ID 修改分类
启用/禁用分类 切换分类状态
类型分类查询 查询指定类型下的所有分类
1)新增分类

接口地址:/admin/dish

请求方式:POST

请求数据类型:application/json

请求示例:

{
  "categoryId": 0,
  "description": "",
  "flavors": [
    {
      "dishId": 0,
      "id": 0,
      "name": "",
      "value": ""
    }
  ],
  "id": 0,
  "image": "",
  "name": "",
  "price": 0,
  "status": 0
}

请求参数:

参数名称 参数说明 请求类型 是否必须 数据类型 schema
dishDTO dishDTO body true DishDTO DishDTO
categoryId false integer(int64)
description false string
flavors false array DishFlavor
dishId false integer
id false integer
name false string
value false string
id false integer(int64)
image false string
name false string
price false number
status false integer(int32)

响应参数:

参数名称 参数说明 类型 schema
code integer(int32) integer(int32)
data object
msg string

响应示例:

{
    "code": 0,
    "data": {},
    "msg": ""
}
2)分页查询分类

接口地址:/admin/dish/page

请求方式:GET

请求数据类型:application/x-www-form-urlencoded

请求参数:

参数名称 参数说明 请求类型 是否必须 数据类型 schema
categoryId query false integer(int32)
name query false string
page query false integer(int32)
pageSize query false integer(int32)
status query false integer(int32)

响应状态:

状态码 说明 schema
200 OK Result«PageResult»
401 Unauthorized
403 Forbidden
404 Not Found

响应参数:

参数名称 参数说明 类型 schema
code integer(int32) integer(int32)
data PageResult PageResult
records array object
total integer(int64)
msg string

响应示例:

{
    "code": 0,
    "data": {
        "records": [],
        "total": 0
    },
    "msg": ""
}
3)删除分类

接口地址:/admin/dish

请求方式:DELETE

请求数据类型:application/x-www-form-urlencoded

请求参数:

参数名称 参数说明 请求类型 是否必须 数据类型 schema
ids ids query true array integer

响应状态:

状态码 说明 schema
200 OK Result
204 No Content
401 Unauthorized
403 Forbidden

响应参数:

参数名称 参数说明 类型 schema
code integer(int32) integer(int32)
data object
msg string

响应示例:

{
    "code": 0,
    "data": {},
    "msg": ""
}
4)修改分类

接口地址:/admin/dish

请求方式:PUT

请求数据类型:application/json

请求示例:

{
  "categoryId": 0,
  "description": "",
  "flavors": [
    {
      "dishId": 0,
      "id": 0,
      "name": "",
      "value": ""
    }
  ],
  "id": 0,
  "image": "",
  "name": "",
  "price": 0,
  "status": 0
}

请求参数:

参数名称 参数说明 请求类型 是否必须 数据类型 schema
dishDTO dishDTO body true DishDTO DishDTO
categoryId false integer(int64)
description false string
flavors false array DishFlavor
dishId false integer
id false integer
name false string
value false string
id false integer(int64)
image false string
name false string
price false number
status false integer(int32)

响应状态:

状态码 说明 schema
200 OK Result
201 Created
401 Unauthorized
403 Forbidden
404 Not Found

响应参数:

参数名称 参数说明 类型 schema
code integer(int32) integer(int32)
data object
msg string

响应示例:

{
    "code": 0,
    "data": {},
    "msg": ""
}
5)启用/禁用分类

接口地址:/admin/dish/status/{status}

请求方式:POST

请求数据类型:application/json

请求参数:

参数名称 参数说明 请求类型 是否必须 数据类型 schema
id id query true integer(int64)
status status path true integer(int32)

响应状态:

状态码 说明 schema
200 OK Result
201 Created
401 Unauthorized
403 Forbidden
404 Not Found

响应参数:

参数名称 参数说明 类型 schema
code integer(int32) integer(int32)
data object
msg string

响应示例:

{
    "code": 0,
    "data": {},
    "msg": ""
}
6)根据类型查询分类

接口地址:/admin/dish/list

请求方式:GET

请求数据类型:application/x-www-form-urlencoded

请求参数:

参数名称 参数说明 请求类型 是否必须 数据类型 schema
categoryId categoryId query false string

响应状态:

状态码 说明 schema
200 OK Result«List«DishVO»»
401 Unauthorized
403 Forbidden
404 Not Found

响应参数:

参数名称 参数说明 类型 schema
code integer(int32) integer(int32)
data array DishVO
categoryId integer(int64)
categoryName string
description string
flavors array DishFlavor
dishId integer
id integer
name string
value string
id integer(int64)
image string
name string
price number
status integer(int32)
updateTime string(date-time)
msg string

响应示例:

{
    "code": 0,
    "data": [
        {
            "categoryId": 0,
            "categoryName": "",
            "description": "",
            "flavors": [
                {
                    "dishId": 0,
                    "id": 0,
                    "name": "",
                    "value": ""
                }
            ],
            "id": 0,
            "image": "",
            "name": "",
            "price": 0,
            "status": 0,
            "updateTime": ""
        }
    ],
    "msg": ""
}

8.2 功能实现

8.2.1 Mapper 层

DishMapper.java

@Select("select count(id) from dish where category_id = #{categoryId}")
Integer countByCategoryId(Long categoryId);

SetmealMapper.java

@Select("select count(id) from setmeal where category_id = #{categoryId}")
Integer countByCategoryId(Long categoryId);

CategoryMapper.java

@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);

Page<Category> pageQuery(CategoryPageQueryDTO categoryPageQueryDTO);

@Delete("delete from category where id = #{id}")
void deleteById(Long id);

void update(Category category);

List<Category> list(Integer type);

CategoryMapper.xml

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

8.2.2 Service 层

CategoryService.java

void save(CategoryDTO categoryDTO);

PageResult pageQuery(CategoryPageQueryDTO categoryPageQueryDTO);

void deleteById(Long id);

void update(CategoryDTO categoryDTO);

void startOrStop(Integer status, Long id);

List<Category> list(Integer type);

8.2.3 Service 实现类

/**
     * 新增分类
     * @param categoryDTO
     */
    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
     */
    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删除分类
     * @param id
     */
    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
     */
    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
     */
    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
     */
    public List<Category> list(Integer type) {
        return categoryMapper.list(type);
    }

8.2.4 Controller 层

CategoryController.java

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

    @Autowired
    private CategoryService categoryService;

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

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

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

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

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

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

关于员工管理和分类管理模块就基本实现了,下一章小柒味来 - 菜品管理模块开发


欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1701220998@qq.com
导航页 GitHub