# API Contracts: 商家自配送 (029) **Date**: 2026-09-08 | 遵守项目 Controller 规范(@RequestHeader token / @RequestBody DTO / @RequestParam 显式标注 / 禁 Map 入参) ## 1. 自配送配置读写(镜像营业时段接口对) ### GET `/chanting/store/getMdSelfDeliveryHours` | 参数 | 位置 | 类型 | 说明 | |------|------|------|------| | token | Header | String | 登录 token | | mdId | Query | Integer | 配置主体门店 id(普通门店=自身;夜市=夜市主门店行) | **响应**:`{ enabled: boolean, mode: Integer(1=全天,2=自定义,null=未设置), hours: [{dayOfWeek, startTime, endTime}] }` (摊位主查询自己摊位 → 返回所辖夜市主门店的配置,便于前端展示"由夜市统一配送"提示;无配置返回 enabled=false 空结构) ### POST `/chanting/store/saveMdSelfDeliveryHours` **入参 DTO** `SelfDeliveryConfigInput`: | 字段 | 类型 | 校验(Service 内,MessageUtils 报错) | |------|------|------| | mdId | Integer | 必填;权限校验见下 | | enabled | Boolean | 必填 | | mode | Integer | enabled=true 时必填,∈{1,2} | | hours | List | mode=2 时必填非空;每项 dayOfWeek∈[1,7]、startTime/endTime 匹配 HH:mm、end>start | **权限规则**: - 门店为摊位(`is_stall=1`)→ 拒绝(key: `no.store.stall.selfdelivery.readonly`,文案"摊位配送方式由夜市主统一设置") - 夜市主门店行(`is_night_market=1`)→ 仅 `store.userId == 登录用户` 可保存 - 普通门店 → `merchantStoreAccessService.requireStoreAccess(loginUser, mdId)`(复用 022 多门店权限) **效果**:pos_store 两列更新;mode=2 时 hours 整体覆盖式重建(先删后插)。已下单订单的 self_delivery 快照不受影响。 ### GET/POST `/chanting/store/{get|save}MyNightMarketSelfDelivery`(夜市主 PC「我的自配送」) - Header `token` 必填;无业务参数(save 传 `SelfDeliveryConfigInput`,mdId 由后端覆盖)。 - 门槛:仅夜市主(info_user.user_type=='3');其余一律拒绝 `no.store.selfdelivery.market.missing`。 - 【2026-10-08 变更】锚点行(is_night_market=1 且 user_id=登录 userId)缺失时:GET 返回 `{enabled:false, mode:null, hours:[]}`;SAVE 自动补建锚点行(纯配置行,is_stall=0)后保存,后建摊位照常生效。 ## 2. 商家配送操作(新增于 PosOrderShOprateController `/system/orderShOprate`) ### POST `/selfDeliveryStart`(开始配送,可选步骤) 入参 DTO `SelfDeliveryOperateInput`:`id: Long`(PosOrder id,必填) | 校验 | 失败响应 | |------|----------| | 订单存在且 `requireMerchantOrderAccess`(夜市主按 shId、商家按门店权限,复用现有方法) | `merchant.order.not.found` / 权限错误 | | `type==0 && self_delivery==1 && qsId==null` | `no.order.selfdelivery.required`(非自配送单) | | `state==2`(已出餐)且 `deliveryStatus==0` | `no.order.status.not.allow` | | afterSaleStatus==0 | `no.order.aftersale.block` | **效果**:deliveryStatus=2;订单日志"商家已开始配送";向用户推送配送中通知(新 key `no.message.push.merchant.delivery.start`,文案区分商家自配送)。Redisson 锁 `order:sh:selfdelivery:{id}` 防并发(镜像 QsOprate 模式)。 ### POST `/selfDeliveryComplete`(确认送达) 入参 DTO 同上(`id` 必填,`qsImg` 可选送达凭证) 校验同上,但状态要求 `state==2 && deliveryStatus∈{0,2}`(允许跳过开始配送); 另须 `payStatus==1`(在线/到付创建即付;商家POS现金/转账外送单须先确认收款——对齐骑手送达语义与管理端"完成单必已支付"不变量,错误 key `no.order.selfdelivery.unpaid`,2026-09-08 核验修复)。 **效果**(事务内): 1. 原子置 `deliveryStatus=3 + state=3 + sdTime=now` 2. `completeSideEffects`(复用/扩展 OrderLifecycleService L491-496 的分支):商品分成 + **自配送运费入账**(UserBilling type="5",user_id=sh_id,amount=freight 全额,divvy=0) 3. 到付单(collectPayment=1):用户账单 type="3" 的 paymentId=sh_id 4. 订单日志"商家已确认送达";向用户推送已送达通知(复用现有送达 key 或新 key,文案区分自配送) ## 3. 下单判定(现有接口行为变化,无新端点) `UserOrderController.createOrder` 与 `PosOrderShOprateController.createOrder` 两个入口在创建每条 PosOrder 时: - 解析门店配置:普通门店读自身;摊位(is_stall=1 且 nightMarketId 非空)读夜市主门店行 - 判定基准:`delryTime` 非空 → 预约时间;否则当前时间 - `SelfDeliveryUtil.matches(...)`([start,end),ISO dayOfWeek)→ 写 `self_delivery=1/0` **响应变化**:订单对象自动携带 `selfDelivery` 字段(全字段序列化),客户端无需额外接口。 ## 4. 骑手侧隔离(现有接口行为变化) | 接口 | 变化 | |------|------| | `GET /system/orderQsOprate/orderList?tab=newTask` | 追加过滤 `self_delivery=0`(NULL 视为 0,兼容存量) | | `GET /system/orderQsOprate/acceptOrder` | 订单 `self_delivery=1` → 拒绝 `no.order.selfdelivery.rider.denied`(防直调,镜像 027 模式) | | `POST /system/orderQsOprate/pickupOrder` / `deliverOrder` | 同上 | ## 5. i18n key 清单 **后端**(`messages.properties` + en_US/vi/zh_CN/zh_TW 共 5 文件): `no.store.stall.selfdelivery.readonly`、`no.store.selfdelivery.hours.invalid`、`no.order.selfdelivery.required`、`no.order.selfdelivery.rider.denied`、`no.order.selfdelivery.status.invalid`、`no.order.selfdelivery.unpaid`、`no.message.push.merchant.accepted.content`(自配送单商家接单推用户,核验修复)、`no.message.push.merchant.delivery.start`、`no.billing.selfdelivery.freight`(账单说明) **商家端前端**(foodie-store `src/lang/`:zh.js/tw.js/en.js/vi.js,key 用有意义英文驼峰): `selfDelivery.*`(开关/模式/星期时段编辑/保存成功)、`orderList.selfDeliveryTag`(订单"自配送"标识)等,四文件 key 完全一致。 ## 6. 前端改动点(foodie-store,商家端 PC) | 位置 | 改动 | |------|------| | 门店信息页 | 自配送区块:开关 + 模式选择 + 星期×多时段编辑(复用营业时段编辑交互);摊位主隐藏该区块(读接口返回夜市配置时只读展示) | | 订单列表/详情 | `selfDelivery=1` 显示"自配送"标签;自配送单不显示"等待骑手接单"类状态 | | 配送操作按钮 | **独立任务(依赖 clarify Q2 暂缓决策)**:开始配送/确认送达按钮调用 §2 接口;接口先行交付 | 注意:foodie-store 文件为 CRLF,编辑用 Python 脚本替换(项目规则)。