# 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**: 创建数据库表,为后续所有任务提供基础 - [x] 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 章节 - [x] T002 手动执行 SQL 建表(提醒开发者操作) --- ## Phase 2: Foundational (后端实体 + 数据层) **Purpose**: 后端核心数据层,所有 Controller 和前端页面依赖此阶段完成 ### 实体类 (5个文件,全部可并行) - [x] 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 - [x] 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 - [x] 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 - [x] 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 - [x] T007 [P] 创建 PromotionUserCoupon 实体 `ruoyi-system/src/main/java/com/ruoyi/system/domain/PromotionUserCoupon.java` — 字段: id, userId, batchId, storeId, status, orderId, receiveTime, useTime, expireTime - [x] 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 接口 (全部可并行) - [x] T008 [P] 创建 PromotionActivityMapper `ruoyi-system/src/main/java/com/ruoyi/system/mapper/PromotionActivityMapper.java` — extends BaseMapper - [x] T009 [P] 创建 PromotionActivityRuleMapper `ruoyi-system/src/main/java/com/ruoyi/system/mapper/PromotionActivityRuleMapper.java` — extends BaseMapper,额外方法 List selectRulesByActivityId(Long activityId) - [x] T010 [P] 创建 PromotionCouponBatchMapper `ruoyi-system/src/main/java/com/ruoyi/system/mapper/PromotionCouponBatchMapper.java` — extends BaseMapper - [x] T011 [P] 创建 PromotionCouponRuleMapper `ruoyi-system/src/main/java/com/ruoyi/system/mapper/PromotionCouponRuleMapper.java` — extends BaseMapper,额外方法 PromotionCouponRule selectRuleByBatchId(Long batchId) - [x] T012 [P] 创建 PromotionUserCouponMapper `ruoyi-system/src/main/java/com/ruoyi/system/mapper/PromotionUserCouponMapper.java` — extends BaseMapper - [x] T012a [P] 创建 PosOrderPromotionMapper `ruoyi-system/src/main/java/com/ruoyi/system/mapper/PosOrderPromotionMapper.java` — extends BaseMapper ### Mapper XML (全部可并行) - [x] T013 [P] 创建 PromotionActivityMapper.xml `ruoyi-system/src/main/resources/mapper/system/PromotionActivityMapper.xml` — resultMap 映射全部字段 - [x] T014 [P] 创建 PromotionActivityRuleMapper.xml `ruoyi-system/src/main/resources/mapper/system/PromotionActivityRuleMapper.xml` — resultMap + selectRulesByActivityId (LEFT JOIN pos_food 获取 productName/productImage/productPrice) - [x] T015 [P] 创建 PromotionCouponBatchMapper.xml `ruoyi-system/src/main/resources/mapper/system/PromotionCouponBatchMapper.xml` — resultMap 映射全部字段 - [x] T016 [P] 创建 PromotionCouponRuleMapper.xml `ruoyi-system/src/main/resources/mapper/system/PromotionCouponRuleMapper.xml` — resultMap + selectRuleByBatchId (LEFT JOIN pos_food) - [x] T017 [P] 创建 PromotionUserCouponMapper.xml `ruoyi-system/src/main/resources/mapper/system/PromotionUserCouponMapper.xml` — resultMap 映射全部字段 - [x] 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) - [x] T018 [US1] 创建 IPromotionActivityService `ruoyi-system/src/main/java/com/ruoyi/system/service/IPromotionActivityService.java` — extends IService,方法: createActivity(activity, rules), selectActivityWithRules(id), endActivity(id) - [x] 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) - [x] T020 [US1] 创建 IPromotionActivityRuleService + PromotionActivityRuleServiceImpl `ruoyi-system/src/main/java/com/ruoyi/system/service/IPromotionActivityRuleService.java` 和 `impl/PromotionActivityRuleServiceImpl.java` — selectRulesByActivityId 委托给 mapper ### Controller (US1) - [x] 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) - [x] T022 [US1] 创建 promotionActivity.js `foodie-store/src/api/promotionActivity.js` — 导出 listPromotionActivity(query), getPromotionActivity(id), addPromotionActivity(data), endPromotionActivity(id),参考 src/api/promotion.js 模式 ### 前端页面 (US1) - [x] 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) - [x] T024 [P] [US2] 创建 IPromotionCouponBatchService `ruoyi-system/src/main/java/com/ruoyi/system/service/IPromotionCouponBatchService.java` — 方法: createBatch(batch, rule), selectBatchWithRule(id), offShelfBatch(id) - [x] 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) - [x] T026 [P] [US2] 创建 IPromotionCouponRuleService + PromotionCouponRuleServiceImpl `ruoyi-system/src/main/java/com/ruoyi/system/service/IPromotionCouponRuleService.java` 和 `impl/PromotionCouponRuleServiceImpl.java` — selectRuleByBatchId 委托给 mapper ### Controller (US2) - [x] 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) - [x] T028 [P] [US2] 创建 promotionCoupon.js `foodie-store/src/api/promotionCoupon.js` — 导出 listPromotionCoupon(query), getPromotionCoupon(id), addPromotionCoupon(data), offShelfPromotionCoupon(id) ### 前端页面 (US2) - [x] 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 工具。 - [x] T030 在 `foodie-store/src/router/index.js` 的 /manage children 中添加路由 promotion-activity → PromotionActivity.vue 和 coupon-batch → CouponBatch.vue - [x] T031 在 `foodie-store/src/components/Aside.vue` 末尾(el-submenu index="6" 之后)添加新的 el-submenu index="7"「营销管理」,含两个 menu-item: 促销活动(/manage/promotion-activity) + 优惠券管理(/manage/coupon-batch) - [x] T032 [P] 在 `foodie-store/src/lang/zh.js` 末尾追加 promoMenu(营销管理/促销活动/优惠券管理)、promoActivity(~40个key覆盖页面全部文案)、couponBatch(~30个key)三个 i18n section - [x] T033 [P] 在 `foodie-store/src/lang/tw.js` 追加同结构繁体中文翻译 - [x] T034 [P] 在 `foodie-store/src/lang/en.js` 追加同结构英文翻译 - [x] T035 [P] 在 `foodie-store/src/lang/vi.js` 追加同结构越南语翻译 **Checkpoint**: 侧边栏显示新菜单,页面可访问,多语言正常 --- ## Phase 5.5: 用户端后端接口 (FR-011~022) **Purpose**: 实现用户端的领券、查券、算价计算后端接口(前端界面由小程序实施,不做) **Goal**: 用户端小程序可调用后端 API 完成领券、查看优惠券、结算页优惠计算 **Independent Test**: 用 Postman 调用算价接口,传入购物车商品,返回两条路径对比 + 最优方案 + 可用券列表 ### Service 层 - [x] 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_time0) **Checkpoint**: 用户端 API 可用——领券、查券、算价计算接口通过 Postman 测试通过 --- ## Phase 5.6: 促销活动修改/删除 (FR-005a, FR-005b) **Purpose**: 商家能修改未开始的活动、删除未开始的活动,参考美团商家端交互 **Goal**: 活动列表操作列按状态显示不同按钮(未开始=编辑+删除,进行中=结束,已结束=无) ### 后端 - [x] T046 在 IPromotionActivityService 中添加 `updateActivity(activity, rules)` — 校验 status=0 才允许修改;@Transactional 先删除旧规则再重新插入 - [x] T047 在 IPromotionActivityService 中添加 `deleteActivity(id)` — 校验 status=0 才允许删除;@Transactional 删除活动+关联规则 - [x] T048 在 ShPromotionActivityController 中添加 `PUT /` (updateActivity) 和 `DELETE /{id}` (deleteActivity) 端点 ### 前端 API - [x] T049 在 `foodie-store/src/api/promotionActivity.js` 中添加 `updatePromotionActivity(data)` 和 `deletePromotionActivity(id)` ### 前端页面 - [x] T050 修改 `foodie-store/src/views/PromotionActivity.vue`: - 操作列按状态显示按钮:未开始(编辑+删除) / 进行中(结束) / 已结束(无) - 编辑:点击后调用 getPromotionActivity(id) 获取详情,回填到创建对话框,tijiaobaocun 判断有 id 则调用 updatePromotionActivity - 删除:二次确认后调用 deletePromotionActivity(id) - 新增 data: `editingId: null`,`tijiaobaocun` 判断 editingId 有值走修改、无值走新增 ### i18n - [x] T051 [P] 在 4 个语言文件(zh/tw/en/vi)的 promoActivity section 中追加:editBtn、deleteBtn、editActivity、deleteConfirm 等新 key **Checkpoint**: 未开始的活动可编辑(修改名称/时间/规则)、可删除(二次确认后删除),进行中只能结束 --- ## Phase 5.7: 优惠券修改/删除 (FR-009a, FR-009b) **Purpose**: 商家能修改未开始/进行中的优惠券、删除未开始的优惠券 **Goal**: 优惠券列表操作列按状态显示不同按钮(未开始=编辑+删除,进行中=编辑+下架,已结束/已下架=无) ### 后端 - [x] T052 在 IPromotionCouponBatchService 中添加 `updateBatch(batch, rule)` — 校验 status=0或1 才允许修改;@Transactional 先删除旧规则再重新插入 - [x] T053 在 IPromotionCouponBatchService 中添加 `deleteBatch(id)` — 校验 status=0 才允许删除;@Transactional 删除券批次+关联规则 - [x] T054 在 ShPromotionCouponController 中添加 `PUT /` (updateBatch) 和 `DELETE /{id}` (deleteBatch) 端点 ### 前端 API - [x] T055 在 `foodie-store/src/api/promotionCoupon.js` 中添加 `updatePromotionCoupon(data)` 和 `deletePromotionCoupon(id)` ### 前端页面 - [x] T056 修改 `foodie-store/src/views/CouponBatch.vue`: - 操作列按状态显示按钮:未开始(编辑+删除) / 进行中(编辑+下架) / 已结束/已下架(无) - 编辑:点击后调用 getPromotionCoupon(id) 获取详情,回填到创建对话框,tijiaobaocun 判断有 id 则调用 updatePromotionCoupon - 删除:二次确认后调用 deletePromotionCoupon(id) - 新增 data: `editingId: null`,`tijiaobaocun` 判断 editingId 有值走修改、无值走新增 ### i18n - [x] T057 [P] 在 4 个语言文件(zh/tw/en/vi)的 couponBatch section 中追加:editBtn、deleteBtn、editCoupon、deleteConfirm 等新 key --- ## Phase 5.8: 免配送费券 (FR-008a) **Purpose**: 新增 couponType=3 免配送费券,商家可创建减免配送费的优惠券 **Goal**: 创建券对话框新增"免配送费券"选项,配置使用门槛+减免金额+同享/互斥 ### 数据库 - [x] T058 在 `updatesql/sql.md` 追加 ALTER 语句更新 promotion_coupon_batch.coupon_type 的 COMMENT 为 '1=满减券 2=商品券 3=免配送费券'(无需实际 ALTER,仅记录) ### 前端页面 - [x] T059 修改 `foodie-store/src/views/CouponBatch.vue` 创建对话框:券类型 radio-group 新增第三个选项"免配送费券"(couponType=3),选中后显示:使用门槛 el-input-number + 减免金额 el-input-number + 叠加属性 radio(同享/互斥),隐藏商品选择 ### i18n - [x] 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。 ### 前端页面 - [x] 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 - [x] 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 相关的所有文案 ### 后端验证 - [x] 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」章节 ### 数据库 - [x] T064 在 `updatesql/sql.md` 追加 2 条 sys_config INSERT:promotion.discount.limit.enabled(默认 true)、promotion.discount.limit.ratio(默认 20),带日期用途注释,提醒开发者手动执行 ### 创建侧校验 - [x] 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 现有校验一致) - [x] T066 挂接校验:`PromotionActivityServiceImpl` createActivity/updateActivity 写规则前调用 checkActivity;`PromotionCouponBatchServiceImpl` createBatch 两个重载开头 + updateBatch 两个重载中"规则实际被替换"分支(receivedCount==0 的 else 分支)调用 checkCouponBatch——已被领取且规则未变的编辑不触发(存量豁免) ### 算价侧预算 - [x] T067 `PromotionCalcServiceImpl.calculate` 加预算累积器 — 开关关闭时预算视为无限;预算 = 比例 × originalAmount;路径A计算标记胜出机制(折扣 vs 第二杯半价);促销实减 = min(促销减免, 剩余预算)(半价胜出时豁免:不占预算不封顶)→ 新客实减 = min(newCustomerReduce, 剩余) → 券实减 = min(couponReduce, 剩余);互斥券分支:促销/新客清零、互斥券自身同样封顶;afterPromotion/finalAmount/明细全部按实减值重算;实付最低兜底沿用现状 - [x] T068 `buildAvailableCoupons` 按剩余预算计算每张候选券实际可抵额度(min(calcCouponReduce, 剩余)),可抵 0 的置为不可用并给原因"本单优惠已达优惠上限";算价明细与 pos_order_promotion 快照反映封顶后实减金额,被封顶项标注上限提示 ### 验证 - [x] T069 JDK21 编译通过 + review 子代理逐点核对:6 个挂接点、半价豁免分支、互斥券路径、预算对明细/快照一致性、开关关闭时与现状逐行为一致(含下架/删除等旁路不受影响) - [x] T070 新增 `PromotionDiscountLimitCheckerTest`:覆盖开关、默认/自定义比例、满减券、商品折扣券、商品抵用券、免配送费券和多商品任一超限。 - [x] T071 新增 `PromotionCouponBatchServiceImplDiscountLimitTest`:覆盖创建与未领取修改的真实拦截结果,证明超限时不落批次/规则数据。 - [x] 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](./spec.md) FR-028~FR-036、[plan.md](./plan.md)「需求变更2026-10-19」、[contracts/free-delivery.md](./contracts/free-delivery.md)、[claude-code-handoff.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`。 - [x] 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`供T075调用;规则仍由既有selectRulesByActivityId读取。 - [x] 先写定向测试:`create_shouldAcceptThresholdWithoutFixedReduction`、`create_shouldRejectEmptyOrMultipleRules`、`save_shouldRejectOtherStoreOwner`、`create_shouldRejectOverlappingConcurrentActivity`;预算20%下门槛100的免运活动允许创建,不要求reduceAmount<=20。 - [x] 运行上述测试确认失败由未实现行为引起,再实现type=5验证、门店权限、门店行事务锁和重叠检查。保留旧类型请求格式及历史行为。 - [x] 增加按日期查询及type=5有效状态统一处理;补`state_shouldStartAndExpireWithoutScheduler`和`edit_shouldRejectEffectivelyStartedActivity`,覆盖status=0但日期已开始、status=1但已过期、status=2人工结束及首尾日期。 - [x] 商家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。 - [x] 先写预算断言:`calculate_shouldFullyWaiveThirtyWithTwentyBudget`(O100/R30/B20/F30/D0/G100/A100)、`calculate_shouldReserveTenBeforeOtherDiscounts`(R10,受限优惠合计最多10)、`calculate_shouldNotWaiveBelowOriginalThreshold`(O99)、`calculate_shouldKeepWaiverAfterGoodsDiscount`(关预算,O100商品优惠30仍免运)。 - [x] 补`calculate_shouldKeepShippingReservationForMutexCoupon`、`availableCoupons_shouldUseSameRemainingBudget`、`calculate_shouldKeepHalfPriceExemption`、`calculate_shouldPreferHalfPriceAfterBudgetCap`(O100/R30、普通满减30被截0、半价优惠25获保留,默认选半价、A75)。旧接口回归无配送上下文仍保持旧结果。 - [x] 运行红测后实现免运独立金额、优先占预算、各路径和候选券一致、forcePath与新上下文预算后选路;命中免运时配送券不可用、appliedCouponId仅实际减免>0才产生;未命中时不借机全面重做旧配送券缺陷。 - [x] 报价Service从数据库核实门店、在售商品、规格归属和数量、地址归属与实际配送模式,服务端取路线/运价并构造内部配送上下文。`quote_shouldRejectForeignAddressAndCrossStoreSpec`、`quote_shouldIgnoreClientSpecPrice`、`quote_shouldFailOnMissingRouteOrFare`;remote全部mock,不调用真实地图。 - [x] 运费边界用受控运价起步20/含3km/每km10,验证3km=20、3.49km=20、3.5km=30、4.25km=32(沿旧HALF_EVEN整数舍入),并覆盖千元舍入;不得改旧getyunfei/getZuiyyfei协议或改用已废弃地图。 - [x] 类型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结算。 - [x] 先写`create_shouldPersistConsistentParentAndChildAmounts`:O100/R30免运,子amount100/freight30/F30/D0/version2;父totalAmount130/actualPayAmount100/totalDiscount30;商品优惠汇总不得减30。 - [x] 补`create_shouldRejectChangedQuoteAndRollback`、`create_shouldRejectMismatchedOrderTypesOrMultipleStoresInV2`、`create_shouldNotUseZeroBenefitCoupon`、`snapshot_shouldKeepShippingSeparateFromGoods`、`legacyCreate_shouldKeepOriginalBehavior`;先运行确认失败,再实现。 - [x] 版本2单店在同一创建事务中权威重算,核对父子客户端应付与服务端结果;将可信价格生成兼容food JSON,使用数据库结果写快照,不信food/amount/freight/specPrice。旧请求维持旧流程,不静默换价。 - [x] 商品促销汇总排除subType=5,免运单独快照;核销使用appliedCouponId,并再次以归属/状态守卫防并发重复使用,失败回滚父单、子单、快照及核销。无实际优惠不凭传入couponId核销。重复ddId按原唯一性/幂等守卫拒绝或返回已固化结果,不重算覆盖或重复核销;新增对应回归用例。 - [x] 新增nullable字段并补Entity/resultMap/select/insert/update/BaseMapper/查询响应链;SQL日期置末,不增加新券表、不重建活动表、不回填旧订单。SQL由开发者手动执行,代码测试用mock或隔离fixture,不直连测试/生产库迁移。 - [x] 定向测试通过,确认同一订单创建后结束活动或跨日期不再重算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。不新增人工“已收运费”状态、支付接口或第二个钱包入账入口。 - [x] 先写`settle_shouldKeepRiderGrossFreightAndSingleWalletEntry`、`merchantBilling_shouldUseGoodsPayableWithoutDoubleSubsidy`、`invoice_shouldUseCustomerPaidFreight`:A100/R30/D0时商品基数及发票100、骑手毛配送费30,重复送达不新增账单。 - [x] 补在线/到付:`cash_shouldShowDirectionForNegativeMerchantNet`(A20/R30,商家给骑手10、向客户收20);在线A100商家给骑手30、客户待收0;到付A100骑手给商家70、客户待收100。 - [x] 补`switch_shouldKeepWaiverSnapshotAndChangeOnlyCashDirection`、`refund_shouldNotReturnMerchantSubsidyToCustomer`、`legacyInvoice_shouldKeepAmountMinusFreight`;先确认红测再实现新version分支。 - [x] 按G=A−D处理新订单商家商品分成,避免新券ID走旧VipUserQuanyi查询;骑手仍走既有freight佣金/税及钱包幂等。两处发票改扣客户实付运费D;线上支付/退款只用A及原渠道交易金额,不额外向客户退F。 - [x] 定向测试通过,核对LINE/OMG创建请求额、切换到付金额、037取消退款上限;第三方测试环境退款失败只能记外部验证限制,不用“状态改成功”替代真实退款。 ### T078 统一验证、商家PC验收与交接文档 **Files**:更新本目录spec/plan/tasks/contracts/handoff的实际实现记录;不修改App源码,不创建新分支。 - [x] 按[handoff](./claude-code-handoff.md)的JDK21命令运行新定向测试与受影响模块回归/构建,执行商家`npm run build:test`。失败直接修复对应问题;未经实际运行的测试不得勾选完成。 - [ ] 在商家PC核验第五Tab、列表/详情、编辑回填、重叠拒绝、活动日期与结束操作;切换zh/tw/en/vi,覆盖门槛必填、非法值和其他门店权限;确认无CouponBatch领券流程、无App/平台PC改动。 - [x] 执行contracts数值矩阵和legacy接口/旧订单回归,核对商家、客户、骑手三方金额与快照、发票;本轮不依赖真实外部付款/退款证明单元用例成功。 - [x] `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。 - [x] 先改测试:`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 仍拒绝)。 - [x] 实现:orderType 白名单 0/1/2;自取/堂食短路配送上下文;自配送 eligible=false 运费照算;运行定向测试通过。 - [x] 同步 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 同步。 - [x] 修正报价 to_examine 反写('1'=审核通过可售,与菜单 eq 口径一致)并补待审核拒绝用例;036 报价服务存在同款反写,属并行会话暂停代码,恢复时另修。 - [x] 统一规格唯一源 food_spec_relation:PosFoodController 去掉三处“关联为空回退读 food_sku”;报价 resolveSpecSurcharge 公共化;订单 buildServerFoodJson 加价同源调用。 - [x] 联调复查补漏:/searchfor 与 /getfoodlist 两处无条件读 food_sku 的端点一并改为 loadFoodSpecsMap 关联源(2026-10-19 追加)。 - [x] 回填脚本 updatesql/backfill_food_spec_relation.py(幂等:缺组/缺值按 JSON 建,缺关联补建);测试库已执行(9078/9115 等 18 条关联入表);**生产执行前不得部署去回退版本**。 - [x] invalidProduct/invalidSpec 六语言文案带商品ID(商品[9115]无效或不属于该门店)。 - [x] 定向测试与相关回归通过;提交并同步 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