# 多店结算数据模型(实施版) 日期:2026-10-13。仅适用于服务端新建的多店批次;未关联批次的普通及历史订单保持原语义。金额统一以整数 TWD 存业务快照,渠道需要最小货币单位时只在网关适配层转换,不回写订单金额。 ## 1. 持久化实体 | 实体 | 唯一约束/核心字段 | 职责 | |---|---|---| | `order_checkout_config` | 固定 `id=1`;`enabled`、`fence_meters`、`max_stores`、`confirmation_buffer_minutes`、`tiers_json`、`version` | 平台全局配置专表,固定项用独立列,动态折扣阶梯用 JSON;版本用于报价指纹和后台乐观锁,不接 `sys_config`。 | | `order_checkout_batch` | `id`;`parent_dd_id` 唯一;`(user_id,request_key)` 唯一;专用 `checkout_status`、`version`、`create_time`、`operation_deadline_at`、`final_deadline_at`、`confirmation_buffer_minutes` | 服务端批次标识、幂等创建、15 分钟发起截止和创建时缓冲快照;专用状态只表示统一履约资格,不代表退款到账。 | | `order_checkout_batch` 规则列 | `anchor_store_id`、`address_id`、`same_rider`、`fence_meters`、`max_store_count`、`rule_version`、`matched_min_stores`、`discount_ratio`、`total_amount` | 创建时冻结本次真实参与集合与计价规则;配置变化不追改。 | | `order_checkout_batch` 生效时间(待 T023 实现) | `activated_at` | 最后有效成员合法就绪、批次首次原子生效时写入,不能用某子单实收时间或后续 `update_time` 代替;生效后不因部分取消/重试改写。 | | `order_checkout_member` | `id`;`pos_order_id` 唯一;`(batch_id,store_id)` 唯一;`sort_no`、`payment_state`、`pay_type`、`ready_at`、`original_freight`、`discounted_freight`、`food_amount`、`amount`、`coupon_id` | 每店子单金额和付款/到付确认事实。`pay_type` 允许未选择;真实实收只来自渠道记录,不能把到付确认当作到账。 | | `order_checkout_payment_attempt` | `id`;`(channel,merchant_order_no)` 唯一;`member_id`、`pay_type`、`state`、`requested_at`、`dispatched_at`、`gateway_transaction_id`、`gateway_paid_at`、`verified_at` | 保存第 15 分钟前服务端发起的证据及其后缓冲内可信结果。请求预留不是实际发起;回调、查询及重试按原交易号对账。 | | `order_delivery_group` | `id`;`batch_id` 唯一;`rider_id`、`status`、`version` | 仅同骑手批次创建;组是一个抢单任务,不是合并收款或强制同步取送状态。 | | `order_delivery_group` 开放时间(待 T023 实现) | `opened_at` | 首次实际开放给骑手时原子写入并创建通知意图,不能从预约计划时点推算,也不因重试/部分取消改写。无配送组成员使用原 `display_time` 记录首次实际开放。 | | `order_checkout_outbox` | `id`;`(event_type,aggregate_id)` 唯一;`status`、`next_retry_at`、`attempts` | 生效/预约开放后的新任务推送,组按组、非组按子单去重;提交后发送、失败可重试、取消后不可再开放。 | | `order_checkout_refund_intent` | `id`;`(member_id,reason)` 唯一;`channel`、`amount`、`merchant_order_no`、`gateway_transaction_id`、`status`、`next_retry_at` | 跟踪 LINE/OMG 原路全额退款、未知结果和人工处理;意图创建不等于到账。到付未收不建网关退款意图。 | 不在 `pos_order` 增加多店标志:以 `order_checkout_member.pos_order_id` 的唯一关联作为不可伪造服务端标识;老订单无关联即沿旧流程。容量统计与组列表通过成员表关联 `order_delivery_group`,避免为纯读取再加一列。父单 `order_parent` 沿用现有结构和子单号关联,其 `orderStatus` 不复用为批次生效或实收状态。 2026-10-16 已确认的时间列是目标模型,当前 Entity/Mapper/SQL 尚待 T023 一批实现;本文更新不代表数据库已有这些列。地址与坐标使用建批时复制到父/子订单的快照;`address_id` 只保留原地址关联,不在付款、列表、取送时回读地址簿覆盖快照,旧单笔改址入口也不能改组成员。 ## 2. 状态和不变量 - 批次:`PENDING → ACTIVE` 或 `PENDING → CANCELLED`;`CANCELLED` 永不重开。到最终确认截止仍未全部合法就绪时关批次,截止后收到真实付款也只进入退款/对账,不转 `ACTIVE`。 - 成员:`UNSELECTED → ONLINE_PENDING → ONLINE_PAID`,或 `UNSELECTED → COD_CONFIRMED`;渠道失败/未知不得标记就绪。未决在线尝试不能改选到付。`ONLINE_PAID` 与 `COD_CONFIRMED` 均计就绪,但仅前者计已实收。 - 发起线上尝试、重试、换方式、到付确认均要求 `now < operation_deadline_at`;已有实际发起且可核实的在线尝试允许在 `now <= final_deadline_at` 完成。缓冲内不新建尝试。 - 同一父批次读写必须以批次行锁或状态版本 CAS 保护;激活/取消必须在同一事务内重新读取全部成员事实。外部网关 HTTP 不持有数据库事务锁。 - 组接单:骑手锁 → 组行锁 → 成员子单按 ID 加锁;资格、现有任务容量、组门店上限和全部成员可接状态在锁内校验;全部分配或全部失败。单笔接单入口发现组成员必须拒绝拆抢。 - 子单创建时写当前业务字段快照,但 `isDisplay` 不等于批次准入;骑手列表、直接接单、商家操作及新单通知均校验批次已 `ACTIVE` 和预约旧时点已开放。普通订单不套批次期限。 - 组任务只有全部有效成员都履约结束或合法取消后释放。独立取餐、独立送达、独立结算/通知按子单进行,部分取消不重算余单折扣。 - 已确认接入 037:无人接单基准为 `max(activated_at, opened_at, 预约开始)`(即时忽略预约);部分子单先付款不启动计时,部分取消不重置。超时与接组在统一锁/事务规则下互斥,未承接组取消剩余有效成员并关联现有渠道退款事实;无组成员使用父批次生效与本人实际开放时间按独立订单判断。 - 尚未生效预约批次在激活时已到/超过原结束时间则转 `CANCELLED` 并退款,不自动改即时;恢复扫描补处理。已经 `ACTIVE` 的任务不因预约结束由此路径取消,继续依履约及 037 无人接单规则处理。 ## 3. 平台配置和金额 独立 `order_checkout_config` 单行保存全局开关、围栏距离米(默认 1000)、最多店数(默认 5)、缓冲分钟(默认 5)及 JSON 动态折扣阶梯(初始 `{2:0.9,3:0.8}`),配置保存时校验:围栏/最多店数/缓冲为正整数,门槛为不小于 2 的唯一整数,系数 `0 < ratio <= 1` 且最多四位小数,与批次快照 `DECIMAL(7,4)` 一致。开关/围栏/门店上限/阶梯缺失或非法时保守关闭新合并报价;缓冲异常回退 5 分钟,不变为无限。配置版本在同一行 CAS 增加并纳入报价指纹;创建时重算与报价版本不一致则要求重报。配置表缺行时读取显示关闭态、保存时提示执行 036 手动迁移,不能偷偷初始化为启用。 2026-10-16 已确认:降低 `max_stores` 前检查未结束、未承接组的有效成员数,存在超出新值者拒绝保存且不增加配置版本,包含尚未生效的已建组;已接组继续履约。检查与建批的配置读取/版本重验以及组取消/承接必须具有一致并发保护,不能检查后又按旧配置建出超限未接组。 首店门店坐标为圆心、对每个后续门店作球面直线距离,米精度比较 `distance <= fence_meters`;不以道路/用户地址距离代替。圈外店不进入本批次父/子单或金额;创建成员集合必须与确认报价一致,否则拒绝。 每店原运费按现有有效规则权威计算;同骑手批次取最高满足门槛档位,对每店分别执行 `original_freight × ratio` 后 `HALF_UP` 到整数 TWD。子单 `amount = food_amount - valid_promotion_and_coupon_reduction + discounted_freight`,父单总额为子单实付之和。用户到付代收该子单 `amount`,给商家 `amount - discounted_freight`,商家给骑手折后运费;不存在跨商家补差。取消余单金额不重算。 ## 4. 迁移与兼容 DDL 只登记到 `updatesql/sql.md`,不自动执行。迁移前不得开启新入口。已有父/子单不回填批次关联;老单店转账、自取、夜市自配送、已有 LINE/OMG/到付均沿旧流程。普通平台配送预约只补旧规则时点扫描,不套批次付款截止。