feat: 完善履约路由、订单服务与自动核销

This commit is contained in:
yml2213
2026-11-05 14:59:58 +08:00
parent 517d763f17
commit 1df88cc639
54 changed files with 3166 additions and 318 deletions
@@ -0,0 +1,70 @@
# 履约渠道重试与大小写归一修复
日期:2026-11-05
## 修复范围
1. `fulfillment_tasks` 的历史 `closed / failed / manual_review / retry_pending / expired /
cancelled` 记录不再占用唯一履约单元;`work_orders` 的 `cancelled` 记录也不再占用订单项
唯一键。旧记录保留,同时允许创建新的活跃履约尝试。
2. 自动补载体改为独立水位 `carrier_reconcile`,每轮从上次位置继续,不再重复扫描最早 200 条。
3. `closed / cancelled / manual_review` 载体不再被自动重试,必须先由运营在待分类队列点击
“补载体”确认,避免系统擅自重开已经人工关闭的任务。
4. `重试接单匹配` 只处理明确配置了 `work_order` 渠道的 SKU,禁止自动发货或人工 SKU 被误分流。
5. 待分类队列显示 `pending_subreasons`、已配置渠道和运营锁定数量,不再把所有情况都显示成
“无履约载体”。
6. 迁移 129 回填内部 SKU 的大小写、全半角、空白规范化结果,并合并规范化后重复的渠道配置。
## 大小写口径
内部 SKU 统一使用:
```text
NFKC -> 删除全部空白 -> 转小写
```
因此:
```text
黑色ROCK长袖T恤
黑色rock长袖t恤
```
会归一到同一个渠道配置键。平台 SKU 和内部 SKU 的精确/模糊查询都继续使用该口径。
## 部署后核对
部署生产时把 `PUBLISHED_MIGRATION_MAX_VERSION` 提升到 `129`。
```sql
SELECT pipeline, last_item_id, last_success_at, last_error
FROM finance_watermarks
WHERE pipeline IN ('carrier_reconcile', 'carrier_dispatch', 'channel_assign');
SELECT count(*) AS uppercase_internal_skus
FROM internal_sku_channels
WHERE internal_sku ~ '[A-Z]';
SELECT canonicalize_internal_sku(internal_sku), channel, count(*)
FROM internal_sku_channels
WHERE enabled
GROUP BY 1, 2
HAVING count(*) > 1;
SELECT
fulfillment_pending_reason,
fulfillment_pending_detail->>'carrierIssue' AS carrier_issue,
count(*)
FROM order_items
WHERE fulfillment_status = 'pending'
GROUP BY 1, 2
ORDER BY 3 DESC;
```
预期:
- `uppercase_internal_skus = 0`;
- 规范化后的渠道重复数 = 0;
- `carrier_reconcile` 持续更新;
- `carrier_task_closed` 只能通过运营点击“补载体”处理;
- `no_channel_capability` 仍需补齐商品与渠道配置,不能靠重试载体解决。
@@ -7,7 +7,7 @@ status: stable
# 订单服务收集与状态页设计
> 状态:现行。最后校准:2026-11-04。代码分支:`dev_11.3`。
> 状态:现行。最后校准:2026-11-05。代码分支:`dev_11.3`。
## 1. 目标
@@ -19,7 +19,7 @@ status: stable
/api/v1/o/* 公开 API
```
一个接单分类对应一套订单服务配置。接单模板继续通过 `work_product_rules.category_id` 绑定分类,同一分类下的所有模板复用该分类的客户资料配置。
一个接单分类对应一套订单服务配置。接单模板继续通过 `work_product_rules.category_id` 绑定分类,同一分类下的所有模板复用该分类的客户资料配置。二级分类优先使用自己的已发布配置;没有独立已发布配置时,自动继承一级分类模板。
## 2. 配置真源
@@ -46,6 +46,15 @@ order_service_profile_versions
订单项 → 接单模板 → category_id → 当前发布的订单服务版本
```
分类配置解析按分类链由近到远执行:
```text
二级分类已发布配置
> 一级分类已发布配置
```
二级分类只有草稿、配置已归档或尚未创建时,均继续继承一级分类的已发布版本。后台接单分类默认只展示一级分类,二级分类折叠展示,并标记“继承 vN”。
这样不会出现“先确定分类才能选默认模板、但分类又来自模板”的循环依赖。
## 3. 工单快照
@@ -64,6 +73,8 @@ work_orders.service_profile_snapshot_json
{
"profileId": 1,
"categoryId": 1,
"sourceCategoryId": 1,
"inherited": false,
"versionId": 2,
"versionNo": 2,
"config": {}
@@ -79,13 +90,19 @@ work_orders.service_profile_snapshot_json
历史工单没有快照时继续读取其 `requirement_json`,不会突然切换到新分类配置。
发布新版本时,会同步该分类下仍未提交资料且未指派打手的旧工单:
发布新版本时,会同步该分类下仍未提交资料且未指派打手的旧工单。发布一级分类配置时,还会同步其下尚未独立发布配置的二级分类工单:
- 写入最新配置版本和快照;
- 同步 `requirement_json.fields`;
- 有有效拼单份额的工单不会覆盖;
- 已提交、已指派、已进入履约的工单保持不变。
继承快照会记录:
- `categoryId`:工单实际所属分类;
- `sourceCategoryId`:最终命中的配置分类;
- `inherited`:是否继承父级配置。
字段配置优先使用标准字段:
- `gameId`:游戏 ID,按字符串保存并校验为纯数字,避免超长 ID 精度丢失;