# 多店结算接口契约(实施版) 以下是 036 后端接口契约,部署前仍须执行已登记迁移并完成 App 联调;路径存在不代表已上线。路径均为 `/system/multiStoreCheckout` 前缀;用户接口使用 `@RequestHeader String token`,POST 使用明确 DTO 与 `@RequestBody`,GET 参数显式声明。App 由前端人员按本文接入,本仓库不修改 App 源码。错误响应使用现有 `AjaxResult` 及项目 i18n 消息。新用户/骑手接口必须发送 `X-Multi-Store-Capability: batch-cards-v1`;缺失或未知值返回 `errorCode=UPGRADE_REQUIRED`。普通单旧接口不要求此头。两端卡片语义见 [联合方案](../frontend-display-plan.md)。 ## 用户流程 | 方法与路径 | 请求关键字段 | 成功响应 | 拒绝条件 | |---|---|---|---| | `POST /quote` | 地址 ID、顺序候选店与商品/规格/数量、同骑手、各店即时/预约时段、逐店备注 | `quoteToken`、有效至、锚点/围栏、参与店及逐店原运费/折后运费/商品价/实付/可选支付方式、圈外店和不兼容原因、规则版本 | 店/商品/配送资格不合格、支付方式仅转账、报价配置非法;圈外店仅报告不创建 | | `POST /batches` | `requestKey`、`quoteToken`、参与店 ID 列表、逐店 `paymentMethods:[{storeId,payType}]`,可选顶层发票字段同普通下单 | 父批次号、子单号/金额/已选方式及状态/可选方式、两个截止时间;逐店备注已按报价快照固化到子单 `remarks`,可开票子单保存发票意图;含未付线上子单为 `PENDING`,全到付为 `ACTIVE`,但 `ACTIVE` 不等于已开放 | 报价过期、成员/地址/价格/规则变化、逐店方式缺漏/重复/夹带非参与店/不可用、电子发票无可开票店或资料校验失败、权限不符;同用户同 requestKey 且付款及发票载荷相同返回原批次,否则拒绝冲突 | | `GET /batches/{batchId}` | token + 路径批次 ID | 批次/逐店支付选择、真实实收、到付确认、退款与履约状态、截止时间、关联订单 | 非本人批次拒绝,不暴露其他用户父/子/组 | | `POST /batches/members/payment` | body `{batchId,orderId,payType,clientRequestId}`,`payType` = 1/2/3/5 | 新批次的安全改选:到付返回批次状态,线上返回 `ONLINE_PENDING`;网关创建仍走原接口 | 批次已生效/过操作截止、该店在途支付已实收或渠道核实未通过、商家不可用、归属不符;6 转账始终不支持。存在在途尝试时服务端先逐笔向渠道核实,确定未付(OMG UNPAID/FAILED、LINE 未捕获)即放行改选 | | `POST /batches/cancel` | body `{batchId,reason:"USER_CANCEL",clientRequestId}` | 当前入口只返回是否成功关闭未生效批次的布尔值;退款真实进度待详情接口返回 | 已 `ACTIVE` 须走子单取消规则;退款处理中不伪称退款成功 | | `POST /batches/members/cancel` | body `{batchId,orderId,reason:"USER_CANCEL",clientRequestId}` | 当前入口只返回是否成功取消该子单;退款真实进度待详情接口返回,其他成员费用快照不变 | 未生效不允许单店取消;骑手已接/取餐拒绝用户取消;并发接组只能一方成功 | 每店报价另外区分 `discountedFreight`(骑手毛运费)、`merchantDeliverySubsidy`(商家免运承担)、`customerDeliveryFee`(客户运费)及 `amount`(优惠后商品金额+客户运费)。商家满额免运以多店折后运费为基数:原运费40、八折后32、全免时客户运费0、商家补贴32、骑手仍收32。到付给商家金额为 `amount - discountedFreight`;普通单的免运口径不改。 回调/补单仍使用现有 LINE/OMG 入口,服务端以原订单/尝试号识别新批次成员:先验签与核实实收、落支付事实,再在事务内聚合批次;不得直接发履约新单通知。到付只表示用户确认按现有流程交餐付款,不伪称线上实收。用户列表应展示逐店已选方式、部分付款、缓冲核实、待批次生效、退款中并提供继续结算入口;普通旧单字段语义不变。 036 子单调用原 LINE/OMG 创建入口、OMG 重试入口同样须携带上述能力头,旧版 App 不能通过原网关入口绕过逐店收银。原 LINE/OMG 网关查询、异步回调、补单继续按原交易事实核实,不以客户端能力头作为可信付款事实。 LINE 创建使用 `{ddId:orderNo}`;OMG 创建使用 `{orderId:orderNo,paymentMethod:"CREDIT"}` 对应 `payType=2`,或 `paymentMethod:"APPLE_PAY"` 对应 `payType=5`。`POST /batches/members/payment` 只选付款方式,不替代上述网关创建,也不返回跳转凭证。 ## 骑手/平台 - 骑手待抢列表对同骑手配送组只返回一个任务,显示各店及总计;已生效、到预约开放时点才可见。`POST /system/multiStoreCheckout/groups/accept`,JSON `{groupId,version,clientRequestId}`,原子承接全部有效成员;旧单笔接单 API 对组成员拒绝拆抢。骑手仍逐子单取餐/送达。接组须以创建时首店为位置基准,在同一骑手锁内重验资格、位置时效、接单范围、容量与成员状态。首店到其他门店的合并围栏另行校验,不用最近店替代首店。组详情路径为 `GET /groups/{groupId}`,待抢/已接列表和角标使用下节的统一口径。 - 组详情 `members[]` 逐子单返回 `legacyOrder`、`juli`、`jvli`:`juli` 在传入骑手 `longitude`/`latitude`(成对可选,只传其一拒绝)时按定位到订单坐标的直线公里距离计算,与统一列表同口径,不传为空;`jvli` 为建单时服务端道路里程的公里快照,并与 `legacyOrder.jvli` 一致;历史无快照时为空。组卡顶层没有单一门店配送距离。 - 商家已接未取但实际无法供餐:`POST /merchant/members/cancelUnavailable`,body `{batchId,orderId,reason:"MERCHANT_UNAVAILABLE",clientRequestId}`。仅授权门店、已接组中尚未被骑手取餐的本子单可取消;本店实收登记原路退款意图,其他店不补差;最后一个有效子单取消时结清组和父单。 - 平台配置 `GET/PUT /system/multiStoreCheckout/config`:开关、围栏米、最多店数、缓冲分钟、可增删改折扣阶梯;保存会校验并更新规则版本,仅影响新报价。平台退款跟进列表显示各商家渠道、实收、退款意图/结果/待人工处理,不提供“手工标记渠道已退款”。 - 平台退款跟进只读接口 `GET /system/multiStoreCheckout/admin/refunds?page=1&size=20&status=ALL`,可按 `PENDING/PROCESSING/UNKNOWN/FAILED/MANUAL_REVIEW/REFUNDED` 过滤;需要现有 `system:order:list` 权限,按退款意图逐笔分页,返回 `batchId/memberId/orderId/storeId/channel/amount/merchantOrderNo/intentStatus/gatewayStatus/effectiveStatus`。仅 `REFUNDED` 表示原渠道已证实退款;测试 OMG 无法实际退款时不得手工置为成功。 - 平台 `GET /system/order/list` 仍以子单为行;当前页 036 真成员附加 `multiStore={orderId,batchId,groupId,parentOrderNo,checkoutStatus,groupStatus,paymentState}`,普通单为 `null`,不改变原分页数量。平台 `GET /system/multiStoreCheckout/admin/batches/{batchId}` 使用 `system:order:list` 权限,返回只读批次和逐店进度,不暴露 App 的 `allowedActions`;既有 App `/batches/{batchId}` 仍严格检查用户归属。平台普通状态修改入口对 036 成员直接拒绝,非 036 订单沿原流程。 - 平台配置只读写独立 `order_checkout_config`,不占用 `sys_config`。GET 返回 `enabled/fenceMeters/maxStores/confirmationBufferMinutes/version/tiers`,其中 `tiers` 为门槛数字键到系数的对象;PUT 将 `tiers` 作为 `{minimumStores,ratio}` 数组传入,并回传新版本。缺表或缺行时读取保守关闭,保存拒绝并提示先执行 036 迁移;客户端旧 `version` 不能覆盖新配置。 - 新任务通知出站与预约旧时点扫描按持久记录幂等重试;组只推一个待抢任务,独立成员各推一个。用户付款通知与订单取餐/送达通知继续独立,不在未全额就绪时发骑手/商家履约新单。 - 036 组新任务推送额外带 `groupId`、`eventId`;前端用 `groupId` 打开组详情,并按 `eventId` 去重重试推送。后端 `SENT` 是已提交到项目现有异步推送通道,不等于设备已送达。 ## 时间和兼容示例 14:00 创建:建批请求已逐店选方式,到付当场确认;14:15 前可发起线上支付,未有网关尝试的待生效成员可合法改选。14:14 已实际发起的线上尝试可在 14:20 前完成付款及可信确认;14:16 不可新发起、重试或改选;14:20 仍未全部就绪则关闭,迟到实收只退款不激活。截止在创建时快照,不因配置变更或重试延长。 普通单仍调用现有 `/createOrder`、LINE/OMG/到付入口并保持原校验与状态;新接口不得仅凭客户端传入的批次号放宽原入口。预约订单不重定义旧时间语义;2026-10-16 已确认新多店接入 037 现有无人接单规则,按父批次统一生效时间及实际开放时点判断,不以逐子单支付时间开始。 ## 2026-10-16 已确认的边界补充(尚待实现) - 批次详情返回 `activatedAt`,组详情返回 `openedAt`,并由服务端给出真实阶段/允许操作;App 不从付款时间推导无人接单倒计时。按现有阈值自动取消未承接组时,整组从待抢消失,详情保留逐店取消及退款进度,迟到实收不重开。 - 未生效预约在激活时已到或超过结束则取消,返回原预约已过期的业务结果及真实退款进度,不返回可抢状态、不自动改即时;已生效任务不由此检查取消。 - 建批后收货地址/坐标固定,地址簿变化不修改订单;首版无多店改址。用户要换地址按现有整批/子单取消权限处理后重新报价,不能只修改某店或绕过已接单禁取消。 - 配置 PUT 降低最大门店数时,存在超限未结束未承接组则保存失败、原配置和版本不变;提示先处理存量组。已接组不受影响;失败不暗中取消订单或改变费用。 ## 组合列表、角标和详情的实施目标(2026-10-19) 本节定义前端接入契约,尚未上线。新用户端调用 `GET /user/cards?page=1&size=10&tab=all`,同一外卖订单列表返回普通单与 036 批次卡;新骑手端调用 `GET /rider/tasks?page=1&size=10&tab=newTask&longitude=...&latitude=...`,返回普通外卖任务与同骑手配送组卡。骑手角标调用 `GET /rider/taskCount?longitude=...&latitude=...`;用户各主阶段数量如页面需要,由 `GET /user/cardCount` 同口径返回。两端均使用 token 确认身份,参数显式声明,不接受传入用户/骑手 ID。`GET /batches/{batchId}` 与 `GET /groups/{groupId}` 为组合详情;普通单点击仍走现有详情。退款/售后是补充筛选,可以与一个主流程页签交叉,不改变唯一主阶段。骑手列表/角标方法已归原骑手控制器,对外 URL 与响应不变;待抢组须实际开放并写入 `openedAt` 才可见,不能仅凭 `ACTIVE` 判断。 统一列表响应沿现有骑手列表习惯使用 `data.records`、`data.total`、`data.current`、`data.size`;`page` 从 1 起,`size` 限定为正数并设置服务端上限。`total` 是过滤后的卡片数,不是子单数。先按归属、Tab、开放/资格和组合关系得到卡片集合,再排序、分页,最后批量填充本页成员摘要;不能先分页 `pos_order` 再在 App 拼组。相同排序键以 `cardKey` 稳定打破平局;新任务有有效骑手坐标时按首店距离升序,其余按现有普通外卖 Tab 的创建时间方向。角标与列表共享同一过滤和组去重规则,刷新/并发变化以服务端最新结果为准。 | 字段 | 用户批次卡 `cardType=BATCH` | 骑手组卡 `cardType=GROUP` | 普通单 `cardType=ORDER` | |---|---|---|---| | 身份 | `cardKey=BATCH:{batchId}`、`batchId`、`parentOrderNo` | `cardKey=GROUP:{groupId}`、`groupId`、`batchId`、`version` | `cardKey=ORDER:{orderId}`、原订单 ID/单号 | | 主阶段 | `checkoutStatus`(PENDING/ACTIVE/CANCELLED)、`primaryTab`、`refundSummary` | `primaryTab`、组承接/结束状态、原预约窗口及开放时间 | 沿原普通订单主阶段,不套用批次状态 | | 进度 | `memberCount`、`readyCount`、`pickedUpCount`、`deliveredCount`、`cancelledCount`、`activeCount` | `memberCount`、`activeCount`、`pickedUpCount`、`deliveredCount`、`cancelledCount` | 旧字段不变;组合计数可为空 | | 金额 | 快照 `totalAmount`、`onlinePaidAmount`、`codPromisedAmount`、`refundPendingAmount`、`refundedAmount`,单位 TWD | `discountedFreightTotal`(配送毛收入)、`codToCollectAmount`、逐店到付给商家额,单位 TWD | 原字段不改 | | 时间/距离 | `createdAt`、`operationDeadlineAt`、`finalDeadlineAt`、`activatedAt` | `createdAt`、`openedAt`、`anchorDistanceMeters`,接单按首店判断 | 旧字段不改 | | 明细/操作 | `members[]` 逐店付款与履约状态、`allowedActions` | `members[]` 逐店出餐/取送状态、`allowedActions`,待抢时联系人脱敏 | 原单笔操作入口 | `members[]` 至少包含 `orderId/orderNo/storeId/storeName/payType/paymentState/orderAmount/discountedFreight/deliveryStatus/cancelStatus/refundStatus`,其中到付子单另有 `codToCollectAmount` 和 `codToMerchantAmount`。只读展示字段由服务端从真实成员计算,客户端不根据某个子单 `payStatus` 推断批次状态、不在本地重算费用、进度或可操作性。接单前组卡不透出超出原单笔待抢范围的用户联系信息;取消成员仍在详情保留历史,但从有效进度和现金待收中剔除。`activeCount` 为当前有效成员数,取消后可下降;`memberCount` 是创建时总数,不下降。已送达成员计入已取餐数,已取消成员不计入分母;混合进度按当前有效成员统计,另外显示 `cancelledCount` 防误解。全组结束前不能仅因某子单售后状态释放容量。 已生效待接组的用户批次卡 `allowedActions` 可包含 `CANCEL_MEMBER`;已接组中骑手逐店摘要按真实 `orderState/deliveryStatus` 返回 `PICKUP_MEMBER` 或 `DELIVER_MEMBER`,最终操作入口仍重新校验状态与骑手身份。 当前只读接口为 `GET /user/cards`、`GET /user/cardCount`、`GET /batches/{batchId}`、`GET /rider/tasks`、`GET /rider/taskCount`、`GET /groups/{groupId}`。组合卡 `records` 为先聚合后分页;普通 `ORDER` 卡附带 `legacyOrder` 原订单快照供旧内容展示。退款进度从 036 意图关联原 LINE/OMG 退款记录:`BRIDGED` 只是已桥接,不代表已到账;只有渠道事实 `REFUNDED` 才计入 `refundedAmount`,未知/失败/人工处理仍计入待处理。骑手待抢组仅用服务端最近位置判断首店范围;接组再次在骑手锁内复核。 旧用户 `/system/order/getorderlist` 与旧骑手单笔列表/角标均不展示 036 子单,不能先对子单分页再让旧 App 拼组。旧 `/system/userOrder/getParentOrderDetail` 对真实 036 父单返回 `UPGRADE_REQUIRED`,新 App 应调用带归属校验的 `GET /batches/{batchId}`;普通父单仍走原接口。 用户主 Tab:`all/pending/active/completed/cancelled`;有退款或售后记录时另支持 `refund` 筛选,同一卡可同时在 `active` 和 `refund` 查询中出现,但在 `all` 仅出现一次。骑手沿原外卖 `newTask/toPickup/delivering/completed/cancelled/refund` 参数:未取完的有效子单存在时始终为 `toPickup`,即使另一子单已送达;有效子单均已取且仍有待送/待处置时为 `delivering`;全部有效成员结束后进入历史。待抢组按 `ACTIVE`、实际开放 `openedAt`、原预约时点和资格过滤,且只计一张卡。业务状态、可操作性和角标由服务端返回,不以 App 页签推导权限。 用户已确认旧版 App 须升级后才能使用多店:发布前确认两端最低兼容版本和强制更新入口,配置开关在两端准备就绪前保持关闭。旧版不能发起新批次、继续逐店收银或承接组;已有批次/已接组遇旧版须引导升级,旧单笔支付/取消/接单接口仍对新批次或组成员实施服务端保护。普通单、历史单、单店转账、自取及夜市自配送继续沿旧接口和原规则;普通平台配送预约只补既有时点开放扫描,不套 036 截止。升级策略只是兼容门禁,不替代 token、订单归属、支付事实、版本和骑手锁校验。 错误至少区分 `UPGRADE_REQUIRED`、`QUOTE_CHANGED`、`PAYMENT_WINDOW_CLOSED`、`PAYMENT_PENDING_CONFIRMATION`、`BATCH_CANCELLED`、`GROUP_CHANGED`、`RIDER_OUT_OF_RANGE`、`RIDER_CAPACITY_FULL`、`ORDER_ALREADY_ACCEPTED`、`FORBIDDEN`,并以项目 i18n 消息返回;实施者可在既有错误结构中承载稳定业务码。列表过滤后的接组接口必须重新校验并返回明确原因,不能信任卡片上次可见的状态。