一张订单的旅程:聚水潭销售业务的数据地图

给销售业务画一张数据地图,最难的不是罗列状态,而是看清订单在流转中如何「变形」:什么时候会被取消?什么时候会被合并、拆分?哪些状态是有效的业务事实,哪些只是被取代的旧壳?

本文以聚水潭 ERP(开放平台 API + 同步镜像库)为对象,把销售业务摊开成「业务—数据—分析」三层,重点讲清一条订单的完整生命周期,以及取消、合并、拆分、异常这四种变化。所有结论来自官方文档与沙箱实测的交叉验证,文末附指标口径的落地建议。

聚水潭销售业务数据地图:业务-数据-分析三层全景

一、先看全景:一张三层地图

数据地图回答三个问题:业务怎么运转、事实怎么记录、问题怎么回答。三层自下而上:

  • 业务层:主链七个环节(建单 → 付款 → ERP 审核 → 打单拣货 → 出库发货 → 买家签收 → 售后),加上四类特殊变化(取消、合并、拆分、异常)。
  • 数据层:订单域(orders / order_items)、出库域(outbound_order / outbound_order_item)、售后域(after_sale_order 及明细、收货、退款表),靠 o_id → io_id → as_id 串联。
  • 分析层:三个主题——销售转化、履约与变化、售后与退款,每个主题由问题构成,问题由带聚合方式的指标支撑。

这张图最有价值的部分不是主链,而是四条岔路。主链人人都会画,岔路才是数据对不上、指标算不准的地方。

二、主链:真正推动订单的只有四个状态

官方给了 10 个整单状态,但主链只有四个:

  • WaitPay 待付款 → WaitConfirm 已付款待审核 → Delivering 发货中 → Sent 已发货

把状态放到时间轴上(下图),三件事一目了然:

聚水潭订单生命周期时间轴

① 谁在推动,边界在哪里。 买家付款推动 t1,ERP 审核推动 t2(自动审核,每 10 分钟一轮),仓库打单出库推动 t4。开放平台能写的只有三件事:上传建单、拆单、取消/反取消——审核没有接口,发货也推不动,平台从打单开始就只是观察者。

② 两个「等待段」不在主链上。 WaitFConfirm(等财务审核)和 WaitOuterSent(等供销商/外仓)插在审核之后,走完仍回主链发货。看板上把它们并进「待发货」是对的,单独给它们做图反而看不出「卡在哪」。

③ 审核是岔路的总闸门。 合并发生在 ERP 审核阶段(同仓合并),拆分实测在 WaitConfirm 状态成功,异常也多在审核时卡住——四条岔路的进入时点全部落在「已付款、未发货」这个窗口。

小结:盯住 t1 到 t4 这段窗口,就盯住了订单的全部变化。

三、四条岔路:订单的四种变化

这是本文的重点。四种变化共享同一个进入窗口,但机制、可逆性、对数据的影响完全不同。

取消:可逆的终态

  • 任意未发货状态都可取消(官方白名单:待付款、发货中、异常、已付款待审核等)。
  • `Sent`、`Split`、`Merged` 不在白名单——实测返回 code=130「该状态不允许取消」。已发货的单要「取消」得走售后,被合并/拆分的单要去动接手的那张单。
  • 取消可逆:反取消接口可回到 WaitConfirm。所以 Cancelled 不算「死了」,台账要按最新状态处理。
  • 有一个坑:官方白名单里的「已审核待配快递」「等供销商发货」是 ERP 界面用语,在 API 的 10 个状态里没有直接对应取值。「这单能不能取消」不要用状态枚举自己推——映射不到的状态交人工判断。

异常:系统也会把单「拉进」异常

  • 典型原因:缺货、商品编码缺失、电子面单未配置、用户申请退款。判断原因要读 question_type + question_desc 两个字段(支持自定义,永远列不全)。
  • 关键认知:异常不只是人工点出来的。上传报文里 items[].refund_status 出现任意值会自动转异常,部分退(refund_qty>0)也会自动拉异常。售后期间订单在 WaitConfirm ⇄ Question 之间来回跳。
  • 开放平台没有「转正常」接口——异常只能人工在 ERP 处理,平台只能等。

合并与拆分:原单成壳,业务搬家

这两种变化机制相同:原单变成空壳,业务事实完整复制到新单上。

  • 合并:ERP 自动审核规则里的「同仓合并」触发。原单变 Merged,link_o_id 指向主单;主单 is_merge=true,merge_so_id 记录被并的线上单号。
  • 拆分:平台可调 drporder/split(本项目场景:缺货时把有货部分拆出去先发)。原单变 Split 成空壳,不可再取消;子单 is_split=true,link_o_id 指回原单。
  • 注意 `link_o_id` 方向相反:合并是「原单→主单」,拆分是「子单→原单」。官方定义只有一句「被合并被拆分的订单内部单号」,用之前先核对方向。
  • 别用 `link_o_id` 推断「谁是当前有效单」:实测 55 张子单里有 6 张的「父单」状态不是 Split——因为父单在拆完之后又继续流转或被取消了。可靠判据只有 status ∈ {Merged, Split} 本身。

一张图看懂家族关系

合并与拆分:订单家族与壳单

小结:四种变化的共同点是「订单离开了主链」。取消可逆、异常可回、合并拆分不可逆但业务还在——只是搬到了另一张单上。做数据口径时,合并拆分是唯一会造成纯重复的变化,下一节展开。

四、状态迁移:哪些会发生,哪些不会

把状态间的迁移关系列全,能避免代码里写出「想当然」的路径。

会发生的迁移(按证据强度):

  • 官方原句:WaitPay → WaitConfirm(付款)、WaitConfirm → Delivering(审核)、Delivering → Sent(出库)、Question → WaitConfirm(人工转回)。
  • 实测验证:WaitConfirm → Cancelled(取消,code=0)、Cancelled → WaitConfirm(反取消)、WaitConfirm → Question(转异常)、WaitConfirm → Split(拆单)。
  • 推断(沙箱 0 命中但枚举存在):WaitConfirm → WaitFConfirm、WaitConfirm → WaitOuterSent,走完分别回 Delivering / Sent。

不会发生的迁移(别在代码里假设它们):

  • Sent → 任何状态。发货是单向的——item_status 里有「发货后取消」(SentCancelled),但整单状态没有。
  • Split / Merged → Cancelled。原单已成空壳,实测 code=130。
  • Cancelled → Delivering。反取消只回到 WaitConfirm,没有「跳回发货中」的路径。
  • Delete 不是 order.status 的取值——它属于明细状态和出库单状态,整单枚举里没有它。

还有一条容易漏的:/open/order/sent/upload 可以把任意未发货态直接推成 Sent(回传外部平台发货用)。如果你在维护状态机,这个「跳变」路径要考虑进去。

五、壳单:重复计数的根源与去重口径

合并和拆分各做过一次沙箱全量实测,结论非常干净:

  • 金额复制是精确的。拆分家族 24 个可对账家族,子单合计 ÷ 原单金额全部等于 1.0000;合并家族同理。件数也是 24/24 = 1.0000。
  • 壳单仍返回明细。36/36 张壳单照样返回 items——不只主表重复,明细级、件数级的统计同样会重复。
  • `so_id` 会串族。拆分后原单与全部子单共用同一个 so_id(实测 29 组 so_id 被 87 个 o_id 共用,极端的一个 so_id 覆盖 5 张单)。按 so_id 聚合会把一整个家族并成一行。

由此得到状态的两分法:

  • 有效状态(每张代表一笔独立业务):WaitPay、WaitConfirm、WaitFConfirm、Delivering、WaitOuterSent、Question、Sent、Cancelled——计入经营口径。Sent 是完成、Cancelled 是终止,它们不是重复。
  • 历史状态(已被后继单取代的壳):Merged、Split——剔除,仅用于溯源。

口径三原则:

  1. 唯一键 = `o_id`,so_id 只当「一族」的分组线索;
  2. 基数 = 排除 `Merged`/`Split` 后的有效单,且要防止在日期窗口边界上把家族切断(子单可能落在另一个窗口,宁可留一张壳,不要丢一笔业务);
  3. 比率必须标明分母——「有效单 / 已付款单 / 已发货单」是三个不同的分母:取消率按有效单算是 15.5%,按已付款单算是 16.1%(沙箱样本),生产上这个差距才是有意义的信号(待付款流失 ≠ 付款后取消)。

另外要区分「单据数」和「业务单数」:1 张单拆成 3 张,排除壳单后仍算 3 张单据、但只是 1 笔业务。count(distinct o_id) 回答仓储工作量,count(distinct so_id) 回答「卖了多少单」——两个都对,但同一张页面只能选一个,并写清楚。

六、数据如何记录:三套状态与表关系

一套订单报文里有三套状态,同名的 WaitConfirm 语义不同:

  • 整单 order.status:WaitConfirm = 已付款待审核;
  • 明细 items[].item_status:17 个取值,WaitConfirm = 等待审核,另有 Delete、Lock、SentCancelled 等整单没有的值(沙箱还实测出官方枚举没有的 Disabled,源于店铺「禁售」标签);
  • 出库单 status:WaitConfirm = 待出库——与整单的「已付款待审核」完全两回事。

筛选器写「已付款待审核」而表格列「待出库」,就是这么来的。前端必须固定用单一口径的状态标签函数。

落到镜像库的表关系(PostgreSQL,同步程序只写、分析只读):

  • 订单域:orders(o_id 主键,含 link_o_id、is_split、is_merge)→ order_items(o_id 外键 + sku_id);
  • 出库域:outbound_order(io_id 主键,o_id 外键)→ outbound_order_item(io_id 外键)——注意实测明细表只覆盖约 10% 的出库单,件数与动销 SKU 类指标要用前先验覆盖度;
  • 售后域:after_sale_order(as_id 主键,o_id 外键)→ after_sale_order_item;退货入库走 after_sale_received 及明细;退款金额在 `after_sale_refund`——它补上了消息推送没有金额字段的缺口,退款金额率因此可算;
  • 主数据旁挂:jst_prod.products(sku_id)、shop(shop_id)。

整单状态在 Sent 之后不再变化——发货之后的一切退款/退货,都记录在售后域的表里,order.status 全程停在 Sent。所以「这单结束了没有」不能用 status 回答,要去看售后单。

结语:地图的价值在断点

画完这张地图,最有产出的不是把流程画对了,而是暴露了三个断点:

  • 签收数据缺失:sign_time 在测试环境全空,签收率、签收时效暂时不可算——上线前要确认生产是否回写;
  • 出库明细覆盖不全:outbound_order_item 只覆盖约 10% 的出库单,依赖它的指标(动销 SKU、出库件数)暂时不可信;
  • 只有状态快照:接口返回的永远是「当前状态」,要算各阶段停留时长、待付款流失率,得自己按时间累积快照,或订阅推送落事件表。

数据地图不是画一次就完的图。每接一个分析项目,就在图上修订、加密;每发现一个断点,就在图上打一个问号。把看不见的流程变成看得见的问题清单——这才是这张图真正的用处。

数据说明:文中沙箱实测数据来自聚水潭公共测试环境(2026-09 ~ 2026-10 只读扫描),公共沙箱由全网测试者共享,状态分布(如异常单占比 70%)不代表生产环境,生产上线前需按自有数据重新验证。

No comments yet