本文拆清三类名称相近但生命周期不同的业务:首配方案生成首批配货采购单;预订单先支付定金或预付款,再分次提货生成采购单;旧预购表仍保留历史查询和兼容逻辑。

1. 业务目标

  1. 为新站或指定站点按经营方案生成首批商品清单。
  2. 将首配方案、基础包、商品和供应商转成正式采购订单。
  3. 支持普通和拼团预订单活动。
  4. 预订单下单时锁定商品、数量、折前/折后价和定金规则。
  5. 支付完成后按活动提货窗口分批提货。
  6. 每次提货生成来源可追踪的采购单。
  7. 采购关闭、退货或售后时归还可提数量。
  8. 全部提货后完成剩余退款或结算并关闭生命周期。
  9. 兼容旧 t_scm_preorder* 与新 t_pre_order* 数据。

2. 核心概念

概念业务对象最终结果
首配方案t_bs_delivery_plan 及模板商品直接生成首批配货采购单
微仓首配微仓首配清单和销售转换可能转销售单/补货链,非本文首批采购完全同义
新预订单t_pre_order / t_pre_order_info支付后分次生成采购单
旧预订单t_scm_preorder*历史预购、支付、提货和核销兼容
预订单活动活动主数据 + t_pre_order_activity_settings定义定金、最小提货、提货窗口和拼团
提货从预订单额度中选择商品和批量创建一张采购订单
首配退货采购退货类型 30-Cxx-18退首配采购商品

3. 业务总图

flowchart TD
  subgraph First["首配链"]
    A["首配方案/模板/商品"] --> B["选择方案、仓库和数量"]
    B --> C["按直发/非直发/广宣品分组"]
    C --> D["首批配货采购单 30-Cxx-09"]
    D --> E["支付/出库/入库"]
    E --> F["首配退货 30-Cxx-18"]
  end
  subgraph Pre["新预订单链"]
    G["预订单活动配置"] --> H["创建 t_pre_order 主从"]
    H --> I["订单中心预订单"]
    I --> J["支付定金/预付款"]
    J --> K["待提货"]
    K --> L["一次或多次提货"]
    L --> M["预订采购单 srcOrderType=12"]
    M --> N["采购履约"]
    N --> O["关闭回退/售后/退款/完成"]
  end

4. 代码地图

4.1 新预订单

路径责任
application/controllers/scm/InvPre.php活动、预订单、提货、支付和页面接口
application/service/scm/InvPreService.php新旧预购页面编排和兼容逻辑
application/Services/PreOrders/PreOrderService.php新预订单领域主服务
application/Services/PreOrders/PreOrderCheckService.php是否可提货校验
application/Services/PreOrders/PreOrderStatisticService.php拼团、已提金额等统计
application/Services/PreOrders/PreOrderNotifyService.php订单中心售后、取消、关闭和退款回调
application/controllers/tasks/PreOrder.php超时取消、完成、通知等周期任务
application/models/orders/PreOrderModel.php新预订单主表
application/models/orders/PreOrderInfoModel.php新预订单明细

4.2 首配

路径责任
application/controllers/scm/InvPo.php首配页面及采购入口
application/service/scm/InvPoService.php首配方案、商品分组、订单校验和采购落单
application/Services/FirstMatch/FirstMatchSer.php首配方案商品和规则
application/controllers/moveMall/FirstMatch.php微仓首配接口
public/js/api/firstMatch.js首配前端 API
public/js/page/firstMatch.js方案选择、数量和确认交互

5. 表结构地图

5.1 新预订单

表用途关键字段
t_pre_order新预订单主表pre_order_no,order_status,refund_status,order_qty,pickedup_qty
t_pre_order_info商品快照和逐行提货量inv_id,sku_id,qty,pickedup_qty,price,amount
t_pre_after_sale预订单售后售后单、数量、状态、退款
t_pre_order_province活动可提货省份activity_id,province_code
t_pre_order_activity_settings定金和提货配置类型、定金、最小批量、最大次数
t_pre_activity_group_level拼团阶梯版本、阶梯、折扣
t_pre_activity_group_level_detail拼团固定商品价SKU、阶梯固定价

5.2 旧预订单

表用途
t_scm_preorder旧预订单主表
t_scm_preorder_info旧预订单商品
t_scm_preorder_pay旧支付记录
t_scm_preorder_spend旧提货记录
t_scm_burning_bill旧核销单
t_admin_preorder*运营端旧预订单、商品和服务站配置

5.3 首配

表用途
t_bs_delivery_plan首配方案
t_bs_setout_template首配基础包模板
t_bs_setout_template_item方案与模板关系
t_bs_setout_goods模板商品和数量
t_scm_po_order*最终首批配货/预订采购单

6. 单据关系

erDiagram
  SALE_ACTIVITY ||--|| PRE_ORDER_SETTING : configures
  SALE_ACTIVITY ||--o{ PRE_ORDER : creates
  PRE_ORDER ||--o{ PRE_ORDER_INFO : contains
  PRE_ORDER ||--o{ PO_ORDER : picked_as
  PO_ORDER ||--o{ PO_ORDER_INFO : contains
  PRE_ORDER ||--o{ PRE_AFTER_SALE : refunds
  DELIVERY_PLAN ||--o{ SETOUT_TEMPLATE_ITEM : contains
  SETOUT_TEMPLATE ||--o{ SETOUT_TEMPLATE_ITEM : selected_by
  SETOUT_TEMPLATE ||--o{ SETOUT_GOODS : contains
  DELIVERY_PLAN ||--o{ PO_ORDER : creates_first_order

预订单和采购单不是同一张单:一张预订单可以多次提货,对应多张采购单。

7. 订单类型字典

7.1 采购 orderType

类型值含义
预订采购30-Cxx-08由预订/提货产生的采购
首批配货30-Cxx-09首配方案采购
首配退货30-Cxx-18首配商品采购退货
普通采购30-Cxx-01非首配/预订单普通采购
预售专属授信30-Cxx-71新预售授信订单

7.2 采购来源 srcOrderType

值含义
0普通来源
1活动
12新预订单

排查采购单时必须同时看 orderType 与 srcOrderType。仅看 30-Cxx-08 不能证明一定来自当前新预订单实现,历史链路可能复用类型。

8. 新预订单状态机

枚举:PreOrderEnums。

状态值含义
待支付1已提交,等待支付
待提货2支付成功,可等活动窗口
提货中3至少一次提货,尚未全部提完
全部提货4枚举存在,实际完成逻辑需结合任务确认
已取消9用户取消
超时自动取消10待支付超时
已关闭11退款/活动失败/外部关闭
已完成12全部提货且退款/结算完成
stateDiagram-v2
  [*] --> ToPay: 创建预订单
  ToPay --> ToPick: 支付成功
  ToPay --> Cancel: 用户取消
  ToPay --> AutoCancel: 支付超时
  ToPick --> Picking: 第一次提货
  Picking --> Picking: 后续分批提货
  Picking --> PickFinish: 已提数量达到订购数量
  ToPick --> Closed: 售后/活动失败/外部关闭
  Picking --> Closed: 剩余额度关闭
  PickFinish --> Finished: 退款或最终结算完成
  Closed --> Finished: 特定退款完成分支
  Cancel --> [*]
  AutoCancel --> [*]
  Finished --> [*]

代码中部分方法在数量相等时仍从 STATUS_PICKING 扫描,再由完成任务推进 STATUS_ORDER_FINISH,实际是否使用 STATUS_PICK_FINISH=4 需以生产数据确认。

9. 退款状态

状态值含义
无退款0正常可提货
退款中1不允许继续提货
退款完成2根据来源关闭或完成

退款来源:

值来源
1DGJ 全部提货后自动退款
2OPS 内勤发起
3拼团失败

OPS 退款完成时 Model 会将主单状态更新为已关闭。

10. 售后状态

状态值含义
已提交/待审核1售后已创建
审核通过/待退款4等资金退款
已取消8售后取消
退款完成9售后结束
stateDiagram-v2
  [*] --> Submitted
  Submitted --> Checked: 审核通过
  Submitted --> Canceled: 取消
  Checked --> Finished: 退款完成
  Canceled --> [*]
  Finished --> [*]

11. 活动提货状态

状态值含义
未开始0不能提货
已开始1可按规则提货
已结束2不再允许新提货
预提货3预提货阶段,状态文案表未完整映射

拼团状态:进行中 0、成功 5、失败 9。

12. 定金方式

PreOrderSettingEnums 中 deposit_type:

值文案定金算法
0固定金额比例handsel_amount = 订单金额 × handsel_payrate
1固定单价deposit_total = 订购批数 × deposit_price
flowchart TD
  A["活动配置"] --> B{"deposit_type"}
  B -->|按金额 0| C["订单折后金额 × 定金比例"]
  B -->|按件数 1| D["总批数 × 每批定金"]
  C --> E["handsel_amount(分)"]
  D --> E

预订单主表金额以分存储,列表格式化时除以 100。

13. 创建预订单

13.1 入口

Controller 方法:scm/InvPre::createPreOrder();核心最终进入 PreOrderService。

13.2 主流程

sequenceDiagram
  participant UI as 预售活动页面
  participant InvPre as InvPre Controller/Service
  participant Pre as PreOrderService
  participant Setting as 活动配置
  participant DB as t_pre_order*
  participant ODC as 订单中心
  UI->>InvPre: 活动、计划、模板、商品和数量
  InvPre->>Pre: submit/createPreOrder
  Pre->>Setting: 查询定金和最小提货配置
  Pre->>Pre: 校验活动未结束、总批数>=最小批量
  Pre->>Pre: 从采购 Entry 复制主从快照
  Pre->>DB: 事务写主表和明细
  Pre->>ODC: submit 预订单
  ODC-->>Pre: odc_order_no
  Pre->>DB: 回写外部单号
  Pre-->>UI: pre_order_no

13.3 主表快照

字段来源/含义
pre_order_no本地生成
source_type活动预订单=1
source_id活动 ID
source_order_no上游来源单号
order_status初始待支付=1
min_pickup_type由活动销售类型映射
activity_plan_id/template_id下单时计划和模板快照
order_qty/order_amount明细汇总
batch_num总订购批数
handsel_amount定金

13.4 外部调用风险

订单中心 submit() 在本地数据库 transaction 回调内部调用。外部提交成功后,本地事务若回滚,会形成订单中心有单、本地无单;重试还可能重复创建。必须依赖外部幂等键 pre_order_no 或补偿查询。

14. 明细金额和数量

formatPreOrderInfoList() 计算:

order_qty = Σ detail.qty
order_origin_amount = Σ(detail.qty × origin_price)
order_amount = Σ(detail.qty × price)

明细字段:

字段含义
qty订购数量
pickedup_qty已用于提货生成采购单的数量
origin_price折前单价,分
price折后/拼团最终单价,分
amount折后金额,分
is_gift是否赠品

15. 拼团价

flowchart TD
  A["统计活动总参团批数"] --> B["匹配有效阶梯"]
  B -->|无阶梯| C["group_price = 原折后价"]
  B -->|固定价活动| D["按 SKU 读取阶梯 fixed_price"]
  B -->|折扣活动| E["price × level_discount_rate / 100"]
  D --> F["group_amount = group_price × qty"]
  E --> F
  C --> F
  F --> G{"是否覆盖预订单价格"}
  G -->|是| H["price/amount 使用拼团价"]
  G -->|否| I["只用于展示/统计"]

拼团阶梯变化可能改变最终价格。必须明确锁价时点:下单、成团、提货还是退款。代码支持在某些格式化场景覆盖原价,生产业务口径待确认。

16. 支付

采购支付服务的场景码 senceCode=preOrder:

sequenceDiagram
  participant UI as 预订单支付页
  participant Po as InvPoService
  participant Pre as PreOrderService
  participant Pay as 支付中心
  UI->>Po: getPayInfoNew(senceCode=preOrder, orderNos)
  Po->>Pre: 查询预订单
  Po->>Po: 每单必须 STATUS_TO_PAY
  Po->>Pay: 创建支付请求
  Pay-->>Po: 支付参数
  Pay-->>Pre: 支付成功回调
  Pre->>Pre: order_status 1 -> 2,写 pay_time

首配 0 元订单代码明确无需支付;首配与普通采购同批支付还有前端提示“同批次一起支付/取消支付”。批次字段和支付中心幂等需结合采购专题检查。

17. 可提货判断

页面 canPick 同时要求:

  1. 活动计划当前允许提货。
  2. order_qty > pickedup_qty。
  3. refund_status=0。
  4. 主单状态为待提货或提货中。
flowchart TD
  A["预订单"] --> B{"计划可提货"}
  B -->|否| X["不可提"]
  B -->|是| C{"仍有未提数量"}
  C -->|否| X
  C -->|是| D{"无退款"}
  D -->|否| X
  D -->|是| E{"状态=待提货/提货中"}
  E -->|否| X
  E -->|是| F["canPick=true"]

18. 最小提货规则

类型枚举校验对象
按批量1本次提货批数不低于最小批量,并满足商品最小起订量
按金额2本次提货金额不低于配置

活动还可能限制:最大提货次数、提货省份、计划时间、每商品剩余数量和拼团是否成功。

19. 创建提货采购单

Controller:scm/InvPre::createPickUpOrder();Service:PreOrderService::createPoOrder() / createPoOrderNew()。

sequenceDiagram
  participant UI as 提货页面
  participant Check as PreOrderCheckService
  participant Pre as PreOrderService
  participant Info as t_pre_order_info
  participant PO as 采购服务
  participant ODC as 订单中心
  UI->>Check: 校验状态/窗口/退款/次数/数量
  UI->>Pre: preOrderNo + entries + delayData
  Pre->>Info: 锁定/读取可提商品
  Pre->>PO: 创建 orderType=预订采购, srcOrderType=12
  PO-->>Pre: 采购单号和明细
  Pre->>Info: pickedup_qty += 本次提货量
  Pre->>Pre: 主单已提数量/次数/批数累加
  Pre->>ODC: 同步提货数量
  Pre-->>UI: 采购单结果

20. 数量账

20.1 主单

字段含义
order_qty预订单总订购件数
pickedup_qty累计已提件数
pickedup_times提货次数
batch_num总订购批数
picked_batch_num已提批数
picked_num_true真实提货数量 V2 口径

20.2 明细

明细未提数量 = qty - pickedup_qty
主单未提数量 = order_qty - pickedup_qty
可提商品条件 = detail.pickedup_qty < detail.qty

20.3 守恒

预订单订购数量
= 当前累计有效提货采购数量
+ 已关闭/退回到预订单的数量
+ 尚未提货数量

每个明细:0 <= pickedup_qty <= qty
主单 pickedup_qty ≈ Σ明细 pickedup_qty

picked_num_true 与 pickedup_qty 的差异语义需要根据 V2 业务确认,不能直接互换。

21. 原子更新

PreOrderInfoModel::batchIncryOrderInfoPickedQty() 使用表达式:

正数:pickedup_qty = pickedup_qty + abs(qty)
负数:pickedup_qty = pickedup_qty - abs(qty)

它支持提货增加和关闭回退,但必须在同一事务内校验上下界。单纯原子加减不能阻止并发两次提货合计超过 qty,除非更新条件包含 pickedup_qty + :delta <= qty。

22. 提货采购关闭回退

当预订单来源采购单关闭/退回时,returnPickedQty() / V2 会:

  1. 根据采购明细构造需要归还的预订单数量。
  2. 明细 pickedup_qty 减少。
  3. 主单累计已提数量减少。
  4. 根据是否整次关闭决定是否归还 pickedup_times/picked_batch_num。
  5. 同步订单中心提货数量。
sequenceDiagram
  participant PO as 预订采购单
  participant Pre as PreOrderService
  participant Detail as 预订单明细
  participant Main as 预订单主表
  participant ODC as 订单中心
  PO->>Pre: 关闭/退回采购数量
  Pre->>Detail: pickedup_qty -= returnQty
  Pre->>Main: pickedup_qty -= totalReturn
  opt 整次提货作废
    Pre->>Main: pickedup_times/picked_batch_num 回退
  end
  Pre->>ODC: 同步最新提货数量

重复关闭通知如果没有来源采购单幂等记录,会重复归还额度,导致负数或可重复提货。

23. 订单中心同步

新预订单创建、提货数量变化、售后和关闭都与订单中心交互。

事件DGJ 行为
预订单提交调用 PreOrderProvider::submit,回写 odc_order_no
提货变化syncPreOrderPickQtyToOdc
售后创建PreOrderNotifyService::createPreRefundOrder
售后取消cancelPreRefundOrder
预订单关闭closePreOrder
退款完成preOrderRefundFinish

数据库事务无法回滚订单中心,需要 pre_order_no、外部售后单号和事件 ID 幂等。

24. 售后与退款

flowchart TD
  A["创建预订单售后"] --> B["主单 refund_status=退款中"]
  B --> C{"售后审核结果"}
  C -->|取消| D["恢复无退款,可继续提货"]
  C -->|通过| E["等待资金退款"]
  E --> F["退款完成回调"]
  F --> G{"退款来源"}
  G -->|OPS| H["预订单关闭"]
  G -->|全部提货自动退款| I["预订单完成"]
  G -->|拼团失败| J["拼团失败关闭"]

售后数量归还和资金退款是两套状态,不能只看 refund_status 判断可提数量是否已正确回退。

25. 全部提货完成

任务会扫描 order_status=提货中、refund_status=无退款 且 order_qty=pickedup_qty 的订单,进入 finishPreOrder(),完成剩余定金/差额退款或外部结算,再更新 STATUS_ORDER_FINISH=12。

flowchart TD
  A["order_qty = pickedup_qty"] --> B{"refund_status=0 且状态=3"}
  B -->|否| X["不处理"]
  B -->|是| C["计算已提金额/应退金额"]
  C --> D["发起退款或完成处理"]
  D --> E{"外部成功"}
  E -->|否| F["保持可补偿状态"]
  E -->|是| G["状态=12,写 finish_time"]

金额计算要使用拼团最终价、已提数量和定金,不应只按原订单总额减采购单金额猜测。

26. 取消和超时

PreOrderModel::cancelOrder() 使用条件:

sid + pre_order_no + order_status=待支付

因此已支付订单不能走普通取消。用户取消状态 9,任务超时取消状态 10,便于区分责任和退款逻辑。

stateDiagram-v2
  ToPay --> UserCancel: 用户主动
  ToPay --> AutoCancel: 超时任务
  ToPay --> Paid: 支付回调抢先
  UserCancel --> [*]
  AutoCancel --> [*]
  Paid --> ToPick

支付回调与超时取消并发时,支付更新也应带 order_status=1 条件。当前 updateOrderPayed() 仅按 sid + pre_order_no 更新,迟到支付是否可能把已取消单改成待提货,需要重点确认。

27. 新预订单 API

27.1 接口总表

方法路径用途
POST/scm/invpre/createpreorder创建预订单
POST/scm/invpre/createpickuporder提货并创建采购单
POST/scm/invpre/getpayresult查询支付结果
POST/scm/invpre/activityquery活动查询
POST/scm/invpre/new_activity_save保存预订单活动
POST/scm/invpre/getpolicylist政策/方案列表
POST/scm/invpre/preorderquerynew新预订单查询页面
POST/scm/invpre/preorderqueryaftersale预订单退款查询

真实请求字段主要由旧页面表单和 Service 校验决定,联调前从浏览器 Network 固化完整契约。

27.2 提货请求示意

POST /scm/invpre/createpickuporder
Content-Type: application/json

{
  "preOrderNo": "PRE-EXAMPLE",
  "locationId": 50001,
  "entries": [
    {"id": 60001, "invId": 30001, "qty": 10}
  ],
  "delayData": {}
}

示例只表达业务字段,不代表 Controller 真实 content-type;旧 CodeIgniter 页面可能使用表单和 JSON 字符串。

28. 首配配置模型

erDiagram
  DELIVERY_PLAN ||--o{ SETOUT_TEMPLATE_ITEM : selects
  SETOUT_TEMPLATE ||--o{ SETOUT_TEMPLATE_ITEM : belongs_to
  SETOUT_TEMPLATE ||--o{ SETOUT_GOODS : has
  BS_GOODS ||--o{ SETOUT_GOODS : referenced
对象作用
方案面向站点选择的首配经营方案
模板/基础包一组商品配置
方案模板关系一个方案组合多个基础包
模板商品SKU、数量和可能的品牌/供应商规则

模板变化只影响后续新建首配单,历史采购订单必须保留下单快照。

29. 首配页面 API

前端 public/js/api/firstMatch.js 调用:

POST /scm/InvPo/firstMatch

页面还会选择方案、仓库、模板和商品数量,最终调用 InvPoService::firstMatch() / confirmDeliveryPlan() 等方法形成订单数据。

30. 首配商品生成

sequenceDiagram
  participant UI as 首配页面
  participant PO as InvPoService
  participant FM as FirstMatchSer
  participant Plan as 方案/模板表
  participant Goods as 商品缓存/供给
  participant Balance as OPS退款账户
  UI->>PO: sid + 方案 + 仓库
  PO->>Plan: 查询方案和模板商品
  PO->>FM: 计算商品、数量和广宣品规则
  PO->>Goods: 获取 SKU、最小起订量、价格、供应商
  PO->>PO: 分直发/非直发/广宣品
  PO->>Balance: 查询退款账户余额
  PO-->>UI: 分组明细、总数量、总金额

30.1 三类分组

分组代码数据特点
直发item['1']还会按供应商拆组
非直发item['2']统一仓配
广宣品item['3']特定规则下赠送/0 元

广宣品价格和金额可以为 0,但仍计入数量。支付金额和采购数量不能用同一总数推断。

31. 首配订单限制

普通采购保存时会判断是否属于必须走新首配入口的商品/站点,若不允许普通首配,会提示切换到首配订单页面。

首配订单统一按期货处理。支付、出库和入库仍进入采购主链,但订单类型、批次、0 元处理和退货类型不同。

32. 首配采购状态

首配最终使用采购订单状态机,详见 14_采购完整排查手册.md。典型链路:

stateDiagram-v2
  Draft --> Submitted
  Submitted --> ToPay: 在线支付
  Submitted --> WaitingOut: 无需支付/挂账
  ToPay --> WaitingOut: 支付成功
  WaitingOut --> Delivery: 部分/全部发货
  Delivery --> WaitingIn: 待入库
  WaitingIn --> Finished: 入库完成
  Submitted --> Closed: 关闭
  WaitingOut --> Closed: 未发货关闭

首配 0 元订单无需支付,但状态如何直接推进应以采购 Service 为准。

33. 首配退货

首配退货类型为 30-Cxx-18,页面有独立列表和详情文案。

flowchart TD
  A["选择原首配采购入库商品"] --> B["校验可退数量和额度"]
  B --> C["生成首配退货申请"]
  C --> D["审核/出库"]
  D --> E["供应商/OPS处理"]
  E --> F["退款和采购数量账更新"]

首配退货额度过期由订单中心事件 ordercenter_aftersale_return_quota_expire 通知,不能仅按普通采购退货判断。

34. 首配与电池站

电池站菜单中会隐藏首配采购。后端仍应防止电池站绕过菜单调用首配接口。详见 42_电池用户_蓄电池可售范围专题.md。

35. 旧预订单兼容

旧表 t_scm_preorder* 仍被 InvPreService、历史报表和核销逻辑使用。新旧体系区别:

维度旧预订单新预订单
主表t_scm_preordert_pre_order
商品t_scm_preorder_infot_pre_order_info
支付/提货独立 pay/spend 表主单累计字段 + 来源采购单
外部订单历史 NC/核销订单中心 odc_order_no
状态旧 Service 私有映射PreOrderEnums

排查时先根据表和单号确认版本,不能把新状态值套到旧表。

36. 任务和补偿

任务入口 application/controllers/tasks/PreOrder.php 负责或调用:

  • 待支付超时取消。
  • 拼团成功/失败处理。
  • 可提货通知。
  • 全部提货后的退款/完成。
  • 订单中心/支付状态补偿。
flowchart TD
  A["定时扫描"] --> B{"任务类型"}
  B -->|待支付超时| C["状态 1 -> 10"]
  B -->|拼团结束| D["成功开放提货 / 失败关闭退款"]
  B -->|提货窗口| E["发送站内消息/通知"]
  B -->|全部提货| F["完成退款和状态 12"]
  B -->|外部不一致| G["查询支付/订单中心并补偿"]

每个任务必须有固定扫描边界、幂等键和状态条件,详细治理见第 40 篇。

37. 通知

消息类型 PRE_ORDER_PICK_UP=60,跳转到“预购订单查询(新)”。

通知只提示用户,不改变提货状态。重复通知应去重,漏通知不应阻止用户从订单页面提货。

38. 关键并发窗口

38.1 支付与取消

sequenceDiagram
  participant Timeout as 超时任务
  participant Pay as 支付回调
  participant DB as 预订单
  Timeout->>DB: status 1 -> 10 条件更新
  Pay->>DB: 更新为 status 2
  Note over Timeout,Pay: 支付更新若不带原状态条件,迟到回调可能复活已取消订单

38.2 两次提货

sequenceDiagram
  participant A as 提货请求A
  participant B as 提货请求B
  participant DB as 预订单明细
  A->>DB: 读取未提=10
  B->>DB: 读取未提=10
  A->>DB: 提货10并创建采购单
  B->>DB: 提货10并创建采购单
  Note over A,B: 无行锁/条件更新时累计提货20,超过订购10

38.3 关闭重复回退

同一采购关闭事件重复消费会重复减 pickedup_qty。需要以来源采购单号+明细+关闭事件做幂等。

39. 完整排查 SOP

flowchart TD
  A["拿到预订单号/首配采购单号"] --> B{"属于首配还是新/旧预订单"}
  B -->|首配| C["查 orderType=30-Cxx-09、方案和模板快照"]
  B -->|新预订单| D["查 t_pre_order 主从"]
  B -->|旧预订单| E["查 t_scm_preorder*"]
  D --> F["查状态/退款/活动提货窗口"]
  F --> G["按来源单号查全部采购单"]
  G --> H["汇总有效提货、关闭和售后数量"]
  H --> I["校验主从数量守恒"]
  I --> J["查订单中心/支付中心状态"]
  J --> K["查任务和回调日志"]
  C --> L["查采购支付/出库/入库/退货"]

40. 常见问题

现象首查可能原因
预订单不能支付状态是否 1已取消、已支付、订单中心状态异常
支付成功仍待支付支付回调回调漏失、sid/单号不匹配
不能提货canPick 四条件窗口、退款、状态、无剩余量
可提数量比预期少明细 pickedup_qty已生成采购单、未回退关闭量
可提数量变多关闭回退重复回退或采购单重复关闭事件
提货超过订购量并发缺行锁/条件原子更新
全部提货未完成完成任务状态/退款条件不命中、外部退款失败
拼团价错误阶梯和总参团批数阶梯版本、固定价/折扣、锁价时点
首配金额为 0是否广宣品/0 元单业务正常或模板价格错误
首配商品与模板不同历史快照模板已变,不应反改旧单

41. 只读 SQL

41.1 新预订单主单

SELECT id, sid, pre_order_no, odc_order_no,
       order_status, refund_status,
       order_qty, pickedup_qty, picked_num_true,
       batch_num, picked_batch_num, pickedup_times,
       order_amount, handsel_amount,
       source_type, source_id, activity_plan_id,
       order_time, pay_time, finish_time, refund_time
FROM t_pre_order
WHERE sid = :sid
  AND pre_order_no = :pre_order_no;

41.2 明细守恒

SELECT id, inv_id, sku_id, qty, pickedup_qty,
       qty - pickedup_qty AS unpicked_qty,
       price, amount, is_gift
FROM t_pre_order_info
WHERE sid = :sid
  AND pre_order_no = :pre_order_no
ORDER BY id;
SELECT SUM(qty) AS detail_order_qty,
       SUM(pickedup_qty) AS detail_picked_qty,
       SUM(qty - pickedup_qty) AS detail_unpicked_qty
FROM t_pre_order_info
WHERE sid = :sid
  AND pre_order_no = :pre_order_no;

41.3 来源采购单

SELECT id, sid, billNo, orderType, srcOrderType, srcOrderNo,
       billStatus, closeType, paymentType, billDate
FROM <po_order_shard>
WHERE sid = :sid
  AND srcOrderType = 12
  AND srcOrderNo = :pre_order_no
ORDER BY id;

字段 srcOrderNo 需按当前 DDL 确认,部分实现可能使用其他来源字段。

41.4 活动配置

SELECT *
FROM t_pre_order_activity_settings
WHERE activity_id = :activity_id;

SELECT *
FROM t_pre_activity_group_level
WHERE activity_id = :activity_id
ORDER BY level_step;

41.5 首配方案

SELECT p.*, i.*, t.name AS template_name
FROM t_bs_delivery_plan p
LEFT JOIN t_bs_setout_template_item i ON i.plan_id = p.id
LEFT JOIN t_bs_setout_template t ON t.id = i.template_id
WHERE p.id = :plan_id;

表真实关联字段以生产 DDL 为准。

42. 代码检索

rg -n "STATUS_TO_PAY|STATUS_TO_PICK|STATUS_PICKING|STATUS_ORDER_FINISH" \
  application/KzData/Enums/PreOrderEnums.php

rg -n "submitPreOrder|createPoOrder|returnPickedQty|finishPreOrder|closePreOrder" \
  application/Services/PreOrders

rg -n "ORDERTYPE_PRE_ORDER|ORDERTYPE_FIRST_ORDER|SRC_ORDER_TYPE_PRE_ORDER" application

rg -n "BS_DELIVERY_PLAN|BS_SETOUT_TEMPLATE|firstMatch|confirmDeliveryPlan" \
  application public/js

43. 故障树

flowchart TD
  A["首配/预订单异常"] --> B{"是哪类单据"}
  B -->|首配| C["方案模板 -> 商品分组 -> 采购单"]
  B -->|新预订单| D{"下单/支付/提货/关闭/退款哪一阶段"}
  B -->|旧预订单| E["旧表/支付/提货/核销链"]
  D -->|下单| F["活动配置/最小批量/订单中心"]
  D -->|支付| G["状态1/支付中心/迟到回调"]
  D -->|提货| H["窗口/退款/剩余量/并发"]
  D -->|关闭| I["采购来源/数量回退/幂等"]
  D -->|退款| J["售后状态/资金状态/完成任务"]

44. 已识别风险

编号风险影响
R43-01新旧预订单并存状态、表和报表口径容易混用
R43-02本地事务内调用订单中心外部成功本地回滚导致孤儿单
R43-03支付成功更新未见原状态条件迟到支付可能复活已取消单
R43-04提货先读剩余量再累计并发可能超提
R43-05明细原子加减无上下界条件可超过 qty 或减成负数
R43-06关闭回退依赖事件幂等重复消息会重复归还额度
R43-07pickedup_qty 与 picked_num_true 双口径报表和可提数量可能不一致
R43-08STATUS_PICK_FINISH=4 与完成扫描逻辑并存状态 4 是否真实使用不明确
R43-09拼团价可在格式化时覆盖锁价时点和历史价格解释困难
R43-10活动提货状态 3 文案映射不完整页面可能显示未知状态
R43-11订单中心/支付/退款跨系统本地事务不能保证最终一致
R43-12首配模板实时变化若未完整快照,历史单解释困难
R43-13首配 0 元和广宣品有数量无金额金额汇总不能代表商品数量
R43-14电池站只靠菜单隐藏首配接口可能被绕过
R43-15预订单直发不允许直采退货通用退货入口可能误导用户
R43-16通知与提货状态解耦漏通知不能用来判断不可提
R43-17完成依赖周期任务调度失败会长期停在提货中
R43-18来源采购单字段和唯一键待确认无法可靠去重提货和回退

45. 回归清单

45.1 预订单创建

  • [ ] 普通活动、拼团活动。
  • [ ] 按金额定金、按批数定金。
  • [ ] 最小提货批量边界。
  • [ ] 活动未开始、可下单、已结束提货。
  • [ ] 普通商品、赠品、套餐。
  • [ ] 订单中心成功/超时/重复提交。

45.2 支付

  • [ ] 待支付正常支付。
  • [ ] 重复支付回调。
  • [ ] 用户取消与支付并发。
  • [ ] 超时取消与迟到支付。
  • [ ] 预售专属授信。
  • [ ] 支付查询和补偿。

45.3 提货

  • [ ] 未开始、预提货、已开始、已结束。
  • [ ] 一次全部提货、多次部分提货。
  • [ ] 按件数/按金额最小限制。
  • [ ] 最大提货次数和省份限制。
  • [ ] 两请求并发提同一商品。
  • [ ] 赠品、最小起订量和箱规。
  • [ ] 创建采购单失败时数量不增加。
  • [ ] 数量增加后订单中心同步失败补偿。

45.4 关闭和售后

  • [ ] 部分关闭归还数量。
  • [ ] 整次提货关闭归还次数/批数。
  • [ ] 重复关闭消息。
  • [ ] 售后创建、取消、审核、退款完成。
  • [ ] OPS 退款、全部提货自动退款、拼团失败。
  • [ ] 全部提货完成任务。

45.5 首配

  • [ ] 单模板、多模板方案。
  • [ ] 直发、非直发、广宣品。
  • [ ] 0 元商品和退款账户余额。
  • [ ] 仓库未选、商品失效、最小起订量。
  • [ ] 普通采购绕过首配拦截。
  • [ ] 电池站接口拦截。
  • [ ] 首配支付、出库、入库和首配退货。

46. 改动影响面

改动最小回归
PreOrderEnums页面、任务、支付、提货、售后、订单中心回调
submitPreOrder活动下单、金额、订单中心幂等
createPoOrder*预订单、采购、库存、支付、数量回写
returnPickedQty*采购关闭、售后、重复事件、可提数量
finishPreOrder全提、退款、资金和状态完成
活动阶梯拼团统计、锁价、提货价和退款
首配方案页面、直发拆单、广宣品、采购和退货

47. 生产待确认项

  1. 新旧预订单当前流量比例和历史数据截止时间。
  2. t_pre_order.pre_order_no、odc_order_no 的唯一索引。
  3. 预订单到采购单的正式来源字段及唯一约束。
  4. 订单中心提交和提货同步的幂等键、重试与查询接口。
  5. 支付回调对已取消/已关闭预订单的迟到处理。
  6. 提货行锁或条件原子更新的生产实现。
  7. picked_num_true 与 pickedup_qty 的业务差异。
  8. 状态 4“全部提货”是否仍实际使用。
  9. 拼团最终价锁定时点和阶梯版本快照。
  10. 提货状态 3“预提货”的页面和业务规则。
  11. 完成、超时、通知任务的真实调度频率和告警。
  12. 预订单退款资金来源、金额公式和支付中心回调。
  13. 首配方案/模板管理入口和历史快照字段。
  14. 首配采购后端对电池站的强制校验。
  15. 旧预订单报表和新预订单报表是否已经统一口径。

48. 证据索引

主题代码路径
预订单状态application/KzData/Enums/PreOrderEnums.php
活动配置状态application/KzData/Enums/PreOrderSettingEnums.php
采购类型application/KzData/Enums/PoOrderEnums.php
Controllerapplication/controllers/scm/InvPre.php
页面编排application/service/scm/InvPreService.php
新预订单核心application/Services/PreOrders/PreOrderService.php
提货校验application/Services/PreOrders/PreOrderCheckService.php
统计application/Services/PreOrders/PreOrderStatisticService.php
外部回调application/Services/PreOrders/PreOrderNotifyService.php
定时任务application/controllers/tasks/PreOrder.php
主表 Modelapplication/models/orders/PreOrderModel.php
明细 Modelapplication/models/orders/PreOrderInfoModel.php
首配采购application/service/scm/InvPoService.php
首配页面public/js/page/firstMatch.js

49. 最终理解

flowchart LR
  A["活动/方案快照准确"] --> F["业务正确"]
  B["支付状态可靠"] --> F
  C["提货数量原子守恒"] --> F
  D["采购关闭可幂等回退"] --> F
  E["跨系统退款最终一致"] --> F

首配是“一次性形成首批采购”,预订单是“先获得未来提货权,再多次转采购”。整个专题最核心的正确性不是单据状态,而是额度守恒:任何一件商品都只能处于未提、有效采购履约、已回退或售后退款中的一个位置;支付、订单中心和任务失败不能让它被重复提货,也不能让合法额度永久丢失。

请求-日志-数据变更追踪卡

多入口请求链路

场景调用方与入口请求载荷/上下文Controller/ConsumerService/Provider汇合点最终业务事实
创建预订单PC scm/InvPresid、商品、总额度、有效期、配置InvPre ControllerInvPreService/PreOrderServicepreorder bill预订单主明细和可提额度
提货转采购PC/任务preorder、SKU、提货量、收货信息InvPre/PreOrder taskCheckService + InvPoServicepreorder + PO bill锁定额度并生成采购单
首配/moveMall/FirstMatch站点、首配方案、商品数量FirstMatch ControllerFirstMatchSerfirst-match ID + PO一次性形成首批采购
回调/回退支付/订单中心/PreOrder taskPO 支付/关闭/退款状态Pay/Order/PreOrder ConsumerNotify/Statistic ServicePO + preorder relation完成提货或回退额度

日志证据矩阵

| 链路段 | 日志来源 | 可检索锚点 | 成功信号 | 失败信号 | 与下一段关联方式 | | --- | --- | --- | --- | --- | --- | --- | | 预订单 | InvPre/PreOrderService | request_id、preorder no、sid、SKU | 主明细额度 commit | 重复、配置/有效期失败 | preorder ID 进入提货 | | 转采购 | Check/InvPo Service | preorder、PO billNo、SKU、qty | 锁额度和 PO 关系完整 | 额度已扣但 PO 创建失败、重复提货 | relation 串联两单 | | 支付履约 | Pay/Order Consumer | pay/message ID、PO/preorder | 有效 PO 转已用额度 | 回调乱序、关闭后支付 | PO 关系查额度状态 | | 回退/任务 | PreOrder task | task batch、PO、SKU、return qty | 仅无效履约额度回退且重跑 0 变化 | 重复回退、漏回退 | batch+relation 数量对账 |

环节数据变更台账

步骤代码位置事务读取事实写入表/缓存/MQ字段或数量变化回查证据
建预订单PreOrderService预订单事务配置、商品、总额度SCM_PRE_ORDER(_INFO)total=n;available=n;used/frozen=0;status 初始化preorder+SKU 额度
提货锁额PreOrderCheckService锁额事务available、有效期、幂等关系预订单明细/提货关系available -n、frozen +npreorder+request key
生成采购InvPoService跨预订/采购边界需验证frozen 额度、商品地址SCM_PO_ORDER*、关系PO insert;order type/source 写明preorder+PO+SKU
支付/履约Notify/Statistic Service回调事务PO 最终有效状态预订额度统计frozen -n、used +n;状态按总额推进pay/message ID、额度守恒
关闭/售后回退PreOrderNotify/Task幂等事务PO 关闭/退款最终量、已回退量额度/关系/统计frozen/used -n、available +n,不超过原提货量relation、回退流水、重跑 0 变化

子模块追踪:preorder-create 预订单创建与定金

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
创建预订PC 创建预订单/商品额度preorderNo、sid、SKU、total、depositapplication/controllers/scm/InvPre.php -> application/Services/PreOrders/PreOrderService.php配置、商品、总额度、有效期和来源唯一性预订单本地事务 insert 主明细,total/available=n、used/frozen=0、状态初始化request ID + preorder + SKU + amounts配置/重复/金额非法整单回滚;超时按 preorderNo 回查
定金关系预订需定金时生成支付关系preorderNo、deposit payNoapplication/service/scm/InvPreService.php应付定金、已有支付和订单状态本地资金关系 none -> pending;外部支付事务外request ID + preorder/payNo + deposit外部超时先查支付终态,禁止重复定金单

子模块追踪:preorder-pay 预订单支付

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
支付回调预订单定金/货款成功message ID、payOrderNo、preorderNoapplication/Services/PreOrders/PreOrderNotifyService.php外部成功、已有支付、预订单允许态和金额回调本地事务 payment pending -> confirmed、订单状态单向推进message ID + pay/preorder + amount重复 0 增量;预订单账户方向不套普通采购
全景回查支付成功预订单不动preorder/pay IDs、payment rowsapplication/Services/PreOrders/PreOrderStatisticService.php支付明细、订单态、额度统计和关联采购查询只读;金额/状态/额度闭环pay/preorder + ledger/status外部成功只补本地回调,不直接改状态无支付流水

子模块追踪:preorder-pick-check 可提货与最小提货校验

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
提货校验用户选择预订单商品提货preorder/entry、request key、qty、timeapplication/Services/PreOrders/PreOrderCheckService.phppaid/valid、available、最小提货量、倍数和已有关系校验查询只读 不写;不满足时额度/采购 0 变化request ID + preorder/entry + qty + rule页面可提不等于提交可提;提交锁内重新校验
多行汇总一次提多个 SKUpreorder、item list、total requestapplication/Services/PreOrders/PreOrderCheckService.php每行 available/min、订单总限制和重复 key本地锁额事务前形成标准 item setrequest ID + preorder + per-SKU result任一行失败整单不锁额,避免部分提货

子模块追踪:preorder-pick-po 提货转采购

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
锁额建单提货确认生成采购preorder/request key、SKU、qty、addressapplication/controllers/scm/InvPo.php -> application/service/scm/InvPoService.phpfrozen 额度、商品地址、采购类型/来源和已有关系跨预订/采购本地事务边界:PO insert、预订关系 insertrequest ID + preorder/po + SKU/qtyPO 创建失败释放本次 frozen;来源键防重复采购
三方回查预订单额度扣了无采购preorder/request/po IDsapplication/models/orders/PreOrderInfoModel.php锁额流水、关系、采购主明细查询只读;三方数量相同all IDs + relation/row counts已有 PO 只补关系/响应,不再建单

子模块追踪:preorder-quota 提货额度原子更新

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
原子锁额并发提货提交preorder/entry、request key、qtyapplication/Services/PreOrders/PreOrderCheckService.phpavailable、frozen、used、有效期和版本条件更新本地事务 available -n, frozen +n,写唯一提货关系request ID + preorder/request + before/after + rowsaffected rows=0 视为额度不足/并发冲突,不继续建 PO
守恒核对额度负数/不平preorder/entry、all relationsapplication/Services/PreOrders/PreOrderStatisticService.phptotal、available、frozen、used、returned查询只读;total=available+frozen+used 按返还口径调整preorder + relation IDs + totals禁止直改汇总;按原 request/PO 做幂等补偿

子模块追踪:preorder-close-return 采购关闭与额度回退

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
关闭回调关联采购取消/关闭message ID、po/preorder relation、close qtyapplication/Services/PreOrders/PreOrderNotifyService.phpPO 最终关闭、未履约量、frozen/used 和已回退单回调本地事务 frozen/used -n, available +n,不超过提货量message ID + po/preorder + return qty支付/履约不确定不回退;重复 0 增量
回退回查PO 关闭但 available 未恢复po/preorder、return ledgerapplication/Services/PreOrders/PreOrderStatisticService.php采购关闭明细、额度流水和统计查询只读;关系级 picked=used/frozen+returnedboth order IDs + ledger只补缺失回退流水/统计,不重复关闭采购

子模块追踪:preorder-aftersale 预订单售后与退款

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
售后申请预订单取消/退款preorderNo、aftersale/refundNo、amountapplication/Services/PreOrders/PreOrderService.php已支付/已提货、可退金额、关联采购和已有售后售后本地事务 none -> pending,写原支付/预订关系request ID + preorder/aftersale/refund已转采购部分按责任边界处理,避免双退
退款回调支付中心退款结果message ID、original/refund payNoapplication/Services/PreOrders/PreOrderNotifyService.php外部终态、已退额和预订单状态回调本地事务 refunded +n、售后 pending -> completemessage ID + original/refund/preorder重复 0 增量;额度与资金分别验收

子模块追踪:firstmatch-config 首配方案配置与商品生成

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
配置方案运营/站端维护首配规则plan ID、sid、rules/productsapplication/controllers/moveMall/FirstMatch.php -> application/Services/FirstMatch/FirstMatchSer.php站点、方案版本、商品和冲突规则配置本地事务 plan/version/status old -> new、全量关系替换request ID + plan/sid + product count使用中关键规则按合同限制;失败整批回滚
生成商品按方案/站点生成推荐首配商品plan/sid、generation batchapplication/Services/FirstMatch/FirstMatchSer.php当前方案、商品可用性、库存/价格和历史生成生成本地事务/派生表 old set -> generated setbatch + plan/sid + candidates/results只生成符合规则商品;重跑稳定键不重复关系

子模块追踪:firstmatch-po 首配采购单生成与履约

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
生成采购站点确认首配方案plan/generation ID、sourceOrderNo、itemsapplication/Services/FirstMatch/FirstMatchSer.php -> application/service/scm/InvPoService.php生成商品、价格、供应商、地址和来源唯一性采购本地事务 insert PO/关系,写首配 orderType/sourcerequest ID + plan/source/po + SKU qty多供应商部分失败按已建关系回滚/关闭;来源键幂等
履约统计支付/发货/入库回调po/plan IDs、message IDapplication/Services/PreOrders/PreOrderNotifyService.phpPO 支付/发收/关闭和首配关系单回调本地事务更新首配统计 old -> recomputedmessage + po/plan + qty/status采购事实为准;统计缺失只补统计/通知

子模块追踪:firstmatch-return 首配退货与补偿任务

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
首配退货原首配采购发起退货plan/po/return billNos、SKU、qtyapplication/service/scm/InvPoService.php原入库、已退、首配来源和库存退货与库存本地事务 returnQty +n、库存 qty -nrequest ID + three billNos + transType超退/重复零写入;不按普通来源丢失首配关系
补偿任务扫描关系/统计/回退差异task batch、plan/po IDsapplication/controllers/tasks/PreOrder.phpPO 最终态、额度/统计/退货和已有补偿每关系本地事务只补 old -> expected,重跑 0 变化task + batch + IDs + before/after小批、前置事实不符 skip;不重做库存/支付成功段