【苍穹外卖实战】Day02:从零构建员工与分类管理CRUD模块

张开发
2026/4/13 7:05:19 15 分钟阅读

分享文章

【苍穹外卖实战】Day02:从零构建员工与分类管理CRUD模块
1. 从零搭建员工管理模块第一次接触苍穹外卖项目时我发现员工管理模块是整个后台系统的基石。这个模块需要实现最基础的CRUD功能但其中包含的技术细节却非常值得深挖。记得当时为了搞清楚DTO和Entity的区别我整整查阅了三天资料。1.1 新增员工功能实现新增员工时遇到的第一个技术难题就是到底该用DTO还是Entity来接收前端数据这个问题困扰了我很久。后来通过实践发现当接口传输的数据结构与数据库表结构差异较大时使用DTO能带来更好的灵活性。比如在我们的员工表中有create_time、update_time等字段但前端表单根本不需要提交这些数据。这时候DTO就派上用场了// EmployeeDTO.java public class EmployeeDTO { private String username; private String name; private String phone; private Integer sex; private String idNumber; // 仅包含前端需要提交的字段 } // Employee.java (Entity) public class Employee { private Long id; private String username; private String password; // ...其他字段 private LocalDateTime createTime; private LocalDateTime updateTime; private Long createUser; private Long updateUser; // 包含所有数据库字段 }在Controller层的实现中我们使用RequestBody接收JSON数据PostMapping public Result save(RequestBody EmployeeDTO employeeDTO) { employeeService.save(employeeDTO); return Result.success(); }Service层的转换逻辑也很关键。我习惯使用Spring的BeanUtils进行属性拷贝然后再补充Entity特有的字段public void save(EmployeeDTO employeeDTO) { Employee employee new Employee(); BeanUtils.copyProperties(employeeDTO, employee); // 补充特有字段 employee.setPassword(DigestUtils.md5DigestAsHex(123456.getBytes())); employee.setStatus(StatusConstant.ENABLE); employee.setCreateTime(LocalDateTime.now()); employee.setUpdateTime(LocalDateTime.now()); employee.setCreateUser(BaseContext.getCurrentId()); employee.setUpdateUser(BaseContext.getCurrentId()); employeeMapper.insert(employee); }1.2 异常处理与ThreadLocal应用在实现新增功能时我踩过一个坑没有处理用户名重复的异常。当数据库设置了唯一索引重复插入会抛出SQLIntegrityConstraintViolationException。后来我在全局异常处理器中添加了专门的处理逻辑ExceptionHandler public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){ String message ex.getMessage(); if(message.contains(Duplicate entry)){ String[] split message.split( ); String username split[2]; return Result.error(username 已存在); } return Result.error(未知错误); }另一个技术难点是如何获取当前登录用户ID。通过JWT解析出用户ID后我使用ThreadLocal实现了线程安全的传递// 在拦截器中 String token request.getHeader(token); Claims claims JwtUtil.parseToken(token); Long empId Long.valueOf(claims.get(empId).toString()); BaseContext.setCurrentId(empId); // 在Service中 Long currentId BaseContext.getCurrentId();ThreadLocal的原理很有意思它为每个线程提供了独立的变量副本完美解决了多线程环境下的数据隔离问题。2. 分页查询与时间格式化2.1 分页查询实现分页查询是后台管理系统的高频功能。我选择使用PageHelper插件它通过拦截SQL自动添加LIMIT语句大大简化了开发工作。首先定义分页查询DTOpublic class EmployeePageQueryDTO { private String name; private Integer page 1; private Integer pageSize 10; }Controller层实现非常简单GetMapping(/page) public ResultPageResult page(EmployeePageQueryDTO dto) { PageResult pageResult employeeService.pageQuery(dto); return Result.success(pageResult); }Service层的核心是PageHelper.startPage()方法public PageResult pageQuery(EmployeePageQueryDTO dto) { PageHelper.startPage(dto.getPage(), dto.getPageSize()); PageEmployee page employeeMapper.pageQuery(dto); return new PageResult(page.getTotal(), page.getResult()); }Mapper层的SQL需要支持动态查询select idpageQuery resultTypecom.sky.entity.Employee select * from employee where if testname ! null and name ! and name like concat(%,#{name},%) /if /where order by update_time desc /select2.2 时间格式化问题在测试分页接口时我发现返回的LocalDateTime格式不符合前端要求。经过排查找到了两种解决方案第一种是在每个时间字段上加注解JsonFormat(pattern yyyy-MM-dd HH:mm:ss) private LocalDateTime updateTime;但这种方式需要修改所有实体类比较麻烦。我最终选择了第二种方案 - 自定义消息转换器Override protected void extendMessageConverters(ListHttpMessageConverter? converters) { MappingJackson2HttpMessageConverter converter new MappingJackson2HttpMessageConverter(); converter.setObjectMapper(new JacksonObjectMapper()); converters.add(0, converter); }其中JacksonObjectMapper自定义了时间序列化规则public class JacksonObjectMapper extends ObjectMapper { public JacksonObjectMapper() { // ... SimpleModule simpleModule new SimpleModule() .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer( DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss))); registerModule(simpleModule); } }3. 员工状态管理与编辑功能3.1 启用禁用账号功能这个功能看似简单但实现时需要考虑权限控制和操作日志。我的实现方案是使用路径变量传递状态值PostMapping(/status/{status}) public Result startOrStop(PathVariable Integer status, Long id) { employeeService.startOrStop(status, id); return Result.success(); }Service层采用Builder模式构建更新对象public void startOrStop(Integer status, Long id) { Employee employee Employee.builder() .id(id) .status(status) .updateTime(LocalDateTime.now()) .updateUser(BaseContext.getCurrentId()) .build(); employeeMapper.updateById(employee); }对应的Mapper XML使用了动态SQLupdate idupdateById parameterTypeemployee update employee set if teststatus ! nullstatus #{status},/if if testupdateTime ! nullupdate_time #{updateTime},/if if testupdateUser ! nullupdate_user #{updateUser}/if /set where id #{id} /update3.2 编辑员工信息编辑功能需要两个接口查询回显和保存修改。查询接口很简单GetMapping(/{id}) public ResultEmployee getById(PathVariable Long id) { Employee employee employeeService.getById(id); return Result.success(employee); }保存修改的接口需要注意数据转换PutMapping public Result update(RequestBody EmployeeDTO employeeDTO) { employeeService.update(employeeDTO); return Result.success(); }Service层的实现复用了很多新增功能的逻辑public void update(EmployeeDTO employeeDTO) { Employee employee new Employee(); BeanUtils.copyProperties(employeeDTO, employee); employee.setUpdateTime(LocalDateTime.now()); employee.setUpdateUser(BaseContext.getCurrentId()); employeeMapper.updateById(employee); }4. 菜品分类管理模块4.1 模块导入与业务规则菜品分类管理与员工管理有很多相似之处可以直接导入代码。但有几个特殊业务规则需要注意分类名称必须唯一新增分类默认状态为禁用删除分类前需要检查是否关联菜品导入顺序建议Mapper → Service → Controller这样可以避免编译错误。4.2 分类删除的特殊处理删除功能需要先做关联检查public void deleteById(Long id) { // 检查菜品关联 Integer count dishMapper.countByCategoryId(id); if(count 0){ throw new DeletionNotAllowedException(分类下存在菜品); } // 检查套餐关联 count setmealMapper.countByCategoryId(id); if(count 0){ throw new DeletionNotAllowedException(分类下存在套餐); } categoryMapper.deleteById(id); }对应的SQL查询Select(select count(id) from dish where category_id #{categoryId}) Integer countByCategoryId(Long categoryId);4.3 分类状态管理与员工状态管理类似但需要注意默认状态的处理public void save(CategoryDTO categoryDTO) { Category category new Category(); 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); }在实际项目中我发现分类管理经常需要按类型查询所以专门实现了这个接口GetMapping(/list) public ResultListCategory list(Integer type) { ListCategory list categoryService.list(type); return Result.success(list); }对应的Service实现public ListCategory list(Integer type) { return categoryMapper.list(type); }Mapper XML中的动态查询select idlist resultTypeCategory select * from category where status 1 if testtype ! null and type #{type} /if order by sort ,create_time desc /select在完成这些功能后我深刻体会到良好的分层架构和统一的异常处理机制对项目的重要性。特别是在处理分类删除时的业务校验如果没有提前设计好异常体系代码会变得非常混乱。

更多文章