tasks.md 46 KB

Tasks: 促销 + 优惠券系统

Input: Design documents from /specs/008-promotion-coupon/ Prerequisites: plan.md (required), spec.md (required for user stories)

Format: [ID] [P?] [Story] Description

  • [P]: Can run in parallel (different files, no dependencies)
  • [Story]: Which user story this task belongs to (US1=促销活动, US2=优惠券)
  • Include exact file paths in descriptions

Phase 1: Setup (数据库 + 基础设施)

Purpose: 创建数据库表,为后续所有任务提供基础

  • T001 追加 6 张新表 DDL 到 foodie_server/updatesql/sql.md(promotion_activity, promotion_activity_rule, promotion_coupon_batch, promotion_coupon_rule, promotion_user_coupon, pos_order_promotion),DDL 来自 spec.md Data Model 章节
  • T002 手动执行 SQL 建表(提醒开发者操作)

Phase 2: Foundational (后端实体 + 数据层)

Purpose: 后端核心数据层,所有 Controller 和前端页面依赖此阶段完成

实体类 (5个文件,全部可并行)

  • T003 [P] 创建 PromotionActivity 实体 ruoyi-system/src/main/java/com/ruoyi/system/domain/PromotionActivity.java — 字段: id, storeId, type, name, status, startTime, endTime, createTime, updateTime;非持久化: List rules, boolean editable。使用 @Data @TableName @TableId(type=IdType.AUTO) @JsonFormat 注解,参考 SalesPromotion.java
  • T004 [P] 创建 PromotionActivityRule 实体 ruoyi-system/src/main/java/com/ruoyi/system/domain/PromotionActivityRule.java — 字段: id, activityId, productId, threshold(BigDecimal), reduceAmount(BigDecimal), discountRate(BigDecimal), minQuantity(Integer);非持久化: productName, productImage, productPrice
  • T005 [P] 创建 PromotionCouponBatch 实体 ruoyi-system/src/main/java/com/ruoyi/system/domain/PromotionCouponBatch.java — 字段: id, storeId, name, couponType, totalCount, remainCount, receivedCount, status, startTime, endTime, validDays, createTime, updateTime;非持久化: PromotionCouponRule rule, boolean editable
  • T006 [P] 创建 PromotionCouponRule 实体 ruoyi-system/src/main/java/com/ruoyi/system/domain/PromotionCouponRule.java — 字段: id, batchId, productId, isMutex(Integer), threshold(BigDecimal), amount(BigDecimal), discountRate(BigDecimal);非持久化: productName, productImage, productPrice
  • T007 [P] 创建 PromotionUserCoupon 实体 ruoyi-system/src/main/java/com/ruoyi/system/domain/PromotionUserCoupon.java — 字段: id, userId, batchId, storeId, status, orderId, receiveTime, useTime, expireTime
  • T007a [P] 创建 PosOrderPromotion 实体 ruoyi-system/src/main/java/com/ruoyi/system/domain/PosOrderPromotion.java — 字段: id, orderId, promoType, promoSubType, promoId, userCouponId, promoName, promoDetail, reduceAmount, pathSummary, createTime
  • Mapper 接口 (全部可并行)

    • T008 [P] 创建 PromotionActivityMapper ruoyi-system/src/main/java/com/ruoyi/system/mapper/PromotionActivityMapper.java — extends BaseMapper
    • T009 [P] 创建 PromotionActivityRuleMapper ruoyi-system/src/main/java/com/ruoyi/system/mapper/PromotionActivityRuleMapper.java — extends BaseMapper,额外方法 List selectRulesByActivityId(Long activityId)
    • T010 [P] 创建 PromotionCouponBatchMapper ruoyi-system/src/main/java/com/ruoyi/system/mapper/PromotionCouponBatchMapper.java — extends BaseMapper
    • T011 [P] 创建 PromotionCouponRuleMapper ruoyi-system/src/main/java/com/ruoyi/system/mapper/PromotionCouponRuleMapper.java — extends BaseMapper,额外方法 PromotionCouponRule selectRuleByBatchId(Long batchId)
    • T012 [P] 创建 PromotionUserCouponMapper ruoyi-system/src/main/java/com/ruoyi/system/mapper/PromotionUserCouponMapper.java — extends BaseMapper
    • T012a [P] 创建 PosOrderPromotionMapper ruoyi-system/src/main/java/com/ruoyi/system/mapper/PosOrderPromotionMapper.java — extends BaseMapper
    • Mapper XML (全部可并行)

      • T013 [P] 创建 PromotionActivityMapper.xml ruoyi-system/src/main/resources/mapper/system/PromotionActivityMapper.xml — resultMap 映射全部字段
      • T014 [P] 创建 PromotionActivityRuleMapper.xml ruoyi-system/src/main/resources/mapper/system/PromotionActivityRuleMapper.xml — resultMap + selectRulesByActivityId (LEFT JOIN pos_food 获取 productName/productImage/productPrice)
      • T015 [P] 创建 PromotionCouponBatchMapper.xml ruoyi-system/src/main/resources/mapper/system/PromotionCouponBatchMapper.xml — resultMap 映射全部字段
      • T016 [P] 创建 PromotionCouponRuleMapper.xml ruoyi-system/src/main/resources/mapper/system/PromotionCouponRuleMapper.xml — resultMap + selectRuleByBatchId (LEFT JOIN pos_food)
      • T017 [P] 创建 PromotionUserCouponMapper.xml ruoyi-system/src/main/resources/mapper/system/PromotionUserCouponMapper.xml — resultMap 映射全部字段
      • T017a [P] 创建 PosOrderPromotionMapper.xml ruoyi-system/src/main/resources/mapper/system/PosOrderPromotionMapper.xml — resultMap 映射全部字段

      Checkpoint: 数据层完成,可进入 Service 和 Controller


      Phase 3: User Story 1 - 商家创建促销活动 (Priority: P1) 🎯 MVP

      Goal: 商家能创建满减/折扣/第二份半价/新客立减活动,查看活动列表,结束进行中的活动

      Independent Test: 商家创建一个满减活动(满20减5/满40减12),列表显示进行中,能查看详情,能结束活动

      Service 层 (US1)

      • T018 [US1] 创建 IPromotionActivityService ruoyi-system/src/main/java/com/ruoyi/system/service/IPromotionActivityService.java — extends IService,方法: createActivity(activity, rules), selectActivityWithRules(id), endActivity(id)
      • T019 [US1] 创建 PromotionActivityServiceImpl ruoyi-system/src/main/java/com/ruoyi/system/service/impl/PromotionActivityServiceImpl.java — 实现 createActivity (@Transactional: 插入activity+批量插入rules, 根据 startTime 设置初始 status), selectActivityWithRules (查activity+查rules), endActivity (status→2)
      • T020 [US1] 创建 IPromotionActivityRuleService + PromotionActivityRuleServiceImpl ruoyi-system/src/main/java/com/ruoyi/system/service/IPromotionActivityRuleService.java 和 impl/PromotionActivityRuleServiceImpl.java — selectRulesByActivityId 委托给 mapper
      • Controller (US1)

        • T021 [US1] 创建 ShPromotionActivityController ruoyi-admin/src/main/java/com/ruoyi/app/mendian/ShPromotionActivityController.java — @RestController @RequestMapping("/system/shPromotionActivity"),参考 ShSalesPromotionController。端点: GET /list (page,size,storeId,type?,status? → LambdaQueryWrapper 分页), GET /{id} (selectActivityWithRules), POST / (RequestBody含rules→createActivity), PUT /{id}/end (endActivity)

        前端 API (US1)

        • T022 [US1] 创建 promotionActivity.js foodie-store/src/api/promotionActivity.js — 导出 listPromotionActivity(query), getPromotionActivity(id), addPromotionActivity(data), endPromotionActivity(id),参考 src/api/promotion.js 模式

        前端页面 (US1)

        • T023 [US1] 创建 PromotionActivity.vue foodie-store/src/views/PromotionActivity.vue — 促销活动管理页面:
          • 列表区域: 面包屑 + 门店选择(复用xuanzhemd模式) + 状态筛选radio(全部/未开始/进行中/已结束) + el-table(ID/名称/类型el-tag/状态el-tag/时间/操作-结束按钮) + el-pagination
          • 创建对话框 el-dialog width=750px + el-tabs v-model=activeTab:
          • Tab1 满减(type=1): 活动名称 + 时间范围 + 动态档位行(v-for, 每行两个el-input-number:满X减Y) + 添加/删除档位按钮
          • Tab2 折扣(type=2): 活动名称 + 时间范围 + 折扣组列表(v-for, 每组含折扣率+已选商品table+添加商品按钮) + 添加/删除折扣区按钮。同一商品只能分配到一个折扣区,添加时自动从其他折扣区移除
          • Tab3 第二份半价(type=3): 活动名称 + 时间范围 + 已选商品table + 选择商品按钮
          • Tab4 新客立减(type=4): 活动名称 + 时间范围 + 减免金额el-input-number
          • 商品选择弹窗: 分类el-select + 搜索el-input + 商品el-table + el-pagination (复用Quanyi.vue商品加载API模式)
          • 提交逻辑 tijiaobaocun(): 根据activeTab转换数据为 {storeId,type,name,startTime,endTime,rules:[...]},折扣组展开为扁平rules,调用addPromotionActivity
          • 互斥提示: 创建对话框顶部展示互斥规则提示文案(FR-006):"满减、折扣、第二份半价三选一(系统自动选最优),新客立减可叠加"
          • i18n: 所有文字用 $t('promoActivity.xxx')

        Checkpoint: 促销活动完整功能可用——创建4种类型活动、查看列表、查看详情、结束活动


        Phase 4: User Story 2 - 商家创建优惠券 (Priority: P2)

        Goal: 商家能创建满减券/商品券,设置同享/互斥属性和库存,查看列表,下架券

        Independent Test: 商家创建"满30减5"满减券100张,列表显示,能查看详情,能下架

        Service 层 (US2)

        • T024 [P] [US2] 创建 IPromotionCouponBatchService ruoyi-system/src/main/java/com/ruoyi/system/service/IPromotionCouponBatchService.java — 方法: createBatch(batch, rule), selectBatchWithRule(id), offShelfBatch(id)
        • T025 [P] [US2] 创建 PromotionCouponBatchServiceImpl ruoyi-system/src/main/java/com/ruoyi/system/service/impl/PromotionCouponBatchServiceImpl.java — 实现 createBatch (@Transactional: remainCount=totalCount, 插入batch+rule), selectBatchWithRule (查batch+查rule), offShelfBatch (status→3)
        • T026 [P] [US2] 创建 IPromotionCouponRuleService + PromotionCouponRuleServiceImpl ruoyi-system/src/main/java/com/ruoyi/system/service/IPromotionCouponRuleService.java 和 impl/PromotionCouponRuleServiceImpl.java — selectRuleByBatchId 委托给 mapper

        Controller (US2)

        • T027 [US2] 创建 ShPromotionCouponController ruoyi-admin/src/main/java/com/ruoyi/app/mendian/ShPromotionCouponController.java — @RestController @RequestMapping("/system/shPromotionCoupon")。端点: GET /list (page,size,storeId,couponType?,status?), GET /{id} (selectBatchWithRule), POST / (RequestBody含rule→createBatch), PUT /{id}/offShelf

        前端 API (US2)

        • T028 [P] [US2] 创建 promotionCoupon.js foodie-store/src/api/promotionCoupon.js — 导出 listPromotionCoupon(query), getPromotionCoupon(id), addPromotionCoupon(data), offShelfPromotionCoupon(id)

        前端页面 (US2)

        • T029 [US2] 创建 CouponBatch.vue foodie-store/src/views/CouponBatch.vue — 优惠券管理页面:
          • 列表区域: 面包屑 + 门店选择 + 状态筛选(全部/未开始/进行中/已结束/已下架) + el-table(ID/名称/类型/总库存/剩余/已领取/状态/时间/有效期/操作-下架按钮) + el-pagination
          • 创建对话框 el-dialog width=600px:
          • 券类型 el-radio-group (满减券/商品券)
          • 通用字段: 名称el-input, 门店el-select, 总量el-input-number, 有效天数el-input-number, 时间范围el-date-picker
          • 满减券(v-if couponType===1): 叠加属性el-radio(同享/互斥), 使用门槛el-input-number, 减免金额el-input-number
          • 商品券(v-if couponType===2): 选择商品按钮(复用商品弹窗), 折扣率el-input-number
          • 提交逻辑 tijiaobaocun(): 构建 {storeId,name,couponType,totalCount,validDays,startTime,endTime,rule:{isMutex,threshold,amount,...}},调用addPromotionCoupon
          • i18n: 所有文字用 $t('couponBatch.xxx')

        Checkpoint: 优惠券完整功能可用——创建满减券/商品券、查看列表、查看详情、下架券


        Phase 5: Menu + Router + i18n (跨 US)

        Purpose: 前端菜单、路由、多语言——连接所有页面的基础设施

        注意: 前端文件使用 CRLF 换行,必须用 Python 脚本编辑,不能用 Edit 工具。

        • T030 在 foodie-store/src/router/index.js 的 /manage children 中添加路由 promotion-activity → PromotionActivity.vue 和 coupon-batch → CouponBatch.vue
        • T031 在 foodie-store/src/components/Aside.vue 末尾(el-submenu index="6" 之后)添加新的 el-submenu index="7"「营销管理」,含两个 menu-item: 促销活动(/manage/promotion-activity) + 优惠券管理(/manage/coupon-batch)
        • T032 [P] 在 foodie-store/src/lang/zh.js 末尾追加 promoMenu(营销管理/促销活动/优惠券管理)、promoActivity(~40个key覆盖页面全部文案)、couponBatch(~30个key)三个 i18n section
        • T033 [P] 在 foodie-store/src/lang/tw.js 追加同结构繁体中文翻译
        • T034 [P] 在 foodie-store/src/lang/en.js 追加同结构英文翻译
        • T035 [P] 在 foodie-store/src/lang/vi.js 追加同结构越南语翻译

        Checkpoint: 侧边栏显示新菜单,页面可访问,多语言正常


        Phase 5.5: 用户端后端接口 (FR-011~022)

        Purpose: 实现用户端的领券、查券、算价计算后端接口(前端界面由小程序实施,不做)

        Goal: 用户端小程序可调用后端 API 完成领券、查看优惠券、结算页优惠计算

        Independent Test: 用 Postman 调用算价接口,传入购物车商品,返回两条路径对比 + 最优方案 + 可用券列表

        Service 层

        • T039 [P] 创建 IPromotionUserCouponService + PromotionUserCouponServiceImpl ruoyi-system/src/main/java/com/ruoyi/system/service/IPromotionUserCouponService.java 和 impl/PromotionUserCouponServiceImpl.java — 方法: receiveCoupon(userId, batchId, storeId)(检查限领1张、乐观锁扣库存 UPDATE remain_count=remain_count-1 WHERE remain_count>0、计算expireTime=领取日期+validDays天23:59:59、@Transactional), selectStoreCouponsForUser(userId, storeId)(查询门店进行中券批次+用户是否已领标记,不分页), selectMyCoupons(userId, status?, storeId?)(返回含storeName,实时判断expire_time<NOW()则status→2)
        • T040 创建 IPromotionCalcService + PromotionCalcServiceImpl ruoyi-system/src/main/java/com/ruoyi/system/service/IPromotionCalcService.java 和 impl/PromotionCalcServiceImpl.java — 核心算价逻辑:
          • calculate(storeId, items[{productId,quantity}], userId, couponId?, forcePath?) → 返回路径对比结果
          • 内部逻辑:
          • 根据productId从数据库查询商品实际价格(不信任前端传价)
          • 查门店所有进行中的促销活动(满减、折扣、第二份半价、新客立减)
          • 路径A(走折扣/第二份半价):分别计算折扣和第二份半价各一遍,取更优作为路径A结果。折扣商品按discountRate,第二份半价按每2件第2件半价,非活动商品原价
          • 路径B(走满减):所有商品原价 → 匹配满减最高可达档位 → 合计
          • 对比选最优(金额相等默认选B);无促销时两条路径同价返回原价
          • 查用户是否为该门店新客(查询该门店已完成订单数=0),新客则叠加新客立减
          • 如传couponId:检查互斥规则(互斥券不能与满减/折扣叠加,商品券对应商品如在折扣/第二份半价活动中则该券不可用),同享券threshold基于促销后金额判断
          • 所有金额用BigDecimal计算,最终ROUND_HALF_UP保留2位小数,最低¥0.01
          • 返回 availableCoupons(可用券列表)供前端展示
          • 返回两条路径明细 + optimalPath + 最终金额 + details数组

        Controller

        • T041 创建 UserPromotionCouponController ruoyi-admin/src/main/java/com/ruoyi/app/user/UserPromotionCouponController.java — @RestController @RequestMapping("/app/userPromotionCoupon")。端点: GET /storeCoupons (storeId → selectStoreCouponsForUser), POST /receive (batchId,storeId → receiveCoupon), GET /myCoupons (status?,storeId? → selectMyCoupons)
        • T042 创建 UserPromotionCalcController ruoyi-admin/src/main/java/com/ruoyi/app/user/UserPromotionCalcController.java — @RestController @RequestMapping("/app/userPromotionCalc")。端点: POST /calculate (RequestBody含storeId+items[{productId,quantity}]+couponId?+forcePath?,不传price,通过登录态获取userId → calculate)

        Mapper 补充查询

        • T043 [P] 在 PromotionActivityMapper 中添加 selectActiveByStoreId(storeId) — 查询门店所有进行中的促销活动,算价时使用
        • T044 [P] 在 PromotionActivityRuleMapper 中添加 selectActiveRulesByStoreId(storeId) — 一次性查出门店所有进行中活动的规则(LEFT JOIN promotion_activity ON status=1),算价时使用
        • T045 [P] 在 PromotionCouponBatchMapper 中添加 selectActiveByStoreId(storeId) — 查询门店进行中且有余量的券批次(status=1 AND remain_count>0)

        Checkpoint: 用户端 API 可用——领券、查券、算价计算接口通过 Postman 测试通过


        Phase 5.6: 促销活动修改/删除 (FR-005a, FR-005b)

        Purpose: 商家能修改未开始的活动、删除未开始的活动,参考美团商家端交互

        Goal: 活动列表操作列按状态显示不同按钮(未开始=编辑+删除,进行中=结束,已结束=无)

        后端

        • T046 在 IPromotionActivityService 中添加 updateActivity(activity, rules) — 校验 status=0 才允许修改;@Transactional 先删除旧规则再重新插入
        • T047 在 IPromotionActivityService 中添加 deleteActivity(id) — 校验 status=0 才允许删除;@Transactional 删除活动+关联规则
        • T048 在 ShPromotionActivityController 中添加 PUT / (updateActivity) 和 DELETE /{id} (deleteActivity) 端点

        前端 API

        • T049 在 foodie-store/src/api/promotionActivity.js 中添加 updatePromotionActivity(data) 和 deletePromotionActivity(id)

        前端页面

        • T050 修改 foodie-store/src/views/PromotionActivity.vue:
          • 操作列按状态显示按钮:未开始(编辑+删除) / 进行中(结束) / 已结束(无)
          • 编辑:点击后调用 getPromotionActivity(id) 获取详情,回填到创建对话框,tijiaobaocun 判断有 id 则调用 updatePromotionActivity
          • 删除:二次确认后调用 deletePromotionActivity(id)
          • 新增 data: editingId: null,tijiaobaocun 判断 editingId 有值走修改、无值走新增

        i18n

        • T051 [P] 在 4 个语言文件(zh/tw/en/vi)的 promoActivity section 中追加:editBtn、deleteBtn、editActivity、deleteConfirm 等新 key

        Checkpoint: 未开始的活动可编辑(修改名称/时间/规则)、可删除(二次确认后删除),进行中只能结束


        Phase 5.7: 优惠券修改/删除 (FR-009a, FR-009b)

        Purpose: 商家能修改未开始/进行中的优惠券、删除未开始的优惠券

        Goal: 优惠券列表操作列按状态显示不同按钮(未开始=编辑+删除,进行中=编辑+下架,已结束/已下架=无)

        后端

        • T052 在 IPromotionCouponBatchService 中添加 updateBatch(batch, rule) — 校验 status=0或1 才允许修改;@Transactional 先删除旧规则再重新插入
        • T053 在 IPromotionCouponBatchService 中添加 deleteBatch(id) — 校验 status=0 才允许删除;@Transactional 删除券批次+关联规则
        • T054 在 ShPromotionCouponController 中添加 PUT / (updateBatch) 和 DELETE /{id} (deleteBatch) 端点

        前端 API

        • T055 在 foodie-store/src/api/promotionCoupon.js 中添加 updatePromotionCoupon(data) 和 deletePromotionCoupon(id)

        前端页面

        • T056 修改 foodie-store/src/views/CouponBatch.vue:
          • 操作列按状态显示按钮:未开始(编辑+删除) / 进行中(编辑+下架) / 已结束/已下架(无)
          • 编辑:点击后调用 getPromotionCoupon(id) 获取详情,回填到创建对话框,tijiaobaocun 判断有 id 则调用 updatePromotionCoupon
          • 删除:二次确认后调用 deletePromotionCoupon(id)
          • 新增 data: editingId: null,tijiaobaocun 判断 editingId 有值走修改、无值走新增

        i18n

        • T057 [P] 在 4 个语言文件(zh/tw/en/vi)的 couponBatch section 中追加:editBtn、deleteBtn、editCoupon、deleteConfirm 等新 key

        Phase 5.8: 免配送费券 (FR-008a)

        Purpose: 新增 couponType=3 免配送费券,商家可创建减免配送费的优惠券

        Goal: 创建券对话框新增"免配送费券"选项,配置使用门槛+减免金额+同享/互斥

        数据库

        • T058 在 updatesql/sql.md 追加 ALTER 语句更新 promotion_coupon_batch.coupon_type 的 COMMENT 为 '1=满减券 2=商品券 3=免配送费券'(无需实际 ALTER,仅记录)

        前端页面

        • T059 修改 foodie-store/src/views/CouponBatch.vue 创建对话框:券类型 radio-group 新增第三个选项"免配送费券"(couponType=3),选中后显示:使用门槛 el-input-number + 减免金额 el-input-number + 叠加属性 radio(同享/互斥),隐藏商品选择

        i18n

        • T060 [P] 在 4 个语言文件(zh/tw/en/vi)的 couponBatch section 中追加:typeFreeDelivery(免配送费券)等 key

        Checkpoint: 创建券时可选"免配送费券",提交数据 couponType=3,含 threshold + amount + isMutex

        Checkpoint: 未开始的券可编辑+删除,进行中的券可编辑+下架,已结束/已下架无操作


        Phase 5.9: 折扣商品改为逐商品设折扣 (需求变更)

        Purpose: 将折扣商品 UI 从「折扣区分组」改为「逐商品设折扣」(参考美团),提升商家体验

        Goal: 商家创建折扣活动时直接选择商品并逐个设折扣率,不再需要先建折扣区

        背景: 经美团/饿了么调研,主流外卖平台都是逐商品设折扣,不使用折扣区分组。后端数据模型已支持(promotion_activity_rule 每行有独立 productId + discountRate),改动集中在前端 UI。

        前端页面

        • T061 修改 foodie-store/src/views/PromotionActivity.vue 折扣 Tab (Tab2) 的 UI 和逻辑:
          • 移除:折扣区分组 UI(建区/删区按钮、折扣组 v-for 循环)
          • 改为扁平商品列表,每行 = 商品名称 + 原价 + 折扣率控件(-/+按钮+输入框,范围10%~99%,步长0.1) + 折后价(自动计算) + 移除按钮
          • 添加商品按钮:点击打开商品选择弹窗(复用已有弹窗),选中后默认折扣率90%,添加到列表
          • 重复商品限制:选择商品时如已存在于列表则提示并跳过
          • 折后价实时计算:原价 × 折扣率,显示在每行
          • 提交逻辑简化:tijiaobaocun() 中折扣 Tab 直接构建 rules: [{productId, discountRate}, ...],无需展开分组
          • 编辑回填:编辑折扣活动时,从 rules 数组直接回填到商品列表,每行设置对应的 discountRate
          • 折扣率校验:前端提交时校验所有折扣率在 0.10~0.99 范围内,超出范围阻止提交

        i18n

        • T062 [P] 更新 4 个语言文件 foodie-store/src/lang/ 的 zh.js、tw.js、en.js、vi.js:
          • 移除折扣区相关 key(如 addDiscountZone、deleteZone、discountZone 等)
          • 新增/修改:discountRate(折扣率)、discountedPrice(折后价)、originalPrice(原价)、removeProduct(移除)、addDiscountProduct(添加折扣商品)、discountRateRange(范围10%-99%)、productAlreadyAdded(商品已添加)等 key
          • 更新 promoActivity section 中与折扣 Tab 相关的所有文案

        后端验证

        • T063 验证后端接口兼容性:确认 ShPromotionActivityController 的 POST/PUT 端点已支持接收扁平的 rules: [{productId, discountRate}] 格式(不需要改动),确认编辑回填接口 GET /{id} 返回的 rules 中每条包含 productId 和 discountRate(已有 LEFT JOIN pos_food)

        Checkpoint: 商家创建折扣活动时 UI 为扁平商品列表,每个商品独立设折扣率,不再有折扣区分组概念。编辑时正确回填。折后价实时计算。后端接口无需改动。


        Phase 5.10: 优惠幅度 20% 限制 (2026-09-02 需求变更,防恶意竞争)

        Purpose: 平台开关控制的双层限制——创建/修改时各项自身 ≤ 20%(比例可配),算价时总优惠预算层层封顶

        Goal: 启用开关后,商家无法创建超比例的促销/优惠券;任何订单实际总优惠 ≤ 比例 × 商品原价;关闭开关行为与现状完全一致

        详细口径: 见 spec.md Session 2026-09-02 与 FR-023~FR-027;技术方案见 plan.md「需求变更 2026-09-02」章节

        数据库

        • T064 在 updatesql/sql.md 追加 2 条 sys_config INSERT:promotion.discount.limit.enabled(默认 true)、promotion.discount.limit.ratio(默认 20),带日期用途注释,提醒开发者手动执行

        创建侧校验

        • T065 新建 ruoyi-system/src/main/java/com/ruoyi/system/service/impl/PromotionDiscountLimitChecker.java(@Component)— 读 sys_config 开关+比例;checkActivity(activity, rules):满减每档 reduceAmount ≤ 比例×threshold、折扣每商品 discountRate ≥ 1−比例、type 3(第二杯半价)/type 4(新客立减)跳过;checkCouponBatch(batch, rule 或 rules):满减券/免配送费券 amount ≤ 比例×threshold(免配送费券 threshold 为空先拒绝提示设门槛)、商品折扣券 discountRate ≥ 1−比例、商品抵用券 amount ≤ 比例×PosFood.price(PosFoodMapper 查价,多商品券逐条查);超限抛 ServiceException(中文、指明具体档位/券,风格与两 Service 现有校验一致)
        • T066 挂接校验:PromotionActivityServiceImpl createActivity/updateActivity 写规则前调用 checkActivity;PromotionCouponBatchServiceImpl createBatch 两个重载开头 + updateBatch 两个重载中"规则实际被替换"分支(receivedCount==0 的 else 分支)调用 checkCouponBatch——已被领取且规则未变的编辑不触发(存量豁免)

        算价侧预算

        • T067 PromotionCalcServiceImpl.calculate 加预算累积器 — 开关关闭时预算视为无限;预算 = 比例 × originalAmount;路径A计算标记胜出机制(折扣 vs 第二杯半价);促销实减 = min(促销减免, 剩余预算)(半价胜出时豁免:不占预算不封顶)→ 新客实减 = min(newCustomerReduce, 剩余) → 券实减 = min(couponReduce, 剩余);互斥券分支:促销/新客清零、互斥券自身同样封顶;afterPromotion/finalAmount/明细全部按实减值重算;实付最低兜底沿用现状
        • T068 buildAvailableCoupons 按剩余预算计算每张候选券实际可抵额度(min(calcCouponReduce, 剩余)),可抵 0 的置为不可用并给原因"本单优惠已达优惠上限";算价明细与 pos_order_promotion 快照反映封顶后实减金额,被封顶项标注上限提示

        验证

        • T069 JDK21 编译通过 + review 子代理逐点核对:6 个挂接点、半价豁免分支、互斥券路径、预算对明细/快照一致性、开关关闭时与现状逐行为一致(含下架/删除等旁路不受影响)
        • T070 新增 PromotionDiscountLimitCheckerTest:覆盖开关、默认/自定义比例、满减券、商品折扣券、商品抵用券、免配送费券和多商品任一超限。
        • T071 新增 PromotionCouponBatchServiceImplDiscountLimitTest:覆盖创建与未领取修改的真实拦截结果,证明超限时不落批次/规则数据。
        • T072 新增 PromotionCalcServiceImplDiscountLimitTest:覆盖20%总预算、促销占用剩余预算、候选券不可用、自定义比例、开关关闭及金额边界;使用 JDK21 运行定向测试(18/18)和 ruoyi-system 全量测试(71/71)。

        Checkpoint: 开关启用后创建侧拦截超比例设置、算价侧总优惠 ≤ 比例×商品原价;关闭开关全链路回到现状


        Phase 5.11: 满额免运优先(2026-10-19 需求追加)

        功能状态(2026-10-19 更新):代码已由 Claude Code 实现(T074~T077 全部完成;T078 自动化项完成,浏览器手工验收与 SQL 手动执行待开发者)。未创建分支/工作树、未改App。

        必读:spec.md FR-028~FR-036、plan.md「需求变更2026-10-19」、contracts/free-delivery.md、claude-code-handoff.md。表中的S/指ruoyi-system/src/main/java/com/ruoyi/system/,A/指ruoyi-admin/src/main/java/com/ruoyi/app/;测试路径按各自模块src/test/java同包。商家前端根目录为D:/QtwCode/foodie/foodie-store。

        • T073 记录促销自动生效、原价门槛、全免优先允许超预算、半价豁免保留及商家承担规则;补充商家PC/App不动/Claude Code执行边界。

        T074 商家PC与活动管理API(完整管理闭环)

        Files:

        • Modify S/domain/PromotionActivity.java、PromotionActivityRule.java、S/mapper/PromotionActivityMapper.java、S/service/impl/PromotionActivityServiceImpl.java、PromotionDiscountLimitChecker.java。
        • Modify ruoyi-system/src/main/resources/mapper/system/PromotionActivityMapper.xml、必要的规则Mapper映射、A/mendian/ShPromotionActivityController.java。
        • Create S/dto/PromotionActivitySaveRequest.java(字段兼容现有POST/PUT,body DTO,不用Map/Bean Validation)。
        • Modify 商家src/views/PromotionActivity.vue、src/lang/{zh,tw,en,vi}.js,必要时仅扩展src/api/promotionActivity.js;不修改CouponBatch.vue、菜单或平台PC。
        • Test create ruoyi-system/src/test/java/com/ruoyi/system/service/impl/PromotionActivityServiceImplFreeDeliveryTest.java,extend PromotionDiscountLimitCheckerTest.java;create ruoyi-admin/src/test/java/com/ruoyi/app/mendian/ShPromotionActivityFreeDeliveryTest.java。

        Interfaces:沿用既有创建/修改/详情/列表/结束/删除路径,增加type=5与单条threshold。产出PromotionActivityMapper.selectActiveFreeDeliveryByStoreId(Long storeId, Date now): List<PromotionActivity>供T075调用;规则仍由既有selectRulesByActivityId读取。

        • 先写定向测试:create_shouldAcceptThresholdWithoutFixedReduction、create_shouldRejectEmptyOrMultipleRules、save_shouldRejectOtherStoreOwner、create_shouldRejectOverlappingConcurrentActivity;预算20%下门槛100的免运活动允许创建,不要求reduceAmount<=20。
        • 运行上述测试确认失败由未实现行为引起,再实现type=5验证、门店权限、门店行事务锁和重叠检查。保留旧类型请求格式及历史行为。
        • 增加按日期查询及type=5有效状态统一处理;补state_shouldStartAndExpireWithoutScheduler和edit_shouldRejectEffectivelyStartedActivity,覆盖status=0但日期已开始、status=1但已过期、status=2人工结束及首尾日期。
        • 商家PC增加第五个Tab:活动名、有效日期、原价满额门槛;创建/编辑/详情/列表/结束/删除和reset回填齐全。四语言key全部位于promoActivity中,建议typeFreeDelivery、freeDeliveryThreshold、freeDeliveryOriginalPriceHint、freeDeliveryMerchantCostHint;用现有页面样式,不新建免运券批次。
        • 定向测试通过,手工验证创建/编辑/详情/结束及四语言;保留CRLF,核对diff与i18n key一致性。本批可独立验收,但不宣称用户App已有免运展示。(定向测试12/12+10/10+4/4已过;CRLF与四语言key一致性已核对;浏览器手工验收待开发者执行)

        T075 可信完整报价与预算计算(含兼容契约)

        Files:

        • Create S/dto/PromotionDeliveryContext.java(内部可信配送上下文);modify S/service/IPromotionCalcService.java、S/service/impl/PromotionCalcServiceImpl.java、S/dto/PromotionCalcRequest.java(CartItem增加specValueIds)、PromotionCalcResponse.java。
        • Create A/order/dto/FoodDeliveryPromotionQuoteRequest.java、A/order/FoodDeliveryPromotionQuoteService.java;modify A/user/UserPromotionCalcController.java新增body报价方法并修正过时Javadoc。
        • Modify ruoyi-admin/src/main/resources/i18n/messages{,_zh_CN,_zh_TW,_en_US,_th_TH}.properties并create messages_vi.properties;现有资源包新key一致,新增vi父语言包为vi/vi_VN提供越南文,不能用th_TH泰文代替vi。商家PC仍须zh/tw/en/vi四份文案。
        • Test create ruoyi-system/src/test/java/com/ruoyi/system/service/impl/PromotionCalcServiceImplFreeDeliveryTest.java、ruoyi-admin/src/test/java/com/ruoyi/app/order/FoodDeliveryPromotionQuoteServiceTest.java;extend原PromotionCalcServiceImplDiscountLimitTest.java。

        Interfaces:保留calculate(PromotionCalcRequest, Long): PromotionCalcResponse;新增calculate(PromotionCalcRequest, Long, PromotionDeliveryContext): PromotionCalcResponse。产出FoodDeliveryPromotionQuoteService.quote(FoodDeliveryPromotionQuoteRequest, Long): PromotionCalcResponse供新POST /app/userPromotionCalc/deliveryQuote及T076调用。新增响应字段、旧finalAmount语义和JSON见contracts。

        • 先写预算断言:calculate_shouldFullyWaiveThirtyWithTwentyBudget(O100/R30/B20/F30/D0/G100/A100)、calculate_shouldReserveTenBeforeOtherDiscounts(R10,受限优惠合计最多10)、calculate_shouldNotWaiveBelowOriginalThreshold(O99)、calculate_shouldKeepWaiverAfterGoodsDiscount(关预算,O100商品优惠30仍免运)。
        • 补calculate_shouldKeepShippingReservationForMutexCoupon、availableCoupons_shouldUseSameRemainingBudget、calculate_shouldKeepHalfPriceExemption、calculate_shouldPreferHalfPriceAfterBudgetCap(O100/R30、普通满减30被截0、半价优惠25获保留,默认选半价、A75)。旧接口回归无配送上下文仍保持旧结果。
        • 运行红测后实现免运独立金额、优先占预算、各路径和候选券一致、forcePath与新上下文预算后选路;命中免运时配送券不可用、appliedCouponId仅实际减免>0才产生;未命中时不借机全面重做旧配送券缺陷。
        • 报价Service从数据库核实门店、在售商品、规格归属和数量、地址归属与实际配送模式,服务端取路线/运价并构造内部配送上下文。quote_shouldRejectForeignAddressAndCrossStoreSpec、quote_shouldIgnoreClientSpecPrice、quote_shouldFailOnMissingRouteOrFare;remote全部mock,不调用真实地图。
        • 运费边界用受控运价起步20/含3km/每km10,验证3km=20、3.49km=20、3.5km=30、4.25km=32(沿旧HALF_EVEN整数舍入),并覆盖千元舍入;不得改旧getyunfei/getZuiyyfei协议或改用已废弃地图。
        • 类型5按单店平台骑手外送生效;新入口拒绝自取/堂食/商家自配送,不接入036。修正Controller/DTO关于商品金额、整单金额及顺序的注释。定向测试通过后核对contracts示例。

        T076 订单固化、真实用券与SQL(创建闭环)

        Files:

        • Modify S/domain/PosOrder.java、ruoyi-system/src/main/resources/mapper/system/PosOrderMapper.xml及受影响的订单VO/映射;保留既有freight和金额列单位。
        • Modify A/order/dto/OrderCreatItem.java、A/order/UserOrderController.java、A/order/OrderPromotionHelper.java;必要的父单映射按现有OrderParent接口更新。
        • Modify S/dto/PromotionCalcResponse.java(PromotionDetail快照扩展)、S/domain/PosOrderPromotion.java及其Mapper注释/字段映射(如需要);modify updatesql/sql.md仅在最后按执行当日登记3个订单快照字段及枚举说明。
        • Test create ruoyi-admin/src/test/java/com/ruoyi/app/order/OrderPromotionHelperFreeDeliveryTest.java、UserOrderFreeDeliveryCreateTest.java。

        Interfaces:显式OrderCreatItem.promotionPricingVersion=2接入T075.quote;持久化PosOrder.promotionPricingVersion: Integer、merchantDeliverySubsidy: BigDecimal、customerDeliveryFee: BigDecimal;JSON与SQL名见plan。历史NULL保持原行为。产出A/R/F/D、活动快照和仅实际使用的券供T077结算。

        • 先写create_shouldPersistConsistentParentAndChildAmounts:O100/R30免运,子amount100/freight30/F30/D0/version2;父totalAmount130/actualPayAmount100/totalDiscount30;商品优惠汇总不得减30。
        • 补create_shouldRejectChangedQuoteAndRollback、create_shouldRejectMismatchedOrderTypesOrMultipleStoresInV2、create_shouldNotUseZeroBenefitCoupon、snapshot_shouldKeepShippingSeparateFromGoods、legacyCreate_shouldKeepOriginalBehavior;先运行确认失败,再实现。
        • 版本2单店在同一创建事务中权威重算,核对父子客户端应付与服务端结果;将可信价格生成兼容food JSON,使用数据库结果写快照,不信food/amount/freight/specPrice。旧请求维持旧流程,不静默换价。
        • 商品促销汇总排除subType=5,免运单独快照;核销使用appliedCouponId,并再次以归属/状态守卫防并发重复使用,失败回滚父单、子单、快照及核销。无实际优惠不凭传入couponId核销。重复ddId按原唯一性/幂等守卫拒绝或返回已固化结果,不重算覆盖或重复核销;新增对应回归用例。
        • 新增nullable字段并补Entity/resultMap/select/insert/update/BaseMapper/查询响应链;SQL日期置末,不增加新券表、不重建活动表、不回填旧订单。SQL由开发者手动执行,代码测试用mock或隔离fixture,不直连测试/生产库迁移。
        • 定向测试通过,确认同一订单创建后结束活动或跨日期不再重算F/D;完整新字段只来自服务端快照。

        T077 结算、支付方式切换、退款与发票(资金闭环)

        Files:

        • Modify A/order/OrderService.java、A/order/OrderInvoiceService.java、S/domain/PosOrder.java(三个@TableField(exist=false)派生交款属性/getter,不另存列);不修改已废弃PosOrderController.add/edit或PayController/TestTask等入口。
        • 支付/退款生产代码仅在确有金额读取问题时改最小入口,A/pay/PayMethodSwitchService.java、LINE/OMG和037沿用既有流程,不重新实现支付功能。
        • Test create ruoyi-admin/src/test/java/com/ruoyi/app/order/OrderFreeDeliverySettlementTest.java、OrderInvoiceFreeDeliveryTest.java;extend ruoyi-admin/src/test/java/com/ruoyi/app/pay/PayMethodSwitchServiceTest.java及现有相关支付/取消测试。

        Interfaces:读取T076的version、A/R/F/D;历史D为NULL回退freight。派生merchantPayToRider、riderPayToMerchant、customerCollectAmount见contracts。不新增人工“已收运费”状态、支付接口或第二个钱包入账入口。

        • 先写settle_shouldKeepRiderGrossFreightAndSingleWalletEntry、merchantBilling_shouldUseGoodsPayableWithoutDoubleSubsidy、invoice_shouldUseCustomerPaidFreight:A100/R30/D0时商品基数及发票100、骑手毛配送费30,重复送达不新增账单。
        • 补在线/到付:cash_shouldShowDirectionForNegativeMerchantNet(A20/R30,商家给骑手10、向客户收20);在线A100商家给骑手30、客户待收0;到付A100骑手给商家70、客户待收100。
        • 补switch_shouldKeepWaiverSnapshotAndChangeOnlyCashDirection、refund_shouldNotReturnMerchantSubsidyToCustomer、legacyInvoice_shouldKeepAmountMinusFreight;先确认红测再实现新version分支。
        • 按G=A−D处理新订单商家商品分成,避免新券ID走旧VipUserQuanyi查询;骑手仍走既有freight佣金/税及钱包幂等。两处发票改扣客户实付运费D;线上支付/退款只用A及原渠道交易金额,不额外向客户退F。
        • 定向测试通过,核对LINE/OMG创建请求额、切换到付金额、037取消退款上限;第三方测试环境退款失败只能记外部验证限制,不用“状态改成功”替代真实退款。

        T078 统一验证、商家PC验收与交接文档

        Files:更新本目录spec/plan/tasks/contracts/handoff的实际实现记录;不修改App源码,不创建新分支。

        • 按handoff的JDK21命令运行新定向测试与受影响模块回归/构建,执行商家npm run build:test。失败直接修复对应问题;未经实际运行的测试不得勾选完成。
        • 在商家PC核验第五Tab、列表/详情、编辑回填、重叠拒绝、活动日期与结束操作;切换zh/tw/en/vi,覆盖门槛必填、非法值和其他门店权限;确认无CouponBatch领券流程、无App/平台PC改动。
        • 执行contracts数值矩阵和legacy接口/旧订单回归,核对商家、客户、骑手三方金额与快照、发票;本轮不依赖真实外部付款/退款证明单元用例成功。
        • git diff --check与文件范围检查,确认CRLF、i18n key和SQL时间顺序;分别列出已运行检查、失败项/外部未验证项、SQL待手动执行、App未接入的交付边界。
        • 更新任务勾选与实际验收结果。除非用户另行要求,不自行commit/push;用户要求提交时使用中文提交信息,只暂存当前功能文件并报告SHA。

        依赖顺序:T074 → T075 → T076 → T077 → T078。实现前后按同一FR/契约核对,不重新询问已确认的原价门槛与免运优先规则。

        T079 报价入口统一(2026-10-19 追加,spec FR-037)

        Files:Modify A/order/FoodDeliveryPromotionQuoteService.java、A/user/UserPromotionCalcController.java(仅注释);Test extend ruoyi-admin/src/test/java/com/ruoyi/app/order/FoodDeliveryPromotionQuoteServiceTest.java、ruoyi-system/src/test/java/com/ruoyi/system/service/impl/PromotionCalcServiceImplFreeDeliveryTest.java;文档同步 contracts/free-delivery.md 与 app-integration.md。

        • 先改测试:quote_shouldSupportPickupAndDineInWithoutAddress(orderType=1/2 免地址、context(0,false)、不查地址)、quote_shouldQuoteSelfDeliveryWithoutFreeDelivery(自配送 R=30 照算、eligible=false、不再 unsupportedOrder)、calculate_shouldSkipFreeDeliveryWhenContextNotEligible(eligible=false 时 F=0/D=R/无 type=5 查询);quote_shouldRejectUnknownOrderType(orderType=3/null 仍拒绝)。
        • 实现:orderType 白名单 0/1/2;自取/堂食短路配送上下文;自配送 eligible=false 运费照算;运行定向测试通过。
        • 同步 contracts/free-delivery.md、app-integration.md(全场景单一报价入口、v2 下单含自配送 F=0);提交并 cherry-pick 到 test-202609v2。

        依赖:T075(报价服务已存在)。

        T080 规格数据源统一 + 报错可定位(2026-10-19 联调追加,spec FR-038)

        Files:Modify A/mendian/PosFoodController.java(去 food_sku 回退×3)、A/order/FoodDeliveryPromotionQuoteService.java(resolveSpecSurcharge 公共化、to_examine 修正、报错带商品ID)、A/order/OrderPromotionHelper.java(加价同源);Create updatesql/backfill_food_spec_relation.py;i18n 六包两个报错文案带 {0};文档 spec/plan/contracts/app-integration 同步。

        • 修正报价 to_examine 反写('1'=审核通过可售,与菜单 eq 口径一致)并补待审核拒绝用例;036 报价服务存在同款反写,属并行会话暂停代码,恢复时另修。
        • 统一规格唯一源 food_spec_relation:PosFoodController 去掉三处“关联为空回退读 food_sku”;报价 resolveSpecSurcharge 公共化;订单 buildServerFoodJson 加价同源调用。
        • 联调复查补漏:/searchfor 与 /getfoodlist 两处无条件读 food_sku 的端点一并改为 loadFoodSpecsMap 关联源(2026-10-19 追加)。
        • 回填脚本 updatesql/backfill_food_spec_relation.py(幂等:缺组/缺值按 JSON 建,缺关联补建);测试库已执行(9078/9115 等 18 条关联入表);生产执行前不得部署去回退版本。
        • invalidProduct/invalidSpec 六语言文案带商品ID(商品[9115]无效或不属于该门店)。
        • 定向测试与相关回归通过;提交并同步 test-202609v2。

        依赖:T075。


        Phase 6: Polish & 验证

        Purpose: 端到端验证

        • T036 后端启动验证:用 Postman 测试 4 个促销 API + 4 个优惠券 API(商家端)+ 3 个用户端领券 API + 1 个算价 API 的完整流程
        • T036a 算价接口专项验证:构造测试数据(门店有满减+折扣活动+优惠券),验证两条路径计算正确、互斥规则生效、新客立减可叠加
        • T037 前端启动验证:npm run dev → 侧边栏新菜单 → 创建满减活动(3档位) → 创建折扣活动(选择5个商品逐个设不同折扣率) → 创建第二份半价 → 创建新客立减 → 创建满减券 → 创建商品券 → 列表筛选 → 结束活动 → 下架券 → 编辑折扣活动验证回填
        • T038 [P] 切换4种语言验证所有文案显示正确

        Dependencies & Execution Order

        Phase Dependencies

        • Phase 1 (Setup): 无依赖,立即开始
        • Phase 2 (Foundational): 依赖 Phase 1 的表已创建
        • Phase 3 (US1 促销): 依赖 Phase 2 的实体和 Mapper
        • Phase 4 (US2 优惠券): 依赖 Phase 2,可与 Phase 3 并行
        • Phase 5 (Menu/i18n): 依赖 Phase 3 + Phase 4 的页面文件存在
        • Phase 5.5 (用户端接口): 依赖 Phase 2 的实体/Mapper + Phase 3 的促销 Activity Service + Phase 4 的优惠券 Service
        • Phase 5.10 (优惠幅度限制): 依赖 Phase 3/4 的两个 Service 与 Phase 5.5 的算价 Service
        • Phase 5.11 (满额免运): 依赖现有促销管理、算价预算、订单优惠快照及订单结算;T074~T077 已完成,T078 剩浏览器手工验收
        • Phase 6 (验证): 依赖全部完成(包括 Phase 5.9 折扣改动、Phase 5.11 满额免运)

        Parallel Opportunities

        • T003-T007a (6个实体) 可并行
        • T008-T012a (6个Mapper接口) 可并行
        • T013-T017a (6个Mapper XML) 可并行
        • Phase 3 和 Phase 4 可并行(不同文件,不同 Controller)
        • T039 和 T040 可并行(不同 Service)
        • T032-T035 (4个语言文件) 可并行

        Recommended Execution Order

        T001 → T002(手动)
          → T003-T017a (Phase 2, 大量并行)
          → T018-T023 (Phase 3, 促销活动) + T024-T029 (Phase 4, 优惠券) [可并行]
          → T030-T035 (Phase 5, 菜单路由i18n)
          → T039-T045 (Phase 5.5, 用户端接口)
          → T061-T063 (Phase 5.9, 折扣商品改为逐商品设折扣)
          → T064-T072 (Phase 5.10, 优惠幅度20%限制)
          → T073 (已确认规则记录) → T074-T078 (Phase 5.11, 满额免运,代码已实现)
          → T036-T038 (Phase 6, 验证)
        

        Notes

        • 2026-10-19增量优先:T074~T078按本轮handoff执行,用户未另行要求前不自动commit/push、不创建分支;下列历史提交约定不覆盖本轮交接边界。

        • [P] tasks = 不同文件,无依赖,可并行

        • [Story] label 映射到 spec 中的 User Story (US1=促销, US2=优惠券)

        • 每个任务完成后 commit

        • 前端文件 CRLF 换行,用 Python 脚本编辑

        • 旧代码(SalesPromotion, VipQuanyi)不动

        • PromotionUserCoupon 实体和 Mapper 在 Phase 2 创建,Service/Controller 在 Phase 5.5 实现

        • PosOrderPromotion 实体和 Mapper 在 Phase 2 创建,算价 Service 在 Phase 5.5 使用

        • 用户端前端(小程序界面)不在本范围,只做后端 API