♪苍穹外卖♪Day2 | 项目日记
♪苍穹外卖♪Day2 | 项目日记
今日完成内容
今天主要完成了员工管理模块的完善和分类管理模块的完整开发,另外开始搭建 AOP 公共字段自动填充的框架。
一、员工管理模块完善
昨天已经完成了员工的登录、退出、新增和分页查询,今天在此基础上补全了三个接口。
1. 启用/禁用员工账号
在EmployeeController中新增接口:
@PostMapping("/status/{status}")@ApiOperation("启用禁用员工账号")publicResultstartOrStop(@PathVariable("status")Integerstatus,Longid){log.info("启用禁用员工账号:{},{}",status,id);employeeService.startOrStop(status,id);returnResult.success();}Service 层使用 Builder 模式构建实体对象,只设置status和id,调用 Mapper 的update方法:
@OverridepublicvoidstartOrStop(Integerstatus,Longid){Employeeemployee=Employee.builder().status(status).id(id).build();employeeMapper.update(employee);}这里的update使用的是动态 SQL,只更新非空字段,这是关键 —— 后面编辑员工信息也复用同一个 update 方法。
[!注意]
超级管理员(admin)不允许被禁用,这个业务约束在项目后续会加上。
2. 根据 ID 查询员工信息
@GetMapping("/{id}")@ApiOperation("根据id查询员工信息")publicResult<Employee>getById(@PathVariableLongid){Employeeemployee=employeeService.getById(id);returnResult.success(employee);}在 Service 层做了密码脱敏处理,返回数据时将密码替换为****:
@OverridepublicEmployeegetById(Longid){Employeeemployee=employeeMapper.getById(id);employee.setPassword("****");returnemployee;}3. 编辑员工信息
@PutMapping@ApiOperation("编辑员工信息")publicResultupdate(@RequestBodyEmployeeDTOemployeeDTO){log.info("编辑员工信息",employeeDTO);employeeService.update(employeeDTO);returnResult.success();}Service 层先用BeanUtils.copyProperties把 DTO 转成实体,然后设置修改时间和修改人:
@Overridepublicvoidupdate(EmployeeDTOemployeeDTO){Employeeemployee=newEmployee();BeanUtils.copyProperties(employeeDTO,employee);employee.setUpdateTime(LocalDateTime.now());employee.setUpdateUser(BaseContext.getCurrentId());employeeMapper.update(employee);}4. 动态 SQL 更新
EmployeeMapper.xml中的 update 语句使用<set>+<if>实现动态更新:
<updateid="update"parameterType="Employee">update employee<set><iftest="username != null">username = #{username},</if><iftest="name != null">name = #{name},</if><iftest="password != null">password = #{password},</if><iftest="phone != null">phone = #{phone},</if><iftest="sex != null">sex = #{sex},</if><iftest="idNumber != null">id_number = #{idNumber},</if><iftest="status != null">status = #{status},</if><iftest="updateTime != null">update_time = #{updateTime},</if><iftest="updateUser != null">update_user = #{updateUser},</if></set>where id = #{id}</update>这样启用禁用和编辑信息都能共用一个 update 方法,只需要设置需要修改的字段即可。
二、分类管理模块(完整 CRUD)
这是今天工作量最大的部分,从 Controller 到 Mapper 全部新建。
1. 新建分类
@PostMapping@ApiOperation("新增分类")publicResult<String>save(@RequestBodyCategoryDTOcategoryDTO){categoryService.save(categoryDTO);returnResult.success();}新增分类默认状态为禁用(0),需要手动启用后才生效:
publicvoidsave(CategoryDTOcategoryDTO){Categorycategory=newCategory();BeanUtils.copyProperties(categoryDTO,category);category.setStatus(StatusConstant.DISABLE);// 设置审计字段category.setCreateTime(LocalDateTime.now());category.setUpdateTime(LocalDateTime.now());category.setCreateUser(BaseContext.getCurrentId());category.setUpdateUser(BaseContext.getCurrentId());categoryMapper.insert(category);}Mapper 层使用注解方式直接写 SQL:
@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})")voidinsert(Categorycategory);2. 分类分页查询
和员工分页查询一样的套路,PageHelper 一把梭:
publicPageResultpageQuery(CategoryPageQueryDTOcategoryPageQueryDTO){PageHelper.startPage(categoryPageQueryDTO.getPage(),categoryPageQueryDTO.getPageSize());Page<Category>page=categoryMapper.pageQuery(categoryPageQueryDTO);returnnewPageResult(page.getTotal(),page.getResult());}XML 中的查询支持按分类名称和类型筛选,按 sort 升序、创建时间降序排列:
<selectid="pageQuery"resultType="com.sky.entity.Category">select * from category<where><iftest="name != null and name != ''">and name like concat('%',#{name},'%')</if><iftest="type != null">and type = #{type}</if></where>order by sort asc , create_time desc</select>3. 删除分类(含关联检查)
这是今天学到的一个业务细节 ——删除分类前必须检查是否有关联的菜品或套餐。
publicvoiddeleteById(Longid){// 查询当前分类是否关联了菜品Integercount=dishMapper.countByCategoryId(id);if(count>0){thrownewDeletionNotAllowedException(MessageConstant.CATEGORY_BE_RELATED_BY_DISH);}// 查询当前分类是否关联了套餐count=setmealMapper.countByCategoryId(id);if(count>0){thrownewDeletionNotAllowedException(MessageConstant.CATEGORY_BE_RELATED_BY_SETMEAL);}categoryMapper.deleteById(id);}为此新建了DishMapper和SetmealMapper中的countByCategoryId方法:
// DishMapper.java@Select("select count(id) from dish where category_id = #{categoryId}")IntegercountByCategoryId(LongcategoryId);// SetmealMapper.java@Select("select count(id) from setmeal where category_id = #{categoryId}")IntegercountByCategoryId(LongcategoryId);如果有关联数据,抛出自定义的DeletionNotAllowedException,由全局异常处理器统一返回错误信息。
4. 修改分类
@PutMapping@ApiOperation("修改分类")publicResult<String>update(@RequestBodyCategoryDTOcategoryDTO){categoryService.update(categoryDTO);returnResult.success();}XML 中同样使用动态 SQL,只更新传入的非空字段:
<updateid="update"parameterType="Category">update category<set><iftest="type != null">type = #{type},</if><iftest="name != null">name = #{name},</if><iftest="sort != null">sort = #{sort},</if><iftest="status != null">status = #{status},</if><iftest="updateTime != null">update_time = #{updateTime},</if><iftest="updateUser != null">update_user = #{updateUser}</if></set>where id = #{id}</update>5. 启用/禁用分类
和员工的启用禁用一样,使用 Builder 模式构建:
publicvoidstartOrStop(Integerstatus,Longid){Categorycategory=Category.builder().id(id).status(status).updateTime(LocalDateTime.now()).updateUser(BaseContext.getCurrentId()).build();categoryMapper.update(category);}6. 根据类型查询分类列表
这个接口是给前端用的,查询菜品分类(type=1)或套餐分类(type=2),只返回启用状态的数据:
@GetMapping("/list")@ApiOperation("根据类型查询分类")publicResult<List<Category>>list(Integertype){List<Category>list=categoryService.list(type);returnResult.success(list);}<selectid="list"resultType="Category">select * from category where status = 1<iftest="type != null">and type = #{type}</if>order by sort asc,create_time desc</select>三、AOP 公共字段自动填充(框架搭建)
今天还开始搭了 AOP 自动填充的框架,但反射赋值逻辑还没写完。
自定义注解@AutoFill
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public@interfaceAutoFill{//数据库操作类型:插入INSERT,更新UPDATEOperationTypevalue();}切面类AutoFillAspect
@Aspect@Component@Slf4jpublicclassAutoFillAspect{@Pointcut("execution(* com.sky.mapper.*.*(..))&&@annotation(com.sky.annotation.AutoFill)")publicvoidautoFillPointCut(){}@Before("autoFillPointCut()")publicvoidautoFill(JoinPointjoinPoint){log.info("开始进行公共字段自动填充...");//获取当前被拦截的数据库操作类型MethodSignaturesignature=(MethodSignature)joinPoint.getSignature();AutoFillautoFill=signature.getMethod().getAnnotation(AutoFill.class);OperationTypeoperationType=autoFill.value();//获取当前被拦截的方法参数---实体对象Object[]args=joinPoint.getArgs();//准备赋值的数据//根据当前不同的操作类型,为对应的属性通过反射赋值}}思路是:在 Mapper 的 insert/update 方法上加@AutoFill注解,AOP 拦截后通过反射自动给createTime、updateTime、createUser、updateUser赋值,就不用每次在 Service 里手动设置了。明天把反射赋值逻辑补完。
完成的功能列表
| 功能 | 状态 |
|---|---|
| 员工启用/禁用 | ✅ 已完成 |
| 根据 ID 查询员工 | ✅ 已完成 |
| 编辑员工信息 | ✅ 已完成 |
| 分类新增 | ✅ 已完成 |
| 分类分页查询 | ✅ 已完成 |
| 分类删除(含关联检查) | ✅ 已完成 |
| 分类修改 | ✅ 已完成 |
| 分类启用/禁用 | ✅ 已完成 |
| 根据类型查询分类列表 | ✅ 已完成 |
| AOP 公共字段自动填充 | 🔲 框架搭建完成,反射赋值待补 |
遇到的问题
1. 动态 SQL update 的复用问题
员工的启用禁用和编辑信息都需要 update,但要更新的字段不同。一开始想写两个方法,后来发现用<set>+<if>动态 SQL 就能完美复用一个 update,只需要在 Service 层设置不同的字段就行。
2. 删除分类的业务约束
最开始直接写了个deleteById,后来发现分类下可能关联了菜品和套餐,直接删除会导致外键关联问题。需要先查dish表和setmeal表,有关联数据就抛异常。
3. AOP 切入点表达式
切入点写的是execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill),这样只会拦截 Mapper 层中标注了@AutoFill的方法,不会影响其他方法。
学习收获
- MyBatis 动态 SQL——
<set>+<if>标签可以实现按需更新,避免每次写不同的 update 方法 - AOP + 自定义注解—— 通过自定义注解标记需要自动填充的方法,AOP 统一拦截处理,实现了解耦
- 业务删除的关联检查—— 删除操作不能简单执行,需要先检查关联关系,保证数据完整性
- Builder 模式—— 只需要更新少量字段时,用
@Builder注解构建对象比 new + 全部 setter 更简洁 - 密码脱敏—— 查询员工信息返回前端时,密码需要替换为
****,不能明文暴露
