tasks.md 17 KB


description: "Task list for 029-merchant-self-delivery"

Tasks: 商家自配送 (029)

Input: Design documents from /specs/029-merchant-self-delivery/

Prerequisites: plan.md, spec.md, research.md, data-model.md, contracts/api.md, quickstart.md

Tests: 已包含——spec SC-001/SC-004/SC-005 要求验收用例集 100%,quickstart 场景 1 即单测。测试任务先写并确认 RED,再实现转 GREEN。

Organization: 按用户故事分阶段,每个故事可独立实现与验证。

Path Conventions

  • 后端仓库根:E:\QtwCode\foodie\foodie_server(下述相对路径基于此)
  • 商家端 PC:E:\QtwCode\foodie\foodie-store(CRLF,用 Python 脚本编辑)
  • 编译:临时 JAVA_HOME=C:\Users\qmj\.jdks\graalvm-jdk-21.0.7;本项目无 mapper XML 新增(新表走 MyBatis-Plus BaseMapper),但改 XML 时须 python -c "import xml.etree.ElementTree as ET; ET.parse(...)" 校验

Phase 1: Setup (Shared Infrastructure)

  • T001 将 3 条 DDL(pos_store 2 列、pos_store_self_delivery_hours 建表、pos_order 1 列,见 data-model.md)登记到 updatesql/sql.md,标注 -- 2026-09-08 029 商家自配送;不直接执行
  • T002 [P] 新建实体 ruoyi-system/src/main/java/com/ruoyi/system/domain/PosStoreSelfDeliveryHours.java(MyBatis-Plus 注解风格对齐 OperatingHours)与 ruoyi-system/src/main/java/com/ruoyi/system/mapper/PosStoreSelfDeliveryHoursMapper.java(BaseMapper,无 XML)
  • T003 [P] 实体加字段:ruoyi-system/src/main/java/com/ruoyi/system/domain/PosStore.java(+selfDeliveryEnabled/selfDeliveryMode)、ruoyi-system/src/main/java/com/ruoyi/system/domain/PosOrder.java(+selfDelivery);grep PosStoreMapper.xml/PosOrderMapper.xml 若有涉及全列的 resultMap/select 则同步
  • T004 [P] 后端 i18n:ruoyi-admin/src/main/resources/i18n/ 下 5 个 properties(messages、zh_CN、zh_TW、en_US、vi)统一加 contracts/api.md §5 全部 key

Phase 2: Foundational (Blocking Prerequisites)

⚠️ US 全部依赖本阶段完成

  • T005 [P] 先写测试 ruoyi-admin/src/test/java/com/ruoyi/app/utils/SelfDeliveryUtilTest.java:[开始,结束) 边界(18:00:00 命中/21:00:00 不命中)、按星期、多段任一命中、全天、跨天(周二配置周一时刻不命中);确认 RED
  • T006 实现 ruoyi-admin/src/main/java/com/ruoyi/app/utils/SelfDeliveryUtil.java:matches(enabled, mode, hours, target LocalDateTime),HH:mm 定长比较、ISO dayOfWeek 过滤、区间含首不含尾;T005 转 GREEN
  • T007 [P] 先写测试 ruoyi-admin/src/test/java/com/ruoyi/app/order/SelfDeliveryServiceTest.java:配置主体解析(普通门店=自身/摊位→夜市主门店行/夜市主门店行不存在→未配置回落)、保存校验(自定义至少一段、end>start、HH:mm 格式、dayOfWeek∈[1,7]);确认 RED
  • T008 实现 ruoyi-admin/src/main/java/com/ruoyi/app/order/SelfDeliveryService.java:resolveConfigStore(PosStore)、isSelfDelivery(store, target)、saveConfig(input)(整体覆盖式:先删后插)、getConfig(mdId);T007 转 GREEN

Phase 3: User Story 1 - 普通商家开启自配送并完成配送 (Priority: P1) 🎯 MVP

Goal: 普通商家配置自配送→即时单命中标记→不进骑手池→商家接单/出餐/(可选开始配送)/确认送达全链路。

Independent Test: quickstart 场景 3+5+6(隔离/流转/可选步骤)。

  • T009 [P] [US1] 先写契约测试:ruoyi-admin/src/test/java/com/ruoyi/app/order/PosOrderShOprateControllerTest.java 补用例——self_delivery=1 单接单/出餐放行(现状被 requireRiderAssigned 拦)、selfDeliveryStart/Complete 状态校验(state≠2 拒绝、重复开始拒绝);确认 RED
  • T010 [P] [US1] 先写契约测试:ruoyi-admin/src/test/java/com/ruoyi/app/order/PosOrderQsOprateControllerTest.java 补用例——acceptOrder 对 self_delivery=1 拒绝(no.order.selfdelivery.rider.denied);确认 RED
  • T011 [US1] 配置接口:ruoyi-admin/src/main/java/com/ruoyi/app/mendian/PosStoreController.java 加 GET /getMdSelfDeliveryHours 与 POST /saveMdSelfDeliveryHours(@RequestHeader token + DTO);新建 ruoyi-admin/src/main/java/com/ruoyi/app/mendian/dto/SelfDeliveryConfigInput.java(含内嵌 SelfDeliveryHourInput,无 Bean Validation 注解,校验在 Service);普通商家走 merchantStoreAccessService.requireStoreAccess
  • T012 [US1] ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderShOprateController.java 的 requireRiderAssigned 对 self_delivery=1 放行(research F2)
  • T013 [US1] 同文件新增 POST /selfDeliveryStart(state=2 且 deliveryStatus=0 → 2)与 POST /selfDeliveryComplete(state=2 且 deliveryStatus∈{0,2} → 原子置 deliveryStatus=3+state=3+sdTime);新建 DTO ruoyi-admin/src/main/java/com/ruoyi/app/order/dto/SelfDeliveryOperateInput.java(id 必填、qsImg 可选);Redisson 锁 order:sh:selfdelivery:{id};订单日志;用户推送(区分自配论文案)
  • T014 [US1] 同文件 chuCan 出餐推送:self_delivery=1 跳过骑手推送、保留用户推送
  • T015 [US1] 下单判定接入:ruoyi-admin/src/main/java/com/ruoyi/app/order/UserOrderController.java 的 createOrderChild 与 PosOrderShOprateController.java 的商家端 createOrder,创建 PosOrder 时调 SelfDeliveryService.isSelfDelivery(store, 基准时间) 写 self_delivery(本阶段基准=当前时间;预约基准在 T026 接入)
  • T016 [US1] 骑手隔离:ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderQsOprateController.java——newTask 过滤追加 self_delivery=0(NULL 视为 0 兼容存量)、acceptOrder/pickupOrder/deliverOrder 对 self_delivery=1 抛 no.order.selfdelivery.rider.denied(防直调,镜像 027 模式)
  • T017 [US1] 冒烟:按 quickstart 场景 3(池隔离+快照)、场景 5(流转,暂只核状态不核账单——账单在 US5)、场景 6(可选开始配送)走通(并入 T040 统一回归——需 DDL 已执行+服务启动)

Checkpoint: 普通商家即时单 MVP 端到端可用。


Phase 4: User Story 2 - 夜市主统一自配送旗下摊位订单 (Priority: P1)

Goal: 夜市主在夜市主门店行配置,对旗下全部摊位生效;夜市主可见并可操作摊位自配送订单;摊位无配置入口。

Independent Test: quickstart 场景 2(配置权限+摊位只读)+ 夜市摊位单不进池。

  • T018 [P] [US2] 先写测试:SelfDeliveryServiceTest 补——夜市主门店行归属校验(store.userId==登录用户且 isNightMarket=1 才可保存)、摊位(isStall=1)保存拒绝、摊位判定读夜市主门店行配置;确认 RED
  • T019 [US2] ruoyi-admin/src/main/java/com/ruoyi/app/mendian/PosStoreController.java 配置接口权限完善:夜市主门店行归属校验、摊位拒绝(no.store.stall.selfdelivery.readonly);getMdSelfDeliveryHours 对摊位 mdId 返回所辖夜市配置(前端只读展示)
  • T020 [US2] 判定摊位解析接线:SelfDeliveryService.resolveConfigStore 摊位(isStall=1 且 nightMarketId 非空)→ 按 nightMarketId 查夜市主门店行(isNightMarket=1 且 userId=nightMarketId);用测试库核验夜市主门店行存在性,缺失则视为未配置回落骑手池(research D1 待验证项)
  • T021 [US2] 夜市主操作核验:requireMerchantOrderAccess(PosOrderShOprateController)对 shId=夜市主放行 selfDeliveryStart/Complete(补契约测试用例);同一父单下多摊位子单 self_delivery 一致性用例
  • T022 [US2] 冒烟:quickstart 场景 2 + 夜市摊位外送单(时段内)self_delivery=1 且不进 newTask、夜市主 token 可完成配送(并入 T040;库内核验 SQL 见下)

Checkpoint: 夜市场景与普通商家场景并存,同一套操作端点。


Phase 5: User Story 3 - 用户端显示"商家自配送" (Priority: P1)

Goal: 订单数据透出自配送标记与配送状态;推送文案区分;客户端 App 展示由外部配合(本仓库交付字段与文案)。

Independent Test: quickstart 相关断言——订单接口响应含 selfDelivery;推送文案 key 生效;骑手无推送。

  • T023 [P] [US3] 测试核验:用户/商家/骑手各订单 list 与 detail 接口(UserOrderController 订单查询、PosOrderShOprateController.orderList/orderDetail、getParentOrderDetail)响应 JSON 含 selfDelivery 字段(全字段序列化,research F16)——补断言用例
  • T024 [US3] 推送接线核验:selfDeliveryStart→no.message.push.merchant.delivery.start、selfDeliveryComplete→送达文案(复用现有送达 key,文案区分自配送);断言 self_delivery=1 全流程不触发骑手推送

Phase 6: User Story 4 - 预约单按预约送达时间判定 (Priority: P2)

Goal: delryTime 非空的订单按预约时间(而非下单时刻)判定自配送时段。

Independent Test: quickstart 场景 4。

  • T025 [P] [US4] 先写测试:SelfDeliveryServiceTest/UtilTest 补——预约时段内(15:00 下单预约 19:00,时段 18:00-21:00 → 命中)、预约时段外、预约次日(按次日的星期+时刻);确认 RED
  • T026 [US4] 下单入口基准接入:T015 两处判定调用改传 delryTime 非空 ? delryTime 解析值 : 当前时间(ruoyi-admin/.../order/UserOrderController.java、PosOrderShOprateController.java);注意 delryTime 字符串格式的解析与失败回落(解析失败按即时单处理并记 warn 日志)
  • T027 [US4] 冒烟:quickstart 场景 4 两笔预约单判定正确(并入 T040)

Phase 7: User Story 5 - 自配送运费结算归配送方 (Priority: P2)

Goal: 确认送达后运费全额(不抽成)入账 user_id=sh_id;到付归因同步;无骑手分成。

Independent Test: quickstart 场景 5 账单核对(本阶段补全账单断言)。

  • T028 [US5] 前置核验:grep 全库 setType(" 与 user_billing 现有 type 语义,确认 "5" 未被占用(research D5);若占用换值并同步 data-model.md/contracts
  • T029 [P] [US5] 先写测试:OrderService 结算用例——type="5" 账单(user_id=sh_id、amount=freight 全额、divvy=0、dedup 键 userId+type+ddId)、夜市摊位单 shId=夜市主、到付单 type="3" 账单 payment_id=sh_id;确认 RED
  • T030 [US5] 实现 ruoyi-admin/src/main/java/com/ruoyi/app/order/OrderService.java#setSelfDeliveryFreightBilling(含 walletService.addBalance(sh_id)),并在 ruoyi-admin/src/main/java/com/ruoyi/app/order/OrderLifecycleService.java#completeSideEffects 增分支:type==0 && self_delivery==1 && qsId==null 调用之(管理端 018 与商家送达共用此副作用)
  • T031 [US5] 到付归因:selfDeliveryComplete 中 collectPayment=1 → 用户账单 type="3" 的 paymentId=sh_id(镜像 QsOprate.updateUserBill,注意不依赖 qsId)
  • T032 [US5] 冒烟:quickstart 场景 5 全量账单 SQL 核对(type=5 存在、骑手分成不存在、商品分成照常)(并入 T040;账单 SQL 断言见 quickstart 场景5)

Phase 8: User Story 6 - 商家端配置与订单标识 (Priority: P2)

Goal: foodie-store(E:\QtwCode\foodie\foodie-store)门店设置自配送区块 + 订单"自配送"标识,四语言。

Independent Test: quickstart 场景 7。

  • T033 [P] [US6] 定位前端结构:阅读 foodie-store 门店信息页(营业时段编辑所在组件)与订单列表/详情组件、src/lang/ 四语言文件对应对象层级,输出改动点清单(无代码改动)
  • T034 [US6] 【2026-09-10 决策:PC 普通商家功能隐藏不做;夜市主子集已由 T042 落地】门店信息页加自配送区块:开关 + 模式(全天/自定义)+ 星期×多时段编辑(复用营业时段编辑交互),调 get/saveMdSelfDeliveryHours;摊位主只读展示"由夜市主统一配送"(foodie-store 对应门店设置组件,Python 脚本编辑保 CRLF)- [ ] T035 [P] [US6] i18n:foodie-store src/lang/zh.js、tw.js、en.js、vi.js 四文件同步加 selfDelivery.* keys(有意义英文驼峰、按对象层级插入、四文件 key 一致)- [ ] T036 [US6] 订单列表/详情:selfDelivery=1 显示"自配送"标签($t),不显示等待骑手类状态- [ ] T037 [US6] 【可裁剪:依赖 clarify Q2 端覆盖决策,未决策前跳过】PC 配送操作按钮:开始配送/确认送达调 /system/orderShOprate/selfDeliveryStart|Complete
  • T038 [US6] 冒烟:quickstart 场景 7(配置保存回显、标识、四语言、摊位主无编辑入口) ---

【转外部配合】T033 探索结论:foodie-store 无门店信息页与订单列表页(在商家 App),本仓库前端无可挂载点,界面归商家 App 接入(见 delivery.md 外部配合清单);i18n 前端 key 随 App 端实施

Phase 9: Polish & Cross-Cutting Concerns

  • T039 构建与测试全绿:临时 JAVA_HOME 下 mvn -pl ruoyi-system,ruoyi-admin -am compile + mvn -pl ruoyi-admin test;若有 XML 改动跑 ET.parse 校验
  • T040 quickstart 场景 8(快照不回滚)+ 场景 1-8 整体回归一遍【待开发者执行 DDL 后进行:启动服务按 quickstart 场景1-8 回归】
  • T041 子代理逐项核验:对照 spec.md FR-001~FR-012 与 SC-001~SC-005 逐条核对实现落实(含骑手直调接口探测),输出核对清单
  • T042 交付说明:更新 specs/029 文档状态;列出客户端 App(uni-app)外部配合清单——订单字段 selfDelivery 含义、展示"商家自配送"与配送状态映射(0 备餐中/2 配送中/3 已送达)

Dependencies & Execution Order

Phase Dependencies

  • Phase 1/2(Setup+Foundational): 立即开始;T002/T003/T004 与 T005/T007 并行;T006 依赖 T005,T008 依赖 T007
  • US1(Phase 3): 依赖 Phase 2 全部;T009/T010 可并行先写
  • US2(Phase 4): 依赖 T011(配置接口存在)与 T013(操作端点存在),其余可并行
  • US3(Phase 5): 依赖 T013/T015(字段已写入订单)
  • US4(Phase 6): 依赖 T015(判定接入点存在)
  • US5(Phase 7): 依赖 T013(送达端点存在)
  • US6(Phase 8): 依赖 T011/T019(后端配置接口完整);T037 额外依赖 clarify Q2 决策
  • Polish(Phase 9): 依赖全部期望交付的 Story 完成

User Story Dependencies

  • US1 为核心链路,US2/US3/US4/US5/US6 均在其端点/字段之上增量,但各自可独立验证
  • US4 与 US5、US6 无相互依赖,可并行

Parallel Opportunities

  • Phase 1:T002/T003/T004 三路并行(不同文件)
  • Phase 2:T005 与 T007 并行(测试文件不同)
  • Phase 3:T009 与 T010 并行(不同测试文件)
  • Phase 8:T035(语言文件)与 T034/T036(组件)不同文件可并行
  • US4/US5/US6 三个 Phase 可由不同人并行

Implementation Strategy

MVP First (US1)

  1. Phase 1 + Phase 2 → 基础设施
  2. Phase 3 (US1) → 普通商家即时单端到端
  3. STOP 验证:quickstart 场景 1/3/5/6 通过即可演示

Incremental Delivery

  1. US1 → US2(夜市)→ US3(标识透出)→ US4(预约)→ US5(结算)→ US6(商家端界面)
  2. 每步独立验证不回退;T037 等 Q2 决策后再启用

Notes

  • [P] = 不同文件且无未完成依赖
  • foodie-store 文件 CRLF,一律 Python 脚本编辑(项目规则)
  • T028 若 type="5" 被占用:换值后同步更新 data-model.md 与 contracts/api.md
  • 不触碰废弃代码清单(research.md「涉及的废弃代码」)

  • [x] T041 【2026-09-10 变更】普通门店保存自配送配置改为一律拒绝:仅夜市主门店行且夜市主本人可保存;新增提示 no.store.selfdelivery.nightmarket.only(六语言,th_TH 顺带补本 key);测试 PosStoreControllerSelfDeliveryTest.normalStoreCannotSaveSelfDeliveryConfig(原 normalStoreSaveGoesThroughStoreAccess 断言翻转)

  • [x] T042 【2026-09-10 追加】夜市主 PC 自配送设置入口:后端 my 读写接口(token 反查门店行+六语言拒绝文案+3 个控制器测试);foodie-store 新页面/路由/菜单/API/四语言 i18n

  • [x] T043 【2026-09-16 补正】补登记 3 条自配送 DDL;已支付自配送订单统一改为通知门店/夜市主而非骑手;type=5 账单显示统一为自配送运费;用户下单缺失 shId 时按已解析的门店归属回填。

  • [ ] T044 【外部配合】商家手机 App 的订单页面源码不在当前工作区:接入自配送标签及 selfDeliveryStart/selfDeliveryComplete 操作入口后,才能完成商家侧实际操作闭环。

  • [ ] T045 【范围外】用户 App(msd-user)按用户要求不修改;如后续需要,接入自配送标识、状态文案以及骑手入口隔离。

  • [ ] T046 【待用户确认后实施】复用 foodie-store 首页订单管理:夜市主仅查看 type=0 && selfDelivery=1 的旗下摊位订单;提供全部、备餐中、待配送、配送中、已完成、已取消/售后 Tab。摊位主保留接单/出餐;夜市主仅在待配送/配送中使用 selfDeliveryStart、selfDeliveryComplete。详见 delivery.md「夜市主 PC 订单配送调度」。