feat(fulfillment): 建立履约统一状态视图与分类队列

This commit is contained in:
yml2213
2026-11-05 20:13:34 +08:00
parent e8b76b3b73
commit ed450fac79
12 changed files with 1783 additions and 179 deletions
+3 -2
View File
@@ -1,7 +1,7 @@
# order_site 文档索引
> 最后补充:2026-11-03,新增《订单全流程与内部 SKU 架构》,代码基线 `main` / `6e4758a3`。
> 数据基线:生产 dump `backups/postgres/order_site-20261022-092601.dump`(数据 2026-07-10 ~ 2026-10-22);本地已校准到 105(103 财务基础、104 财务控制/L1、105 拼单/寄卖客服),生产按收敛后的 102 → 105 路径执行。历史全量数字仅作基线,**上线验收以 [财务上线初始化与验收-2026-10-26.md](财务上线初始化与验收-2026-10-26.md) 为准**。
> 最后补充:2026-11-05,新增《履约系统架构复盘与优化方案》,代码基线 `e8b76b3b`,生产数据库基线迁移 `136`。
> 数据基线:生产 dump `backups/postgres/order_site-20261022-092601.dump`(数据 2026-07-10 ~ 2026-10-22);历史全量数字仅作基线,**上线验收以 [财务上线初始化与验收-2026-10-26.md](财务上线初始化与验收-2026-10-26.md) 为准**。
> 本文是 `docs/` 的唯一入口。**整体财务流程看 [财务流程总览-2026-11-01.md](财务流程总览-2026-11-01.md)**;数据模型看 [成本中心与履约模型.md](成本中心与履约模型.md),接口看 [成本中心API契约.md](成本中心API契约.md)。
> 新增文档必须在下表登记状态;过期文档不再直接改写结论,统一移入 `docs/archive/`。
@@ -131,6 +131,7 @@
| [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md) | **现行唯一口径** | 履约方式、成本模型、人工全覆盖、历史策略、实施顺序、运营日检与验收 SQL |
| [财务流程总览-2026-11-01.md](财务流程总览-2026-11-01.md) | 现行 | 断代后财务全流程:六条管道、成本快照、事实、总账、退款、异常、运维与测试 |
| [订单全流程与内部SKU架构.md](订单全流程与内部SKU架构.md) | 现行 | 平台入单、中文内部 SKU、渠道判定、四类履约载体、商品归一与财务落账的完整链路 |
| [履约系统架构复盘与优化方案-2026-11-05.md](履约系统架构复盘与优化方案-2026-11-05.md) | 方案 | 履约状态、载体、尝试、异常队列与后台信息架构的分阶段重构方案 |
| [订单服务收集与状态页设计-2026-11-03.md](订单服务收集与状态页设计-2026-11-03.md) | 现行 | 分类级资料配置、发布版本与工单快照、`/o` 客户状态页、自动发布核销和通知预留 |
| [成本中心与履约模型.md](成本中心与履约模型.md) | 现行 | 商品、成本版本、快照、异常、销售事实的数据模型;迁移 `103`–`105`;履约回填;迁移基线与本地库同步;索引与性能 |
| [成本中心API契约.md](成本中心API契约.md) | 现行 | 成本中心接口字段替换总表、枚举、模块边界(后端 5 模块 + 前端 4 个固定 Tab) |
@@ -0,0 +1,741 @@
---
type: 架构优化方案
description: 全面复盘订单履约链路、后台队列、任务载体和财务边界,并给出分阶段优化方案。
tags: [订单, 履约, 状态机, 后台队列, 架构优化]
status: draft
---
# 履约系统架构复盘与优化方案
> 状态:**方案**,尚未实施。
> 最后校准:2026-11-05。
> 代码基线:`e8b76b3b`。
> 数据库基线:迁移 `136`。
> 生产核对方式:只读 SQL、后台页面、现行文档与源码交叉检查。
## 实施进度
2026-11-05 已在 `dev_11.5_2` 开始阶段 0 和阶段 1:
- 已完成:迁移 `137` 新增 `v_fulfillment_item_state` 统一读模型。
- 已完成:履约问题按 `classification / configuration / carrier / manual_review / after_sales / system` 分类。
- 已完成:待分类查询返回每类问题的订单项数量,而不是只返回原因集合。
- 已完成:保存渠道、补载体、确认重试、解除锁定和归档按当前问题分类限定作用域。
- 已完成:新增 `npm run audit:fulfillment` 只读审计脚本。
- 待继续:正式 `fulfillments / attempts / issues / events` 表、统一写入口、独立售后队列和生产存量修复。
## 一、结论
这套系统并不需要推倒重写,但不能继续靠新增字段、JSON 原因和页面按钮修补。
当前最根本的问题是:业务已经从“单渠道自动发货”演变为“多渠道履约 + 众包工单 + 人工客服 + 三方卡券 + 电子凭证 + 售后 + 财务”,底层却仍用一套为早期单渠道设计的状态表达所有问题。
优化方向必须是:
```text
订单商业状态
≠ 履约路由状态
≠ 载体状态
≠ 执行任务状态
≠ 异常状态
≠ 操作锁定
≠ 财务状态
```
下一阶段的核心目标不是增加功能,而是建立六个不变式:
1. 每笔订单项都有清晰、可计算的履约状态。
2. `assigned` 必须意味着载体已具备,不能只意味着渠道已指定。
3. 每个订单项单元最多一个当前履约尝试,历史尝试只追加。
4. 所有待办必须有结构化异常代码、数量、责任人和允许动作。
5. 退款、取消、人工复核、配置缺失不能继续共用一个“无载体”状态。
6. 所有后台动作的作用域必须与页面展示的作用域完全一致。
## 二、项目规模与现状证据
本次盘点基线:
| 项目 | 数量 |
| --- | ---: |
| 后端 TypeScript 文件 | 620 |
| 前端 TSX 文件 | 116 |
| 数据库迁移 | 136 |
| 文档 | 149 |
| 本地业务表 | 97 |
| 本地视图 | 3 |
| 索引 | 436 |
| 约束 | 421 |
| 后端测试文件 | 161 |
| 真 PostgreSQL 集成测试文件 | 5 |
生产履约数据的关键事实:
- 待分类页面显示 15 个 SKU,实际对应 40 个订单项。
- 5 个 SKU 组混合了两种及以上互斥问题。
- 2 个组同时包含“用户退款/上游取消”和“可重试关闭任务”。
- 1 个组同时包含“人工复核”和“可重试关闭任务”。
- 1 个组中部分订单锁定、部分未锁定。
- 84 个订单项为 `assigned=接单平台`,但没有任何未取消工单。
- 81 个订单项为 `assigned=自动发货`,但没有有效自动发货任务。
- 当前仍存在 5 个订单项、10 条重复任务尝试。
这说明页面问题不是单纯的 UI 排版问题,而是底层状态与动作语义没有收敛。
## 三、系统领域地图
当前系统实际包含以下业务域:
```text
订单接入
├─ 91 / 快手
├─ PDD
└─ 抖音码速达
订单归一
├─ 中文内部 SKU
├─ 标准商品
└─ 平台 SKU 映射
履约
├─ 渠道规则
├─ 自动发货任务
├─ 接单平台工单
├─ 人工客服
├─ 三方卡券
├─ 统一结算
└─ 电子凭证核销
打手众包
├─ 模板匹配
├─ 发布、抢单、指派
├─ 资料、验收、售后
└─ 钱包与奖励
寄卖仓库
├─ 提报、审核、库存
├─ 内部现货匹配
└─ 押金、结算、撤销
财务
├─ 成本中心
├─ 成本快照
├─ 销售事实
├─ 退款
├─ 平台对账
└─ 总账与关账
后台
├─ 待办与异常
├─ 用户与权限
├─ 平台配置
└─ 审计与实时事件
```
履约不是一条链,而是订单、执行、售后和财务之间的交叉层。它必须拥有自己的领域模型,不能继续寄生在订单项和通用任务表上。
## 四、当前主要问题
### P0-1:`assigned` 语义失真
当前 `order_items.fulfillment_status` 只有:
```text
pending / assigned / archived
```
文档期望 `assigned` 表示“渠道确定且履约条件具备”,生产实际存在大量 `assigned` 但没有有效载体的数据。
后果:
- 待分类页看不到 `assigned` 的异常。
- 后台统计把订单算作已分配。
- 自动修复任务需要额外扫描“已分配但缺载体”。
- 用户无法仅凭订单状态判断真实履约阶段。
### P0-2:异常状态存在自由 JSON 中
`fulfillment_pending_detail` 混合保存:
- 缺内部 SKU。
- 缺渠道。
- 缺人工配置。
- 缺接单模板。
- 缺自动发货映射。
- 任务关闭。
- 任务失败。
- 人工复核。
- 用户退款。
- 上游取消。
后果:
- 无法可靠统计。
- 历史原因容易过期。
- 同一 SKU 的不同订单项原因被 `DISTINCT` 压平。
- UI 只能显示“发生过什么”,无法显示“每个问题多少单”。
### P0-3:待分类按 SKU 聚合,动作却需要按订单项执行
页面一行代表一个内部 SKU,但行内可能同时包含:
- 退款订单。
- 可重试关闭订单。
- 人工复核订单。
- 缺配置订单。
- 锁定订单。
后果:
- 一个按钮无法准确表达动作范围。
- 用户不知道会处理哪些订单。
- 前端只能根据“是否存在某种原因”决定是否显示按钮。
- 后端即使按规则跳过部分订单,页面结果也会显得不可预测。
### P0-4:任务同时承担当前载体和历史尝试
`fulfillment_tasks` 既是当前执行对象,又是履约尝试历史。
后果:
- 同一订单项出现两条任务时,页面看起来像两个订单。
- 自动重建任务时容易绕过“运营确认”。
- 当前任务只能通过最大 ID 推断。
- 任务状态关闭后,订单项仍可能保持 `assigned`。
### P0-5:退款和履约异常没有彻底分开
生产存在:
- `USER_APPLY_REFUND` 任务已关闭,但订单项仍在履约队列。
- `affiliate_dash_cancelled` 任务已关闭,但订单仍显示自动发货。
- 退款订单与可重试任务混合在同一个 SKU 组。
退款、取消应该进入售后或商业订单终态,而不是继续留在“无履约载体”。
### P1-1:多个服务直接写履约字段
`order_items` 的履约字段至少被以下链路修改:
- 入单服务。
- 渠道判定。
- 载体补建。
- 自动发货回退。
- 人工配置。
- 任务同步。
- 财务退款。
- 后台人工操作。
缺少唯一 `FulfillmentStateService` 作为写入口,状态一致性的证明成本很高。
### P1-2:状态、锁定和原因过载
- `fulfillment_status` 同时承担分类、分配和归档。
- `fulfillment_locked` 同时表示运营指定、自动暂停、配置未完成。
- `fulfillment_pending_reason` 同时表示路由、配置、载体和售后。
### P1-3:后台信息架构不匹配业务
当前“履约渠道”放在财务菜单下,但实际使用者主要是运营、客服和支持。
用户需要在以下页面之间来回跳:
- 商品与成本。
- 履约渠道。
- 任务。
- 接单平台。
- 订单。
- 电子凭证。
- 财务。
没有一个统一的“履约中心”告诉用户:
- 当前有多少订单未分类。
- 有多少缺配置。
- 有多少缺载体。
- 有多少人工复核。
- 有多少退款待处理。
- 哪些自动恢复失败。
### P1-4:文档已经开始漂移
现有多篇文档的状态描述不完全一致:
- `docs/README.md` 仍写旧迁移基线。
- `订单全流程与内部SKU架构.md` 假定 `assigned` 代表载体已具备。
- `成本中心与履约模型.md` 含历史商品数量。
- 多份阶段方案仍被登记为现行,但代码已继续演进。
文档不是无效,而是缺少“单一口径 + 历史归档”的强约束。
### P2-1:超大文件和直接 SQL 过多
当前存在多个超大文件:
- `WorkOrdersPanel.tsx` 4,667 行。
- `channel-assign-service.ts` 2,217 行。
- `product-catalog-service.ts` 2,120 行。
- `AdminTaskDetailPage.tsx` 2,017 行。
这些文件本身不是错误,但它们通常意味着:
- 多个用例共享一个大服务。
- 查询、状态转换、展示逻辑耦合。
- 修改一个动作时容易影响另一个动作。
### P2-2:集成测试比例过低
161 个后端测试文件中只有 5 个真实 PostgreSQL 集成测试。
大量测试是纯函数或源码断言,这能防止低级回归,但不能证明:
- 多个服务并发写状态正确。
- SQL 与约束一致。
- 调度任务重复执行安全。
- 页面动作和后端作用域一致。
## 五、目标领域模型
### 5.1 五层状态
建议分成五个正交维度:
| 层 | 含义 | 示例 |
| --- | --- | --- |
| 商业订单状态 | 是否支付、退款、取消 | `paid / refunding / refunded / cancelled` |
| 履约路由状态 | 是否已决定渠道 | `unrouted / configured` |
| 载体状态 | 是否有实际执行对象 | `not_required / missing / creating / ready / failed / terminal` |
| 执行状态 | 当前尝试进行到哪里 | `created / submitted / executing / succeeded / failed / cancelled` |
| 异常状态 | 当前阻塞是什么 | `open / acknowledged / waiting / resolved` |
不要继续增加一个无限扩张的 `fulfillment_status` 枚举。
### 5.2 核心对象
推荐长期模型:
```text
orders
└─ order_items
└─ fulfillments
├─ internal_sku
├─ channel
├─ route_status
├─ carrier_status
├─ current_attempt_id
├─ lock_reason
└─ issues
└─ attempts
└─ events
```
#### Fulfillment
一个订单项对应一个履约单。若未来支持拆包裹、拆批次,可扩展为一对多。
建议字段:
```text
id
order_item_id
internal_sku
channel
route_status
carrier_status
current_attempt_id
lock_reason
created_at
updated_at
```
#### Attempt
每次真实执行尝试单独一行,只追加,不覆盖:
```text
id
fulfillment_id
attempt_no
attempt_type
external_ref
status
result_code
result_message
started_at
finished_at
superseded_by_attempt_id
context_json
```
任务表、工单表可继续作为不同执行器的专用表,但必须有统一映射到 Attempt 的方式。
#### Issue
异常独立于履约主状态:
```text
id
fulfillment_id
attempt_id
issue_code
severity
source
retryable
owner_role
status
first_seen_at
last_seen_at
resolved_at
detail_json
```
#### Event
所有关键状态变化写追加式事件:
```text
fulfillment_id
attempt_id
event_type
from_state
to_state
actor_type
actor_id
payload_json
created_at
```
### 5.3 标准 IssueCode
建议先固定以下异常代码:
```text
missing_internal_sku
missing_channel
manual_config_missing
work_rule_missing
affiliate_mapping_missing
carrier_missing
carrier_create_failed
carrier_closed
carrier_manual_review
carrier_retry_pending
user_refund
platform_cancelled
routing_conflict
state_inconsistent
```
每个 `IssueCode` 必须定义:
- 是否可自动处理。
- 是否允许重试。
- 由哪个角色处理。
- 是否阻断财务成本。
- 是否阻断关账。
- 允许的 UI 动作。
## 六、目标后台信息架构
建议从“履约渠道”升级为统一的“履约中心”:
```text
履约中心
├─ 总览
├─ 未分类
├─ 配置待补
├─ 载体待处理
├─ 人工复核
├─ 售后/退款
├─ 系统异常
├─ 渠道规则
└─ SKU 映射
```
### 总览
展示:
- 待分类订单项数。
- 缺配置数。
- 缺载体数。
- 人工复核数。
- 退款/取消数。
- 系统状态不一致数。
- 最老未处理时长。
- 自动恢复成功率。
### 行级信息
每个订单项至少展示:
```text
内部 SKU
目标渠道
当前载体
当前尝试
异常代码
异常持续时间
是否锁定
责任人
允许动作
```
### 动作原则
1. 混合组不提供一键执行动作,只提供“进入明细”。
2. 单组只有一种异常且作用域完全一致时,才允许批量动作。
3. 所有批量动作先展示影响数量和订单项范围。
4. 退款、取消、人工复核永远不能和普通“补载体”共用按钮。
5. 运营锁定、解除锁定必须记录原因和操作者。
## 七、优化路线
### 阶段 0:止血与口径统一
周期:1 至 2 天。
目标:
- 不再让错误动作出现在混合组。
- 阻止退款/取消订单被重新发货。
- 建立生产只读审计 SQL。
- 统一文档现行口径。
工作项:
1. 混合组存在不可重试异常时,隐藏整组“确认重试”。
2. 每个异常显示准确订单项数量,而不是只显示原因集合。
3. 退款、取消进入独立售后分类。
4. 文档索引标注旧文档状态,停止同时引用冲突口径。
5. 增加生产审计脚本:
- `assigned` 但无载体。
- pending 但无结构化异常。
- 同一订单项多个当前尝试。
- 退款订单仍在履约队列。
验收:
- 混合组不再出现作用域不确定的批量动作。
- 退款、取消订单自动重试次数为 0。
- 所有现行文档基线与迁移号一致。
### 阶段 1:统一读模型
周期:3 至 5 天。
目标:先不改核心表,建立一个每个订单项一行的权威读模型。
新增视图:
```text
v_fulfillment_item_state
```
建议字段:
```text
order_item_id
order_id
platform_order_id
internal_sku
channel
route_status
carrier_status
current_attempt_id
current_attempt_type
current_attempt_status
issue_code
issue_count
retryable
locked
lock_reason
owner_role
first_issue_at
last_issue_at
age_minutes
```
后台所有履约列表、异常统计、自动修复扫描都先改为读取该视图。
验收:
- 待分类页不再直接拼接 `pending_detail`。
- 同一订单项只出现一次。
- 每个页面行都有唯一 `issue_code`。
- 生产异常口径与视图统计一致。
### 阶段 2:收敛写入口
周期:1 至 2 周。
目标:建立唯一履约状态写入口。
新增服务:
```text
FulfillmentStateService
```
负责:
- 设置渠道。
- 创建当前尝试。
- 更新载体状态。
- 打开和关闭异常。
- 加锁和解锁。
- 退款/取消终态转换。
- 写履约事件。
其他服务只能调用该服务,不再直接更新履约字段。
建议先覆盖:
1. `channel-assign-service`
2. `carrier-creation-service`
3. `carrier-reconciliation-service`
4. `manual-fulfillment-config-service`
5. 退款服务
6. 后台任务动作
验收:
- 生产代码中只有 `FulfillmentStateService` 可以更新履约核心字段。
- 所有状态变化都有事件。
- 自动任务重复执行不会产生第二个活跃尝试。
### 阶段 3:拆分队列与动作
周期:1 周,可与阶段 2 并行。
工作项:
- 待分类只保留“缺内部 SKU / 缺渠道”。
- 缺配置进入配置待补。
- 缺任务/工单进入载体待处理。
- 人工复核进入人工队列。
- 退款、取消进入售后队列。
- `assigned` 但无载体进入系统异常。
验收:
- 每类队列只包含一种主问题。
- 每类队列有明确责任角色。
- 页面按钮与后端动作作用域一致。
- 待分类数量不再包含售后和配置异常。
### 阶段 4:数据模型升级
周期:2 至 4 周。
目标:将读模型和写入口稳定后,再引入正式表。
工作项:
1. 新增 `fulfillments`。
2. 新增 `fulfillment_attempts`。
3. 新增 `fulfillment_issues`。
4. 新增 `fulfillment_events`。
5. 从现有订单项、任务、工单回填。
6. 建立双写和差异核对。
7. 读路径切换。
8. 停止旧字段写入。
9. 最后归档旧字段和兼容代码。
禁止一次性切换后立即删除旧字段。必须经过:
```text
回填
→ 双写
→ 读对比
→ 以新模型为准
→ 停止旧写
→ 观察
→ 删除旧逻辑
```
### 阶段 5:工程治理
周期:持续。
工作项:
- 拆分超过 1,500 行的页面和核心服务。
- 所有状态转换使用领域状态机。
- 关键跨表流程增加 PostgreSQL 集成测试。
- 调度任务统一采用幂等键和租约。
- 文档按“现行 / 方案 / 历史”强制归档。
- 建立生产数据质量日报。
## 八、优先级
| 优先级 | 事项 | 目标 |
| --- | --- | --- |
| P0 | 混合组动作收敛 | 防止错误重试和跨范围操作 |
| P0 | 退款/取消隔离 | 商业终态不再进入履约队列 |
| P0 | 统一读模型 | 让页面和统计使用同一事实 |
| P1 | 履约写入口 | 消除多服务直接改状态 |
| P1 | 队列拆分 | 让不同角色处理不同类型问题 |
| P1 | 当前尝试指针 | 历史尝试不再看起来像当前任务 |
| P2 | 正式履约表 | 建立长期可扩展模型 |
| P2 | 文档与测试治理 | 防止再次漂移 |
## 九、验收指标
上线后应持续满足:
```text
assigned 且无有效载体,超过 15 分钟:0
退款/取消订单出现在履约待办:0
同一订单项单元多个活跃尝试:0
pending 订单无结构化 issue_code:0
后台动作作用域与页面展示不一致:0
未记录事件的履约状态变化:0
混合异常组出现整组重试按钮:0
```
运营指标:
```text
待分类平均处理时长
配置待补平均处理时长
人工复核平均处理时长
售后关闭平均处理时长
自动恢复成功率
人工干预率
```
## 十、明确不做
1. 不做一次性的“大重写”。
2. 不继续增加通用 `pending_reason` 字符串。
3. 不把所有状态合并成一个超长枚举。
4. 不允许按 SKU 对混合异常整组执行危险动作。
5. 不自动重开关闭、退款、取消或人工复核任务。
6. 不在没有读模型和双写验证的情况下直接删除旧字段。
7. 不只修 UI 文案而不修状态来源。
8. 不把历史任务直接删除,必须保留审计链。
## 十一、建议的第一批任务
第一批只做低风险、高收益工作:
1. 增加 `v_fulfillment_item_state`。
2. 待分类页改为读该视图。
3. 混合组隐藏批量重试。
4. 每个异常显示订单项数量。
5. 退款、取消进入独立队列。
6. 增加 4 个生产审计 SQL 和日报。
7. 修正文档索引和现行基线。
这批完成后再决定是否进入 `fulfillments/attempts/issues` 正式表改造。
## 十二、待拍板问题
1. 一个订单项是否永远只有一个履约单,还是未来允许拆包裹、拆批次。
2. 接单平台、自动发货和人工是否都统一映射为 Attempt。
3. 三方卡券和统一结算是否需要“零载体尝试”记录。
4. 退款、取消由平台状态自动驱动,还是允许运营手工确认。
5. 哪类 Issue 可以自动重试,哪类必须人工确认。
6. 各异常的责任角色是运营、客服、财务还是系统自动。
7. 财务关账是否必须等待所有履约异常关闭。