2025.10.11项目学习Java

sky-takeout-day02-note

Sky-takeout Day02 【前言】Day02 的 Task 是实现最基础的 CRUD 功能,流程:先做需求分析,再开始编写代码,然后进行功能测试(接口文档、前后端联调)以及代码完善。首先,通过员工管理模块的新增、分页查询、启用禁用账号、编辑这四大功能,来了解简单的 CRUD…

Sky-takeout Day02

【前言】
Day02 的 Task 是实现最基础的 CRUD 功能,流程:先做需求分析,再开始编写代码,然后进行功能测试(接口文档、前后端联调)以及代码完善。
首先,通过员工管理模块的新增、分页查询、启用禁用账号、编辑这四大功能,来了解简单的 CRUD 如何写。
其次,因为菜品分类管理模块的逻辑与员工管理的几乎一样,所以就直接导入了,想巩固的话可以自己手敲一遍,加深印象。

Contents

  • 新增员工
  • 员工分页查询
  • 启用禁用员工账号
  • 编辑员工
  • 导入菜品分类管理模块代码

1. 新增员工

Q1:用DTO还是Entity实体类?

一般来说

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

  • Entity 管数据库,DTO 管接口。通过分层隔离,Entity 聚焦业务模型和持久化,DTO 聚焦 API 契约和传输效率

    维度 Entity(实体类) DTO(数据传输对象)
    核心职责 映射数据库表结构,承载业务逻辑 封装接口参数 / 响应,实现跨层数据传输
    所属层级 持久层 / 领域层(与数据库直接交互) 应用层 / 表现层(Controller、Service 间传输)
    数据库关联 强关联(字段与表结构一一对应) 无关联(结构由 API 需求决定)
    设计重点 数据完整性、业务规则(如 ORM 注解、非空约束) API 契约稳定性、传输性能(仅含必要字段)
    image-20251010153653291

代码编写

EmployeeController.java

/**
     * 保存员工信息
     *
     * @param employeeDTO 员工信息DTO
     * @return 返回统一的Result封装对象
     */
    @PostMapping
    @ApiOperation(value = "保存(新增)员工信息")
    public Result save(@RequestBody EmployeeDTO employeeDTO) {
        log.info("保存(新增)员工信息:{}", employeeDTO);
        // 调用service层的方法来处理保存逻辑
        employeeService.save(employeeDTO);
        return Result.success();
    }

EmployeeServiceImpl.java

/**
 * 保存员工信息
 *
 * @param employeeDTO 员工信息
 */
@Override
public void save(EmployeeDTO employeeDTO) {
    // 先将 DTO 转换为实体对象, 再进行保存, 用spring中的BeanUtils的copyProperties方法
    Employee employee = new Employee();
    BeanUtils.copyProperties(employeeDTO, employee);
    // 再将实体类的剩余属性设置好
    employee.setStatus(StatusConstant.ENABLE); // 设置默认状态为启用
    employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes())); // 设置初始密码123456, 需要进行MD5加密
    employee.setCreateTime(LocalDateTime.now());
    employee.setUpdateTime(LocalDateTime.now());
    employee.setCreateUser(10L); // TODO: 这里先写死,后面改为当前登录用户的id
    employee.setUpdateUser(10L); // TODO: 这里先写死,后面改为当前登录用户的id
    // 最后调用Mapper层的方法进行保存
    employeeMapper.insert(employee);
}

EmployeeMapper.java

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

功能测试

首先先设置一下全局token, 另外过期时间可以设置久一点(在application.yml中jwt部分设置), 避免总是手动设置.

image-20251010161839592 image-20251010161821040

测试成功,返回201
image-20251010162002733

代码功能完善

当前存在问题:

  • 用户名有unique约束, 若重复会抛异常, 异常还没处理
  • 新增员工时的登录用户id还是定死的
image-20251010162153307

解决用户名重复异常的问题

image-20251010163541987

要将抛出的 SQLIntegrityConstraintViolationException 异常,加入全局异常处理器中, 返回异常消息给前端, 保证友好性.
在GlobalExceptionHandler中添加:

/**
 * 捕获SQL异常
 * @param ex 捕获到的异常对象
 * @return 返回封装的异常信息
 */
@ExceptionHandler
public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
    // Duplicate entry 'hwan1' for key 'employee.idx_username';
    String message = ex.getMessage();
    if(message.contains("Duplicate entry")){
        String[] split = message.split(" ");
        String username = split[2];
        String msg = username + MessageConstant.ALREADY_EXIST;
        return Result.error(msg);
    } else {
        return Result.error(MessageConstant.UNKNOWN_ERROR);
    }
}

解决设置当前用户id的问题

  • JWT 工作流程图

    image-20251010164604620
  • 通过Jwt令牌中携带的用户id来获取新增员工时需要设置的当前用户属性

    image-20251010164716025

Q2: 如何传递拦截器获取的用户id给service?

  • 解析出登录员工 id 后,如何传递给 Service 的 save 方法?

    ​ =>使用ThreadLocal

    介绍:

    ThreadLocal 并不是一个 Thread,而是 Thread 的局部变量。
    ThreadLocal 为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。

    常用方法:

    • public void set(T value) 设置当前线程的线程局部变量的值

    • public T get() 返回当前线程所对应的线程局部变量的值

    • public void remove() 移除当前线程的线程局部变量

  • 项目中在BaseContext工具类已经封装了对应方法

    image-20251010165529633
  • 解决流程图

    image-20251010165222324
  • 在拦截器Interceptor, 将用户id存入ThreadLocal中

    image-20251010170118979
  • 在Service里, 从ThreadLocal中获取当前登录用户的id

    image-20251010170259654

2. 员工分页查询

接口分析

接口设计
image-20251010184016854

注意事项:

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

代码编写

用到了分页插件PageHelper

PageHelper 基本原理

PageHelper 是基于 MyBatis 的分页插件,通过拦截 SQL 执行过程动态添加分页逻辑(如 MySQL 的 LIMIT),并自动统计总记录数。其核心优势是:

  • 无需修改 SQL,只需在代码中添加分页参数;
  • 支持多数据库(MySQL、Oracle 等);
  • 通过 ThreadLocal 存储分页参数,保证线程安全。

定义分页查询DTO分页结果封装

image-20251010195314831

EmployeeController.java

/**
 * 员工分页查询
 *
 * @param employeePageQueryDTO 员工分页查询DTO
 * @return 返回统一的Result封装对象
 */
@ApiOperation("员工分页查询")
@GetMapping("/page")
public Result<PageResult> page(EmployeePageQueryDTO employeePageQueryDTO) {
    log.info("员工分页查询:参数{}", employeePageQueryDTO);
    PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO);
    return Result.success(pageResult);
}

EmployeeServiceImpl.java

/**
 * 员工分页查询
 *
 * @param employeePageQueryDTO 分页查询信息
 * @return 分页结果
 */
@Override
public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
    PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize());
    Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);
    return new PageResult(page.getTotal(), page.getResult());
}

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 update_time desc
</select>

解决分页查询时间格式问题

调试时发现时间格式有问题:
image-20251010191630040

有两种解决方案

  • 方式一:在属性加上注解,可行,但是需要在每个时间属性上都要加上该注解,使用较麻烦,不能全局处理。

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

@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    log.info("扩展消息转换器...");
    // 创建消息转换器对象
    MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
    // 设置对象转换器,底层使用Jackson将Java对象转为json
    converter.setObjectMapper(new JacksonObjectMapper());
    // 将上面的消息转换器对象追加到mvc框架的转换器集合中
    // 这里前面的 0 是优先级,设置在最前面,优先使用我们定义的转换器
    converters.add(0, converter);
}

JacksonObjectMapper中定义了时间格式
image-20251010194037109

3. 启用禁用员工账号

接口设计

image-20251010203227868

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

2). 同时,把 id 传递过去,明确对哪个用户进行操作。

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

代码编写

EmployeeController.java

/**
 * 启用禁用员工账号
 *
 * @param status 状态
 * @param id     员工id
 * @return 返回统一的Result封装对象
 */
@PostMapping("/status/{status}")
@ApiOperation("启用禁用员工账号")
public Result<Void> startOrStop(@PathVariable Integer status, Long id) {
    log.info("启用禁用员工账号:status={}, id={}", status, id);
    employeeService.startOrStop(status, id);
    return Result.success();
}

EmployeeServiceImpl.java

/**
 * 启用或禁用员工账号
 *
 * @param status 状态
 * @param id     员工id
 */
@Override
public void startOrStop(Integer status, Long id) {
    Employee employee = Employee.builder()
            .id(id)
            .status(status)
            .build();
    employeeMapper.updateById(employee);
}

EmployeeMapper.xml

根据传入的Employee对象中的非空字段,选择性地更新employee表中的对应记录。它的核心特点是只更新需要修改的字段,避免不必要的数据库操作。

<update id="updateById" 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>

这里这个功能的实现虽然只需要修改状态值,但是可以把其他需要根据主键动态修改的属性都加上,方便后续复用

4. 编辑员工

接口设计

编辑员工的页面原型

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

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

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

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

设计接口如下:
image-20251011192924595
image-20251011192944602

代码编写

以下代码包含了:根据id查询员工信息回显给前端、修改员工信息

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

EmployeeController.java

/**
 * 根据id查询员工信息
 *
 * @param id 员工id
 * @return 返回统一的Result封装对象
 */
@GetMapping("/{id}")
@ApiOperation("根据id查询员工信息")
public Result<Employee> getById(@PathVariable Long id) {
    log.info("根据id查询员工信息:id={}", id);
    Employee employee = employeeService.getById(id);
    return Result.success(employee);
}

/**
 * 修改员工信息
 *
 * @param employeeDTO 员工信息DTO
 * @return 返回统一的Result封装对象
 */
@PutMapping
@ApiOperation("修改员工信息")
public Result<Void> update(@RequestBody EmployeeDTO employeeDTO) {
    log.info("修改员工信息:{}", employeeDTO);
    employeeService.update(employeeDTO);
    return Result.success();
}

EmployeeServiceImpl.java

/**
 * 根据id查询员工信息
 *
 * @param id 员工id
 * @return 员工信息
 */
@Override
public Employee getById(Long id) {
    Employee employee = employeeMapper.getById(id);
    if (employee == null) {  // 如果没有查询到员工信息, 则抛出异常
        throw new AccountNotFoundException(MessageConstant.ACCOUNT_NOT_FOUND);
    }
    return employee; // 返回员工信息
}

@Override
public void update(EmployeeDTO employeeDTO) {
    // 传来的是DTO对象, 要将 DTO 转换为实体对象
    Employee employee = new Employee();
    BeanUtils.copyProperties(employeeDTO, employee);
    // 设置修改相关属性,修改时间和修改人
    employee.setUpdateTime(LocalDateTime.now());
    employee.setUpdateUser(BaseContext.getCurrentId());
    employeeMapper.updateById(employee);  // 更新员工信息 (前面在写启用禁用员工账号时,在Mapper中已经配置了根据id动态更新)
}

EmployeeMapper.java

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

功能测试

  • 1)根据id查询员工并回显信息:返回员工信息json数据

    image-20251011194957644
  • 2)修改信息

    image-20251011200650746

5. 导入菜品分类管理模块

产品原型分析

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

下面以分析 菜品分类 相关功能来举例, 套餐的类似。

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

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

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

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

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

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

注意事项

  • 导入时如果想避免看到一堆红的报错,就按照Mapper =》Servie =》 Controller的顺序来导入;
  • 因为在删除分类等分类相关业务时,会设计到 Setmeal 和 Dish ,要判断分类地下是否还挂有菜品和套餐,所以这里也需要导入SetmealMapper和DishMapper
  • 其次,因为这些代码是我们直接拷贝进来的,可能不会自动编译,保险起见最好在 maven 那里 compile 手动编译一下,防止报错
  • 业务规则:
    • 分类名称必须是唯一的
    • 分类按照类型可以分为菜品分类和套餐分类
    • 新添加的分类状态默认为“禁用”,
      • 为什么?=》如果默认是启用的话,新增分类时,该分类下是空的,在用户端显示是空的,没用意义,为了避免这种情况,直接把默认状态设为禁用即可)

代码导入

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 分类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 0-禁用,1-启用
     * @param id 分类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);
    }
}

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 分类信息
     */
    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 分类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 分类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);
    }
}

CategoryMapper.java

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 分类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);
}

DishMapper.java

package com.sky.mapper;

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

@Mapper
public interface DishMapper {

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

}

SetmealMapper.java

package com.sky.mapper;

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

@Mapper
public interface SetmealMapper {

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

}

CategoryMapper.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.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 ,create_time desc
    </select>
</mapper>

附:其他盲区知识补充

在 Spring MVC 中,@RequestBody注解的核心作用是将 HTTP 请求体中的数据绑定到控制器方法的参数上。是否需要添加该注解,主要取决于参数的数据来源请求的内容格式

一、需要添加@RequestBody的场景

当参数的数据来自HTTP 请求体(Request Body),且请求体格式为application/jsonapplication/xml等非表单格式时,必须使用@RequestBody

典型场景:

  1. 前端发送 JSON 格式数据当前端通过POSTPUT等请求,以JSON格式提交数据(Content-Type: application/json)时,后端需要用@RequestBody接收并将 JSON 转换为 Java 对象。

    示例:前端请求(JSON 格式):

    // 请求体内容
    {
      "username": "zhangsan",
      "password": "123456"
    }

    后端 Controller 接收:

    @PostMapping("/login")
    public Result login(@RequestBody User user) {  // 需要@RequestBody将JSON转换为User对象
        // 处理登录逻辑
    }
  2. 接收复杂对象或数组当参数是复杂 Java 对象(包含多个字段)、集合(如ListMap)或数组时,即使请求体是其他格式(如 XML),也需要@RequestBody绑定数据。

    示例(接收数组):

    @PostMapping("/batch-delete")
    public Result batchDelete(@RequestBody List<Long> ids) {  // 接收JSON数组[1,2,3]
        // 批量删除逻辑
    }

二、不需要添加@RequestBody的场景

当参数的数据来自URL 路径(PathVariable)查询参数(Query String)表单提交(Form Data) 时,不需要@RequestBody

典型场景:

  1. **参数来自 URL 路径(@PathVariable)**参数嵌在 URL 中(如/user/123中的123),用@PathVariable接收,无需@RequestBody

    @GetMapping("/user/{id}")
    public Result getUser(@PathVariable Long id) {  // 从URL路径获取id
        // 查询用户逻辑
    }
  2. **参数来自查询参数(@RequestParam)**参数在 URL 末尾(如/search?name=zhangsan&age=20),用@RequestParam接收(简单类型可省略该注解)。

    @GetMapping("/search")
    public Result search(@RequestParam String name, Integer age) {  // 从查询参数获取
        // 搜索逻辑
    }
  3. **表单提交(application/x-www-form-urlencoded)**前端通过表单提交数据(Content-Type: application/x-www-form-urlencoded),此时参数在请求体中,但格式是键值对(非 JSON),无需@RequestBody,Spring 会自动绑定到对象。

    示例:前端表单:

    <form method="post" action="/register">
        <input name="username" value="zhangsan">
        <input name="password" value="123456">
    </form>

    后端接收:

    @PostMapping("/register")
    public Result register(User user) {  // 无需@RequestBody,自动绑定表单字段
        // 注册逻辑
    }

三、关键判断依据

  1. 数据位置:参数来自请求体(Body)还是 URL(路径 / 查询参数)?
    • 来自请求体 → 可能需要@RequestBody(除非是表单键值对)。
    • 来自 URL → 不需要。
  2. 请求格式:请求体是 JSON/XML 还是表单键值对?
    • JSON/XML → 需要@RequestBody
    • 表单键值对(application/x-www-form-urlencoded) → 不需要(复杂对象也能自动绑定)。
  3. 参数类型:简单类型(String、int 等)还是复杂对象 / 集合?
    • 复杂对象 / 集合且来自请求体 → 需要@RequestBody
    • 简单类型且来自查询参数 / 表单 → 不需要。

总结

@RequestBody的核心是处理请求体中的非表单格式数据(如 JSON),将其转换为 Java 对象。如果参数来自 URL 或表单提交的键值对,则无需添加该注解。


四、比较用 == 还是 Objects.equals() ?

比较数字包装类(如 IntegerLong 等)时,== 存在潜在问题,推荐用更安全的方式(如 Objects.equals)替代。具体原因和建议如下:

1. == 的问题:比较 “引用地址” 而非 “值相等”

包装类(如 Integer)是对象== 比较的是对象的内存地址是否相同,而非 “值是否相等”。

  • 示例 1(缓存范围内,看似正常):

    Integer a = 127;
    Integer b = 127;
    System.out.println(a == b); // true(因为 Integer 缓存了 -128~127 的对象)
  • 示例 2(超出缓存范围,逻辑错误):

    Integer c = 128;
    Integer d = 128;
    System.out.println(c == d); // false(超出缓存,是两个不同对象)

    此时 cd值都是 128,但 == 结果为 false(因为是不同对象实例)。

2. == 无法安全处理 null

如果比较的对象包含 null== 会直接抛出 NullPointerException

Integer a = null;
Integer b = 10;
System.out.println(a == b); // 直接抛 NPE

推荐用法:Objects.equals(...)

java.util.Objects.equals(Object a, Object b) 是最安全的方式,原因:

  • 内部先判断 ab 是否为 null避免空指针异常
  • 再调用 equals 方法比较值是否相等(而非引用地址)。

示例:

Integer a = 128;
Integer b = 128;
System.out.println(Objects.equals(a, b)); // true(值相等)

Integer c = null;
Integer d = 10;
System.out.println(Objects.equals(c, d)); // false(安全,无 NPE)

总结:当需要比较 ** 数字包装类的 “值是否相等”** 时,应避免用 ==,推荐使用 Objects.equals(...) 保证逻辑正确且避免空指针。


五、什么时候xml映射文件「可以不指定」parameterType

MyBatis 3.4.0 及以上版本(苍穹外卖用的是高版本,如 3.5.x),满足以下条件时,parameterType 可省略:

1. 参数是「JavaBean 类型」(如 EmployeeEmployeeDTO

高版本 MyBatis 会通过 反射 自动推断参数类型:

  • 若 Mapper 接口方法的参数是 Employee(如 void update(Employee employee)),MyBatis 会直接识别参数类型为 Employee,无需显式写 parameterType="Employee"

例如:即使去掉 parameterType,以下 SQL 仍能正常运行:

<update id="update"> <!-- 省略 parameterType -->
    update employee
    <set>
        <if test="status != null">status = #{status},</if>
    </set>
    where id = #{id}
</update>

2. 参数用 @Param 注解显式标记

若 Mapper 接口方法用 @Param 指定参数名,MyBatis 会通过 @Param 识别参数,无需 parameterType

  • Mapper 接口:

    void updateStatus(@Param("id") Long id, @Param("status") Integer status);
  • 映射文件(无需parameterType)

    <update id="updateStatus">
        update employee set status = #{status} where id = #{id}
    </update>

六、什么时候xml映射文件「必须指定」parameterType

只有两种场景下,parameterType 是必须显式声明的:

参数类型是「集合 / 数组 / Map」等非 JavaBean 类型

当参数是 ListArrayMap 等特殊类型时,MyBatis 无法通过方法参数自动推断具体类型,必须显式指定 parameterType

  • 示例 1:批量删除员工(参数是List)

    <delete id="batchDelete" parameterType="java.util.List">
        delete from employee where id in
        <foreach collection="list" item="id" open="(" separator="," close=")">
            #{id}
        </foreach>
    </delete>
  • 示例 2:用 Map 传多参数(参数是Map)

    <select id="getByMap" parameterType="java.util.Map" resultType="Employee">
        select * from employee 
        where name = #{name} and status = #{status}
    </select>