day9--- 苍穹外卖-day03

14 阅读6分钟

你在 Day03 真正要练会的 6 件事

Day03 的核心不是“抄代码”,而是把一套后台典型 CRUD 能力做扎实:

  1. AOP + 注解 + 反射消灭“公共字段重复赋值”(createTime/createUser/updateTime/updateUser)
  2. 做一个可复用的 文件上传接口(OSS) ,供菜品图片等功能使用
  3. 完成 新增菜品(含口味表批量插入) ,并用事务保证一致性
  4. 完成 菜品分页查询(多条件 + 分类名联表回显)
  5. 完成 删除菜品(规则校验:起售/被套餐关联/级联删除口味)
  6. 完成 修改菜品(回显 + 基本信息更新 + 口味先删后插)

学完 Day03,你应该能“自己设计并落地一个后台模块”,而不只是跟着敲。


Day03 代码地图(你要在这些地方来回切换)

  • sky-server

    • controller/admin:接口入口(DishController、CommonController)
    • service/impl:业务编排(事务、校验、组合表操作)
    • aspect:AutoFillAspect(公共字段统一填充)
    • config:OssConfiguration(把 AliOssUtil 交给 Spring 管)
  • sky-pojo

    • DTO/VO:DishDTO、DishPageQueryDTO、DishVO 等
  • sky-common

    • properties:AliOssProperties 读取 yml 配置
    • utils:AliOssUtil 真正上传到 OSS
  • resources/mapper

    • DishMapper.xml / DishFlavorMapper.xml / SetmealDishMapper.xml:SQL 落点

第一部分:公共字段自动填充(Day03 的“内功”)

1) 为什么要做

你之前在新增/修改员工、分类时都在业务里写 createTime/updateTime 等字段赋值,重复且容易漏。Day03 用 AOP 统一处理:只要是 insert/update 的 mapper 方法,自动补齐公共字段

2) 设计要点(你必须想清楚这 3 个问题)

  • 我拦截谁? —— 拦截 com.sky.mapper.*.*(..) 且方法上有 @AutoFill 的方法
  • 我怎么知道这是 INSERT 还是 UPDATE? —— 注解里带枚举 OperationType
  • 我往谁身上填字段? —— mapper 方法的第一个参数实体对象,用反射调用 setter(setCreateTime 等)

3) 落地步骤(按这个顺序做,少走弯路)

Step A:定义注解 @AutoFill(标记“需要自动填充”的 mapper 方法)
Step B:写切面 AutoFillAspect(前置通知里:取 operationType → 取实体参数 → 反射 set)
Step C:在 mapper 方法上加注解(CategoryMapper、EmployeeMapper、DishMapper 的 insert/update)
Step D:把 service 里手动赋值那坨代码注释掉(否则会重复/混乱)

4) 调试验证清单(你一条条过)

  • ✅ 能进 AutoFillAspect.autoFill(),日志出现 “开始进行公共字段自动填充...”
  • ✅ 执行插入时 SQL 带上 create_time/create_user/update_time/update_user(看控制台 SQL)
  • ✅ update 时只改 updateTime/updateUser(符合规则)

5) 高频坑(很容易卡你 1 小时)

  • 反射找不到方法:实体类必须有 setCreateTime(LocalDateTime) 这种标准 setter,且名字要跟常量一致(别手滑写成 setCreate_time)。
  • joinPoint.getArgs()[0] 不是实体:如果你的 mapper 方法第一个参数不是实体(例如多个参数),此方案会失效,需要改切面策略。
  • currentId 为空:BaseContext 必须在登录校验/拦截器里提前 set(不然 createUser/updateUser 会是 null)

第二部分:文件上传(CommonController + OSS)

你要的结果

前端选择图片 → 调 /admin/common/upload → 返回图片 URL,后续新增菜品直接把 URL 存数据库。

实现骨架(你要理解“配置怎么流动”)

  1. yml 配置 endpoint/key/bucket(dev 环境)
  2. AliOssProperties 读取配置
  3. OssConfiguration 生成 AliOssUtil Bean
  4. CommonController.upload():生成随机文件名、调用 aliOssUtil.upload() 返回路径

建议你顺手加的 3 个“实战增强”

  • 上传前校验后缀(jpg/png/webp),避免乱传
  • 限制文件大小(Spring 配置 + 代码兜底)
  • OSS key 不要硬编码进仓库(用本地 dev 配置或环境变量)

第三部分:新增菜品(dish + dish_flavor,两张表一次搞定)

1) 关键数据结构

DishDTO 里 flavors 是个 List(可以为空)

2) 关键链路(你要能背出来)

Controller 收 DTO → Service 开事务 → insert dish 拿到主键 → 批量插入 flavors

3) 一次写对的要点

  • dishMapper.insert(dish) 必须能回填主键(useGeneratedKeys="true" keyProperty="id"
  • flavors 可能为空:空就别 insertBatch(你文档里就是这么处理的)
  • 必须加事务:dish 插入成功但 flavors 失败时要整体回滚

第四部分:菜品分页查询(最像真实后台开发)

你要解决的“展示问题”

列表里除了菜品字段,还要回显:

  • 图片:数据库存的是 URL/文件名,前端直接展示
  • 分类:页面显示的是分类名称,所以要联表查 categoryName

实现套路(通用!以后所有分页都这么写)

  • DTO:page/pageSize/name/categoryId/status
  • Service:PageHelper.startPage → mapper 查 Page → new PageResult(total, list)
  • SQL:left join + 动态 where + order by create_time desc

自测(别只看页面,要会用接口测)

Swagger:/admin/dish/page,注意 token 失效要重新登录拿 token


第五部分:删除菜品(规则校验 + 事务)

业务规则你必须“先校验再删除”

  • 起售中的菜品不能删
  • 被套餐关联的菜品不能删
  • 删除菜品时要删除口味表数据

正确实现顺序(非常重要)

  1. 遍历 ids:查 dish,若 status==ENABLE → 直接抛异常
  2. 查 setmeal_dish 是否有关联:有则抛异常
  3. 真正删除:dish 表 + dish_flavor 表(循环删)
  4. 事务包住(中途失败要回滚)

第六部分:修改菜品(回显 + 更新 + 口味重建)

1) 回显:根据 id 查菜品 + 查口味

DishVO = dish + flavors

2) 修改:基本信息更新 + 口味先删后插

流程非常典型:update dish → delete flavors → insertBatch new flavors
并且 dishMapper.update 也走 AutoFill,自动补 updateTime/updateUser


建议你这样学 Day03:2 小时“闭环训练”

第 1 轮(30 分钟):只做自动填充闭环

  • 给 CategoryMapper / EmployeeMapper / DishMapper 的 insert/update 加 @AutoFill
  • 跑新增分类/修改分类,看 SQL 是否自动补齐字段

第 2 轮(40 分钟):打通“上传 → 新增菜品”闭环

  • /admin/common/upload 返回 URL
  • 新增菜品接口保存 dish + dish_flavor(表里能查到)

第 3 轮(50 分钟):分页/删除/修改三连

  • 分页:多条件查(name/categoryId/status)
  • 删除:分别测“起售不能删”“被套餐关联不能删”“停售能删且口味被删”
  • 修改:回显成功 → 改价格/口味 → 保存成功

你做完后,用这 10 个问题自测(答不上来就回头补)

  1. 为什么 AutoFill 放在 mapper 层方法上而不是 service?
  2. AOP 里怎么判断 INSERT/UPDATE?
  3. 为什么 joinPoint 的第一个参数要是实体对象?
  4. useGeneratedKeys 的作用是什么?没有会怎样?
  5. 为什么新增菜品必须加 @Transactional
  6. 分页查询为什么要 PageHelper.startPage 写在 mapper 调用之前?
  7. 联表查分类名为什么用 left join?
  8. 删除菜品为什么要先校验再删除?顺序反了会怎样?
  9. 修改口味为什么是“先删后插”而不是“对比增删改”?
  10. 修改 dish 时 updateTime/updateUser 是谁填的?在哪里填?

如果你愿意更“像带练”一点:你把你现在卡住的点(比如:AutoFill 进不去、dishId 回填为 null、分页查不到分类名、删除一直提示被套餐关联等)贴出报错/日志/相关方法代码,我就按 Day03 的链路帮你快速定位到具体类、具体 SQL 或具体配置。