feat(order-service): 重构分类资料收集与订单状态链路

This commit is contained in:
yml2213
2026-11-03 19:46:49 +08:00
parent 6e4758a3c7
commit 9f14e8de7a
59 changed files with 4866 additions and 1734 deletions
+3 -1
View File
@@ -1,6 +1,6 @@
# order_site 文档索引
> 最后校准:2026-10-27,代码基线 `dev`(合并 `dev1023` + 人工成本完整性修复 + 财务 P0 控制,迁移基线收敛为 `103`–`105`;**未部署生产**)。
> 最后补充: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) 为准**。
> 本文是 `docs/` 的唯一入口。**整体财务流程看 [财务流程总览-2026-11-01.md](财务流程总览-2026-11-01.md)**;数据模型看 [成本中心与履约模型.md](成本中心与履约模型.md),接口看 [成本中心API契约.md](成本中心API契约.md)。
> 新增文档必须在下表登记状态;过期文档不再直接改写结论,统一移入 `docs/archive/`。
@@ -130,6 +130,8 @@
| --- | --- | --- |
| [财务与履约口径总纲-2026-10-23.md](财务与履约口径总纲-2026-10-23.md) | **现行唯一口径** | 履约方式、成本模型、人工全覆盖、历史策略、实施顺序、运营日检与验收 SQL |
| [财务流程总览-2026-11-01.md](财务流程总览-2026-11-01.md) | 现行 | 断代后财务全流程:六条管道、成本快照、事实、总账、退款、异常、运维与测试 |
| [订单全流程与内部SKU架构.md](订单全流程与内部SKU架构.md) | 现行 | 平台入单、中文内部 SKU、渠道判定、四类履约载体、商品归一与财务落账的完整链路 |
| [订单服务收集与状态页设计-2026-11-03.md](订单服务收集与状态页设计-2026-11-03.md) | 现行 | 分类级资料配置、发布版本与工单快照、`/o` 客户状态页、自动发布核销和通知预留 |
| [成本中心与履约模型.md](成本中心与履约模型.md) | 现行 | 商品、成本版本、快照、异常、销售事实的数据模型;迁移 `103`–`105`;履约回填;迁移基线与本地库同步;索引与性能 |
| [成本中心API契约.md](成本中心API契约.md) | 现行 | 成本中心接口字段替换总表、枚举、模块边界(后端 5 模块 + 前端 4 个固定 Tab) |
| [人工支出与备用金.md](人工支出与备用金.md) | 现行(P0 已实施) | 确认履约流程/权限、客服备用金台账、本人账户与编号规则 |
+576
View File
@@ -0,0 +1,576 @@
---
type: 架构设计
description: 从平台入单、内部中文 SKU、履约渠道与载体,到商品归一、成本快照和财务事实的完整订单链路。
tags: [订单, 履约, 内部SKU, 渠道, 成本, 财务]
status: stable
---
# 订单全流程与内部 SKU 架构
> 状态:**现行**。最后校准:2026-11-03。
> 代码基线:`main` / `6e4758a3`。
> 相关文档:[履约与财务分流统一方案-2026-11-02.md](履约与财务分流统一方案-2026-11-02.md)、[财务流程总览-2026-11-01.md](财务流程总览-2026-11-01.md)、[成本中心与履约模型.md](成本中心与履约模型.md)、[接单模板匹配引擎重构设计.md](接单模板匹配引擎重构设计.md)。
## 一、核心结论
当前系统以**中文内部 SKU**作为业务路由主键:
```text
平台订单
→ 解析中文内部 SKU
→ internal_sku_channels 决定履约渠道
→ 创建真实履约载体
→ 商品归一
→ 成本快照与销售事实
→ 财务总账
```
四个概念必须分开:
| 概念 | 回答的问题 | 主要存储 |
| ------------ | ------------------ | -------------------------------------------------- |
| 中文内部 SKU | 业务上这是什么商品 | `order_items.manual_sku_key`、快照字段、多张配置表 |
| 履约渠道 | 这单由谁执行 | `internal_sku_channels` |
| 履约载体 | 实际执行对象是什么 | `work_orders` / `fulfillment_tasks` / 人工配置 |
| 标准商品 | 财务上归到哪个商品 | `product_catalog` |
一句话记忆:
> **内部 SKU 决定业务路由,标准商品决定财务归属,接单模板决定接单平台具体创建哪种工单。**
## 二、订单入口
所有来源最终汇入统一订单服务:
```text
apps/backend/src/services/order/order-service.ts
upsertOrderFromSource()
```
### 2.1 91 卡券
```text
/open-91 下单接口
→ buildOpen91SourceEvent
→ upsertOrderFromSource
```
91 负责订单骨架;快手 send-code 可补充商品与 SKU 信息。两者在业务上属于同一个 `kuaishou-91` 来源,但落库的 `orders.provider` 仍可能是 `91kaquan`。
### 2.2 抖音码速达
```text
trade_TradePaid Webhook
→ 拉取 order.orderDetail
→ buildDouyinMashidaSourceEvent
→ upsertOrderFromSource
```
随后生成电子凭证、同步平台券码、发送买家领取链接。
### 2.3 拼多多
采用两段式:
```text
PDD Cookie/MMS 拉单
→ pdd_raw_orders 原始表
→ mapPddOrderToSourceEvent
→ upsertOrderFromSource
```
原始订单与业务订单分离,便于失败重放。
## 三、统一入单与内部 SKU
### 3.1 入单主流程
```text
SourceOrderEvent
→ resolveOrderItemForFulfillment()
→ 必要时查询 internal_sku_mappings
→ resolveInternalSkuKey()
→ 写入 orders / order_items
→ 写入收入分摊
```
核心代码:
- `apps/backend/src/services/fulfillment/product-resolution-service.ts`
- `apps/backend/src/services/fulfillment/internal-sku-resolver.ts`
- `apps/backend/src/repositories/order-item-repo.ts`
### 3.2 内部 SKU 的文本规则
内部 SKU 统一规范化:
```text
NFKC
去掉全部空白
转小写
```
只有包含中文的值才能成为内部 SKU。纯数字码、英文平台 SKU 必须先经过映射。
### 3.3 `manual_sku_key` 生成规则
`order_items.manual_sku_key` 是数据库生成列,取值顺序:
```text
spec_json.skuNick
→ spec_json.outSkuSn
→ sku_code
→ sku_name
```
对应迁移:
```text
apps/backend/src/db/migrations/106_manual_sku_key.sql
```
### 3.4 平台字段映射为中文内部 SKU
当平台只提供纯数字码或非中文 SKU 时,使用:
```text
internal_sku_mappings
```
字段:
```text
provider
shop_id
match_type sku_code / sku_nick / out_sku_sn / spec / sku_name
match_value
internal_sku
priority
enabled
source
```
匹配优先级:
```text
精确 provider + shop
> provider
> shop
> 全局
```
对应页面:
```text
履约配置 → SKU 映射
```
### 3.5 内部 SKU 具体存放在哪里
`order_items` 没有单独的 `internal_sku` 列。实际分布在:
```text
order_items.manual_sku_key 生成列
order_items.spec_json.skuNick 入单时写入
order_items.item_snapshot_json.internalSku
```
同时可能写入:
```json
{
"internalSku": "5个潮汐礼券",
"internalSkuChannel": "work_order",
"classification": {
"channel": "work_order",
"canonicalSku": "5个潮汐礼券"
}
}
```
内部 SKU 也会出现在:
```text
internal_sku_mappings.internal_sku
internal_sku_channels.internal_sku
manual_sku_registry.sku_key
manual_fulfillment_configs.sku_key
```
订单进入 `assigned` 或 `archived` 后,后续平台重推不会覆盖已固化的内部 SKU 和履约路由快照。
## 四、订单状态与履约状态
必须区分四层状态:
| 层 | 字段或表 | 含义 |
| -------------- | ------------------------------------------------------ | ----------------------------------- |
| 平台交易状态 | `orders.order_status` / `orders.pay_status` | 平台订单是否创建、支付、关闭、退款 |
| 订单项路由状态 | `order_items.fulfillment_status` | `pending` / `assigned` / `archived` |
| 履约执行状态 | `work_orders.status` / `fulfillment_tasks.task_status` | 工单或自动发货任务执行到哪一步 |
| 财务状态 | `cost_snapshots` / `sales_daily_facts` / 总账 | 成本、收入事实与记账状态 |
`order_items.fulfillment_status = assigned` 只表示:
> 渠道已经确定,并且相应履约条件已具备。
它**不代表已经交付完成**。实际完成情况要看工单、任务或人工成本快照。
## 五、渠道判定
### 5.1 唯一渠道真源
```text
internal_sku_channels
```
核心字段:
```text
internal_sku
channel
priority
is_fallback
enabled
canonical_product_id
config_json
source
```
### 5.2 主渠道与兜底
```text
priority 1–89 主渠道
priority 90+ 兜底渠道
is_fallback 是否兜底
```
规则:
- 同一内部 SKU 只能有一个主渠道;
- 同优先级出现多个主渠道时判定为 `routing_conflict`;
- 主渠道无法创建载体时,才尝试显式兜底渠道。
对应页面:
```text
履约配置 → 渠道规则
```
> `product_channel_capabilities` 主要是历史迁移来源;运行时渠道判断读取的是 `internal_sku_channels`。
### 5.3 渠道载体参数
| 渠道 | `config_json` 关键字段 | 真实载体 |
| --------------------- | ---------------------- | ------------------- |
| `affiliate_dash` | `affiliateSku` | `fulfillment_tasks` |
| `work_order` | `workRuleKey` | `work_orders` |
| `manual` | `manualConfigId` | 人工履约配置 |
| `third_party_voucher` | `voucherSku` | 无履约载体,只统计 |
## 六、真实载体与最终渠道
渠道判定会同时检查真实载体:
```text
work_orders 中未取消工单
fulfillment_tasks 中有效的 affiliate_dash 任务
fulfillment_tasks 中有效的 manual_dispatch 任务
```
判定顺序:
```text
1. 运营锁定 → 跳过自动判定
2. 已有唯一真实载体 → 按载体确定渠道
3. 多个真实载体 → 冲突
4. 无载体 → 读取 internal_sku_channels
5. 主渠道创建载体失败 → 尝试显式 fallback
6. 仍失败 → 保持 pending
```
常见待处理原因:
| 原因 | 含义 |
| --------------------------- | -------------------------------------- |
| `no_internal_sku` | 没有解析出中文内部 SKU |
| `no_channel_capability` | 没有匹配的渠道配置 |
| `no_carrier` | 有渠道配置,但任务/工单/人工配置未就绪 |
| `routing_conflict` | 渠道配置冲突 |
| `manual_config_missing` | 人工渠道缺少人工履约配置 |
| `work_rule_missing` | 接单渠道缺少 `workRuleKey` |
| `affiliate_mapping_missing` | 自动发货缺少 affiliate SKU 映射 |
| `existing_carrier_conflict` | 已存在其他真实履约载体 |
核心代码:
- `apps/backend/src/services/fulfillment/channel-assign-service.ts`
- `apps/backend/src/services/fulfillment/carrier-creation-service.ts`
- `apps/backend/src/services/fulfillment/carrier-evidence-service.ts`
- `apps/backend/src/services/fulfillment/carrier-reconciliation-service.ts`
## 七、四种履约方式
### 7.1 接单平台 `work_order`
```text
内部 SKU
→ internal_sku_channels.work_order
→ config_json.workRuleKey
→ 匹配 work_product_rules
→ work_orders
→ 打手大厅
→ 抢单/接单
→ in_progress
→ 提交验收资料
→ pending_acceptance
→ accepted
```
接单模板匹配仍然依赖平台字段,例如:
- 来源、平台、店铺
- 商品 ID、SKU 编码、SKU 昵称、商品标题
- 卖家/店铺白名单
- 数量系列
因此存在两层判断:
```text
内部 SKU 渠道配置:是否走接单平台
接单模板匹配:具体创建哪一种工单
```
如果旧订单在修复前停在“无渠道能力”,可对待分类队列执行“重试接单匹配”。匹配成功后会创建工单并转为 `assigned/work_order`。
核心代码:
- `apps/backend/src/services/worker-platform/sync-worker-orders-service.ts`
- `apps/backend/src/services/worker-platform/work-product-match-mappers.ts`
- `apps/backend/src/services/fulfillment/pending-channel-repair-service.ts`
客户资料与订单进度统一通过 `/o/:platformOrderNo` 查询。分类级资料配置、
工单配置版本快照、自动发布和客户状态文案见
[订单服务收集与状态页设计-2026-11-03.md](订单服务收集与状态页设计-2026-11-03.md)。
### 7.2 自动发货 `affiliate_dash`
```text
内部 SKU
→ config_json.affiliateSku
→ fulfillment_tasks
→ affiliate-dash 执行器
→ 领取/发码/核销
```
自动发货必须同时具备:
- `internal_sku_channels.affiliate_dash`
- 可用的 affiliate SKU 映射
- 实际创建成功的 `fulfillment_tasks`
自动任务失败后,如果存在显式人工兜底,可切换为 `manual`。
### 7.3 人工客服 `manual`
```text
内部 SKU
→ manual_fulfillment_configs
→ 人工履约渠道 + 消耗积分
→ 客服确认履约
→ 扣备用金或平台积分
→ 写 manual_case 成本快照
```
保存人工 SKU 配置时会自动:
- 创建或关联 `man:<内部 SKU>` 标准商品;
- 同步 `internal_sku_channels`;
- 回填最近 30 天订单;
- 重试待处理的人工渠道订单。
对应页面:
```text
商品与成本中心 → 成本配置 → 人工客服
```
### 7.4 第三方卡券 `third_party_voucher`
```text
内部 SKU
→ internal_sku_channels.third_party_voucher
→ 不创建任务
→ 不创建工单
→ 仅参与收入、采购成本与财务统计
```
“无载体”是卡券渠道的正常状态,不属于错误。
## 八、商品归一
渠道确定后,订单项再进入商品维度:
```text
order_items.canonical_product_id
→ product_catalog
```
标准商品编码前缀:
| 前缀 | 来源 |
| ------ | ------------ |
| `aff:` | 自动发货 |
| `tpl:` | 接单模板 |
| `wo:` | 历史工单证据 |
| `tpv:` | 第三方卡券 |
| `man:` | 人工客服 |
商品归一顺序:
```text
人工配置指定的商品
→ 接单模板对应商品
→ affiliate 商品
→ tpv 商品
→ 中文内部 SKU 命中 canonical_name
→ channel_product_mappings 历史映射兜底
```
注意:
> 商品映射成功不代表渠道路由成功。
> `channel_product_mappings` 解决“财务上归到哪个商品”,`internal_sku_channels` 解决“由谁履约”。
对应页面:
```text
商品与成本中心 → 渠道映射
商品与成本中心 → 标准商品
```
## 九、财务链路
```text
channel_assign
→ carrier_dispatch
→ product_normalize
→ cost_snapshot
→ sales_facts
→ ledger_post
```
| 管道 | 作用 | 产物 |
| ------------------- | ----------------------------------- | ----------------------------------- |
| `channel_assign` | 规则 + 载体证据确定渠道 | `fulfillment_channel/status/source` |
| `carrier_dispatch` | 补人工配置、补工单/任务、发现缺载体 | 载体与异常队列 |
| `product_normalize` | 订单项关联标准商品 | `canonical_product_id` |
| `cost_snapshot` | 按渠道与成本版本计算成本 | `cost_snapshots` |
| `sales_facts` | 收入、退款、成本汇总 | `sales_daily_facts` |
| `ledger_post` | 事实转复式记账 | `finance_journal_entries` |
核心代码:
```text
apps/backend/src/services/scheduler/finance-pipeline-jobs.ts
```
## 十、配置总表
| 要解决的问题 | 后台位置 | 数据表 |
| --------------------- | -------------------------------------- | --------------------------------------------------- |
| 平台码 → 中文内部 SKU | 履约配置 → SKU 映射 | `internal_sku_mappings` |
| 内部 SKU → 履约渠道 | 履约配置 → 渠道规则 | `internal_sku_channels` |
| 平台 SKU → 财务商品 | 商品与成本中心 → 渠道映射 | `channel_product_mappings` |
| 标准商品与成本版本 | 商品与成本中心 → 标准商品 | `product_catalog` / `cost_versions` |
| 人工积分与现金成本 | 商品与成本中心 → 成本配置 → 人工客服 | `manual_fulfillment_configs` |
| 接单模板及匹配范围 | 接单平台 → 接单模板 | `work_product_rules` / `work_product_rule_mappings` |
| 自动发货 SKU 映射 | 平台配置 / 自动发货配置 | affiliate 配置 + `fulfillment_tasks` |
| 第三方卡券 | 商品与成本中心 → 成本配置 → 第三方卡券 | `tpv:` 商品 + `internal_sku_channels` |
## 十一、常见误区
### 11.1 “商品映射有了,为什么还是待分类?”
因为商品映射和渠道路由是两套配置:
```text
平台 SKU → 财务商品 channel_product_mappings
内部 SKU → 履约渠道 internal_sku_channels
```
两者不能互相替代。
### 11.2 “接单模板存在,为什么没有自动建工单?”
还需要满足:
- 内部 SKU 已配置为 `work_order`,或能通过模板自动补渠道;
- 模板的店铺/卖家范围、SKU 或名称条件实际命中;
- 模板处于启用状态;
- 自动建单开关允许自动创建。
### 11.3 “内部 SKU 相似,为什么没有复用配置?”
内部 SKU 按规范化文本精确匹配。例如:
```text
银月精灵/星谷精灵
(2选1)银月精灵/星谷精灵
银月精灵/星谷精灵【2选1】
```
在当前模型中仍是三个不同键,除非通过映射统一。
### 11.4 “待分类行能不能直接删除?”
待分类队列来自真实订单项,不建议提供直接删除。正确方式是修复配置并重新分流:
- 修 `internal_sku_mappings`
- 修 `internal_sku_channels`
- 修接单模板匹配范围
- 修人工履约配置
- 点击“重试接单匹配”
- 执行财务/履约管道
直接删除会掩盖未履约收入和财务缺口。
## 十二、关键表速查
| 表 | 职责 |
| ---------------------------- | ----------------------------------------------- |
| `orders` | 平台订单交易头 |
| `order_items` | 商品明细、收入、内部 SKU 派生字段、履约路由结果 |
| `internal_sku_mappings` | 平台字段 → 中文内部 SKU |
| `internal_sku_channels` | 中文内部 SKU → 履约渠道、优先级、兜底、载体参数 |
| `manual_sku_registry` | 扫描到的人工履约 SKU 清单 |
| `manual_fulfillment_configs` | 人工履约渠道、积分和成本模式 |
| `work_product_rules` | 接单模板 |
| `work_product_rule_mappings` | 接单模板匹配范围 |
| `work_orders` | 接单平台真实履约载体 |
| `fulfillment_tasks` | 自动发货等执行任务 |
| `product_catalog` | 标准商品 |
| `channel_product_mappings` | 平台 SKU → 标准商品 |
| `cost_versions` | 成本金额版本 |
| `cost_snapshots` | 订单项成本事实 |
| `sales_daily_facts` | 按日期、店铺、商品、渠道汇总的销售事实 |
| `finance_journal_entries` | 财务总账凭证 |
## 十三、关键代码入口
| 文件 | 职责 |
| -------------------------------------------------------------------------- | ----------------------------------------------- |
| `apps/backend/src/services/order/order-service.ts` | 所有来源统一订单落库与消费者分流 |
| `apps/backend/src/services/fulfillment/product-resolution-service.ts` | 平台字段归一、内部 SKU 解析、affiliate SKU 映射 |
| `apps/backend/src/services/fulfillment/internal-sku-resolver.ts` | 内部 SKU 文本规范化与 TS 解析口径 |
| `apps/backend/src/services/fulfillment/internal-sku-mapping-service.ts` | 平台字段 → 内部 SKU 映射 |
| `apps/backend/src/services/fulfillment/intake-channel-decision-service.ts` | 入单时应用内部 SKU 渠道计划 |
| `apps/backend/src/services/fulfillment/internal-sku-channel-service.ts` | 读取、规划、写入内部 SKU 渠道 |
| `apps/backend/src/services/fulfillment/channel-assign-service.ts` | 基于规则与真实载体确定最终渠道 |
| `apps/backend/src/services/fulfillment/carrier-creation-service.ts` | 幂等创建工单/任务/确认人工配置 |
| `apps/backend/src/services/worker-platform/sync-worker-orders-service.ts` | 接单模板匹配与工单创建 |
| `apps/backend/src/services/order/delivery-task-service.ts` | 自动发货等履约任务创建 |
| `apps/backend/src/services/finance/manual-fulfillment-service.ts` | 人工确认履约、扣款与成本快照 |
| `apps/backend/src/services/cost-center/product-normalize-service.ts` | 订单项 → 标准商品 |
| `apps/backend/src/services/scheduler/finance-pipeline-jobs.ts` | 渠道、载体、商品、成本、事实、总账管道 |
| `apps/backend/src/services/fulfillment/pending-channel-repair-service.ts` | 待分类订单重试接单模板匹配 |
@@ -0,0 +1,204 @@
---
type: 架构设计
description: 分类级订单服务配置、配置版本快照、/o 客户资料与订单状态页、自动发布及短信通知预留。
tags: [订单服务, 资料收集, 工单, 自动发布, 通知]
status: stable
---
# 订单服务收集与状态页设计
> 状态:现行。最后校准:2026-11-03。代码分支:`dev_11.3`。
## 1. 目标
旧的 `/collect`、`/supplement` 和通用资料收集方式已经下线。新方案统一使用:
```text
/o 输入订单号
/o/:platformOrderNo 查询资料与订单进度
/api/v1/o/* 公开 API
```
一个接单分类对应一套订单服务配置。接单模板继续通过 `work_product_rules.category_id` 绑定分类,同一分类下的所有模板复用该分类的客户资料配置。
## 2. 配置真源
### 2.1 草稿与发布版本
配置表:
```text
order_service_profiles
order_service_profile_versions
```
- 后台保存只修改 profile 草稿。
- 点击“发布新版本”后,将当前草稿写入不可变版本。
- 新工单只读取 `current_version_id` 对应的版本 JSON。
- 已发布后继续编辑会把 profile 标记为 `draft`,但线上继续使用上一个已发布版本。
- 归档只阻止新工单再选择该配置,不影响已有工单快照。
### 2.2 分类与接单模板
不维护“分类默认接单模板”这一反向字段。分类关系以接单模板自己的 `category_id` 为准:
```text
订单项 → 接单模板 → category_id → 当前发布的订单服务版本
```
这样不会出现“先确定分类才能选默认模板、但分类又来自模板”的循环依赖。
## 3. 工单快照
建工单时在同一业务步骤内固化:
```text
work_orders.service_profile_id
work_orders.service_profile_version_id
work_orders.service_profile_snapshot_json
```
快照包含:
```json
{
"profileId": 1,
"categoryId": 1,
"versionId": 2,
"versionNo": 2,
"config": {}
}
```
工单创建时,如果分类存在已发布订单服务配置:
- `requirement_json` 从快照字段生成。
- 存在必填字段或必填截图时,工单进入 `pending_material`。
- 所有字段选填且截图不要求时,工单直接进入 `unassigned`。
- 后续修改配置不会影响该工单。
历史工单没有快照时继续读取其 `requirement_json`,不会突然切换到新分类配置。
## 4. 客户页面
`/o` 支持:
- 按平台订单号查询一单多工单。
- 动态文本、长文本、下拉、单选、数字、日期字段。
- 一张分类引导图。
- 图片截图规则,必填时最少 1 张。
- 提交后禁止再次修改。
- 每 15 秒、页面重新可见或窗口重新聚焦时刷新状态。
客户状态文案:
| 状态 | 文案方向 |
| --- | --- |
| `pending_material` | 请补充资料 |
| `unassigned` | 已提交,等待客服安排 |
| `open` | 已发布大厅,高峰期请耐心等待 |
| `in_progress` | 已接单,30–60 分钟内检查邮箱 |
| `pending_acceptance` | 交付已提交,正在确认 |
| `accepted` | 已完成,检查邮件并联系客服处理问题 |
| `problem` | 客服处理中 |
| `cancelled` | 订单已取消 |
## 5. 文件上传
公开上传入口:
```text
POST /api/v1/o/:platformOrderNo/files?workOrderId=...
```
上传文件记录归属:
```text
uploader_type = collect
uploader_id = work-order-<id>
scene = collect-material
```
最终提交时逐个校验 `file_assets`:
- 文件存在且状态为 active。
- 上传者类型和工单 ID 一致。
- scene 必须为 `collect-material`。
- 客户截图只接受 JPG、PNG、WebP。
客户端上传返回的文件对象不会被直接信任。
## 6. 自动发布与核销
开启自动发布后,客户提交资料与自动发布任务在同一数据库事务内完成。
任务表:
```text
order_service_auto_publish_jobs
```
任务状态:
```text
pending → processing → succeeded
↘ pending(可重试)
↘ failed
pending/processing → skipped
```
并发安全:
- `pending + processing` 对同一工单唯一。
- worker 使用一条 `UPDATE ... FROM (SELECT ... FOR UPDATE SKIP LOCKED)` 原子领取。
- `locked_until` 作为租约,进程中断后可被重新领取。
核销规则:
- `requireVoucherConsume=true`:核销失败时不发布到大厅,任务进入重试。
- `requireVoucherConsume=false`:核销尽力执行,失败不阻止发布。
- `retryMax` 转为任务总尝试次数 `retryMax + 1`。
- 后台展示失败原因并允许重置任务重试。
## 7. 通知预留
通知表:
```text
customer_order_notifications
```
首版只记录事件,不实际发送短信。当前接入:
- `material_submitted`
- `work_order_published`
后续接短信时消费 pending 通知即可;用户状态提示不依赖短信。
## 8. 已下线
- `/collect` 页面和 hash 入口。
- `/api/v1/collect` 路由。
- `worker-collect-service`。
- 前端旧 collect API 封装和类型。
- `/supplement` 只保留重定向到 `/o`。
## 9. 本地验证
```bash
cd apps/backend
DATABASE_URL='postgres://postgres:postgres@127.0.0.1:5432/order_site' npm run db:migrate
ORDER_SERVICE_SMOKE=1 \
DATABASE_URL='postgres://postgres:postgres@127.0.0.1:5432/order_site' \
npm exec tsx scripts/order-service-smoke.ts
```
烟测覆盖:
1. 草稿发布与版本读取。
2. 工单配置快照。
3. 客户资料提交。
4. 禁止重复提交。
5. 自动发布任务处理。
6. 发布后客户页显示“高峰期”状态。