# Research: 商家自配送 (029) **Date**: 2026-09-08 | **Status**: Complete(全部 NEEDS CLARIFICATION 已解决,无遗留) 代码现状核查于 2026-09-08,分支 `test-202609v2`(commit af8cc41 附近)。 ## 现状事实(设计依据) | # | 事实 | 出处 | |---|------|------| | F1 | 骑手为抢单模式:newTask 池过滤 `type=0 AND delivery_status=0 AND state=0 AND pay_status=1 AND after_sale_status=0 AND qs_id IS NULL` | PosOrderQsOprateController.orderList | | F2 | 商家接单/出餐对外送单强制 `deliveryStatus=1 AND qsId!=null`(骑手已接单才能出餐)——**自配送单必须放宽,否则商家无法流转订单** | PosOrderShOprateController.requireRiderAssigned (L495) | | F3 | 商家端订单可见性:userType=1/5 走 merchantStoreAccessService(022 多门店);userType=3(夜市主)按 `shId=loginUserId`;摊位主按 `mdId=user.storeId` → **摊位订单的 shId 即夜市主 userId** | PosOrderShOprateController.orderList (L615-636) | | F4 | 摊位订单分成对象已是夜市主(按 nightMarketId 取用户、抽其 commission),但钱包入摊位余额 addStallBalance | OrderService.setSanghuBilling (L54-114) | | F5 | 骑手运费分成:freight × 骑手 commission,余额入骑手钱包,含到付 behalfAmount;UserBilling type="0",dedup 键 (userId,type,ddId) | OrderService.setQishouBilling (L121-159) | | F6 | 送达完成副作用统一挂 `completeSideEffects`:setSanghuBilling 恒调 + `type=0 && qsId!=null` 才 setQishouBilling;管理端 018 状态机与骑手送达都走它 | OrderLifecycleService (L491-496) | | F7 | 管理端状态机不变量:非外送不能有配送状态;delivery=3 ⇒ state=3;完成单 payStatus=1 且无售后 | OrderLifecycleService.validateResultInvariants (L433-444) | | F8 | 营业时段按星期几存(OperatingHours: mdId/startTime/endTime/dayOfWeek/isOpen,HH:mm 字符串);判定工具 OperatingUtil.storeIsOperatingOfDay 用 `LocalDate.now().getDayOfWeek().getValue()`(ISO 1=周一..7=周日)+ DateUtil.isLegalTime | OperatingHours.java, OperatingUtil.java | | F9 | **isLegalTime 边界=含首含尾**(==start 或 ==end 返回 true)——与已确认的 [开始,结束) 语义冲突,不能直接复用 | DateUtil.isLegalTime (L134-146) | | F10 | 门店信息接口在 `/chanting/store`,营业时段已有读写接口对 getMdBusinessHours / saveMdBusinessHours——自配送时段镜像该模式 | PosStoreController (L407/L428) | | F11 | 多店结账=OrderParent(ddId) + 每门店一条 PosOrder(parentDdId 关联,子单号 ddId+001…);两处下单入口:UserOrderController.createOrder(用户端)与 PosOrderShOprateController.createOrder(商家端 POS) | UserOrderController.createOrderChild (L214-279) | | F12 | 骑手接单守卫禁止骑手持未完成单再接单(no.exist.undelivered.order)→ "同一骑手多单"机制现状不存在(本功能范围外,另立需求) | PosOrderQsOprateController (L188-197) | | F13 | 到付账单:用户 UserBilling type="3",骑手送达时 paymentId=qsId | PosOrderQsOprateController.updateUserBill (L301-314) | | F14 | 商家出餐推送 chuCan 给用户+骑手双推送;骑手送达推送按 deliveryStatus 1/2/3 取文案 key | PosOrderShOprateController.chuCan、QsOprate.setOrderQsState | | F15 | PosStore 已有 isStall/isNightMarket/nightMarketId;摊位建店时 nightMarketId=夜市主 userId | PosStore.java、StallController (L88) | | F16 | 订单接口普遍全字段序列化 PosOrder("新增字段无需手动添加")→ 新增订单字段自动透出给客户端 | QsOprate.orderList (L404) | ## 设计决策 ### D1: 配置存储——pos_store 两列 + 时段子表(镜像营业时段) **Decision**: `pos_store` 加 `self_delivery_enabled`、`self_delivery_mode`(1=全天/2=自定义);新表 `pos_store_self_delivery_hours`(md_id, day_of_week, start_time, end_time),结构与 OperatingHours 对齐。配置主体:普通门店=自身行;夜市=**夜市主自己的门店行**(isNightMarket=1),摊位判定时经 `store.nightMarketId` 解析到夜市主门店行读配置。 **Rationale**: 完全复用营业时段的 UI 交互、数据形态与团队心智;夜市级单一事实源,天然保证同一夜市统一模式(spec US2)。 **Alternatives**: ①每摊位各存配置再同步——分裂配送风险+同步复杂,弃;②独立无门店关联的配置表——偏离现有模式,弃。 **待实现验证**: 夜市主门店行存在性(isNightMarket=1 且 userId=夜市主)需在实现时用测试库数据确认;若个别夜市无门店行,判定时视为"未配置自配送"回落骑手池。 ### D2: 订单标记——pos_order.self_delivery 快照列 **Decision**: `pos_order` 加 `self_delivery TINYINT(1) DEFAULT 0`,下单判定时写入,终身不变。 **Rationale**: 独立标志字段(spec 决策④);F16 保证自动透出客户端;对存量数据默认 0 无影响。 **Alternatives**: 复用 qsId=商家ID——污染骑手链路(骑手列表按 qsId 聚合、COD 归因、接单守卫均按 qsId+userType=2),弃。 ### D3: 时段判定——新工具方法,不复用 isLegalTime **Decision**: 新增 `SelfDeliveryUtil.matchesSelfDelivery(config, target)`:按 target 的 ISO dayOfWeek 过滤时段段,HH:mm 字符串比较,区间**含开始、不含结束 [start, end)**。判定基准时间:预约单(delryTime 非空)用预约时间,即时单用当前时间。判定时机:两个下单入口(F11)创建 PosOrder 时。 **Rationale**: F9 边界语义冲突必须新写;预约单按预约时间是 spec 已确认决策②。 **Alternatives**: 改 isLegalTime 全局边界——影响营业时段存量行为,风险大,弃。 ### D4: 状态机——跳过 deliveryStatus=1,送达原子完成 **Decision**: 自配送外送单流转:state 0→1(商家接单)→2(出餐)→3(确认送达);deliveryStatus 0→(2 开始配送,**可选**)→3;"确认送达"端点原子置 deliveryStatus=3 + state=3 + sdTime。`requireRiderAssigned` 对 `self_delivery=1` 放行(F2)。 **Rationale**: deliveryStatus=1 语义是"骑手已接单",自配送无此环节;delivery=3⇒state=3 恰好满足管理端不变量(F7);开始配送可选是 clarify Q3 结论。 **Alternatives**: 自配送也走 deliveryStatus=1(语义撒谎,骑手列表查询会误捞),弃。 ### D5: 运费结算——UserBilling 新 type,user_id=sh_id 统一 **Decision**: `completeSideEffects` 增分支:`type=0 && self_delivery=1 && qsId==null` → 生成自配送运费入账:UserBilling `type="5"`(实现时 grep 确认未占用)、`user_id=sh_id`、`amount=freight 全额`、`divvy=0/divvy_rate=0`、`user_type="1"`、钱包 `addBalance(sh_id)`。**不抽成**(clarify Q1)。到付单(collectPayment=1)送达时用户账单 type="3" 的 paymentId=sh_id(镜像 F13)。夜市摊位单 shId=夜市主(F3)→ 运费自然归夜市主,与摊主的分摊线下处理(spec 决策③)。 **Rationale**: F3+F5 使 user_id=sh_id 对普通商家/夜市主统一成立,零分支;新 type 避开 setSanghuBilling 的 (userId,"0",ddId) dedup 冲突。 **Alternatives**: 复用 type="0"——与商品分成 dedup 键冲突(互斥掉一条账单),必须弃;freight 并入商品分成金额——账目不可区分,弃。 ### D6: 骑手侧隔离——池过滤 + 操作直调拒绝 **Decision**: newTask 池查询追加 `self_delivery=0`(含 NULL 兼容存量);QsOprate acceptOrder/pickupOrder/deliverOrder 对 `self_delivery=1` 的单抛错(镜像 027 线下转账单防直调模式)。 **Rationale**: spec FR-004 接口级隔离;直调防御有先例(F1 转账单校验)。 ### D7: 推送——骑手链路全跳过,用户推送文案区分 **Decision**: chuCan 出餐推送:self_delivery=1 时跳过骑手推送、保留用户推送;自配送单"开始配送/已送达"推送用户(新 i18n key,文案区分商家自配送);不触发任何骑手推送。 **Rationale**: spec FR-009;复用 PayPush/MerchantNotificationRouter 现有通道。 ### D8: 配置接口——镜像营业时段接口对 **Decision**: `/chanting/store/getMdSelfDeliveryHours`(GET)+ `/chanting/store/saveMdSelfDeliveryHours`(POST,DTO 承载)。权限:普通商家 merchantStoreAccessService.requireStoreAccess;夜市主要求门店行归属(store.userId=登录用户且 isNightMarket=1);**摊位门店(isStall=1)拒绝**(错误走 MessageUtils)。校验:自定义模式至少一段、每段 end>start、HH:mm 格式。 **Rationale**: F10 镜像现有接口对;spec FR-002 摊位无配置权。 ### D9: 商家端 PC 范围——配置+标识必做,配送操作按钮独立任务 **Decision**: foodie-store:门店设置自配送区块(复用营业时段星期编辑交互)+ 订单列表/详情"自配送"标识,必做。**PC 配送操作按钮(开始配送/确认送达)单列为独立任务,依赖 clarify Q2(端覆盖暂缓决策)**;后端操作接口先行交付,App/PC 界面后接入同一接口。 **Rationale**: Q2 用户明确暂缓;接口与界面解耦后无论 Q2 结论如何都不返工。 ### D10: 平台管理端不在本功能范围 **Decision**: 不改 foodie-admin-vue;订单接口全字段序列化已自动带 self_delivery(F16),管理端未来需要时直接用。 **Rationale**: spec 范围声明(后端+商家端 PC);避免扩散。 ## 涉及的废弃代码 无。本功能不触碰 CLAUDE.md 废弃清单内代码(ZaloPay/PayController/TestTask/OrderAppeal 等);结算挂在在用的 OrderLifecycleService/OrderService 链路。