目录一、公共字段自动填充1.1 问题分析1.2 实现思路1.3 代码开发步骤一自定义注解步骤二自定义切面步骤三在 Mapper 方法上加注解1.4 测试二、新增菜品2.1 需求分析2.2 文件上传实现使用阿里云 OSS2.3 新增菜品实现DTO 设计Controller 层Service 层Mapper 层功能测试三、菜品分页查询3.1 需求分析3.2 代码开发DTO 与 VOController 层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创建时间insertcreate_user创建人 idinsertupdate_time修改时间insert、updateupdate_user修改人 idinsert、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自动填充为当前登录用户 IDadmin 为 1二、新增菜品2.1 需求分析新增菜品时需要选择菜品分类上传菜品图片填写菜品名称、价格、描述可添加多个口味如辣度、忌口业务规则菜品名称唯一菜品必须属于某个分类每个菜品对应一张图片口味可选涉及表dish菜品表、dish_flavor口味表2.2 文件上传实现使用阿里云 OSS1. 配置 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 中已定义AliOssPropertiesComponent 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 ResultString 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 ListDishFlavor 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(); ListDishFlavor 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.javaMapper public interface DishFlavorMapper { /** * 批量插入口味数据 * param flavors */ void insertBatch(ListDishFlavor flavors); }DishMapper.xml使用useGeneratedKeys获取主键?xml version1.0 encodingUTF-8 ? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.sky.mapper.DishMapper insert idinsert useGeneratedKeystrue keyPropertyid 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 /mapperDishFlavorMapper.xml批量插入?xml version1.0 encodingUTF-8 ? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.sky.mapper.DishFlavorMapper insert idinsertBatch insert into dish_flavor (dish_id, name, value) VALUES foreach collectionflavors itemdf separator, (#{df.dishId},#{df.name},#{df.value}) /foreach /insert /mapper功能测试进入到菜品管理---新建菜品三、菜品分页查询3.1 需求分析菜品列表需要分页展示支持按名称、分类、状态筛选。特殊字段处理图片数据库中存的是路径前端需要完整 URL分类名称数据库中存的是分类 ID需要关联分类表查询名称3.2 代码开发DTO 与 VOData 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 ListDishFlavor flavors new ArrayList(); }Controller 层/** * 菜品分页查询 * * param dishPageQueryDTO * return */ GetMapping(/page) ApiOperation(菜品分页查询) public ResultPageResult 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()); PageDishVO page dishMapper.pageQuery(dishPageQueryDTO);//后绪步骤实现 return new PageResult(page.getTotal(), page.getResult()); }Mapper 层/** * 菜品分页查询 * * param dishPageQueryDTO * return */ PageDishVO pageQuery(DishPageQueryDTO dishPageQueryDTO);多表联查select idpageQuery resultTypecom.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 testname ! null and d.name like concat(%,#{name},%) /if if testcategoryId ! null and d.category_id #{categoryId} /if if teststatus ! 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_dish4.2 代码开发Controller 层/** * 菜品批量删除 * * param ids * return */ DeleteMapping ApiOperation(菜品批量删除) public Result delete(RequestParam ListLong ids) { log.info(菜品批量删除{}, ids); dishService.deleteBatch(ids);//后绪步骤实现 return Result.success(); }Service 层/** * 菜品批量删除 * * param ids */ void deleteBatch(ListLong ids);Autowired private SetmealDishMapper setmealDishMapper; /** * 菜品批量删除 * * param ids */ Transactional//事务 public void deleteBatch(ListLong ids) { //判断当前菜品是否能够删除---是否存在起售中的菜品 for (Long id : ids) { Dish dish dishMapper.getById(id);//后绪步骤实现 if (dish.getStatus() StatusConstant.ENABLE) { //当前菜品处于起售中不能删除 throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE); } } //判断当前菜品是否能够删除---是否被套餐关联了 ListLong 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文件中编写SQLMapper public interface SetmealDishMapper { /** * 根据菜品id查询对应的套餐id * * param dishIds * return */ //select setmeal_id from setmeal_dish where dish_id in (1,2,3,4) ListLong getSetmealIdsByDishIds(ListLong dishIds); } ​SetmealDishMapper.xml?xml version1.0 encodingUTF-8 ? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.sky.mapper.SetmealDishMapper select idgetSetmealIdsByDishIds resultTypejava.lang.Long select setmeal_id from setmeal_dish where dish_id in foreach collectiondishIds itemdishId 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 ResultDishVO 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查询口味数据 ListDishFlavor 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}) ListDishFlavor 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()); //重新插入口味数据 ListDishFlavor 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 idupdate update dish set if testname ! nullname #{name},/if if testcategoryId ! nullcategory_id #{categoryId},/if if testprice ! nullprice #{price},/if if testimage ! nullimage #{image},/if if testdescription ! nulldescription #{description},/if if teststatus ! nullstatus #{status},/if if testupdateTime ! nullupdate_time #{updateTime},/if if testupdateUser ! nullupdate_user #{updateUser},/if /set where id #{id} /update六、总结今天完成了菜品管理的完整功能核心知识点如下功能技术要点公共字段填充AOP 自定义注解 反射统一处理 create_time 等字段文件上传阿里云 OSSMultipartFile 接收文件UUID 生成文件名新增菜品多表操作dish dish_flavor事务控制批量插入分页查询PageHelper多表联查left joinVO 封装分类名称删除菜品业务校验起售状态、套餐关联事务控制级联删除修改菜品回显多表查询先删后增处理口味动态 SQL