1. 分类管理功能概述
在餐饮管理系统中,分类管理是基础但至关重要的功能模块。它主要负责对菜品和套餐进行分类管理,包括新增、查询、修改、删除以及状态控制等操作。一个设计良好的分类系统能够有效提升后台管理效率,同时为前端用户提供清晰的产品展示结构。
分类管理通常需要处理以下核心业务场景:
- 分类的增删改查(CRUD)操作
- 分类状态的启用/禁用控制
- 分类与菜品/套餐的关联关系维护
- 分类的分页查询与条件筛选
2. 分类实体设计与数据模型
2.1 Category实体类详解
分类的核心数据模型通过Category类来定义,这个实体类映射数据库中的分类表:
java复制@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Category implements Serializable {
private static final long serialVersionUID = 1L;
private Long id; // 主键ID
private Integer type; // 类型: 1菜品分类 2套餐分类
private String name; // 分类名称
private Integer sort; // 排序权重
private Integer status; // 状态: 0禁用 1启用
private LocalDateTime createTime; // 创建时间
private LocalDateTime updateTime; // 更新时间
private Long createUser; // 创建人ID
private Long updateUser; // 修改人ID
}
关键字段说明:
type字段用于区分菜品分类和套餐分类,这种设计避免了为两种分类创建单独的表,简化了数据模型status字段控制分类的启用/禁用状态,禁用状态的分类不会在前端展示sort字段决定了分类在列表中的展示顺序,数值越小排序越靠前
2.2 自动填充字段的实现
创建时间、更新时间、创建人和修改人这四个字段可以通过AOP+注解的方式实现自动填充,避免在每个业务方法中重复设置:
java复制@AutoFill(value = OperationType.INSERT)
public void save(CategoryDTO categoryDTO) {
Category category = new Category();
BeanUtils.copyProperties(categoryDTO, category);
category.setStatus(StatusConstant.DISABLE); // 默认禁用状态
categoryMapper.insert(category);
}
提示:自动填充的实现通常基于MyBatis的拦截器或Spring AOP,通过识别自定义注解在SQL执行前后自动设置这些通用字段。
3. 分类管理核心功能实现
3.1 新增分类功能
新增分类需要处理两种类型的分类:菜品分类和套餐分类。前端通过type字段区分,后端使用同一套逻辑处理:
java复制public void save(CategoryDTO categoryDTO) {
// 参数校验
if (categoryDTO == null || categoryDTO.getType() == null) {
throw new BusinessException("分类类型不能为空");
}
Category category = new Category();
BeanUtils.copyProperties(categoryDTO, category);
// 设置默认状态为禁用
category.setStatus(StatusConstant.DISABLE);
// 名称重复校验
if (categoryMapper.existsByName(category.getName())) {
throw new BusinessException("分类名称已存在");
}
categoryMapper.insert(category);
}
注意事项:
- 新增分类默认设置为禁用状态,需要管理员手动启用
- 分类名称需要做唯一性校验,避免重复
- 排序字段(sort)应有默认值,通常设置为当前最大sort值+1
3.2 分类分页查询
分页查询是管理后台的标配功能,通常需要支持按类型、状态、名称等条件筛选:
java复制public PageResult pageQuery(CategoryPageQueryDTO queryDTO) {
// 分页参数校验
if (queryDTO.getPage() == null || queryDTO.getPageSize() == null) {
queryDTO.setPage(1);
queryDTO.setPageSize(10);
}
PageHelper.startPage(queryDTO.getPage(), queryDTO.getPageSize());
Page<Category> page = categoryMapper.pageQuery(queryDTO);
return new PageResult(page.getTotal(), page.getResult());
}
对应的Mapper XML中使用动态SQL处理查询条件:
xml复制<select id="pageQuery" resultType="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>
<if test="status != null">
AND status = #{status}
</if>
</where>
ORDER BY sort ASC, create_time DESC
</select>
3.3 根据类型查询分类
前端在新增菜品或套餐时需要获取可用的分类列表,这个接口需要返回指定类型的启用状态分类:
java复制public List<Category> listByType(Integer type) {
return categoryMapper.listByTypeAndStatus(type, StatusConstant.ENABLE);
}
对应的SQL实现:
xml复制<select id="listByTypeAndStatus" resultType="Category">
SELECT * FROM category
WHERE status = #{status}
<if test="type != null">
AND type = #{type}
</if>
ORDER BY sort ASC, create_time DESC
</select>
性能优化建议:
- 这个接口会被频繁调用,可以考虑添加Redis缓存
- 前端可以在初始化时一次性加载所有分类,减少后续请求
4. 分类状态管理与删除
4.1 启用/禁用分类
分类的状态控制通过单独的接口实现,遵循RESTful风格设计:
java复制@PostMapping("/status/{status}")
public Result<String> startOrStop(
@PathVariable Integer status,
@RequestParam Long id) {
categoryService.startOrStop(status, id);
return Result.success();
}
Service层实现逻辑:
java复制public void startOrStop(Integer status, Long id) {
// 状态参数校验
if (!StatusConstant.contains(status)) {
throw new BusinessException("状态参数不合法");
}
Category category = Category.builder()
.id(id)
.status(status)
.updateTime(LocalDateTime.now())
.updateUser(BaseContext.getCurrentId())
.build();
if (categoryMapper.update(category) == 0) {
throw new BusinessException("分类不存在或更新失败");
}
}
4.2 删除分类实现
删除分类前需要检查是否被菜品或套餐引用:
java复制public void deleteById(Long id) {
// 检查菜品关联
Integer dishCount = dishMapper.countByCategoryId(id);
if (dishCount > 0) {
throw new BusinessException("当前分类下存在菜品,不可删除");
}
// 检查套餐关联
Integer setmealCount = setmealMapper.countByCategoryId(id);
if (setmealCount > 0) {
throw new BusinessException("当前分类下存在套餐,不可删除");
}
// 执行删除
if (categoryMapper.deleteById(id) == 0) {
throw new BusinessException("分类不存在或删除失败");
}
}
关联查询SQL示例:
xml复制<select id="countByCategoryId" resultType="int">
SELECT COUNT(*) FROM dish WHERE category_id = #{categoryId}
</select>
5. 分类修改功能实现
分类修改功能需要考虑数据一致性和业务规则:
java复制public void update(CategoryDTO categoryDTO) {
if (categoryDTO.getId() == null) {
throw new BusinessException("分类ID不能为空");
}
// 名称重复校验(排除自身)
if (categoryMapper.existsByNameAndNotId(
categoryDTO.getName(), categoryDTO.getId())) {
throw new BusinessException("分类名称已存在");
}
Category category = new Category();
BeanUtils.copyProperties(categoryDTO, category);
// 设置更新人和更新时间
category.setUpdateUser(BaseContext.getCurrentId());
category.setUpdateTime(LocalDateTime.now());
if (categoryMapper.update(category) == 0) {
throw new BusinessException("分类不存在或更新失败");
}
}
修改时的注意事项:
- 修改分类名称时需要检查是否与其他分类重名
- 修改分类类型(type)时需要特别谨慎,可能影响已关联的菜品/套餐
- 排序字段(sort)修改后,可能需要重新调整其他分类的排序
6. 常见问题与解决方案
6.1 分类删除时的关联检查
在实际操作中,我们经常遇到无法删除分类的情况。除了代码中展示的检查方法外,还可以考虑以下方案:
- 级联删除:配置数据库外键的级联删除规则(谨慎使用)
- 软删除:通过status字段标记删除状态而非物理删除
- 批量检查:对于批量删除操作,需要收集所有无法删除的分类ID和原因
6.2 分类数据一致性问题
当分类被频繁修改时,可能会遇到缓存与数据库不一致的情况。解决方案包括:
- 使用@CacheEvict注解在修改操作后清除缓存
- 设置较短的缓存过期时间
- 实现消息队列通知机制更新缓存
6.3 分类排序冲突处理
多个管理员同时修改分类排序时可能出现冲突,可以通过以下方式优化:
java复制public void updateSort(Long id, Integer newSort) {
// 获取当前分类的原始排序值
Integer originalSort = categoryMapper.selectSortById(id);
if (newSort > originalSort) {
// 上移操作:将[originalSort, newSort]区间的分类sort-1
categoryMapper.batchDecrementSort(originalSort, newSort);
} else {
// 下移操作:将[newSort, originalSort]区间的分类sort+1
categoryMapper.batchIncrementSort(newSort, originalSort);
}
// 更新当前分类的sort值
categoryMapper.updateSort(id, newSort);
}
7. 性能优化与扩展建议
7.1 缓存策略优化
对于高频访问的分类数据,建议采用多级缓存策略:
- 本地缓存(Caffeine):缓存热点分类数据
- Redis缓存:存储全部分类数据
- 数据库:数据持久化存储
java复制@Cacheable(value = "category", key = "#type")
public List<Category> listByType(Integer type) {
return categoryMapper.listByTypeAndStatus(type, StatusConstant.ENABLE);
}
7.2 异步处理方案
对于耗时的分类统计操作,可以引入异步处理:
java复制@Async
public CompletableFuture<CategoryStats> getCategoryStatsAsync(Long categoryId) {
CategoryStats stats = new CategoryStats();
// 异步查询统计信息
return CompletableFuture.completedFuture(stats);
}
7.3 分类树形结构扩展
如果业务需要多级分类,可以扩展为树形结构:
java复制public class Category {
private Long id;
private String name;
private Long parentId; // 父分类ID
private Integer level; // 分类层级
// 其他字段...
}
查询树形分类的SQL可以使用递归CTE(MySQL 8.0+):
sql复制WITH RECURSIVE category_tree AS (
SELECT * FROM category WHERE id = #{rootId}
UNION ALL
SELECT c.* FROM category c
JOIN category_tree ct ON c.parent_id = ct.id
)
SELECT * FROM category_tree ORDER BY level, sort;
在实际开发中,分类管理模块虽然基础,但设计的好坏直接影响整个系统的可维护性和扩展性。建议在实现基础功能的同时,预留适当的扩展点,以应对未来可能出现的业务变化。