从零到一搞定外卖系统套餐管理:手把手教你用MyBatis Plus重构苍穹外卖Day04代码
2026/6/2 14:26:52 网站建设 项目流程

从零到一搞定外卖系统套餐管理:手把手教你用MyBatis Plus重构苍穹外卖Day04代码

在当今快节奏的外卖行业,一个高效、稳定的后台管理系统对于餐厅运营至关重要。作为开发者,我们不仅要实现功能,更要追求代码的优雅和可维护性。本文将带你从零开始,使用MyBatis Plus这一强大的ORM框架,重构苍穹外卖项目中的套餐管理模块,让你的开发效率提升一个档次。

1. 为什么选择MyBatis Plus重构套餐管理

传统MyBatis虽然功能强大,但在日常开发中,我们经常需要编写大量重复的CRUD代码和XML配置。MyBatis Plus作为MyBatis的增强工具,提供了诸多开箱即用的功能:

  • 内置通用Mapper和Service:减少基础CRUD代码量
  • 强大的条件构造器:告别复杂的XML动态SQL
  • Lambda表达式支持:类型安全的查询条件
  • 自动填充功能:简化创建时间、更新时间等字段处理
  • 乐观锁支持:轻松处理并发问题

在套餐管理这种典型的多表关联业务场景中,MyBatis Plus能显著提升开发效率和代码质量。下面我们就来看看具体如何实现。

2. 环境准备与基础配置

2.1 添加MyBatis Plus依赖

首先,在项目的pom.xml中添加MyBatis Plus Starter依赖:

<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>最新版本</version> </dependency>

2.2 配置MyBatis Plus

在application.yml中添加基本配置:

mybatis-plus: configuration: map-underscore-to-camel-case: true # 开启驼峰命名自动转换 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印SQL日志 global-config: db-config: logic-delete-field: delFlag # 逻辑删除字段名 logic-not-delete-value: 0 # 未删除值 logic-delete-value: 1 # 删除值

2.3 实体类改造

为实体类添加MyBatis Plus注解:

@Data @TableName("setmeal") public class Setmeal { @TableId(type = IdType.AUTO) private Long id; private Long categoryId; private String name; private BigDecimal price; private Integer status; private String description; private String image; @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; @TableField(fill = FieldFill.INSERT) private Long createUser; @TableField(fill = FieldFill.INSERT_UPDATE) private Long updateUser; }

3. 核心功能重构实战

3.1 新增套餐功能优化

原始实现中,新增套餐需要手动处理套餐信息和套餐-菜品关联信息,代码较为繁琐。使用MyBatis Plus后,我们可以大幅简化:

@Service public class SetmealServiceImpl extends ServiceImpl<SetmealMapper, Setmeal> implements SetmealService { @Autowired private SetmealDishMapper setmealDishMapper; @Override @Transactional public void saveWithDishes(SetmealDTO setmealDTO) { // 保存套餐基本信息 this.save(setmealDTO); // 处理套餐菜品关联 List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes() .stream() .peek(dish -> dish.setSetmealId(setmealDTO.getId())) .collect(Collectors.toList()); setmealDishMapper.insertBatch(setmealDishes); } }

提示:这里我们继承了MyBatis Plus提供的ServiceImpl,自动获得了基础的CRUD能力。

3.2 分页查询的优雅实现

传统分页需要手动编写分页逻辑和XML配置,MyBatis Plus提供了更简洁的方式:

@Override public PageResult pageQuery(SetmealPageQueryDTO setmealPageQueryDTO) { Page<Setmeal> page = new Page<>(setmealPageQueryDTO.getPage(), setmealPageQueryDTO.getPageSize()); LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper .like(StringUtils.isNotEmpty(setmealPageQueryDTO.getName()), Setmeal::getName, setmealPageQueryDTO.getName()) .eq(setmealPageQueryDTO.getCategoryId() != null, Setmeal::getCategoryId, setmealPageQueryDTO.getCategoryId()) .eq(setmealPageQueryDTO.getStatus() != null, Setmeal::getStatus, setmealPageQueryDTO.getStatus()) .orderByDesc(Setmeal::getCreateTime); this.page(page, queryWrapper); return new PageResult(page.getTotal(), page.getRecords()); }

3.3 套餐状态批量操作

使用MyBatis Plus的Lambda表达式,状态批量操作变得更加类型安全:

@Override @Transactional public void startOrStop(Integer status, List<Long> ids) { // 检查是否可以起售 if (status == StatusConstant.ENABLE) { List<Dish> dishes = dishMapper.selectList( new LambdaQueryWrapper<Dish>() .inSql(Dish::getId, "SELECT dish_id FROM setmeal_dish WHERE setmeal_id IN (" + ids.stream().map(String::valueOf).collect(Collectors.joining(",")) + ")") .eq(Dish::getStatus, StatusConstant.DISABLE) ); if (!dishes.isEmpty()) { throw new SetmealEnableFailedException(MessageConstant.SETMEAL_ENABLE_FAILED); } } // 批量更新状态 this.update( new LambdaUpdateWrapper<Setmeal>() .in(Setmeal::getId, ids) .set(Setmeal::getStatus, status) ); }

4. 复杂关联查询的优化方案

套餐管理中最复杂的部分莫过于处理套餐与菜品之间的多对多关系。MyBatis Plus提供了几种处理方案:

4.1 方案一:自定义SQL结合Lambda

对于复杂的多表关联查询,可以结合自定义SQL和Lambda表达式:

public interface SetmealMapper extends BaseMapper<Setmeal> { @Select("SELECT s.*, c.name AS categoryName " + "FROM setmeal s LEFT JOIN category c ON s.category_id = c.id " + "${ew.customSqlSegment}") Page<SetmealVO> pageQuery(@Param("ew") LambdaQueryWrapper<Setmeal> wrapper, Page<Setmeal> page); }

4.2 方案二:使用@TableField注解处理关联

对于简单的关联关系,可以使用@TableField注解:

@Data public class SetmealVO extends Setmeal { private String categoryName; @TableField(exist = false) private List<SetmealDish> setmealDishes; }

4.3 方案三:使用MyBatis Plus的关联查询插件

对于更复杂的场景,可以考虑使用MyBatis Plus的关联查询插件:

@Override public SetmealVO getByIdWithDishes(Long id) { Setmeal setmeal = this.getById(id); SetmealVO setmealVO = new SetmealVO(); BeanUtils.copyProperties(setmeal, setmealVO); List<SetmealDish> dishes = setmealDishMapper.selectList( new LambdaQueryWrapper<SetmealDish>() .eq(SetmealDish::getSetmealId, id) ); setmealVO.setSetmealDishes(dishes); return setmealVO; }

5. 事务管理与异常处理的最佳实践

在套餐管理中,很多操作需要保证事务一致性。MyBatis Plus与Spring事务管理完美结合:

5.1 声明式事务配置

@Configuration @EnableTransactionManagement public class TransactionConfig { @Bean public TransactionInterceptor transactionInterceptor(PlatformTransactionManager transactionManager) { return new TransactionInterceptor(transactionManager, new AttributesTransactionAttributeSource()); } }

5.2 事务传播行为选择

套��管理中的典型事务场景:

操作类型建议传播行为说明
新增套餐REQUIRED需要完整事务
修改套餐REQUIRED需要完整事务
删除套餐REQUIRED需要完整事务
查询套餐SUPPORTS不需要事务
状态变更REQUIRED需要完整事务

5.3 自定义异常处理

针对业务特点,我们可以定义专门的异常:

@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(SetmealEnableFailedException.class) public Result<String> handleSetmealEnableFailed(SetmealEnableFailedException ex) { return Result.error(ex.getMessage()); } @ExceptionHandler(Exception.class) public Result<String> handleException(Exception ex) { log.error("系统异常", ex); return Result.error("系统繁忙,请稍后再试"); } }

6. 性能优化与缓存策略

在高并发的外卖系统中,套餐数据的读取频率远高于写入频率,合理的缓存策略能显著提升性能:

6.1 二级缓存配置

mybatis-plus: configuration: cache-enabled: true

6.2 Redis缓存实现

@Service public class SetmealServiceImpl extends ServiceImpl<SetmealMapper, Setmeal> implements SetmealService { @Autowired private RedisTemplate<String, Object> redisTemplate; private static final String CACHE_PREFIX = "setmeal:"; @Override @Cacheable(value = "setmeal", key = "#id") public SetmealVO getByIdWithCache(Long id) { return this.getByIdWithDishes(id); } @Override @CacheEvict(value = "setmeal", key = "#setmealDTO.id") public void updateWithCache(SetmealDTO setmealDTO) { this.updateWithDishes(setmealDTO); } }

6.3 缓存一致性保障

对于关键操作,确保缓存与数据库的一致性:

@Override @Transactional @CacheEvict(value = "setmeal", allEntries = true) public void deleteBatchWithCache(List<Long> ids) { // 检查状态 List<Setmeal> setmeals = this.listByIds(ids); setmeals.forEach(setmeal -> { if (setmeal.getStatus() == StatusConstant.ENABLE) { throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE); } }); // 删除套餐 this.removeByIds(ids); // 删除关联菜品 setmealDishMapper.delete( new LambdaQueryWrapper<SetmealDish>() .in(SetmealDish::getSetmealId, ids) ); }

在实际项目中,我发现套餐数据的缓存策略需要根据业务特点灵活调整。对于高频访问但更新不频繁的数据,可以设置较长的缓存时间;而对于价格等敏感信息,则需要更短的缓存时间或实时更新。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询