目录
一、公共字段自动填充
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 | 创建人 id | insert |
| update_time | 修改时间 | insert、update |
| update_user | 修改人 id | insert、update |
这些代码冗余、重复,能不能统一处理?
答案:使用 AOP 切面编程,实现公共字段自动填充。
1.2 实现思路
自定义注解
@AutoFill,标识需要填充的方法自定义切面
AutoFillAspect,拦截注解方法,通过反射赋值在 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-outspring: 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 |