☰
【苍穹外卖】Day03:菜品管理实战,从公共字段填充到完整 CRUD
2026/9/30 2:46:54 网站建设 项目流程

目录

一、公共字段自动填充

1.1 问题分析

1.2 实现思路

1.3 代码开发

步骤一:自定义注解

步骤二:自定义切面

步骤三:在 Mapper 方法上加注解

1.4 测试

二、新增菜品

2.1 需求分析

2.2 文件上传实现

使用阿里云 OSS

2.3 新增菜品实现

DTO 设计

Controller 层

Service 层

Mapper 层

功能测试

三、菜品分页查询

3.1 需求分析

3.2 代码开发

DTO 与 VO

Controller 层

Service 层

Mapper 层

功能测试

接口文档测试

前后端联调测试

四、删除菜品

4.1 需求分析

4.2 代码开发

Controller 层

Service 层

Mapper层

五、修改菜品

5.1 需求分析

5.2 查询回显

Controller 层

Service 层

Mapper层

5.3 保存修改

Controller层

Service 层

Mapper 层

六、总结


在 Day02 中,我们完成了员工管理和分类管理。今天,我们将进入菜品管理模块的开发。菜品是外卖系统的核心业务数据,涉及图片上传、口味管理、多表操作等复杂场景。通过本文,你将掌握 AOP 公共字段自动填充、阿里云 OSS 文件上传、多表联查分页、事务控制等企业级开发必备技能。


一、公共字段自动填充

1.1 问题分析

在员工管理和分类管理中,我们发现每个新增/修改方法都要重复设置以下字段:

字段含义操作类型
create_time创建时间insert
create_user创建人 idinsert
update_time修改时间insert、update
update_user修改人 idinsert、update

这些代码冗余、重复,能不能统一处理?

答案:使用 AOP 切面编程,实现公共字段自动填充。

1.2 实现思路

  1. 自定义注解@AutoFill,标识需要填充的方法

  2. 自定义切面AutoFillAspect,拦截注解方法,通过反射赋值

  3. 在 Mapper 方法上添加@AutoFill注解

技术点:枚举、注解、AOP、反射。

1.3 代码开发

步骤一:自定义注解
/** * 自定义注解,用于标识某个方法需要进行功能字段自动填充处理 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AutoFill { //数据库操作类型:UPDATE INSERT OperationType value(); }

OperationType枚举(已在 sky-common 中定义):

/** * 数据库操作类型 */ public enum OperationType { /** * 更新操作 */ UPDATE, /** * 插入操作 */ INSERT }
步骤二:自定义切面
/** * 自定义切面,实现公共字段自动填充处理逻辑 */ @Aspect @Component @Slf4j public class AutoFillAspect { /** * 切入点 */ @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)") public void autoFillPointCut(){} /** * 前置通知,在通知中进行公共字段的赋值 */ @Before("autoFillPointCut()") public void autoFill(JoinPoint joinPoint){ log.info("开始进行公共字段自动填充..."); //获取到当前被拦截的方法上的数据库操作类型 MethodSignature signature = (MethodSignature) joinPoint.getSignature();//方法签名对象 AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);//获得方法上的注解对象 OperationType operationType = autoFill.value();//获得数据库操作类型 //获取到当前被拦截的方法的参数--实体对象 Object[] args = joinPoint.getArgs(); if(args == null || args.length == 0){ return; } Object entity = args[0]; //准备赋值的数据 LocalDateTime now = LocalDateTime.now(); Long currentId = BaseContext.getCurrentId(); //根据当前不同的操作类型,为对应的属性通过反射来赋值 if(operationType == OperationType.INSERT){ //为4个公共字段赋值 try { Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class); Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class); Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class); Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class); //通过反射为对象属性赋值 setCreateTime.invoke(entity,now); setCreateUser.invoke(entity,currentId); setUpdateTime.invoke(entity,now); setUpdateUser.invoke(entity,currentId); } catch (Exception e) { e.printStackTrace(); } }else if(operationType == OperationType.UPDATE){ //为2个公共字段赋值 try { Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class); Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class); //通过反射为对象属性赋值 setUpdateTime.invoke(entity,now); setUpdateUser.invoke(entity,currentId); } catch (Exception e) { e.printStackTrace(); } } } }
步骤三:在 Mapper 方法上加注解
@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})") @AutoFill(value = OperationType.INSERT) void insert(Category category); /** * 根据id修改分类 * @param category */ @AutoFill(value = OperationType.UPDATE) void update(Category category); }

同时,将业务层中手动赋值的代码注释掉。

1.4 测试

新增一个分类,查看数据库:

  • create_time、update_time自动填充为当前时间

  • create_user、update_user自动填充为当前登录用户 ID(admin 为 1)


二、新增菜品

2.1 需求分析

新增菜品时需要:

  • 选择菜品分类

  • 上传菜品图片

  • 填写菜品名称、价格、描述

  • 可添加多个口味(如辣度、忌口)

业务规则:

  • 菜品名称唯一

  • 菜品必须属于某个分类

  • 每个菜品对应一张图片

  • 口味可选

涉及表:dish(菜品表)、dish_flavor(口味表)

2.2 文件上传实现

使用阿里云 OSS

1. 配置 OSS 参数(application-dev.yml / application.yml ):

sky: alioss: endpoint: oss-cn-hangzhou.aliyuncs.com access-key-id: 你的AccessKeyId access-key-secret: 你的AccessKeySecret bucket-name: sky-take-out
spring: profiles: active: dev #设置环境 sky: alioss: endpoint: ${sky.alioss.endpoint} access-key-id: ${sky.alioss.access-key-id} access-key-secret: ${sky.alioss.access-key-secret} bucket-name: ${sky.alioss.bucket-name}

2. 读取配置(sky-common 中已定义AliOssProperties)

@Component @ConfigurationProperties(prefix = "sky.alioss") @Data public class AliOssProperties { private String endpoint; private String accessKeyId; private String accessKeySecret; private String bucketName; }

3. 生成 OSS 工具类对象:

/** * 配置类,用于创建AliOssUtil对象 */ @Configuration @Slf4j public class OssConfiguration { @Bean @ConditionalOnMissingBean public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties){ log.info("开始创建阿里云文件上传工具类对象:{}",aliOssProperties); return new AliOssUtil(aliOssProperties.getEndpoint(), aliOssProperties.getAccessKeyId(), aliOssProperties.getAccessKeySecret(), aliOssProperties.getBucketName()); } }

4. 定义文件上传接口:

/** * 通用接口 */ @RestController @RequestMapping("/admin/common") @Api(tags = "通用接口") @Slf4j public class CommonController { @Autowired private AliOssUtil aliOssUtil; /** * 文件上传 * @param file * @return */ @PostMapping("/upload") @ApiOperation("文件上传") public Result<String> upload(MultipartFile file){ log.info("文件上传:{}",file); try { //原始文件名 String originalFilename = file.getOriginalFilename(); //截取原始文件名的后缀 dfdfdf.png String extension = originalFilename.substring(originalFilename.lastIndexOf(".")); //构造新文件名称 String objectName = UUID.randomUUID().toString() + extension; //文件的请求路径 String filePath = aliOssUtil.upload(file.getBytes(), objectName); return Result.success(filePath); } catch (IOException e) { log.error("文件上传失败:{}", e); } return Result.error(MessageConstant.UPLOAD_FAILED); } }

2.3 新增菜品实现

DTO 设计
@Data public class DishDTO implements Serializable { private Long id; //菜品名称 private String name; //菜品分类id private Long categoryId; //菜品价格 private BigDecimal price; //图片 private String image; //描述信息 private String description; //0 停售 1 起售 private Integer status; //口味 private List<DishFlavor> flavors = new ArrayList<>(); }
Controller 层
/** * 菜品管理 */ @RestController @RequestMapping("/admin/dish") @Api(tags = "菜品相关接口") @Slf4j public class DishController { @Autowired private DishService dishService; /** * 新增菜品 * * @param dishDTO * @return */ @PostMapping @ApiOperation("新增菜品") public Result save(@RequestBody DishDTO dishDTO) { log.info("新增菜品:{}", dishDTO); dishService.saveWithFlavor(dishDTO);//后绪步骤开发 return Result.success(); } }
Service 层
public interface DishService { /** * 新增菜品和对应的口味 * * @param dishDTO */ public void saveWithFlavor(DishDTO dishDTO); }
@Service @Slf4j public class DishServiceImpl implements DishService { @Autowired private DishMapper dishMapper; @Autowired private DishFlavorMapper dishFlavorMapper; /** * 新增菜品和对应的口味 * * @param dishDTO */ @Transactional public void saveWithFlavor(DishDTO dishDTO) { Dish dish = new Dish(); BeanUtils.copyProperties(dishDTO, dish); //向菜品表插入1条数据 dishMapper.insert(dish);//后绪步骤实现 //获取insert语句生成的主键值 Long dishId = dish.getId(); List<DishFlavor> flavors = dishDTO.getFlavors(); if (flavors != null && flavors.size() > 0) { flavors.forEach(dishFlavor -> { dishFlavor.setDishId(dishId); }); //向口味表插入n条数据 dishFlavorMapper.insertBatch(flavors);//后绪步骤实现 } } }
Mapper 层

DishMapper.java

/** * 插入菜品数据 * * @param dish */ @AutoFill(value = OperationType.INSERT) void insert(Dish dish);

DishFlavorMapper.java

@Mapper public interface DishFlavorMapper { /** * 批量插入口味数据 * @param flavors */ void insertBatch(List<DishFlavor> flavors); }

DishMapper.xml(使用useGeneratedKeys获取主键):

<?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.DishMapper"> <insert id="insert" useGeneratedKeys="true" keyProperty="id"> insert into dish (name, category_id, price, image, description, create_time, update_time, create_user,update_user, status) values (#{name}, #{categoryId}, #{price}, #{image}, #{description}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser}, #{status}) </insert> </mapper>

DishFlavorMapper.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.DishFlavorMapper"> <insert id="insertBatch"> insert into dish_flavor (dish_id, name, value) VALUES <foreach collection="flavors" item="df" separator=","> (#{df.dishId},#{df.name},#{df.value}) </foreach> </insert> </mapper>

功能测试

进入到菜品管理--->新建菜品


三、菜品分页查询

3.1 需求分析

菜品列表需要分页展示,支持按名称、分类、状态筛选。

特殊字段处理:

  • 图片:数据库中存的是路径,前端需要完整 URL

  • 分类名称:数据库中存的是分类 ID,需要关联分类表查询名称

3.2 代码开发

DTO 与 VO
@Data public class DishPageQueryDTO implements Serializable { private int page; private int pageSize; private String name; private Integer categoryId; //分类id private Integer status; //状态 0表示禁用 1表示启用 }
@Data @Builder @NoArgsConstructor @AllArgsConstructor public class DishVO implements Serializable { private Long id; //菜品名称 private String name; //菜品分类id private Long categoryId; //菜品价格 private BigDecimal price; //图片 private String image; //描述信息 private String description; //0 停售 1 起售 private Integer status; //更新时间 private LocalDateTime updateTime; //分类名称 private String categoryName; //菜品关联的口味 private List<DishFlavor> flavors = new ArrayList<>(); }
Controller 层
/** * 菜品分页查询 * * @param dishPageQueryDTO * @return */ @GetMapping("/page") @ApiOperation("菜品分页查询") public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO) { log.info("菜品分页查询:{}", dishPageQueryDTO); PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);//后绪步骤定义 return Result.success(pageResult); }
Service 层
/** * 菜品分页查询 * * @param dishPageQueryDTO * @return */ PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO);
/** * 菜品分页查询 * * @param dishPageQueryDTO * @return */ public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) { PageHelper.startPage(dishPageQueryDTO.getPage(), dishPageQueryDTO.getPageSize()); Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO);//后绪步骤实现 return new PageResult(page.getTotal(), page.getResult()); }
Mapper 层
/** * 菜品分页查询 * * @param dishPageQueryDTO * @return */ Page<DishVO> pageQuery(DishPageQueryDTO dishPageQueryDTO);

多表联查:

<select id="pageQuery" resultType="com.sky.vo.DishVO"> select d.* , c.name as categoryName from dish d left outer join category c on d.category_id = c.id <where> <if test="name != null"> and d.name like concat('%',#{name},'%') </if> <if test="categoryId != null"> and d.category_id = #{categoryId} </if> <if test="status != null"> and d.status = #{status} </if> </where> order by d.create_time desc </select>

功能测试

接口文档测试

启动服务:访问http://localhost:8080/doc.html,进入菜品分页查询接口

注意:使用admin用户登录重新获取token,防止token失效。

点击发送:

前后端联调测试

启动nginx,访问 http://localhost

点击菜品管理


四、删除菜品

4.1 需求分析

业务规则:

  • 支持单个删除和批量删除

  • 起售中的菜品不能删除

  • 被套餐关联的菜品不能删除

  • 删除菜品后,关联的口味数据也要删除

涉及表:dish、dish_flavor、setmeal_dish

4.2 代码开发

Controller 层
/** * 菜品批量删除 * * @param ids * @return */ @DeleteMapping @ApiOperation("菜品批量删除") public Result delete(@RequestParam List<Long> ids) { log.info("菜品批量删除:{}", ids); dishService.deleteBatch(ids);//后绪步骤实现 return Result.success(); }
Service 层
/** * 菜品批量删除 * * @param ids */ void deleteBatch(List<Long> ids);
@Autowired private SetmealDishMapper setmealDishMapper; /** * 菜品批量删除 * * @param ids */ @Transactional//事务 public void deleteBatch(List<Long> ids) { //判断当前菜品是否能够删除---是否存在起售中的菜品?? for (Long id : ids) { Dish dish = dishMapper.getById(id);//后绪步骤实现 if (dish.getStatus() == StatusConstant.ENABLE) { //当前菜品处于起售中,不能删除 throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE); } } //判断当前菜品是否能够删除---是否被套餐关联了?? List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids); if (setmealIds != null && setmealIds.size() > 0) { //当前菜品被套餐关联了,不能删除 throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL); } //删除菜品表中的菜品数据 for (Long id : ids) { dishMapper.deleteById(id);//后绪步骤实现 //删除菜品关联的口味数据 dishFlavorMapper.deleteByDishId(id);//后绪步骤实现 } }
Mapper层

在DishMapper中声明getById方法,并配置SQL:

/** * 根据主键查询菜品 * * @param id * @return */ @Select("select * from dish where id = #{id}") Dish getById(Long id);

创建SetmealDishMapper,声明getSetmealIdsByDishIds方法,并在xml文件中编写SQL:

@Mapper public interface SetmealDishMapper { /** * 根据菜品id查询对应的套餐id * * @param dishIds * @return */ //select setmeal_id from setmeal_dish where dish_id in (1,2,3,4) List<Long> getSetmealIdsByDishIds(List<Long> dishIds); } ​

SetmealDishMapper.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.SetmealDishMapper"> <select id="getSetmealIdsByDishIds" resultType="java.lang.Long"> select setmeal_id from setmeal_dish where dish_id in <foreach collection="dishIds" item="dishId" separator="," open="(" close=")"> #{dishId} </foreach> </select> </mapper>

在DishMapper.java中声明deleteById方法并配置SQL:

/** * 根据主键删除菜品数据 * * @param id */ @Delete("delete from dish where id = #{id}") void deleteById(Long id);

在DishFlavorMapper中声明deleteByDishId方法并配置SQL:

/** * 根据菜品id删除对应的口味数据 * @param dishId */ @Delete("delete from dish_flavor where dish_id = #{dishId}") void deleteByDishId(Long dishId);

五、修改菜品

5.1 需求分析

修改菜品分为两步:查询回显和保存修改。

5.2 查询回显

Controller 层
/** * 根据id查询菜品 * * @param id * @return */ @GetMapping("/{id}") @ApiOperation("根据id查询菜品") public Result<DishVO> getById(@PathVariable Long id) { log.info("根据id查询菜品:{}", id); DishVO dishVO = dishService.getByIdWithFlavor(id);//后绪步骤实现 return Result.success(dishVO); }
Service 层
/** * 根据id查询菜品和对应的口味数据 * * @param id * @return */ DishVO getByIdWithFlavor(Long id);
/** * 根据id查询菜品和对应的口味数据 * * @param id * @return */ public DishVO getByIdWithFlavor(Long id) { //根据id查询菜品数据 Dish dish = dishMapper.getById(id); //根据菜品id查询口味数据 List<DishFlavor> dishFlavors = dishFlavorMapper.getByDishId(id);//后绪步骤实现 //将查询到的数据封装到VO DishVO dishVO = new DishVO(); BeanUtils.copyProperties(dish, dishVO); dishVO.setFlavors(dishFlavors); return dishVO; }
Mapper层
/** * 根据菜品id查询对应的口味数据 * @param dishId * @return */ @Select("select * from dish_flavor where dish_id = #{dishId}") List<DishFlavor> getByDishId(Long dishId);

5.3 保存修改

Controller层
/** * 修改菜品 * * @param dishDTO * @return */ @PutMapping @ApiOperation("修改菜品") public Result update(@RequestBody DishDTO dishDTO) { log.info("修改菜品:{}", dishDTO); dishService.updateWithFlavor(dishDTO); return Result.success(); }
Service 层
/** * 根据id修改菜品基本信息和对应的口味信息 * * @param dishDTO */ void updateWithFlavor(DishDTO dishDTO);
/** * 根据id修改菜品基本信息和对应的口味信息 * * @param dishDTO */ public void updateWithFlavor(DishDTO dishDTO) { Dish dish = new Dish(); BeanUtils.copyProperties(dishDTO, dish); //修改菜品表基本信息 dishMapper.update(dish); //删除原有的口味数据 dishFlavorMapper.deleteByDishId(dishDTO.getId()); //重新插入口味数据 List<DishFlavor> flavors = dishDTO.getFlavors(); if (flavors != null && flavors.size() > 0) { flavors.forEach(dishFlavor -> { dishFlavor.setDishId(dishDTO.getId()); }); //向口味表插入n条数据 dishFlavorMapper.insertBatch(flavors); } }
Mapper 层
/** * 根据id动态修改菜品数据 * * @param dish */ @AutoFill(value = OperationType.UPDATE) void update(Dish dish);
<update id="update"> update dish <set> <if test="name != null">name = #{name},</if> <if test="categoryId != null">category_id = #{categoryId},</if> <if test="price != null">price = #{price},</if> <if test="image != null">image = #{image},</if> <if test="description != null">description = #{description},</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>

六、总结

今天完成了菜品管理的完整功能,核心知识点如下:

功能技术要点
公共字段填充AOP + 自定义注解 + 反射,统一处理 create_time 等字段
文件上传阿里云 OSS,MultipartFile 接收文件,UUID 生成文件名
新增菜品多表操作(dish + dish_flavor),事务控制,批量插入
分页查询PageHelper,多表联查(left join),VO 封装分类名称
删除菜品业务校验(起售状态、套餐关联),事务控制,级联删除
修改菜品回显(多表查询),先删后增处理口味,动态 SQL

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

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

立即咨询