frontend-app-integration.md 40 KB

多店合并下单(036)App 接入文档

本文档供用户端 App 和骑手端 App 接入多店合并下单功能(036)使用,以当前后端实现为准。多店功能的全部业务规则均写在本文档内,接入不需要再查阅其他契约文件。文档只描述接口契约和业务规则,不包含 App 前端实现代码;本仓库不修改 App 源码。平台后台、商家端的 036 管理接口不在本文范围。

更新时间:2026-10-21。036 数据库迁移已在测试库执行并核对表结构;生产库尚未执行。平台多店开关仍关闭、6个相关定时任务保持暂停:接口已通过本地测试,真实支付/退款与App端到端联调尚未验证。测试库无需重复迁移;后续按联调计划部署兼容版本并开启配置和相应任务,生产库须另行执行迁移。

核心概念(先读这段):

  • 用户一次选购至少 2 家门店、由同一骑手合并配送:报价 → 逐店选择支付方式并创建批次 → 完成线上支付(到付已在创建时确认)→ 全部子单就绪后批次生效 → 骑手整组接单、逐店取餐送达。
  • 每家店是一个独立子单(有自己的 orderId/orderNo 和支付方式);批次(batch)是收银单位,配送组(group)是履约单位。
  • App 一律按服务端返回渲染状态、金额和可操作按钮;不在本地推算倒计时、重算费用或推断可操作性。

配送距离和费用由服务端统一计算,App直接展示报价结果,不自行计算道路距离或运费;创建批次时服务端仍会重新校验。2026-10-20后端批量路线优化不改变本文请求参数、响应字段或调用顺序。

父单号和子单号沿用99开头的纯数字格式(父15位、子18位),接口字段类型仍为字符串;App必须原样展示和回传,禁止转为JavaScript Number或用前缀判断多店身份,多店身份以服务端卡片/批次字段为准。

1. 功能范围

当前支持:

  • 用户端:多店报价、逐店选择到付/线上支付(LINE Pay、OMG 信用卡、OMG Apple Pay)并创建批次、安全改选、批次详情、组合订单列表(普通单卡与批次卡同列返回)、整批取消、单店取消。
  • 骑手端:组合任务列表(普通单卡与组卡同列返回)、角标、组详情、整组接单;逐店取餐/送达复用现有单笔接口。
  • 即时配送与预约配送(全批次同一预约时段)。

当前不包含(继续走原流程,036 不改动):

  • 普通单店外卖、闪送、自取、堂食现金、夜市自配送、单店转账的下单与取消。
  • 在线支付新渠道:建批时逐店提交 payType,线上付款仍调用现有 LINE/OMG 创建接口;首次付款不需要另调 5.3 的改选接口。
  • 多店改址:批次创建后收货地址冻结,换地址须取消后重新报价下单。

2. 通用约定

2.1 基础地址和请求头

接口路径均为服务端相对路径,实际请求地址为 {baseUrl}{接口路径};/prod-api 等网关前缀由 App 现有请求层继续添加。

请求头 必填 说明
token 是 登录 JWT。用户/骑手身份均由 token 解析,请求体与查询参数中不提交 userId/riderId
X-Multi-Store-Capability 是 固定值 batch-cards-v1。本文全部 /system/multiStoreCheckout 接口,以及 036 子单调用原 POST /pay/line/create、POST /pay/omg/create、POST /pay/omg/retry 时都必须携带
Content-Type POST 必填 application/json

能力头规则:

  • 能力头缺失或值不是 batch-cards-v1 时,返回 code=500 且带 errorCode=UPGRADE_REQUIRED,msg 为"请更新应用后使用多店结算和配送组功能"。App 收到后应引导升级。
  • 普通单调用原 LINE/OMG 接口不要求此头;旧版 App 不能通过原网关入口绕过逐店收银(服务端已对 036 子单强制校验)。

2.2 统一响应

{ "code": 200, "msg": "操作成功", "data": {} }
  • 以响应体 code=200 判断业务成功,不能只看 HTTP 状态码;msg 已由后端国际化,可直接展示。
  • 除 UPGRADE_REQUIRED 外,其他业务失败没有稳定的 errorCode:展示 msg 并刷新详情,不要按 errorCode 写分支逻辑。

2.3 分页响应

列表接口统一返回:

{ "records": [], "total": 100, "current": 1, "size": 10 }
  • page 从 1 起;size 为 1–50 的正整数,超过 50 报错。
  • total 是卡片数(一个批次/一个组各算 1 张),不是子单数。

2.4 时间与金额

  • 金额单位为新台币元(整数),不是分。
  • 时间为服务端时间字符串(形如 2026-10-21T18:00:00),直接展示,不在 App 重算付款截止或倒计时。

3. 业务模型与状态枚举

3.1 生命周期

报价(quote) → 建批并提交逐店 payType(配送组创建为 WAITING)→ 线上子单逐店支付(如有)→ 全部就绪 ⇒ ACTIVE
                                  └→ 超时/整批取消/预约过期 ⇒ CANCELLED
开放:ACTIVE 且即时/已到预约开放时点 → openedAt 有值、子单可见 → 骑手可抢
配送组:WAITING(建批时即存在,openedAt 有值后可抢)→ ACCEPTED(已接,逐店取送)→ FINISHED / CANCELLED

3.2 批次状态 checkoutStatus

值 含义
PENDING 未生效:处于逐店收银与缓冲核实期,订单不可履约、不可抢
ACTIVE 全部子单合法就绪;配送组在建批时已创建,须等 openedAt 有值才进入骑手待抢列表
CANCELLED 已关闭:未全部就绪超时、用户整批取消或预约过期关闭

PENDING 可能已有部分子单实际付款,须逐店看 paymentState。ACTIVE 只表示批次就绪,不保证已经开放或推送:即时单/已到开放时点的预约单在事务提交后立即尝试开放;未到时点的预约单等待任务扫描。骑手端以组卡的 openedAt 和任务列表为准,不能仅凭 checkoutStatus=ACTIVE 显示可抢。

3.3 子单付款状态 paymentState

值 含义
UNSELECTED 旧批次尚未选择支付方式;新建批次不会产生此状态
ONLINE_PENDING 已选线上方式(LINE/OMG),等待用户发起并完成支付
ONLINE_PAID 服务端收到渠道可信回调,已实际收款
COD_CONFIRMED 用户已确认到付,交餐时向骑手付款

ONLINE_PENDING 仅表示"已选方式",不是实收;支付回跳后必须刷新批次详情,以服务端状态为准。

3.4 支付方式 payType

值 含义 说明
"1" 到付 建批时即确认,无需网关
"2" OMG 信用卡 建批返回 ONLINE_PENDING 后调 /pay/omg/create
"3" LINE Pay 建批返回 ONLINE_PENDING 后调 /pay/line/create
"5" OMG Apple Pay 建批返回 ONLINE_PENDING 后调 /pay/omg/create
"6" 线下转账 新批次不支持,提交即被拒

availablePayTypes 由服务端按门店返回(可能不含某些方式,如商家不可用到付),App 只按返回渲染。

3.5 卡片 cardType / cardKey

cardType cardKey 出现在
ORDER ORDER:{orderId} 用户订单列表、骑手任务列表(普通单,字段在 legacyOrder,点击走原普通单详情)
BATCH BATCH:{batchId} 用户订单列表(多店批次)
GROUP GROUP:{groupId} 骑手任务列表(同骑手配送组,一组一卡)

cardKey 是列表唯一键,分页排序平局时也以它稳定打破。primaryTab 是服务端判定的主页签,App 不自行推导。

3.6 页签 tab

端 取值 说明
用户端 all / pending / active / completed / cancelled / refund refund 是补充筛选:同一张卡可同时出现在 active 和 refund,但在 all 只出现一次
骑手端 newTask / toPickup / delivering / completed / cancelled / refund 只要有有效子单未取完,组始终在 toPickup(即使另一子单已送达);有效子单全部取完且仍有待送才进 delivering

不要先对子单分页再在 App 拼卡;服务端已先聚合后分页。

3.7 allowedActions(仅用于展示按钮)

动作 出现在 含义
SELECT_PAYMENT BATCH 卡 PENDING 且未过 operationDeadlineAt,可继续逐店收银
CANCEL_BATCH BATCH 卡 PENDING,可尝试整批取消
CANCEL_MEMBER BATCH 卡 ACTIVE 且配送组待接,可尝试单店取消
ACCEPT_GROUP GROUP 卡 组待抢,可整组接单
PICKUP_MEMBER / DELIVER_MEMBER GROUP 卡 members[] 本人已接组后,该店当前可取餐/可送达的提示

按钮提交时服务端会重新校验(骑手抢单、状态变化都可能拒绝),失败后刷新详情,不在本地重算余店运费。

3.8 金额与运费口径

逐店金额字段(报价 stores[] 与卡片 members[] 同名同义):

字段 说明
originalFoodAmount / foodAmount 商品原价合计 / 优惠后商品金额
originalFreight / discountedFreight 原运费 / 多店折扣后运费(骑手毛运费)
customerDeliveryFee 客户支付的运费
merchantDeliverySubsidy 商家满额免运承担的补贴
amount 用户应付 = foodAmount + customerDeliveryFee
codToMerchantAmount 到付子单给商家的金额 = orderAmount - discountedFreight

商家满额免运以多店折后运费为基数。例:原运费 40、八折后 32、全免时客户运费 0、商家补贴 32、骑手仍收 32。普通单的免运口径不变。

报价是短期快照,服务端建批时会重算并比对;App 不提交自算金额。

4. 推荐调用流程

4.1 用户端

  1. 按顺序选择至少两家店与商品 → POST /system/multiStoreCheckout/quote。
  2. 展示参与店金额、各店 availablePayTypes 与 excludedStores[];用户为每家参与店选择付款方式,按 storeId 组成 paymentMethods 后一次提交 POST /system/multiStoreCheckout/batches,保存 batchId 与各店 orderId/orderNo。圈外店不创建订单,引导剔除或另行单店下单。
  3. 建批返回的到付子单已是 COD_CONFIRMED,无需再次确认;线上子单为 ONLINE_PENDING,用该店 orderNo 调原渠道创建接口发起付款。仅在用户随后改选方式时调用 POST /system/multiStoreCheckout/batches/members/payment(见 5.3)。
  4. 支付回跳后刷新 GET /system/multiStoreCheckout/batches/{batchId},以服务端状态为准。
  5. 订单列表用 GET /user/cards + GET /user/cardCount;不要调旧列表接口看多店单(旧接口不返回 036 子单/批次)。

4.2 骑手端

  1. 保持现有定位上报(POST /system/position/setRiderposition):待抢组资格用服务端最近位置与首店距离判断。
  2. 列表 GET /rider/tasks、角标 GET /rider/taskCount;组卡一张 = 一组。
  3. 点组卡 → GET /groups/{groupId} → 确认接单 POST /groups/accept(带 version),成功后刷新组详情。
  4. 按 members[].orderNo 调原单笔接口逐店取餐/送达(见 6.5);不要按成员逐单抢、也没有"整组送达"按钮。

5. 用户端接口

以下接口均需 token + X-Multi-Store-Capability: batch-cards-v1。

5.1 多店报价

POST /system/multiStoreCheckout/quote

请求体示例(ID 须替换为真实值):

{
  "addressId": 123,
  "sameRider": true,
  "stores": [
    { "storeId": 101, "deliveryWindow": null,
      "items": [{ "productId": 1001, "quantity": 1, "specValueIds": [] }],
      "couponId": null, "forcePath": null, "remarks": null },
    { "storeId": 102, "deliveryWindow": null,
      "items": [{ "productId": 2001, "quantity": 2, "specValueIds": [] }],
      "couponId": null, "forcePath": null, "remarks": null }
  ]
}
字段 类型 必填 说明
addressId long 是 收货地址 ID,须属于当前用户
sameRider boolean 是 当前版本必须传 true(仅支持同骑手合并配送),传 false 报错
stores array 是 按用户选择顺序排列,至少 2 家;首店是合并围栏与骑手接单距离的固定锚点
stores[].deliveryWindow string 否 null=即时配送;预约传 "yyyy-MM-dd HH:mm-HH:mm"(22 字符,如 2026-10-21 18:00-18:30)。所有店必须完全相同,不得混合即时与预约
stores[].items[].productId long 是 商品 ID;只提交商品/规格 ID,客户端价格不作为计价依据
stores[].items[].quantity int 是 数量
stores[].items[].specValueIds long[] 是 规格 ID 列表,无规格传 []
stores[].couponId long 否 门店优惠券 ID,无传 null
stores[].forcePath string 否 预留字段,正常流程传 null
stores[].remarks string 否 逐店订单备注,随报价快照固化到对应子单 remarks 字段(口径同普通单逐店备注),不参与计价;无备注传 null

建批后的子单 food 商品快照由服务端按 specValueIds 查数据库生成:有规格时同时返回 ask(规格名称数组,兼容原订单详情展示)和 specValueIds(规格值 ID 数组)。免费规格的 otherPrice 可以为 0,但 ask 仍应展示;不要只传客户端的 ask 文本代替规格 ID。此修复只影响新创建的版本 2 快照,不回写历史订单。

响应 data:

字段 类型 说明
quoteToken string 报价令牌,建批时提交
expiresAtEpochMillis long 报价过期时间(毫秒时间戳)
ruleVersion long 规则版本;平台改折扣配置后旧报价不可用
anchorStoreId long 锚点门店 ID(首店)
fenceMeters int 合并围栏半径(米),首店到其他店的距离上限
participatingStoreIds long[] 可参与的门店 ID 列表
stores[] array 逐店报价,字段见 3.8,另有 availablePayTypes
stores[].routeDistanceMeters number 服务端矩阵计算的该店到收货地址道路米数;建批时重新报价,并固化为子单 jvli(公里)
excludedStores[] array 圈外/不兼容门店:{storeId, reason}。不自动创建订单,须剔除或另行结算
freightRatio number 当前折扣系数
fingerprint string 报价指纹(服务端内部使用,App 原样忽略即可)

规则:

  • 店/商品/配送资格不合格、支付方式仅剩转账的店会进 excludedStores 并给 reason;被排除到参与店不足 2 家时报价报错,错误 msg 会点名被排除门店与原因(如"以下门店无法参与多店配送:快食到家(超出与首店的配送范围)。参与门店不足2家,请调整门店后重试")。
  • 报价短期有效,建批时服务端重算比对,商品/价格/规则变化会拒绝建批(msg="商品、规格或优惠信息已变化,请重新结算")。

5.2 创建批次

POST /system/multiStoreCheckout/batches

请求体:

{
  "requestKey": "本次提交固定唯一键",
  "quoteToken": "报价返回值",
  "participatingStoreIds": [101, 102],
  "paymentMethods": [
    { "storeId": 101, "payType": "3" },
    { "storeId": 102, "payType": "1" }
  ]
}
字段 类型 必填 说明
requestKey string 是 幂等键:网络重试须沿用同一键、同一报价与逐店支付选择,才返回原批次;同键改动载荷会被拒,换新键可能创建新批次
quoteToken string 是 5.1 返回的报价令牌;过期报"报价已过期,请重新获取"
participatingStoreIds long[] 是 用户明确确认参与的门店,须是报价参与店的子集且 ≥2 家;夹带圈外店会被拒
paymentMethods array 是 与参与店一一对应的 {storeId,payType};每店恰好一条,payType 是字符串且须在该店报价的 availablePayTypes 内;不支持转账 "6"

不要只传普通单店请求的顶层 paymentMethod 或顶层 payType:多店建批只读取逐店的 paymentMethods,缺少、重复、夹带非参与店或选了该店不可用的方式都会被拒。网络重试须保持同一个 requestKey、quoteToken、参与店和逐店方式;创建成功后要改方式,调用 5.3,不要用同一个 requestKey 重发不同的 paymentMethods。

响应 data:

字段 类型 说明
batchId long 批次 ID,后续接口都用它
parentOrderNo string 父批次号
checkoutStatus string 含未付款线上子单时为 PENDING;全到付时创建即为 ACTIVE。ACTIVE 不等于骑手已可抢,实际开放看组 openedAt 与骑手列表
totalAmount int 批次总金额(TWD 元)
operationDeadlineAt string 操作截止(创建后 15 分钟,见 8.1)
finalDeadlineAt string 最终截止(创建后 20 分钟,见 8.1)
members[] array {orderId, orderNo, storeId, amount, payType, paymentState, availablePayTypes[]},逐店保存好 orderNo

注意:幂等重试返回的 availablePayTypes 可能为空数组,不能当作门店永久不支持付款;以最新报价和付款接口校验为准。

建批响应中的各店 payType / paymentState 是本次已保存的选择。全到付批次会在建批事务提交后立即尝试开放即时单;若是未到开放时点的预约单,仍保持 ACTIVE、等待开放,App 不要因 ACTIVE 提前展示可抢或已推送。

以上示例中,101 店返回 payType="3"、paymentState="ONLINE_PENDING",须继续发起 LINE Pay;102 店返回 payType="1"、paymentState="COD_CONFIRMED",不再调 5.3 确认到付。只有两店均选到付时,建批结果才可直接为 ACTIVE;此时不能再改选支付方式。

5.3 逐店安全改选支付方式

POST /system/multiStoreCheckout/batches/members/payment

请求体:

{ "batchId": 7, "orderId": 701, "payType": "3", "clientRequestId": "本次操作唯一键" }
字段 类型 必填 说明
batchId / orderId long 是 批次与该店子单
payType string 是 "1" 到付 / "2" "3" "5" 线上(见 3.4);"6" 转账不支持
clientRequestId string 是 本次操作唯一键,非空且 ≤64 字符

响应 data(字符串):

场景 返回
payType="1" 改选到付 该店改选后的批次状态:"PENDING"(还有店没就绪)/ "ACTIVE"(全部就绪已生效)/ "CANCELLED"(预约已过期被关闭)
payType="2"/"3"/"5" 线上选择 固定 "ONLINE_PENDING",随后调 5.4 发起真实付款

规则:

  • 新批次已在 5.2 保存逐店方式,首次支付无需调用本接口;本接口仅用于用户修改方式。批次须 PENDING 且未过 operationDeadlineAt,ACTIVE 后不可再改;改选成功后刷新 5.5 批次详情。
  • 线上→线上、线上→到付、到付→线上只在该店从未发起网关交易时允许;一旦有网关尝试记录,含结果未明的尝试,拒绝改选,先沿原渠道查询或完成支付。已真实付款不可改。相同方式重试保持幂等。
  • 该方式必须在门店 availablePayTypes 内;不可用会被拒。

5.4 发起线上支付(原渠道接口)

拿到 ONLINE_PENDING 后,用该店 orderNo 调原创建接口:

payType 接口 请求体
"3" LINE Pay POST /pay/line/create { "ddId": "该店 orderNo" }
"2" OMG 信用卡 POST /pay/omg/create { "orderId": "该店 orderNo", "paymentMethod": "CREDIT" }
"5" OMG Apple Pay POST /pay/omg/create { "orderId": "该店 orderNo", "paymentMethod": "APPLE_PAY" }
  • 036 子单调这些接口(含 OMG 重试 POST /pay/omg/retry)同样必须带能力头。
  • 5.3 只登记选择,不替代网关创建,也不返回跳转凭证。
  • 实收以服务端可信回调为准;支付回跳/ONLINE_PENDING 都不代表实收,回跳后刷新批次详情。

5.5 批次详情

GET /system/multiStoreCheckout/batches/{batchId}

路径参数 batchId。响应 data 为批次卡对象(字段见 7.1/7.2):逐店支付选择、真实实收、到付确认、退款与履约状态、两个截止时间、activatedAt、allowedActions。非本人批次拒绝,不暴露其他用户的父/子/组。

5.6 用户卡片列表

GET /system/multiStoreCheckout/user/cards?page=1&size=10&tab=all
参数 必填 说明
page 否 默认 1
size 否 默认 10,上限 50
tab 否 默认 all,取值见 3.6

响应 data 为分页结构(见 2.3),records[] 为卡片对象(见 7.1):

  • ORDER 卡:原订单快照在 legacyOrder,展示与点击继续走原普通单详情。
  • BATCH 卡:通过 batchId 打开 5.5 批次详情。

5.7 各页签卡片数

GET /system/multiStoreCheckout/user/cardCount

无查询参数。响应 data:

{ "all": 12, "pending": 1, "active": 2, "completed": 8, "cancelled": 3, "refund": 1 }

5.8 整批取消(仅未生效)

POST /system/multiStoreCheckout/batches/cancel
{ "batchId": 7, "reason": "USER_CANCEL", "clientRequestId": "本次操作唯一键" }
  • reason 固定传 "USER_CANCEL";clientRequestId 非空且 ≤64 字符。
  • 仅批次 PENDING 可整批取消;已 ACTIVE 须走 5.9 单店取消。
  • 成功 data=true 只表示取消已受理,不表示渠道退款到账;退款进度看 5.5 详情。

5.9 单店取消(仅已生效且骑手未接组)

POST /system/multiStoreCheckout/batches/members/cancel
{ "batchId": 7, "orderId": 701, "reason": "USER_CANCEL", "clientRequestId": "本次操作唯一键" }
  • 仅批次 ACTIVE 且配送组仍待接(WAITING)时允许;骑手已接组后用户不能主动取消。
  • 成功 data=true 只表示取消已受理;其他成员费用快照不变、不追补余店运费;退款进度看详情。

6. 骑手端接口

以下接口均需骑手 token + X-Multi-Store-Capability: batch-cards-v1。定位上报继续用现有 POST /system/position/setRiderposition。

6.1 任务列表

GET /system/multiStoreCheckout/rider/tasks?page=1&size=10&tab=newTask&longitude=121.5&latitude=25.03
参数 必填 说明
page / size 否 默认 1/10,size 上限 50
tab 否 默认 newTask,取值见 3.6
longitude / latitude 否 骑手坐标:计算 juli 距离(列表与组详情同口径)并用于普通单排序。不能替代服务端位置:待抢组资格仍以服务端最近上报位置与首店距离判定,二者须同时传或同时不传

响应 data 为分页结构,records[] 中 GROUP 卡一张 = 一组(角标与列表同口径,组只计一次)。 普通单卡与组卡 members[] 均附带与旧骑手列表逐单一致的信息:legacyOrder(订单全量快照)、 store(门店对象)、user(用户对象)、goodsCount(food 快照件数合计,口径同统一列表: 普通单卡为本单件数,批次/组卡为有效成员合计,food 缺失或非法为 null)。

距离字段统一口径(列表、组详情一致):

字段 出现位置 含义 计算前提
juli 普通单卡、组卡 members[] 骑手请求定位 → 订单坐标的直线距离(公里,两位小数) 请求带 longitude/latitude,不传为 null
anchorDistanceMeters GROUP 卡 服务端最近骑手位置 → 首店取餐坐标(米),待抢排序也用它 服务端定位上报;待抢资格要求 5 分钟内新鲜
jvli 组卡 members[] 门店 → 收货地址的配送路线距离(公里,订单快照) 无需参数,取自 legacyOrder.jvli,可能为空

待抢组只在批次已生效且达到原预约开放时点、组 openedAt 已写入后可见;新任务有有效坐标时按首店距离升序,其余按创建时间方向。最后成员就绪并提交后,服务端立即尝试开放即时/已到点批次;若失败,由原分钟任务重试,预约未到也由该任务到点开放。App 不依赖固定一分钟延迟,付款后可刷新列表,未出现时以服务端状态为准;对外 URL 保持不变。

6.2 各页签任务数(角标)

GET /system/multiStoreCheckout/rider/taskCount?longitude=121.5&latitude=25.03

参数同 6.1:longitude/latitude 可选、成对传入;待抢(newTask)计数按服务端最近位置与距离上限字典判定,不依赖请求坐标。

响应 data:

{ "newTask": 3, "toPickup": 1, "delivering": 2, "completed": 9, "cancelled": 1, "refund": 0 }

6.3 组详情

GET /system/multiStoreCheckout/groups/{groupId}?longitude=121.5&latitude=25.03

路径参数 groupId;可选 longitude/latitude(骑手定位,传入后 members[].juli 按定位计算,不传为 null)。响应 data 为组卡对象(见 7.1/7.2),关键字段:

  • groupStatus:WAITING 待抢 / ACCEPTED 已接 / FINISHED 已结束 / CANCELLED 已取消。
  • version:整组接单的乐观锁版本。
  • openedAt:组实际开放时点;anchorDistanceMeters:距首店距离。
  • allowedActions:待抢时 ACCEPT_GROUP;本人已接组后 members[] 内出现 PICKUP_MEMBER / DELIVER_MEMBER。
  • codToCollectAmount:有效到付子单的待收总额。

成员摘要(含待抢阶段)附带 legacyOrder/store/user/juli/jvli/goodsCount, 其中 juli 为骑手定位到该店订单坐标的直线距离(公里,两位小数,口径同 6.1 距离字段表):传入 longitude/latitude 时计算,不传或订单无坐标时为 null;jvli 是该店到收货地址的道路距离(公里),与 legacyOrder.jvli 一致。组内每店距离不同,组卡顶层不提供单一 jvli。历史子单若建单时未保存距离则保持 null,不把直线距离伪装成原道路距离。 其他订单字段与旧骑手列表逐单口径一致, 可直接复用旧列表项渲染;已接单后逐店履约仍按子单详情读取(见 6.5)。如产品后续要求待抢阶段 脱敏联系人与地址,须另行变更口径并同步两端。

6.4 整组接单

POST /system/multiStoreCheckout/groups/accept
{ "groupId": 9, "version": 0, "clientRequestId": "本次接单唯一键" }
字段 类型 必填 说明
groupId long 是 组 ID
version long 是 列表/详情返回的版本号;不符返回"配送组状态已变化,请刷新后重试"
clientRequestId string 是 本次接单唯一键,非空且 ≤64 字符
  • 一次调用原子承接全部有效成员;不要按成员逐单抢,旧单笔接单 API 对组成员拒绝拆抢(msg="多店配送组必须整组接单")。
  • 成功 data 返回组 ID;服务端在同一骑手锁内复核资格、位置时效、首店距离、容量与成员状态,并发接组只能一方成功。
  • 位置类失败:位置过期 →"骑手位置已过期,请更新定位";距首店过远 →"距离首店过远,无法接单"。接单距离上限沿用平台字典,缺失时待抢组不可见。

6.5 逐店取餐 / 送达(复用原接口)

接组后按 members[].orderNo 调原单笔接口:

GET  /system/orderQsOprate/getOrderDetial?ddId={该店 orderNo}   -- 取已归属骑手的完整订单/地址
POST /system/orderQsOprate/pickupOrder                           -- body: { "id": 701, "qsImg": "现有图片值" }
POST /system/orderQsOprate/deliverOrder                          -- body: { "id": 701, "qsImg": "现有图片值" }
  • id 是该店子单的 orderId;qsImg 语义与普通单一致。
  • 没有"全部取齐"/"整组送达"按钮,取餐顺序由骑手自行决定;每次操作服务端仍重新校验状态与骑手身份。
  • 组进度:只要有有效子单未取完,组停在 toPickup;全部取完才进 delivering(见 3.6)。

6.6 到付金额口径

字段 位置 含义
codToCollectAmount 组卡 有效到付子单向用户待收的现金总额
codToCollectAmount members[] 该店向用户收的金额
codToMerchantAmount members[] 交给商家的金额 = orderAmount - discountedFreight

线上已付子单不计入现金待收;已取消成员从现金待收与有效进度中剔除(详情保留历史行)。

7. 公共数据对象

7.1 卡片对象(列表 records[] / 批次详情 / 组详情共用)

字段 类型 适用 说明
cardType string 全部 ORDER / BATCH / GROUP
cardKey string 全部 唯一键:ORDER:{orderId} / BATCH:{batchId} / GROUP:{groupId}
orderId long ORDER 普通单订单 ID
legacyOrder object ORDER 原订单完整快照(PosOrder 原字段),旧内容展示用
batchId long BATCH/GROUP 批次 ID
groupId / version long GROUP 组 ID 与接单乐观锁版本
parentOrderNo string BATCH 父批次号
orderNo / storeId / storeName / payType / orderState / deliveryStatus — ORDER 沿用普通单字段语义
checkoutStatus string BATCH 见 3.2
groupStatus string GROUP WAITING / ACCEPTED / FINISHED / CANCELLED
primaryTab string 全部 服务端判定的主页签
refundSummary string BATCH 退款汇总:REFUNDED 已有渠道证实到账;PENDING 等表示仍在处理;null 无退款
memberCount / activeCount int BATCH/GROUP 创建时成员总数(不下降)/ 当前有效成员数(取消后下降)
readyCount int BATCH 已就绪成员数(ONLINE_PAID + COD_CONFIRMED)
pickedUpCount / deliveredCount / cancelledCount int GROUP 未取消且已取(deliveryStatus≥2)/ 已送达(=3)/ 已取消
totalAmount int BATCH 批次总金额快照
onlinePaidAmount / codPromisedAmount int BATCH 线上实收合计 / 到付承诺合计
refundPendingAmount / refundedAmount int BATCH 退款处理中 / 已到账金额
discountedFreightTotal int GROUP 有效成员折后运费合计(骑手配送毛收入)
codToCollectAmount int GROUP 见 6.6
createdAt string 全部 创建时间
operationDeadlineAt / finalDeadlineAt string BATCH 两个截止时间,见 8.1
activatedAt string BATCH 批次实际生效时间
openedAt string GROUP 组实际开放(进入待抢)时间
anchorDistanceMeters number GROUP 距首店距离(米)
members[] array BATCH/GROUP 逐店摘要,见 7.2
allowedActions string[] 全部 见 3.7

7.2 成员对象 members[]

字段 类型 说明
orderId / orderNo long/string 子单 ID / 单号
storeId / storeName long/string 门店
payType string 该店已选支付方式;新建批次必有值,旧批次未选时可能为 null
paymentState string 见 3.3
orderAmount int 该店订单金额
discountedFreight int 该店折后运费
deliveryStatus long 沿用普通单:1 已接单待取餐 / 2 已取餐 / 3 已送达
orderState long 沿用普通单订单状态
cancelStatus string ACTIVE 有效 / CANCELLED 已取消
refundStatus string NONE 无退款记录;其他为退款处理状态,仅渠道证实的 REFUNDED 表示已到账
codToCollectAmount / codToMerchantAmount int 仅到付已确认且未取消的成员返回,见 6.6
allowedActions string[] 仅本人已接组时出现 PICKUP_MEMBER / DELIVER_MEMBER

客户端不得根据某个子单 payStatus/paymentState 推断批次状态,也不在本地重算费用、进度或可操作性。

8. 业务规则汇总

8.1 批次生效与两个截止时间

批次创建时快照两个截止时间(默认创建 +15 分钟 / +20 分钟,以服务端返回为准),不因配置变更或重试延长:

  • 14:00 创建:建批时已逐店选定方式,到付店当场确认;14:15 前可对未发起过网关交易的子单改选方式或发起线上交易。14:14 已实际发起的线上尝试可在 14:20 前完成付款及可信确认;14:16 起不能新发起、换方式或重试;14:20 仍未全部就绪则批次关闭(CANCELLED),迟到实收只退款不激活。
  • 全部店合法就绪才变 ACTIVE(线上须 ONLINE_PAID、到付须 COD_CONFIRMED);部分就绪不开放履约。
  • 全到付在建批时所有成员已确认,可直接返回 ACTIVE;仍需按 5.2 的开放规则判断何时可抢。ACTIVE 后不可用 5.3 改方式。
  • 未生效预约批次在激活时点预约已到/超过结束时间则取消,返回"原预约已过期"业务结果与真实退款进度,不自动改即时;已生效任务不由此取消。

8.2 取消规则

批次状态 用户可做的取消 接口
PENDING 整批取消 POST /batches/cancel
ACTIVE 且组 WAITING 单店取消 POST /batches/members/cancel
ACTIVE 且组 ACCEPTED 不能主动取消 —
  • 所有取消成功只表示受理,不表示渠道退款到账;退款进度看批次详情。
  • 取消店保留历史行但不计入有效分母与现金待收;余店费用快照不变,不追补运费。

8.3 退款展示

  • 退款进度由服务端关联原 LINE/OMG 退款记录投影:只有渠道事实 REFUNDED 才算到账(计入 refundedAmount);BRIDGED 等中间状态仅表示已登记,仍在 refundPendingAmount 处理中。
  • 已实收后批次关闭/取消的,引导用户在详情页跟踪退款,App 不伪称"已退款"。

8.4 预约与无人接单

  • 全批次同一预约时段(报价时强制相同);批次生效时间按父批次统一判定,接入 037 无人接单规则,不以逐子单支付时间起算。
  • activatedAt(批次生效)/openedAt(组开放)由服务端返回,App 不从付款时间推导无人接单倒计时。
  • 无人接单自动取消:整组从待抢列表消失,详情保留逐店取消与退款进度,迟到实收不重开。

8.5 地址冻结

建批后收货地址/坐标固定,地址簿变化不影响订单;首版无多店改址。用户换地址须按 8.2 取消后重新报价下单。

8.6 旧接口隔离与升级门禁

  • 旧用户列表 /system/order/getorderlist、旧骑手单笔列表/角标均不返回 036 子单/批次/组;旧父单详情 /system/userOrder/getParentOrderDetail 对真实 036 父单返回 UPGRADE_REQUIRED,新 App 一律调 5.5。
  • 旧单笔支付/取消/接单入口对 036 子单与组成员实施服务端拒绝(msg="请通过多店批次流程取消订单"等),不存在绕过。
  • 旧版 App 必须强升后才能使用多店;平台多店开关在两端兼容版本验收前保持关闭。普通单、历史单、自取、夜市自配送、单店转账不受影响。

8.7 推送

  • 开放和推送是两个阶段:批次变为 ACTIVE 时登记持久事件,组真正开放后事件转为 OPENED,独立通知任务随后处理;不保证支付回调返回时设备已收到推送。预约未到开放时点、部分店未就绪时不推送。骑手端以 6.1 列表/6.2 角标判断可抢任务,不以推送到达时间判断。
  • 036 组新任务推送额外携带 groupId、eventId:用 groupId 打开组详情,按 eventId 对重试推送去重。
  • 后端推送状态 SENT 只表示已提交到现有异步通道,不等于设备已送达。

9. 常见业务错误与处理建议

msg 文案随 App 语言本地化,下表按简体中文文案识别(按语义匹配即可):

msg 常见原因 App 处理建议
请更新应用后使用多店结算和配送组功能(errorCode=UPGRADE_REQUIRED) 能力头缺失/错误,或旧版 App 调了旧父单详情 引导升级;新接口补齐 X-Multi-Store-Capability: batch-cards-v1
多店结算暂未开放 平台开关未开或 036 迁移未执行 提示暂不可用,隐藏多店入口
多店结算请求无效或参与店铺不足 参数缺失/少于 2 店/状态不允许/幂等键冲突 检查参数后重试;仍失败刷新详情
该店已发起OMG/LINE Pay支付请求,完成前不能更换支付方式;请返回完成支付,或取消本次多店结算重新下单 在途收银台会话经渠道核实未通过或结果未知(换方式前服务端逐笔查询渠道状态) 引导返回原收银台完成支付;或走 5.8/5.9 取消批次后重新下单
订单已支付,无法更换支付方式 该店在途支付经渠道核实已收款 引导刷新支付结果,进批次详情查看就绪进度
收货地址不存在或不属于当前用户 addressId 无效或非本人 让用户重新选择地址
门店不可参与当前多店配送 店在围栏外或不支持合并配送 按 excludedStores 剔除该店
商品、规格或优惠信息已变化,请重新结算 建批时服务端重算与报价不一致 回到购物车重新报价
当前配送费配置不可用,请稍后重试 运费配置缺失 稍后重试报价
报价已过期,请重新获取 quoteToken 超时 重新调 5.1
以下门店无法参与多店配送:…(超出与首店的配送范围/暂无可用的支付方式) 参与店被围栏或支付方式筛到不足 2 家 按点名门店调整选择或换店
请通过多店批次流程取消订单 对 036 子单调了旧取消入口 改用 5.8/5.9
多店配送组必须整组接单 对组成员调了旧单笔接单 API 改用 6.4
配送组状态已变化,请刷新后重试 接单 version 过期或已被他人接走 刷新列表/详情
骑手位置已过期,请更新定位 接单时服务端位置过旧 先上报定位再重试
距离首店过远,无法接单 骑手当前位置距首店超限 移动后再试或放弃该组
接单距离配置不可用,请联系平台 平台距离字典缺失 提示联系平台,等待配置恢复

10. 接入验收清单

  • 两店线上/到付混合、均线上、均到付:建批时每店必须选方式,到付当场确认、全到付立即 ACTIVE;有线上未付时不开放履约,全部就绪才生效;付款回跳后刷新真实状态而非本地置为已付。
  • 建批参数:遗漏某店、重复 storeId、夹带非参与店、传 "6" 或该店不可用方式均被拒;用相同 requestKey 改传不同方式不能覆盖原批次,创建后改选走 5.3。
  • 时间窗:14:15/14:20 两截止行为符合 8.1;过期后所有付款入口置灰。
  • 即时/预约批次:最后成员就绪后即时单可立即开放,未到时点的预约单即使 ACTIVE 也不可抢、不推送;预约过期不激活、只退实收;生效与组开放分别以 activatedAt/openedAt 展示。
  • 无人接单超时:列表和角标只在实际开放后展示,取消后不重新可抢;迟到实收只跟踪退款。
  • 组进度:A 已送达、B 未取餐时组只在 toPickup,双进度与组容量正确;部分取消不追补余店运费。
  • 到付展示:组/成员 codToCollectAmount、codToMerchantAmount 按 6.6 展示,线上单不计现金待收。
  • 列表:用户/骑手组合列表一次返回,total 为卡片数;cardKey 作唯一键;旧列表接口确认看不到 036 单。
  • 升级门禁:旧版两端调 036 接口均收到 UPGRADE_REQUIRED 并提示升级;普通外卖、闪送、自取、堂食现金、夜市自配送、单店转账回归原流程无变化。
  • 所有失败以 msg 展示并刷新详情,除 UPGRADE_REQUIRED 外不依赖 errorCode。
  • 联调前确认:036 SQL 已人工执行、平台多店配置已开启、真实支付渠道与推送可达。