--- 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) - [x] T001 将 3 条 DDL(pos_store 2 列、pos_store_self_delivery_hours 建表、pos_order 1 列,见 data-model.md)登记到 `updatesql/sql.md`,标注 `-- 2026-09-08 029 商家自配送`;不直接执行 - [x] 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) - [x] 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 则同步 - [x] 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 全部依赖本阶段完成** - [x] T005 [P] 先写测试 `ruoyi-admin/src/test/java/com/ruoyi/app/utils/SelfDeliveryUtilTest.java`:[开始,结束) 边界(18:00:00 命中/21:00:00 不命中)、按星期、多段任一命中、全天、跨天(周二配置周一时刻不命中);确认 RED - [x] T006 实现 `ruoyi-admin/src/main/java/com/ruoyi/app/utils/SelfDeliveryUtil.java`:`matches(enabled, mode, hours, target LocalDateTime)`,HH:mm 定长比较、ISO dayOfWeek 过滤、区间含首不含尾;T005 转 GREEN - [x] T007 [P] 先写测试 `ruoyi-admin/src/test/java/com/ruoyi/app/order/SelfDeliveryServiceTest.java`:配置主体解析(普通门店=自身/摊位→夜市主门店行/夜市主门店行不存在→未配置回落)、保存校验(自定义至少一段、end>start、HH:mm 格式、dayOfWeek∈[1,7]);确认 RED - [x] 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(隔离/流转/可选步骤)。 - [x] T009 [P] [US1] 先写契约测试:`ruoyi-admin/src/test/java/com/ruoyi/app/order/PosOrderShOprateControllerTest.java` 补用例——self_delivery=1 单接单/出餐放行(现状被 requireRiderAssigned 拦)、selfDeliveryStart/Complete 状态校验(state≠2 拒绝、重复开始拒绝);确认 RED - [x] T010 [P] [US1] 先写契约测试:`ruoyi-admin/src/test/java/com/ruoyi/app/order/PosOrderQsOprateControllerTest.java` 补用例——acceptOrder 对 self_delivery=1 拒绝(no.order.selfdelivery.rider.denied);确认 RED - [x] 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 - [x] T012 [US1] `ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderShOprateController.java` 的 `requireRiderAssigned` 对 `self_delivery=1` 放行(research F2) - [x] 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}`;订单日志;用户推送(区分自配论文案) - [x] T014 [US1] 同文件 `chuCan` 出餐推送:self_delivery=1 跳过骑手推送、保留用户推送 - [x] T015 [US1] 下单判定接入:`ruoyi-admin/src/main/java/com/ruoyi/app/order/UserOrderController.java` 的 createOrderChild 与 `PosOrderShOprateController.java` 的商家端 createOrder,创建 PosOrder 时调 SelfDeliveryService.isSelfDelivery(store, 基准时间) 写 self_delivery(本阶段基准=当前时间;预约基准在 T026 接入) - [x] 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(配置权限+摊位只读)+ 夜市摊位单不进池。 - [x] T018 [P] [US2] 先写测试:SelfDeliveryServiceTest 补——夜市主门店行归属校验(store.userId==登录用户且 isNightMarket=1 才可保存)、摊位(isStall=1)保存拒绝、摊位判定读夜市主门店行配置;确认 RED - [x] T019 [US2] `ruoyi-admin/src/main/java/com/ruoyi/app/mendian/PosStoreController.java` 配置接口权限完善:夜市主门店行归属校验、摊位拒绝(no.store.stall.selfdelivery.readonly);getMdSelfDeliveryHours 对摊位 mdId 返回所辖夜市配置(前端只读展示) - [x] T020 [US2] 判定摊位解析接线:`SelfDeliveryService.resolveConfigStore` 摊位(isStall=1 且 nightMarketId 非空)→ 按 nightMarketId 查夜市主门店行(isNightMarket=1 且 userId=nightMarketId);用测试库核验夜市主门店行存在性,缺失则视为未配置回落骑手池(research D1 待验证项) - [x] 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 生效;骑手无推送。 - [x] T023 [P] [US3] 测试核验:用户/商家/骑手各订单 list 与 detail 接口(UserOrderController 订单查询、PosOrderShOprateController.orderList/orderDetail、getParentOrderDetail)响应 JSON 含 selfDelivery 字段(全字段序列化,research F16)——补断言用例 - [x] 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。 - [x] T025 [P] [US4] 先写测试:SelfDeliveryServiceTest/UtilTest 补——预约时段内(15:00 下单预约 19:00,时段 18:00-21:00 → 命中)、预约时段外、预约次日(按次日的星期+时刻);确认 RED - [x] 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 账单核对(本阶段补全账单断言)。 - [x] T028 [US5] 前置核验:grep 全库 `setType("` 与 user_billing 现有 type 语义,确认 "5" 未被占用(research D5);若占用换值并同步 data-model.md/contracts - [x] 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 - [x] 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 与商家送达共用此副作用) - [x] 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。 - [x] 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 - [x] 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 回归】 - [x] T041 子代理逐项核验:对照 spec.md FR-001~FR-012 与 SC-001~SC-005 逐条核对实现落实(含骑手直调接口探测),输出核对清单 - [x] 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 订单配送调度」。 - [x] T047 【2026-10-08 变更】夜市主锚点行缺失不再拒绝:测试库全库无 is_night_market=1 行(历史无创建路径), GET 对夜市主返回默认未启用配置;SAVE 经 `SelfDeliveryService.resolveOrCreateNightMarketStoreByOwner` 首次保存自动补建锚点行; 门槛改为显式校验 user_type=='3'(非夜市主仍拒 market.missing)。 测试:SelfDeliveryServiceTest 补建/复用 2 例 + PosStoreControllerSelfDeliveryTest 默认返回/补建保存/类型门槛 3 例。