给销售业务画一张数据地图,最难的不是罗列状态,而是看清订单在流转中如何「变形」:什么时候会被取消?什么时候会被合并、拆分?哪些状态是有效的业务事实,哪些只是被取代的旧壳?
本文以聚水潭 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——剔除,仅用于溯源。
口径三原则:
- 唯一键 = `o_id`,
so_id只当「一族」的分组线索; - 基数 = 排除 `Merged`/`Split` 后的有效单,且要防止在日期窗口边界上把家族切断(子单可能落在另一个窗口,宁可留一张壳,不要丢一笔业务);
- 比率必须标明分母——「有效单 / 已付款单 / 已发货单」是三个不同的分母:取消率按有效单算是 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