diff --git a/README.md b/README.md
index 7e07eb10..334a6109 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
# order_site
-快手自动发货与履约管理系统。
+多渠道订单履约、打手众包接单与寄卖仓库系统。
-前后端分离架构:接收外部订单、按规则匹配发货平台、生成统一领取链接,并提供后台做平台配置、任务追踪和人工复核。
+前后端分离架构:接收 `kuaishou-91`、`pdd`、`douyin-mashida` 三个来源的订单,按规则匹配自动发货或生成打手工单,提供统一领取/兑换链接,并支持平台配置、财务结算、审计与人工复核。
## 技术栈
@@ -26,28 +26,43 @@ deploy/
caddy/ Caddy 反向代理
docker/ 前后端 Dockerfile
mac-dev.sh / ubuntu-deploy.sh
-docs/ 平台接口、履约链路、部署说明
+docs/ 文档索引、架构分析、财务方案、平台接口、部署说明
docker-compose.dev.yml 本地 Docker 开发环境
docker-compose.yml 生产 / 服务器 Compose
```
+## 来源模型(现行)
+
+系统只有三个订单来源:
+
+| sourceKey | 数据通道 | 说明 |
+| --- | --- | --- |
+| `kuaishou-91` | 91 回调 + 快手 send-code | **同一个来源的两条通道**:91 是订单骨架权威源,快手 send-code 是数据补全源与兜底源;共享店铺键空间与同一套匹配上下文 |
+| `pdd` | 拼多多订单同步 | 两段式 `pdd_raw_orders → orders` |
+| `douyin-mashida` | 抖音码速达推送 | 抖店订单、DYC 券、H5/兑换页 |
+
+不要把 91 与快手 send-code 拆成两个来源或两个适配器。代码中的唯一来源键是 `kuaishou-91`(`apps/backend/src/services/product-match/types.ts`)。
+
## 业务边界(简图)
```text
-91 卡券进单
+kuaishou-91(91 回调 + 快手 send-code)
→ 本系统建单 / 匹配商品 / 创建履约任务
→ 返回统一 claimUrl
用户打开领取页
→ 可选:快手行业电子凭证自动核销
- → kuaishou-cloud / kuaishou-feifei / 人工 完成发货
+ → affiliate-dash / 人工 完成发货
后台
→ 订单任务、平台配置、审计、人工复核
接单平台(打手众包)
→ 工单发布到抢单大厅,打手抢单代练
→ 提交验收 / 问题单处置 / 任务时限超时自动判定
+抖音码速达
+ → 抖店订单推送 → 建单 → DYC 券 → 领取/兑换页 → 核销
```
更完整的链路说明见:`docs/履约配置/多发货平台与电子凭证整体链路.md`。
+文档总索引见:`docs/README.md`。
## 本地开发
diff --git a/docs/91卡券/5.当前项目对接配置.md b/docs/91卡券/5.当前项目对接配置.md
index 10508901..c42140b0 100644
--- a/docs/91卡券/5.当前项目对接配置.md
+++ b/docs/91卡券/5.当前项目对接配置.md
@@ -1,5 +1,7 @@
# 91卡券接入当前项目接口配置
+> 来源口径(2026-10-17):`provider='91kaquan'` 是订单通道标识;在商品匹配层,91 回调与快手 send-code 合并为**同一个来源 `kuaishou-91`**。不要把二者当成两个来源或两个适配器。
+
## 1. 对接目标
- `91卡券` 作为外部售卖与自动发货通道。
@@ -69,7 +71,7 @@ https://你的域名/api/v1/open/91
### 5.2 当前项目内部映射(现状)
-代码中已按独立来源落地:
+代码中已按 `kuaishou-91` 来源的订单通道落地:
- `provider = '91kaquan'`
- `platform = 'kuaishou'`
diff --git a/docs/91卡券/6.开发设计.md b/docs/91卡券/6.开发设计.md
index 949b34af..e87b6a63 100644
--- a/docs/91卡券/6.开发设计.md
+++ b/docs/91卡券/6.开发设计.md
@@ -2,6 +2,8 @@
## 1. 目标
+> 来源口径(2026-10-17):`91卡券` 是 **`kuaishou-91` 来源的订单骨架通道**;快手 send-code 是同一来源的数据补全与兜底通道。二者共享匹配上下文与店铺键空间,**不能拆成两个来源或两个适配器**。
+
基于以下对外接口文档完成当前项目接入:
- [3.异步卡密下单.md](./3.异步卡密下单.md)
@@ -19,8 +21,8 @@
## 2. 设计原则
-- `91卡券` 作为**独立订单来源**。
-- `91卡券` 不是新的履约执行器,而是外部售卖、订单输入和自动发货通道。
+- `91卡券` 是 `kuaishou-91` 来源的**订单骨架通道**,不是新的履约执行器;快手 send-code 是同来源的补全/兜底通道。
+- 外部售卖、订单输入和自动发货通道仍由 91 承担;匹配层的来源键统一为 `kuaishou-91`。
- 当前项目真正的交付物仍然是 `claimUrl`。
- 查询接口返回 `20` 的判断标准是“领取链接已生成并可发”,不是“快手最终兑换完成”。
- `buyNum > 1` 时,必须拆成多个 task,并返回多个 card。
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 00000000..6f22adf7
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,98 @@
+# order_site 文档索引
+
+> 最后校准:2026-10-17,代码基线 `main @ 6850bc64`。
+> 本文是 `docs/` 的唯一入口。新增文档必须在上表登记状态;过期文档不再直接改写结论,统一移入 `docs/archive/`。
+
+## 一、先记住四条口径
+
+### 1. 来源只有三个,不是四个
+
+| sourceKey | 包含的数据通道 | 说明 |
+| --- | --- | --- |
+| `kuaishou-91` | 91 回调 + 快手 send-code | **同一个来源的两条通道**:91 是订单骨架权威源,快手 send-code 是数据补全源与兜底源;共享店铺键空间与同一套匹配上下文 |
+| `pdd` | 拼多多订单同步 | 两段式 `pdd_raw_orders → orders` |
+| `douyin-mashida` | 抖音码速达推送 | 抖店订单、DYC 券、H5/兑换页 |
+
+**不要把 91 与快手 send-code 拆成两个来源、两个适配器或两条业务线。** 代码中的唯一来源键是 `kuaishou-91`(`apps/backend/src/services/product-match/types.ts`),适配器是 `adapters/kuaishou-91.ts`。
+
+### 2. 一个订单 = 骨架 + 买家凭证 + 一个执行载体
+
+- 骨架:`orders` + `order_items`
+- 买家凭证:快手电子凭证(`kuaishou_industry_vouchers`)或抖音券(`redeem_certificates`)
+- 执行载体只有三种:`affiliate_dash` 自动发货、`work_orders` 工单、`manual` 人工客服
+- 三者业务上互斥;无自动任务且无工单的已支付订单项就是人工块,必须进入人工 case,不能静默消失
+
+### 3. 成本只在一个地方改
+
+- 商品主数据:`product_catalog`
+- 渠道 SKU 映射:`channel_product_mappings`
+- 成本金额:只在 `cost_versions` 改,按生效日期新增版本
+- 订单/工单/人工单只保存 `cost_snapshot_id`,历史成本永不回写
+- 自动发货、接单平台、人工客服共用同一个成本解析器;匹配不到成本进入 `cost_missing`,不默认 0
+
+### 4. 文档状态
+
+| 状态 | 含义 |
+| --- | --- |
+| 现行 | 与当前代码一致,改动代码时必须同步更新 |
+| 参考 | 厂商协议、外部接口文档,按平台版本更新 |
+| 历史 | 只用于追溯,不得作为实现依据;位于 `docs/archive/` |
+
+## 二、现行文档
+
+| 文档 | 状态 | 用途 |
+| --- | --- | --- |
+| [项目全量分析报告-2026-10-17.md](项目全量分析报告-2026-10-17.md) | 现行 | 当前架构、业务链路、风险与改进路线 |
+| [载体判定与成本中心落地-复核与草案-2026-10-17.md](载体判定与成本中心落地-复核与草案-2026-10-17.md) | 现行(复核+草案) | 只读克隆量化载体分布与人工块、对上一轮结论的复核与修正、两份 SQL 草案说明 |
+| [成本中心与三块履约模型.md](成本中心与三块履约模型.md) | 现行(方案) | 自动发货/接单平台/人工客服三块、统一成本中心、销售事实与人工监管 |
+| [财务系统集成方案.md](财务系统集成方案.md) | 现行(方案) | 财务系统边界、复式记账模型、分期落地 |
+| [影子总账说明.md](影子总账说明.md) | 现行(方案) | 影子总账的定义、运行方式、对账与转正标准 |
+| [接单模板匹配引擎重构设计.md](接单模板匹配引擎重构设计.md) | 现行(P0/P1 已实施) | `kuaishou-91 / pdd / douyin-mashida` 三来源匹配引擎 |
+| [履约配置/多发货平台与电子凭证整体链路.md](履约配置/多发货平台与电子凭证整体链路.md) | 现行 | 进单、履约、领取、电子凭证的整体链路 |
+| [affiliate-dash-发货平台/affiliate-dash对接.md](affiliate-dash-发货平台/affiliate-dash对接.md) | 现行 | affiliate-dash 自动履约接口与字段 |
+| [履约配置/cloudtentacles发货平台.md](履约配置/cloudtentacles发货平台.md) | 历史参考 | 已下线通道的第三方接口样例,不代表现行实现 |
+| [91卡券/6.开发设计.md](91卡券/6.开发设计.md) | 现行 | `kuaishou-91` 来源的 91 侧接入设计 |
+| [抖音-码速达/00-总览.md](抖音-码速达/00-总览.md) | 现行 | 抖音码速达接入总览与增量记录 |
+| [抖音-码速达/12-按SKU分类改造总设计.md](抖音-码速达/12-按SKU分类改造总设计.md) | 现行 | 抖音三层分流与载体唯一 |
+| [部署启动与数据库重建.md](部署启动与数据库重建.md) | 现行 | 本地/生产启动、迁移、重建 |
+| [数据库备份与恢复.md](数据库备份与恢复.md) | 现行 | 备份、恢复、OSS |
+| [数据库SQL约束与运维.md](数据库SQL约束与运维.md) | 现行 | SQL 护栏与生产观测 |
+
+### SQL 草案(未接入迁移)
+
+草案只作设计与复盘依据;接入 `apps/backend/src/db/migrations/` 时再拆成 `NNN_slug.sql`,并先跑 `npm run check:sql`。
+
+| 文件 | 用途 |
+| --- | --- |
+| [drafts/2026-10-17-载体判定与人工块量化.sql](drafts/2026-10-17-载体判定与人工块量化.sql) | 载体分布/人工块量化查询、`carrier_type` 判定与回填、守恒校验、人工成本区间 |
+| [drafts/2026-10-17-成本中心与销售事实-DDL草案.sql](drafts/2026-10-17-成本中心与销售事实-DDL草案.sql) | `product_catalog`、`channel_product_mappings`、`cost_items/versions/snapshots/exceptions`、`sales_daily_facts` 建表草案 |
+| [drafts/2026-10-17-人工支出登记-DDL草案.sql](drafts/2026-10-17-人工支出登记-DDL草案.sql) | `manual_fulfillment_cases`、`manual_expense_records`(付款项目/金额/订单号/付款人/凭证)、`manual_expense_events`;依赖成本中心草案,须在其后应用 |
+
+## 三、厂商协议与外部资料
+
+这些目录是平台方文档或抓取资料,**不作为本项目实现说明**:
+
+| 目录 | 内容 |
+| --- | --- |
+| `91卡券/` | 91 卡券公开接口、签名、加密示例(项目接入结论见 `91卡券/6.开发设计.md`) |
+| `行业电子凭证/` | 快手电子凭证接口、售后、测试指南 |
+| `发货平台-2/` | 早期发货平台协议,现行为 `affiliate_dash`;保留作历史接口对照 |
+| `抖音-码速达/接口API/` | 抖音码速达出网接口 |
+| `抖音-码速达/消息推送/` | 抖音码速达推送消息字段与签名 |
+
+## 四、历史文档
+
+| 位置 | 说明 |
+| --- | --- |
+| [archive/README.md](archive/README.md) | 2026-10-08 及更早的阶段分析、寄卖方案、性能快照、一次性迁移检查脚本 |
+
+## 五、维护规则
+
+1. 改代码影响业务口径时,先改现行文档,再提交代码。
+2. 文档头部必须写「状态 + 最后校准日期 + 代码基线」。
+3. 旧分析不要在原文件上继续堆结论;新建当期报告,旧文件移入 `docs/archive/<日期>/`。
+4. 行号只用于当期基线;跨版本引用优先写文件与函数名。
+5. 新增来源时只加一个 `ProductMatchSourceKey` + 一个适配器;**91 + 快手 send-code 永远算一个 `kuaishou-91`**。
+6. 成本金额只改 `cost_versions`,不要在多个业务表直接改价;历史订单只读 `cost_snapshots`。
+7. 报表维度统一用 `canonical_product_id + shop_id + stat_date`,不要直接用原始 `sku_code` 或 `orders.total_amount`。
+8. 新增 SQL 草案放 `docs/drafts/`,命名 `<日期>-<主题>.sql`;**不得**直接放进 `apps/backend/src/db/migrations/`(后端启动会自动迁移),接入时再拆编号并先跑 `npm run check:sql`。
diff --git a/docs/ affiliate_dash_发货平台/affiliate-dash对接.md b/docs/affiliate-dash-发货平台/affiliate-dash对接.md
similarity index 100%
rename from docs/ affiliate_dash_发货平台/affiliate-dash对接.md
rename to docs/affiliate-dash-发货平台/affiliate-dash对接.md
diff --git a/docs/寄卖子系统技术分析报告.md b/docs/archive/2026-10-08/寄卖子系统技术分析报告.md
similarity index 100%
rename from docs/寄卖子系统技术分析报告.md
rename to docs/archive/2026-10-08/寄卖子系统技术分析报告.md
diff --git a/docs/寄卖库存优化现状与进度追踪.md b/docs/archive/2026-10-08/寄卖库存优化现状与进度追踪.md
similarity index 100%
rename from docs/寄卖库存优化现状与进度追踪.md
rename to docs/archive/2026-10-08/寄卖库存优化现状与进度追踪.md
diff --git a/docs/寄卖系统优化方案.md b/docs/archive/2026-10-08/寄卖系统优化方案.md
similarity index 100%
rename from docs/寄卖系统优化方案.md
rename to docs/archive/2026-10-08/寄卖系统优化方案.md
diff --git a/docs/寄卖系统迁移079上库前检查.sql b/docs/archive/2026-10-08/寄卖系统迁移079上库前检查.sql
similarity index 100%
rename from docs/寄卖系统迁移079上库前检查.sql
rename to docs/archive/2026-10-08/寄卖系统迁移079上库前检查.sql
diff --git a/docs/近期性能与稳定性专项优化分析报告.md b/docs/archive/2026-10-08/近期性能与稳定性专项优化分析报告.md
similarity index 100%
rename from docs/近期性能与稳定性专项优化分析报告.md
rename to docs/archive/2026-10-08/近期性能与稳定性专项优化分析报告.md
diff --git a/docs/项目分析报告-发单系统与寄售仓库.md b/docs/archive/2026-10-08/项目分析报告-发单系统与寄售仓库.md
similarity index 100%
rename from docs/项目分析报告-发单系统与寄售仓库.md
rename to docs/archive/2026-10-08/项目分析报告-发单系统与寄售仓库.md
diff --git a/docs/archive/README.md b/docs/archive/README.md
new file mode 100644
index 00000000..900cbce6
--- /dev/null
+++ b/docs/archive/README.md
@@ -0,0 +1,22 @@
+# 历史文档归档
+
+> 这里的文档只用于追溯当时的分析过程,**不是现行实现说明**。
+> 代码、数据库结构与线上行为以仓库当前 `main` 和 `docs/README.md` 中标记为「现行」的文档为准。
+
+## 2026-10-08
+
+| 文档 | 归档原因 |
+| --- | --- |
+| `项目分析报告-发单系统与寄售仓库.md` | 基线为 `ef491660`,测试数、迁移数、表数与当前实现已明显不一致;由《项目全量分析报告-2026-10-17》替代 |
+| `寄卖子系统技术分析报告.md` | 同上,且部分行号已漂移;寄卖代码仍可参考,但结论需按现行代码复核 |
+| `寄卖系统优化方案.md` | 阶段性优化方案,部分事项已完成,不能作为现行路线图 |
+| `寄卖库存优化现状与进度追踪.md` | 阶段性进度追踪,完成状态已过时 |
+| `近期性能与稳定性专项优化分析报告.md` | 2026-10-01 / 10-02 的生产快照与专项复盘,保留作性能证据,不代表当前容量 |
+| `寄卖系统迁移079上库前检查.sql` | 迁移 079 的一次性上库前检查脚本,生产已执行;保留作审计追溯 |
+
+## 使用建议
+
+- 需要当前架构与风险:看 `docs/项目全量分析报告-2026-10-17.md`。
+- 需要财务系统设计:看 `docs/财务系统集成方案.md`。
+- 需要某个平台协议:看对应的厂商文档目录(`91卡券/`、`行业电子凭证/`、`发货平台-2/`、`抖音-码速达/接口API/`、`抖音-码速达/消息推送/`)。
+- 历史文档中的代码行号、测试数量、迁移数量不要直接引用。
diff --git a/docs/drafts/2026-10-17-人工支出登记-DDL草案.sql b/docs/drafts/2026-10-17-人工支出登记-DDL草案.sql
new file mode 100644
index 00000000..0a704058
--- /dev/null
+++ b/docs/drafts/2026-10-17-人工支出登记-DDL草案.sql
@@ -0,0 +1,255 @@
+-- ============================================================================
+-- 人工支出登记 + 轻量人工处理记录(建表草案)
+-- ----------------------------------------------------------------------------
+-- 状态:草案(DRAFT)。**未接入** apps/backend/src/db/migrations。
+-- 依赖:本文引用 product_catalog / cost_items / cost_snapshots,
+-- 必须**在** `2026-10-17-成本中心与销售事实-DDL草案.sql` **之后**应用。
+-- 接入时应拆成 `104_manual_cases.sql`、`105_manual_expenses.sql`,并先跑 `npm run check:sql`。
+-- 基线:main @ 801bfa0b;依据:2026-10-17 业务口径确认(见 docs/载体判定与成本中心落地-复核与草案-2026-10-17.md §九)。
+--
+-- 业务原话(定优先级):
+-- 「人工块现在意义不大,主要是把流程搞清楚:哪个人处理了哪些东西,付了多少钱。
+-- 现在人工完全监管不到,很多地方需要人工付款,也不知道付款项目、金额、订单号啥的。」
+--
+-- 因此本草案的顺序是:
+-- ① 人工支出可追溯(付款项目 / 金额 / 订单号 / 谁付的 / 凭证)——**核心诉求**
+-- ② 处理人可见(谁在处理哪个订单项)——用轻量 case 承载
+-- ③ SLA / 超时升级 / 监管指标——**延后**,不进本轮最小实现
+-- ============================================================================
+
+
+-- ============================================================================
+-- §1 轻量人工处理记录(谁在处理哪些东西)
+-- ----------------------------------------------------------------------------
+-- 与 docs/成本中心与三块履约模型.md §8.3 的差异:去掉 due_at/SLA 强约束与优先级体系,
+-- 只保留“谁、什么时候、处理了哪个订单项、结果是什么”。SLA 字段等 ③ 阶段再加。
+-- ============================================================================
+
+CREATE TABLE IF NOT EXISTS manual_fulfillment_cases (
+ id BIGSERIAL PRIMARY KEY,
+ case_no TEXT NOT NULL,
+ order_id BIGINT NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
+ order_item_id BIGINT REFERENCES order_items(id) ON DELETE CASCADE,
+ provider TEXT NOT NULL DEFAULT '',
+ shop_id TEXT NOT NULL DEFAULT '',
+ canonical_product_id BIGINT REFERENCES product_catalog(id) ON DELETE RESTRICT,
+ quantity INTEGER NOT NULL DEFAULT 1,
+ status TEXT NOT NULL DEFAULT 'pending_assign',
+ assignee_admin_user_id BIGINT,
+ assignee_name TEXT NOT NULL DEFAULT '',
+ assigned_at TIMESTAMPTZ,
+ started_at TIMESTAMPTZ,
+ completed_at TIMESTAMPTZ,
+ outcome TEXT NOT NULL DEFAULT '',
+ delivery_reference TEXT NOT NULL DEFAULT '',
+ evidence_json JSONB NOT NULL DEFAULT '{}'::jsonb,
+ note TEXT NOT NULL DEFAULT '',
+ created_at TIMESTAMPTZ NOT NULL,
+ updated_at TIMESTAMPTZ NOT NULL
+);
+
+CREATE UNIQUE INDEX IF NOT EXISTS uq_manual_cases_case_no ON manual_fulfillment_cases (case_no);
+-- 一个订单项最多一个 case:与「一个订单项只有一个执行载体」保持一致
+CREATE UNIQUE INDEX IF NOT EXISTS uq_manual_cases_order_item
+ ON manual_fulfillment_cases (order_item_id) WHERE order_item_id IS NOT NULL;
+CREATE INDEX IF NOT EXISTS idx_manual_cases_status ON manual_fulfillment_cases (status, created_at DESC);
+CREATE INDEX IF NOT EXISTS idx_manual_cases_assignee ON manual_fulfillment_cases (assignee_admin_user_id, status);
+CREATE INDEX IF NOT EXISTS idx_manual_cases_order ON manual_fulfillment_cases (order_id);
+
+DO $$
+BEGIN
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_manual_cases_status') THEN
+ ALTER TABLE manual_fulfillment_cases
+ ADD CONSTRAINT chk_manual_cases_status
+ CHECK (status IN ('pending_assign', 'assigned', 'processing', 'waiting_customer',
+ 'pending_review', 'completed', 'failed', 'cancelled'));
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_manual_cases_outcome') THEN
+ ALTER TABLE manual_fulfillment_cases
+ ADD CONSTRAINT chk_manual_cases_outcome
+ CHECK (outcome IN ('', 'delivered', 'refunded', 'failed', 'cancelled'));
+ END IF;
+ -- 完成必须有结果;避免“点完成但什么都没填”
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_manual_cases_completed') THEN
+ ALTER TABLE manual_fulfillment_cases
+ ADD CONSTRAINT chk_manual_cases_completed
+ CHECK (status <> 'completed' OR (completed_at IS NOT NULL AND outcome <> ''));
+ END IF;
+END $$;
+
+COMMENT ON TABLE manual_fulfillment_cases IS '轻量人工处理记录:谁在处理哪个订单项、结果如何(SLA 阶段延后)';
+COMMENT ON COLUMN manual_fulfillment_cases.evidence_json IS '交付凭证:截图对象 key、参考号等;完成后必填(由服务层校验)';
+
+
+-- ============================================================================
+-- §2 人工支出登记(核心诉求)
+-- ----------------------------------------------------------------------------
+-- 必须能回答:这笔钱是什么项目、多少钱、关联哪个订单/订单项、谁付的、付给谁、什么时候、凭证在哪。
+-- 与成本中心的关系:
+-- cost_item_id = “付款项目”,指向 cost_items(cost_type = 'manual_service' 或其它)
+-- approved 后生成 cost_snapshots(subject_type = 'manual_case' 或 'order_item',source = 'manual_input')
+-- 影子总账期:登记 approved 即产生一条业务事件,进复式分录(借:履约成本/费用,贷:其他应付/银行存款)
+-- ============================================================================
+
+CREATE TABLE IF NOT EXISTS manual_expense_records (
+ id BIGSERIAL PRIMARY KEY,
+ expense_no TEXT NOT NULL,
+ case_id BIGINT REFERENCES manual_fulfillment_cases(id) ON DELETE SET NULL,
+ order_id BIGINT REFERENCES orders(id) ON DELETE SET NULL,
+ order_item_id BIGINT REFERENCES order_items(id) ON DELETE SET NULL,
+ work_order_id BIGINT REFERENCES work_orders(id) ON DELETE SET NULL,
+ cost_item_id BIGINT REFERENCES cost_items(id) ON DELETE RESTRICT,
+ expense_category TEXT NOT NULL DEFAULT 'other',
+ amount_fen BIGINT NOT NULL,
+ currency TEXT NOT NULL DEFAULT 'CNY',
+ paid_by_admin_user_id BIGINT,
+ paid_by_name TEXT NOT NULL DEFAULT '',
+ payee_type TEXT NOT NULL DEFAULT 'other',
+ payee_name TEXT NOT NULL DEFAULT '',
+ payee_account TEXT NOT NULL DEFAULT '',
+ payment_method TEXT NOT NULL DEFAULT 'other',
+ paid_at TIMESTAMPTZ NOT NULL,
+ status TEXT NOT NULL DEFAULT 'submitted',
+ approved_by TEXT NOT NULL DEFAULT '',
+ approved_at TIMESTAMPTZ,
+ reject_reason TEXT NOT NULL DEFAULT '',
+ evidence_json JSONB NOT NULL DEFAULT '{}'::jsonb,
+ cost_snapshot_id BIGINT REFERENCES cost_snapshots(id) ON DELETE RESTRICT,
+ -- 预留:退款/仲裁接入后用反向登记冲正,不修改已通过的原始记录
+ reversal_of_id BIGINT REFERENCES manual_expense_records(id) ON DELETE RESTRICT,
+ note TEXT NOT NULL DEFAULT '',
+ created_at TIMESTAMPTZ NOT NULL,
+ updated_at TIMESTAMPTZ NOT NULL
+);
+
+CREATE UNIQUE INDEX IF NOT EXISTS uq_manual_expenses_expense_no ON manual_expense_records (expense_no);
+CREATE INDEX IF NOT EXISTS idx_manual_expenses_order ON manual_expense_records (order_id);
+CREATE INDEX IF NOT EXISTS idx_manual_expenses_item ON manual_expense_records (order_item_id);
+CREATE INDEX IF NOT EXISTS idx_manual_expenses_case ON manual_expense_records (case_id);
+CREATE INDEX IF NOT EXISTS idx_manual_expenses_paid_at ON manual_expense_records (paid_at DESC);
+CREATE INDEX IF NOT EXISTS idx_manual_expenses_status ON manual_expense_records (status, created_at DESC);
+CREATE INDEX IF NOT EXISTS idx_manual_expenses_cost_item ON manual_expense_records (cost_item_id);
+-- 付款人日结对账用
+CREATE INDEX IF NOT EXISTS idx_manual_expenses_payer ON manual_expense_records (paid_by_admin_user_id, paid_at DESC);
+
+DO $$
+BEGIN
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_manual_expenses_amount') THEN
+ ALTER TABLE manual_expense_records
+ ADD CONSTRAINT chk_manual_expenses_amount CHECK (amount_fen <> 0);
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_manual_expenses_status') THEN
+ ALTER TABLE manual_expense_records
+ ADD CONSTRAINT chk_manual_expenses_status
+ CHECK (status IN ('draft', 'submitted', 'approved', 'rejected', 'reimbursed', 'reversed'));
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_manual_expenses_payee_type') THEN
+ ALTER TABLE manual_expense_records
+ ADD CONSTRAINT chk_manual_expenses_payee_type
+ CHECK (payee_type IN ('worker', 'upstream', 'customer', 'platform', 'other'));
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_manual_expenses_method') THEN
+ ALTER TABLE manual_expense_records
+ ADD CONSTRAINT chk_manual_expenses_method
+ CHECK (payment_method IN ('wechat', 'alipay', 'bank', 'cash', 'platform_balance', 'other'));
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_manual_expenses_category') THEN
+ ALTER TABLE manual_expense_records
+ ADD CONSTRAINT chk_manual_expenses_category
+ CHECK (expense_category IN ('goods_purchase', 'compensation', 'refund_advance', 'service_fee',
+ 'freight', 'recharge', 'other'));
+ END IF;
+ -- 已通过/已报销必须有审批信息
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_manual_expenses_approved') THEN
+ ALTER TABLE manual_expense_records
+ ADD CONSTRAINT chk_manual_expenses_approved
+ CHECK (status NOT IN ('approved', 'reimbursed') OR (approved_at IS NOT NULL AND approved_by <> ''));
+ END IF;
+END $$;
+
+COMMENT ON TABLE manual_expense_records IS '人工支出登记:付款项目/金额/订单号/付款人/收款人/凭证,解决人工付款不可追溯';
+COMMENT ON COLUMN manual_expense_records.cost_item_id IS '付款项目 -> cost_items;为空表示未归类,必须进异常队列而不是默认其他';
+COMMENT ON COLUMN manual_expense_records.expense_category IS '兜底分类:cost_item_id 未填时的粗分类,报表需同时暴露未归类占比';
+COMMENT ON COLUMN manual_expense_records.paid_by_admin_user_id IS '实际垫付/付款的后台用户;用于“谁付了多少钱”对账';
+COMMENT ON COLUMN manual_expense_records.evidence_json IS '凭证:支付截图对象 key、流水号、聊天记录 key 等;提交时必填(服务层校验)';
+COMMENT ON COLUMN manual_expense_records.reversal_of_id IS '预留:退款/仲裁接入后按反向登记冲正,禁止 UPDATE 已通过记录';
+COMMENT ON COLUMN manual_expense_records.status IS 'draft草稿/submitted待审/approved已通过/rejected已驳回/reimbursed已报销/reversed已冲正';
+
+-- 状态变更审计(不可改)
+CREATE TABLE IF NOT EXISTS manual_expense_events (
+ id BIGSERIAL PRIMARY KEY,
+ expense_id BIGINT NOT NULL REFERENCES manual_expense_records(id) ON DELETE CASCADE,
+ event_type TEXT NOT NULL,
+ from_status TEXT NOT NULL DEFAULT '',
+ to_status TEXT NOT NULL DEFAULT '',
+ actor_type TEXT NOT NULL DEFAULT 'admin',
+ actor_id TEXT NOT NULL DEFAULT '',
+ payload_json JSONB NOT NULL DEFAULT '{}'::jsonb,
+ created_at TIMESTAMPTZ NOT NULL
+);
+
+CREATE INDEX IF NOT EXISTS idx_manual_expense_events_expense
+ ON manual_expense_events (expense_id, created_at);
+
+COMMENT ON TABLE manual_expense_events IS '人工支出状态变更审计:只追加,不更新、不删除';
+
+
+-- ============================================================================
+-- §3 校验与报表查询
+-- ============================================================================
+
+-- Q1 「谁付了多少钱」:按付款人 × 项目 汇总(日结/周结用)
+-- SELECT r.paid_by_name, coalesce(ci.name, r.expense_category) AS item, count(*) AS records,
+-- round(sum(r.amount_fen) / 100.0, 2) AS yuan
+-- FROM manual_expense_records r
+-- LEFT JOIN cost_items ci ON ci.id = r.cost_item_id
+-- WHERE r.status IN ('approved', 'reimbursed') AND r.paid_at >= now() - interval '7 days'
+-- GROUP BY 1, 2 ORDER BY sum(r.amount_fen) DESC;
+
+-- Q2 未归类占比(必须持续为 0,否则报表口径不可信)
+-- SELECT count(*) FILTER (WHERE cost_item_id IS NULL) AS unclassified,
+-- count(*) AS total,
+-- round(100.0 * count(*) FILTER (WHERE cost_item_id IS NULL) / nullif(count(*), 0), 2) AS unclassified_pct
+-- FROM manual_expense_records WHERE status <> 'draft';
+
+-- Q3 没有关联订单号的支出(你说的“也不知道订单号”,这里必须能点名)
+-- SELECT count(*) FILTER (WHERE order_id IS NULL AND order_item_id IS NULL) AS no_order_link,
+-- round(sum(amount_fen) FILTER (WHERE order_id IS NULL AND order_item_id IS NULL) / 100.0, 2) AS yuan
+-- FROM manual_expense_records WHERE status IN ('approved', 'reimbursed');
+
+-- Q4 订单损益:一个订单项的收入 - 各类成本(含人工支出)
+-- SELECT oi.id AS order_item_id, o.provider, o.shop_id,
+-- oi.revenue_amount_fen AS revenue_fen,
+-- coalesce(sum(cs.total_amount_fen), 0) AS cost_fen,
+-- coalesce(sum(r.amount_fen), 0) AS manual_expense_fen
+-- FROM order_items oi
+-- JOIN orders o ON o.id = oi.order_id
+-- LEFT JOIN cost_snapshots cs ON cs.order_item_id = oi.id
+-- LEFT JOIN manual_expense_records r ON r.order_item_id = oi.id AND r.status IN ('approved', 'reimbursed')
+-- WHERE oi.id = $1
+-- GROUP BY 1, 2, 3, 4;
+
+-- Q5 与成本中心对账:approved 的支出必须都有 cost_snapshot(影子期允许延迟,转正期不允许缺)
+-- SELECT count(*) AS approved_without_snapshot
+-- FROM manual_expense_records
+-- WHERE status IN ('approved', 'reimbursed') AND cost_snapshot_id IS NULL;
+
+
+-- ============================================================================
+-- §4 与财务系统(影子总账)的对接约定
+-- ----------------------------------------------------------------------------
+-- 事件类型(幂等键):
+-- manual_expense:{expense_no}:approved 人工支出登记通过
+-- manual_expense:{expense_no}:reimbursed 已报销(垫付 -> 公司支出)
+-- manual_expense:{expense_no}:reversed 冲正(退款/仲裁接入后启用,本轮仅预留)
+--
+-- 建议分录(approved 时,金额 A):
+-- 借:5101 履约成本-人工(或按 cost_items 映射的科目) A
+-- 贷:2005 其他应付-垫付人 A
+-- reimbursed 时:
+-- 借:2005 其他应付-垫付人 A
+-- 贷:1001 银行存款 / 1002 支付通道待结算 A
+--
+-- 退款/仲裁仍未接入:本轮只保留 reversal_of_id 与 status='reversed',
+-- 不实现自动冲正逻辑;接入后按“反向登记 + 新分录”处理,禁止改动原记录。
+-- ============================================================================
diff --git a/docs/drafts/2026-10-17-成本中心与销售事实-DDL草案.sql b/docs/drafts/2026-10-17-成本中心与销售事实-DDL草案.sql
new file mode 100644
index 00000000..784d3810
--- /dev/null
+++ b/docs/drafts/2026-10-17-成本中心与销售事实-DDL草案.sql
@@ -0,0 +1,400 @@
+-- ============================================================================
+-- 通用成本中心 + 标准商品 + 销售事实(建表草案)
+-- ----------------------------------------------------------------------------
+-- 状态:草案(DRAFT)。**未接入** apps/backend/src/db/migrations。
+-- 接入时应拆成 `101_product_catalog.sql`、`102_cost_center.sql`、`103_sales_daily_facts.sql`,
+-- 并先跑 `npm run check:sql`(SQL 防退化护栏)。
+-- 基线:main @ ccd5ed2a;数据依据:本地只读克隆(生产快照,orders 316,597,数据截至 2026-10-14)。
+-- 金额单位:分(BIGINT)。本文件所有 *_fen 列均为分。
+-- 设计依据:docs/成本中心与三块履约模型.md(三块载体、成本版本与快照)、docs/财务系统集成方案.md。
+--
+-- 目标(业务原话):以后改成本只在一个地方。
+-- -> 所有成本金额只写 cost_versions;业务表只保存 cost_snapshot_id 与派生金额,永不回写历史。
+-- ============================================================================
+
+
+-- ============================================================================
+-- §1 标准商品主数据
+-- ----------------------------------------------------------------------------
+-- 为什么必须先做:同一个商品名对应多个 sku_code(实测「苏尔南钻石100颗」47 个码、
+-- 「指挥官密钥1个」42 个码),直接按 sku_code 或 sku_name 统计都会出错。
+-- ============================================================================
+
+CREATE TABLE IF NOT EXISTS product_catalog (
+ id BIGSERIAL PRIMARY KEY,
+ canonical_sku TEXT NOT NULL,
+ canonical_name TEXT NOT NULL,
+ category_id BIGINT,
+ unit_name TEXT NOT NULL DEFAULT '件',
+ status TEXT NOT NULL DEFAULT 'active',
+ attributes_json JSONB NOT NULL DEFAULT '{}'::jsonb,
+ created_at TIMESTAMPTZ NOT NULL,
+ updated_at TIMESTAMPTZ NOT NULL
+);
+
+CREATE UNIQUE INDEX IF NOT EXISTS uq_product_catalog_canonical_sku ON product_catalog (canonical_sku);
+CREATE INDEX IF NOT EXISTS idx_product_catalog_category ON product_catalog (category_id);
+
+DO $$
+BEGIN
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_product_catalog_status') THEN
+ ALTER TABLE product_catalog
+ ADD CONSTRAINT chk_product_catalog_status CHECK (status IN ('active', 'archived'));
+ END IF;
+END $$;
+
+COMMENT ON TABLE product_catalog IS '标准商品/皮肤主数据:报表统计口径的唯一入口';
+COMMENT ON COLUMN product_catalog.canonical_sku IS '标准编码,报表维度用它,不用原始 sku_code';
+COMMENT ON COLUMN product_catalog.unit_name IS '计量单位:件/个/颗,配合 normalized_quantity 使用';
+
+-- 渠道映射:来源 + 店铺 + 平台SKU -> 标准商品
+CREATE TABLE IF NOT EXISTS channel_product_mappings (
+ id BIGSERIAL PRIMARY KEY,
+ provider TEXT NOT NULL,
+ shop_id TEXT NOT NULL DEFAULT '',
+ platform_sku_code TEXT NOT NULL,
+ platform_sku_name TEXT NOT NULL DEFAULT '',
+ canonical_product_id BIGINT NOT NULL REFERENCES product_catalog(id) ON DELETE RESTRICT,
+ quantity_multiplier INTEGER NOT NULL DEFAULT 1,
+ confidence TEXT NOT NULL DEFAULT 'manual',
+ note TEXT NOT NULL DEFAULT '',
+ created_at TIMESTAMPTZ NOT NULL,
+ updated_at TIMESTAMPTZ NOT NULL
+);
+
+-- 同一来源+店铺+SKU 只能映射到一个标准商品;shop_id 为空表示该来源的全局映射
+CREATE UNIQUE INDEX IF NOT EXISTS uq_channel_product_mappings_key
+ ON channel_product_mappings (provider, shop_id, platform_sku_code);
+CREATE INDEX IF NOT EXISTS idx_channel_product_mappings_product
+ ON channel_product_mappings (canonical_product_id);
+
+DO $$
+BEGIN
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_channel_product_mappings_confidence') THEN
+ ALTER TABLE channel_product_mappings
+ ADD CONSTRAINT chk_channel_product_mappings_confidence
+ CHECK (confidence IN ('exact', 'rule', 'manual'));
+ END IF;
+END $$;
+
+COMMENT ON TABLE channel_product_mappings IS '来源+店铺+平台SKU -> 标准商品映射(报表口径归一化)';
+COMMENT ON COLUMN channel_product_mappings.quantity_multiplier IS 'SKU 名称内含数量时的换算系数,例如「5个」=5';
+
+
+-- ============================================================================
+-- §2 成本中心
+-- ============================================================================
+
+-- 成本项:定义成本类型与计价方式,不存金额
+CREATE TABLE IF NOT EXISTS cost_items (
+ id BIGSERIAL PRIMARY KEY,
+ code TEXT NOT NULL,
+ name TEXT NOT NULL,
+ cost_type TEXT NOT NULL,
+ carrier_type TEXT NOT NULL,
+ pricing_mode TEXT NOT NULL,
+ currency TEXT NOT NULL DEFAULT 'CNY',
+ status TEXT NOT NULL DEFAULT 'active',
+ note TEXT NOT NULL DEFAULT '',
+ created_at TIMESTAMPTZ NOT NULL,
+ updated_at TIMESTAMPTZ NOT NULL
+);
+
+CREATE UNIQUE INDEX IF NOT EXISTS uq_cost_items_code ON cost_items (code);
+
+DO $$
+BEGIN
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_items_cost_type') THEN
+ ALTER TABLE cost_items
+ ADD CONSTRAINT chk_cost_items_cost_type
+ CHECK (cost_type IN ('auto_purchase', 'work_order_reward', 'manual_service', 'platform_fee', 'payment_fee', 'other'));
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_items_carrier_type') THEN
+ ALTER TABLE cost_items
+ ADD CONSTRAINT chk_cost_items_carrier_type
+ CHECK (carrier_type IN ('affiliate_dash', 'work_order', 'legacy', 'manual', 'shared'));
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_items_pricing_mode') THEN
+ ALTER TABLE cost_items
+ ADD CONSTRAINT chk_cost_items_pricing_mode
+ CHECK (pricing_mode IN ('per_item', 'per_order', 'per_hour', 'percent_of_revenue', 'fixed_daily'));
+ END IF;
+END $$;
+
+COMMENT ON TABLE cost_items IS '成本项定义:类型 + 载体 + 计价方式;金额一律放 cost_versions';
+COMMENT ON COLUMN cost_items.carrier_type IS '适用执行载体;shared 表示与载体无关的通用成本(如支付通道费)';
+
+-- 成本版本:**唯一改价入口**,按生效时间新增版本,永不覆盖历史
+CREATE TABLE IF NOT EXISTS cost_versions (
+ id BIGSERIAL PRIMARY KEY,
+ cost_item_id BIGINT NOT NULL REFERENCES cost_items(id) ON DELETE RESTRICT,
+ provider TEXT NOT NULL DEFAULT '',
+ shop_id TEXT NOT NULL DEFAULT '',
+ canonical_product_id BIGINT REFERENCES product_catalog(id) ON DELETE RESTRICT,
+ category_id BIGINT,
+ amount_fen BIGINT NOT NULL,
+ effective_from TIMESTAMPTZ NOT NULL,
+ effective_to TIMESTAMPTZ,
+ approval_status TEXT NOT NULL DEFAULT 'approved',
+ approved_by TEXT NOT NULL DEFAULT '',
+ reason TEXT NOT NULL DEFAULT '',
+ created_at TIMESTAMPTZ NOT NULL,
+ updated_at TIMESTAMPTZ NOT NULL
+);
+
+-- 同一成本项 + 同一适用范围 + 同一生效时间只能有一个版本
+CREATE UNIQUE INDEX IF NOT EXISTS uq_cost_versions_scope
+ ON cost_versions (cost_item_id, provider, shop_id, coalesce(canonical_product_id, 0), effective_from);
+CREATE INDEX IF NOT EXISTS idx_cost_versions_lookup
+ ON cost_versions (cost_item_id, effective_from DESC);
+CREATE INDEX IF NOT EXISTS idx_cost_versions_product
+ ON cost_versions (canonical_product_id, effective_from DESC);
+
+DO $$
+BEGIN
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_versions_amount') THEN
+ ALTER TABLE cost_versions ADD CONSTRAINT chk_cost_versions_amount CHECK (amount_fen >= 0);
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_versions_period') THEN
+ ALTER TABLE cost_versions
+ ADD CONSTRAINT chk_cost_versions_period
+ CHECK (effective_to IS NULL OR effective_to > effective_from);
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_versions_approval') THEN
+ ALTER TABLE cost_versions
+ ADD CONSTRAINT chk_cost_versions_approval
+ CHECK (approval_status IN ('draft', 'pending', 'approved', 'rejected'));
+ END IF;
+END $$;
+
+COMMENT ON TABLE cost_versions IS '成本版本:改价只在这里新增版本;历史成本靠 cost_snapshots 固化,不回写';
+COMMENT ON COLUMN cost_versions.amount_fen IS '金额(分)。pricing_mode=percent_of_revenue 时表示万分比';
+COMMENT ON COLUMN cost_versions.shop_id IS '空串=不限店铺;解析优先级 店铺+SKU > SKU > 分类 > 来源 > 全局';
+
+-- 成本快照:每笔订单项/工单/人工单实际用了哪个版本、多少钱
+CREATE TABLE IF NOT EXISTS cost_snapshots (
+ id BIGSERIAL PRIMARY KEY,
+ subject_type TEXT NOT NULL,
+ subject_id BIGINT NOT NULL,
+ order_id BIGINT,
+ order_item_id BIGINT,
+ work_order_id BIGINT,
+ carrier_type TEXT NOT NULL,
+ cost_item_id BIGINT REFERENCES cost_items(id) ON DELETE RESTRICT,
+ cost_version_id BIGINT REFERENCES cost_versions(id) ON DELETE RESTRICT,
+ canonical_product_id BIGINT REFERENCES product_catalog(id) ON DELETE RESTRICT,
+ quantity NUMERIC(14, 3) NOT NULL DEFAULT 1,
+ unit_amount_fen BIGINT NOT NULL DEFAULT 0,
+ total_amount_fen BIGINT NOT NULL DEFAULT 0,
+ source TEXT NOT NULL DEFAULT 'resolved',
+ resolved_at TIMESTAMPTZ NOT NULL,
+ created_at TIMESTAMPTZ NOT NULL
+);
+
+CREATE INDEX IF NOT EXISTS idx_cost_snapshots_subject ON cost_snapshots (subject_type, subject_id);
+CREATE INDEX IF NOT EXISTS idx_cost_snapshots_item ON cost_snapshots (order_item_id);
+CREATE INDEX IF NOT EXISTS idx_cost_snapshots_work_order ON cost_snapshots (work_order_id);
+-- 幂等:同一主体 + 同一成本项只允许一条快照
+CREATE UNIQUE INDEX IF NOT EXISTS uq_cost_snapshots_subject_item
+ ON cost_snapshots (subject_type, subject_id, cost_item_id);
+
+DO $$
+BEGIN
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_snapshots_subject_type') THEN
+ ALTER TABLE cost_snapshots
+ ADD CONSTRAINT chk_cost_snapshots_subject_type
+ CHECK (subject_type IN ('order_item', 'work_order', 'manual_case'));
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_snapshots_source') THEN
+ ALTER TABLE cost_snapshots
+ ADD CONSTRAINT chk_cost_snapshots_source
+ CHECK (source IN ('resolved', 'legacy_rule', 'manual_input', 'missing'));
+ END IF;
+END $$;
+
+COMMENT ON TABLE cost_snapshots IS '成本快照:下单/建工单/人工完成时固化,后续改价不影响历史';
+COMMENT ON COLUMN cost_snapshots.source IS 'resolved=命中成本中心;legacy_rule=回退旧 work_product_rules 金额;missing=未匹配到(禁止默认 0)';
+
+-- 成本异常队列:匹配不到成本不能默认 0,必须进队列
+CREATE TABLE IF NOT EXISTS cost_exceptions (
+ id BIGSERIAL PRIMARY KEY,
+ subject_type TEXT NOT NULL,
+ subject_id BIGINT NOT NULL,
+ order_item_id BIGINT,
+ work_order_id BIGINT,
+ carrier_type TEXT NOT NULL,
+ reason TEXT NOT NULL,
+ status TEXT NOT NULL DEFAULT 'open',
+ detail_json JSONB NOT NULL DEFAULT '{}'::jsonb,
+ created_at TIMESTAMPTZ NOT NULL,
+ resolved_at TIMESTAMPTZ,
+ resolved_by TEXT NOT NULL DEFAULT ''
+);
+
+CREATE UNIQUE INDEX IF NOT EXISTS uq_cost_exceptions_subject
+ ON cost_exceptions (subject_type, subject_id, reason);
+CREATE INDEX IF NOT EXISTS idx_cost_exceptions_open
+ ON cost_exceptions (status, created_at DESC);
+
+DO $$
+BEGIN
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_cost_exceptions_status') THEN
+ ALTER TABLE cost_exceptions
+ ADD CONSTRAINT chk_cost_exceptions_status CHECK (status IN ('open', 'resolved', 'ignored'));
+ END IF;
+END $$;
+
+COMMENT ON TABLE cost_exceptions IS '成本缺失/冲突异常队列:报表必须把 open 数量暴露出来,禁止静默按 0 计';
+
+
+-- ============================================================================
+-- §3 订单项口径补齐(金额与数量)
+-- ----------------------------------------------------------------------------
+-- 现状实测:order_items 只有 sku_code/sku_name/quantity/spec_json/snapshot,**没有任何金额列**;
+-- 且 quantity 不可信(316,585 项里 316,578 项 = 1,名称带「N个」的有 104,710 项)。
+-- 现状利好:目前每笔已支付订单恰好 1 个订单项(316,585 单 = 316,585 项),
+-- 所以 orders.total_amount 暂时可当订单项收入;但 schema 允许多项,属**潜在**缺口。
+--
+-- 【口径已确认 2026-10-17】收入 = 用户付款金额,取 orders.total_amount:
+-- · 91:total_amount 即用户付款(与快手发码配合)。数据核验:91 已支付 312,569 笔中
+-- 235,271 笔含快手 kuaishouSendCode.paymentFen,其中 235,268 笔与 total_amount **完全一致**
+-- (100.00%),仅 3 笔差 ¥1.80~2.00(2026-09-05 / 09-18 / 10-04,可单独抽查)。
+-- · PDD:order_amount;抖音:pay_amount。三者同为“用户实付”,故统一 revenue_basis = 'user_payment'。
+-- · 打手报酬 = **支出/成本**(最终由打手提现、我方付款),因此按**总额法**:
+-- 收入记用户实付全额,打手报酬作为履约成本,不做代收代付净额。
+-- · 退款/仲裁**暂未接入**:refunded_amount_fen / refunded_at 为**预留字段**,本轮不写逻辑。
+-- ============================================================================
+
+ALTER TABLE order_items
+ ADD COLUMN IF NOT EXISTS canonical_product_id BIGINT REFERENCES product_catalog(id) ON DELETE RESTRICT,
+ ADD COLUMN IF NOT EXISTS normalized_quantity INTEGER,
+ ADD COLUMN IF NOT EXISTS revenue_amount_fen BIGINT,
+ ADD COLUMN IF NOT EXISTS revenue_basis TEXT NOT NULL DEFAULT '',
+ ADD COLUMN IF NOT EXISTS refunded_amount_fen BIGINT NOT NULL DEFAULT 0,
+ ADD COLUMN IF NOT EXISTS refunded_at TIMESTAMPTZ;
+
+DO $$
+BEGIN
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_order_items_revenue_basis') THEN
+ ALTER TABLE order_items
+ ADD CONSTRAINT chk_order_items_revenue_basis
+ CHECK (revenue_basis IN ('', 'user_payment', 'manual_adjustment'));
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_order_items_revenue_amount') THEN
+ ALTER TABLE order_items ADD CONSTRAINT chk_order_items_revenue_amount CHECK (revenue_amount_fen IS NULL OR revenue_amount_fen >= 0);
+ END IF;
+END $$;
+
+CREATE INDEX IF NOT EXISTS idx_order_items_canonical_product ON order_items (canonical_product_id);
+
+COMMENT ON COLUMN order_items.normalized_quantity IS '归一化件数:从 SKU 名称解析并固化,禁止直接用 quantity 做销量';
+COMMENT ON COLUMN order_items.revenue_amount_fen IS '收入(分)= 用户实付,取 orders.total_amount;revenue_basis 通常为 user_payment';
+COMMENT ON COLUMN order_items.refunded_at IS '【预留】退款时间:退款/仲裁暂未接入,本轮不写逻辑;接入后按日净销售额 = 销售额 - 退款额';
+
+-- 过渡:让工单规则逐步改用成本中心(有 cost_item_id 走成本中心,否则回退旧金额字段)
+ALTER TABLE work_product_rules
+ ADD COLUMN IF NOT EXISTS cost_item_id BIGINT REFERENCES cost_items(id) ON DELETE RESTRICT,
+ ADD COLUMN IF NOT EXISTS legacy_cost_source TEXT NOT NULL DEFAULT 'rule_amount';
+
+CREATE INDEX IF NOT EXISTS idx_work_product_rules_cost_item ON work_product_rules (cost_item_id);
+
+COMMENT ON COLUMN work_product_rules.cost_item_id IS '迁移用过渡列:非空则金额取成本中心,空则回退 reward_amount/unit_price_fen';
+COMMENT ON COLUMN work_product_rules.legacy_cost_source IS '回退来源标记:rule_amount=旧规则金额,便于统计未迁移规则数';
+-- 口径已确认(2026-10-17):打手报酬是**支出**(工单结算 -> 可提现 -> 提现打款),
+-- 按总额法计入履约成本,与用户实付收入分开记,不做代收代付净额。
+
+
+-- ============================================================================
+-- §4 销售事实表(店铺 × 日期 × 标准商品)
+-- ============================================================================
+
+CREATE TABLE IF NOT EXISTS sales_daily_facts (
+ id BIGSERIAL PRIMARY KEY,
+ stat_date DATE NOT NULL,
+ provider TEXT NOT NULL,
+ shop_id TEXT NOT NULL DEFAULT '',
+ canonical_product_id BIGINT NOT NULL REFERENCES product_catalog(id) ON DELETE RESTRICT,
+ carrier_type TEXT NOT NULL,
+ order_count INTEGER NOT NULL DEFAULT 0,
+ item_count INTEGER NOT NULL DEFAULT 0,
+ gross_revenue_fen BIGINT NOT NULL DEFAULT 0,
+ refund_amount_fen BIGINT NOT NULL DEFAULT 0,
+ net_revenue_fen BIGINT NOT NULL DEFAULT 0,
+ cost_amount_fen BIGINT NOT NULL DEFAULT 0,
+ cost_missing_count INTEGER NOT NULL DEFAULT 0,
+ gross_profit_fen BIGINT NOT NULL DEFAULT 0,
+ computed_at TIMESTAMPTZ NOT NULL,
+ created_at TIMESTAMPTZ NOT NULL,
+ updated_at TIMESTAMPTZ NOT NULL
+);
+
+CREATE UNIQUE INDEX IF NOT EXISTS uq_sales_daily_facts_key
+ ON sales_daily_facts (stat_date, provider, shop_id, canonical_product_id, carrier_type);
+CREATE INDEX IF NOT EXISTS idx_sales_daily_facts_date ON sales_daily_facts (stat_date DESC);
+CREATE INDEX IF NOT EXISTS idx_sales_daily_facts_shop ON sales_daily_facts (provider, shop_id, stat_date DESC);
+
+DO $$
+BEGIN
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_sales_daily_facts_carrier') THEN
+ ALTER TABLE sales_daily_facts
+ ADD CONSTRAINT chk_sales_daily_facts_carrier
+ CHECK (carrier_type IN ('affiliate_dash', 'work_order', 'legacy', 'manual'));
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_sales_daily_facts_profit') THEN
+ ALTER TABLE sales_daily_facts
+ ADD CONSTRAINT chk_sales_daily_facts_profit
+ CHECK (gross_profit_fen = net_revenue_fen - cost_amount_fen);
+ END IF;
+END $$;
+
+COMMENT ON TABLE sales_daily_facts IS '店铺×日期(Asia/Shanghai)×标准商品×载体 的销售与成本事实;报表直接用这张表';
+COMMENT ON COLUMN sales_daily_facts.stat_date IS '销售日按 Asia/Shanghai 归属,取 paid_at;退款按 refunded_at 归期';
+COMMENT ON COLUMN sales_daily_facts.cost_missing_count IS '成本缺失件数:>0 时该行毛利不可信,报表必须标注';
+
+
+-- ============================================================================
+-- §5 校验与重算(跑完后必须对平,差异不为 0 说明实现有问题)
+-- ============================================================================
+
+-- Q1 成本解析覆盖率:open 异常占比应低于约定阈值,且不得为 0 成本静默通过
+-- SELECT carrier_type, count(*) FILTER (WHERE source = 'missing') AS missing,
+-- count(*) AS total,
+-- round(100.0 * count(*) FILTER (WHERE source = 'missing') / count(*), 2) AS missing_pct
+-- FROM cost_snapshots GROUP BY 1;
+
+-- Q2 毛利守恒:销售事实的净收入必须等于订单项收入汇总(按同一维度)
+-- SELECT f.stat_date, f.provider, f.shop_id,
+-- f.net_revenue_fen AS fact_revenue,
+-- coalesce(sum(oi.revenue_amount_fen - oi.refunded_amount_fen), 0) AS item_revenue
+-- FROM sales_daily_facts f
+-- LEFT JOIN order_items oi ON oi.canonical_product_id = f.canonical_product_id
+-- LEFT JOIN orders o ON o.id = oi.order_id AND o.provider = f.provider AND o.shop_id = f.shop_id
+-- GROUP BY 1, 2, 3, f.net_revenue_fen
+-- HAVING f.net_revenue_fen <> coalesce(sum(oi.revenue_amount_fen - oi.refunded_amount_fen), 0)
+-- LIMIT 100;
+
+-- Q3 历史成本不回写校验:改 cost_versions 后,已有 cost_snapshots 金额不得变化
+-- SELECT count(*) FROM cost_snapshots WHERE resolved_at < now() - interval '1 day' AND created_at <> resolved_at;
+
+
+-- ============================================================================
+-- §6 落地顺序(与 docs/成本中心与三块履约模型.md §九 一致)
+-- 1) §1 商品主数据 + 渠道映射 -> 解决「同一商品 47 个 SKU 码」与销量口径
+-- 2) §3 订单项口径(数量/收入)-> 解决 7.3 倍数量低估与收入口径不统一
+-- 3) §2 成本中心影子期 -> 自动发货接上游固定成本;工单接现有规则金额;人工接按单成本
+-- 4) §4 销售事实表 -> 店铺/日期/皮肤 的销量、收入、成本、毛利
+-- 5) 之后才是影子总账(docs/财务系统集成方案.md)
+--
+-- 【口径已确认 2026-10-17】
+-- a) 收入 = 用户付款金额(orders.total_amount),计收入;已用数据核验与快手 paymentFen 100% 对齐。
+-- b) 打手报酬 = 支出/成本(最终提现打款),按总额法。
+-- c) 人工块:第一优先级是**人工支出可追溯**(付款项目/金额/订单号/付款人/凭证),
+-- 见 `2026-10-17-人工支出登记-DDL草案.sql`;按单还是按小时的取值口径仍待定,先按单登记。
+-- d) 退款/仲裁:**暂未接入,先预留**(字段 + reversal_of_id + 事件类型,不实现逻辑)。
+--
+-- 【仍待确认】
+-- 1) 收入确认时点(付款 / 发货 / 验收 / 售后期结束)——影响利润表与预收账款;
+-- 2) 自动发货 cost_amount 是否稳定、是否含平台积分/汇率;
+-- 3) 平台服务费/支付通道费由谁承担、结算周期与账单口径;
+-- 4) 寄卖货款的采购成本确认时点。
+-- ============================================================================
diff --git a/docs/drafts/2026-10-17-载体判定与人工块量化.sql b/docs/drafts/2026-10-17-载体判定与人工块量化.sql
new file mode 100644
index 00000000..ca9d276f
--- /dev/null
+++ b/docs/drafts/2026-10-17-载体判定与人工块量化.sql
@@ -0,0 +1,287 @@
+-- ============================================================================
+-- 载体判定与人工块量化(只读量化 + 回填草案)
+-- ----------------------------------------------------------------------------
+-- 状态:草案(DRAFT)。**未接入** apps/backend/src/db/migrations。
+-- 接入时应拆成 `100_order_item_carrier.sql`(见 §3),并先跑 `npm run check:sql`。
+-- 基线:main @ ccd5ed2a;数据来源 = 本地只读克隆(生产快照,orders 316,597,数据截至 2026-10-14 14:52)
+-- 金额单位:分(BIGINT)。orders.total_amount / work_orders.reward_amount / source_order_amount_fen 均为分。
+-- 口径基准:§1 所有查询以「已支付订单项」为单位;人工块 = 既无 fulfillment_tasks 也无 work_orders。
+-- 设计依据:docs/成本中心与三块履约模型.md、docs/README.md 第 2 条口径。
+-- ============================================================================
+
+
+-- ============================================================================
+-- §0 只读安全(对生产库执行 §1 时先设置;只跑 SELECT)
+-- ============================================================================
+-- SET default_transaction_read_only = on;
+-- SET statement_timeout = '60s';
+-- 所有查询里的“截止时间”统一由 CTE w 取 max(paid_at),避免写死日期导致窗口漂移。
+
+
+-- ============================================================================
+-- §1 量化查询(只读,可直接对生产/克隆执行)
+-- ============================================================================
+
+-- Q1 载体分布:按来源统计「自动发货 / 工单 / 人工 / 双载体冲突」
+-- 实测(快照,近 30 天):91 人工 38,451(34.2%,¥485.3 万);PDD 人工 2,066(55.6%,¥36.1 万);
+-- 抖音 人工 2/4;双载体冲突 = 0(三载体互斥在数据上成立)。
+WITH w AS (
+ SELECT max(paid_at) AS hi FROM orders WHERE pay_status = 'paid' AND paid_at IS NOT NULL
+),
+base AS (
+ SELECT oi.id AS item_id,
+ o.provider,
+ o.shop_id,
+ o.total_amount,
+ EXISTS (SELECT 1 FROM fulfillment_tasks ft
+ WHERE ft.order_item_id = oi.id AND ft.executor_key = 'affiliate_dash') AS has_auto,
+ EXISTS (SELECT 1 FROM fulfillment_tasks ft WHERE ft.order_item_id = oi.id) AS has_task,
+ EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id) AS has_wo
+ FROM order_items oi
+ JOIN orders o ON o.id = oi.order_id, w
+ WHERE o.pay_status = 'paid'
+ AND o.paid_at >= w.hi - interval '30 days'
+)
+SELECT provider,
+ count(*) AS paid_items,
+ count(*) FILTER (WHERE has_auto) AS auto_items,
+ count(*) FILTER (WHERE has_wo) AS work_order_items,
+ count(*) FILTER (WHERE NOT has_task AND NOT has_wo) AS manual_items,
+ count(*) FILTER (WHERE has_task AND has_wo) AS conflict_items,
+ round(sum(total_amount) FILTER (WHERE NOT has_task AND NOT has_wo) / 100.0) AS manual_yuan,
+ round(100.0 * count(*) FILTER (WHERE NOT has_task AND NOT has_wo) / count(*), 1) AS manual_pct
+FROM base
+GROUP BY provider
+ORDER BY manual_items DESC;
+
+-- Q1b 非 affiliate_dash 的任务执行器(识别历史执行器,避免把“历史自动”误判成人工)
+-- 实测:affiliate_dash 30,902 项;kuaishou_ct_assisted 262 项(= KUAISHOU_CLOUD,已下线,见 executors/registry.ts)
+WITH w AS (
+ SELECT max(paid_at) AS hi FROM orders WHERE pay_status = 'paid' AND paid_at IS NOT NULL
+),
+base AS (
+ SELECT oi.id AS item_id
+ FROM order_items oi JOIN orders o ON o.id = oi.order_id, w
+ WHERE o.pay_status = 'paid' AND o.paid_at >= w.hi - interval '30 days'
+)
+SELECT ft.executor_key, count(DISTINCT b.item_id) AS items, count(*) AS tasks
+FROM base b JOIN fulfillment_tasks ft ON ft.order_item_id = b.item_id
+GROUP BY 1 ORDER BY 2 DESC;
+
+-- Q2 人工块:店铺集中度 + 金额分位(用于判断监管收益与人工成本量级)
+-- 实测:91 只有 8 个店铺,avg ¥126.22 / p50 ¥53 / p90 ¥288;≥¥100 有 13,229 项,≥¥500 有 2,708 项。
+WITH w AS (
+ SELECT max(paid_at) AS hi FROM orders WHERE pay_status = 'paid' AND paid_at IS NOT NULL
+),
+base AS (
+ SELECT o.provider, o.shop_id, o.shop_name, o.total_amount
+ FROM order_items oi JOIN orders o ON o.id = oi.order_id, w
+ WHERE o.pay_status = 'paid' AND o.paid_at >= w.hi - interval '30 days'
+ AND NOT EXISTS (SELECT 1 FROM fulfillment_tasks ft WHERE ft.order_item_id = oi.id)
+ AND NOT EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id)
+)
+SELECT provider,
+ count(DISTINCT shop_id) AS shops,
+ count(*) AS items,
+ round(avg(total_amount) / 100.0, 2) AS avg_yuan,
+ round(percentile_disc(0.5) WITHIN GROUP (ORDER BY total_amount) / 100.0, 2) AS p50_yuan,
+ round(percentile_disc(0.9) WITHIN GROUP (ORDER BY total_amount) / 100.0, 2) AS p90_yuan,
+ count(*) FILTER (WHERE total_amount >= 10000) AS ge_100y,
+ count(*) FILTER (WHERE total_amount >= 50000) AS ge_500y
+FROM base GROUP BY provider ORDER BY items DESC;
+
+-- Q3 人工块是否存在任何“完成信号”
+-- 实测:40,519 项全部 order_status='paid' AND pay_status='paid' AND claim_status='active' —— 零完成信号。
+WITH w AS (
+ SELECT max(paid_at) AS hi FROM orders WHERE pay_status = 'paid' AND paid_at IS NOT NULL
+)
+SELECT o.order_status, o.pay_status, o.claim_status, count(*) AS items,
+ round(sum(o.total_amount) / 100.0) AS yuan
+FROM order_items oi JOIN orders o ON o.id = oi.order_id, w
+WHERE o.pay_status = 'paid' AND o.paid_at >= w.hi - interval '30 days'
+ AND NOT EXISTS (SELECT 1 FROM fulfillment_tasks ft WHERE ft.order_item_id = oi.id)
+ AND NOT EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id)
+GROUP BY 1, 2, 3 ORDER BY items DESC;
+
+-- Q4 月度趋势:确认人工块不是历史遗留
+-- 实测:2026-08 人工 46,337 / 已支付 142,969;2026-09 39,148 / 103,010;2026-10(部分月)17,469 / 55,647。
+SELECT to_char(o.paid_at, 'YYYY-MM') AS mon,
+ count(*) AS paid_items,
+ count(*) FILTER (
+ WHERE NOT EXISTS (SELECT 1 FROM fulfillment_tasks ft WHERE ft.order_item_id = oi.id)
+ AND NOT EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id)
+ ) AS manual_items
+FROM order_items oi JOIN orders o ON o.id = oi.order_id
+WHERE o.pay_status = 'paid' AND o.paid_at >= '2026-08-01'
+GROUP BY 1 ORDER BY 1;
+
+-- Q5 数量口径:quantity 不可用于“卖了多少件”
+-- 实测(全量已支付 316,585 项):quantity=1 的有 316,578 项;quantity>1 仅 7 项;
+-- 其中 104,710 项的名称里带「N个」;sum(quantity)=316,593,而按名称解析 sum=2,316,222 —— 相差 7.3 倍。
+SELECT count(*) AS items,
+ count(*) FILTER (WHERE quantity = 1) AS qty_eq_1,
+ count(*) FILTER (WHERE quantity = 1 AND sku_name ~ '[0-9]+[ ]*个') AS qty1_name_has_num,
+ count(*) FILTER (WHERE quantity > 1) AS qty_gt_1,
+ sum(quantity) AS sum_quantity,
+ sum(coalesce((regexp_match(sku_name, '([0-9]+)[ ]*个'))[1]::int, quantity)) AS sum_name_quantity
+FROM order_items oi JOIN orders o ON o.id = oi.order_id
+WHERE o.pay_status = 'paid';
+
+-- Q6 SKU 多码一名:同一商品名对应多个 sku_code(必须先做映射再统计)
+-- 实测(全量已支付):苏尔南钻石100颗 47 个码 / 3,319 项;指挥官密钥1个 42 个码 / 26,748 项;
+-- 指挥官密钥5个 41 个码;指挥官密钥10个 37 个码;苏尔南钻石50颗 35 个码。
+SELECT sku_name, count(DISTINCT sku_code) AS distinct_sku, count(*) AS items
+FROM order_items oi JOIN orders o ON o.id = oi.order_id
+WHERE o.pay_status = 'paid'
+GROUP BY 1
+HAVING count(DISTINCT sku_code) > 5
+ORDER BY 2 DESC
+LIMIT 20;
+
+
+-- ============================================================================
+-- §2 判定规则(唯一口径,代码与回填必须一致)
+-- ----------------------------------------------------------------------------
+-- 执行载体 carrier_type ∈ { affiliate_dash, work_order, legacy, manual }
+-- ※ 与文档三分类的差异:数据中存在已下线执行器(kuaishou_ct_assisted / kuaishou_feifei /
+-- kuaishou-industry)的存量任务,单列 legacy,不并入 affiliate_dash,也不并入 manual。
+-- 近 30 天 legacy = 262 项。需业务确认其归属(建议只读展示、不计入自动发货口径)。
+--
+-- 判定优先级(严格顺序,命中即停):
+-- 1. 存在 fulfillment_tasks 且 executor_key IN ('affiliate_dash') -> affiliate_dash
+-- 2. 存在 fulfillment_tasks 且 executor_key IN ('kuaishou_ct_assisted',
+-- 'kuaishou_feifei', 'kuaishou-industry') -> legacy
+-- 3. 存在 work_orders(任意状态,含已取消) -> work_order
+-- 4. 以上都无 -> manual
+-- 命中 1/2 且 3 同时成立 = 冲突,写入异常队列,不静默取其一(当前实测 0 条)。
+--
+-- carrier_status ∈ { pending, active, completed, failed, cancelled }
+-- 终态判定:affiliate_dash 取 task_status IN ('redeemed','closed');work_order 取 status='accepted';
+-- manual 只能由人工 case 完成时写入(当前无表,见 docs/成本中心与三块履约模型.md §6)。
+-- 宽限期:paid_at 之后 N 小时(建议 24h)内允许 carrier_type 为空(判定未跑完);
+-- 超过宽限期仍无载体 -> 强制置 manual 并建 case,禁止长期为空。
+--
+-- 重要:本判定是**派生列**,不是事实来源。事实来源仍是 fulfillment_tasks / work_orders 存在性;
+-- 派生列只用于报表、监管与成本挂载,必须提供重算作业(见 §4)。
+-- ============================================================================
+
+
+-- ============================================================================
+-- §3 回填草案(接入时拆成 100_order_item_carrier.sql;分三步,可回滚)
+-- ============================================================================
+
+-- 3.1 加列(ADD COLUMN + 常量 DEFAULT 在 PG11+ 为 metadata-only,不重写 31 万行)
+ALTER TABLE order_items
+ ADD COLUMN IF NOT EXISTS carrier_type TEXT,
+ ADD COLUMN IF NOT EXISTS carrier_status TEXT,
+ ADD COLUMN IF NOT EXISTS carrier_decided_at TIMESTAMPTZ,
+ ADD COLUMN IF NOT EXISTS carrier_reason TEXT NOT NULL DEFAULT '';
+
+DO $$
+BEGIN
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_order_items_carrier_type') THEN
+ ALTER TABLE order_items
+ ADD CONSTRAINT chk_order_items_carrier_type
+ CHECK (carrier_type IS NULL OR carrier_type IN ('affiliate_dash', 'work_order', 'legacy', 'manual'));
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'chk_order_items_carrier_status') THEN
+ ALTER TABLE order_items
+ ADD CONSTRAINT chk_order_items_carrier_status
+ CHECK (carrier_status IS NULL OR carrier_status IN ('pending', 'active', 'completed', 'failed', 'cancelled'));
+ END IF;
+END $$;
+
+COMMENT ON COLUMN order_items.carrier_type IS '执行载体(派生): affiliate_dash(自动发货)/work_order(接单工单)/legacy(已下线执行器存量)/manual(人工客服)';
+COMMENT ON COLUMN order_items.carrier_status IS '载体状态: pending/active/completed/failed/cancelled';
+COMMENT ON COLUMN order_items.carrier_decided_at IS '载体判定时间(重算作业会更新)';
+COMMENT ON COLUMN order_items.carrier_reason IS '载体判定原因或人工改派说明';
+
+-- 3.2 回填(只填 NULL,可反复执行;建议按 10 万行分批,避免长事务)
+-- 注意:不要在 UPDATE 里直接 JOIN 多张一对多表,统一用 EXISTS,避免行放大。
+UPDATE order_items oi
+SET carrier_type = CASE
+ WHEN EXISTS (SELECT 1 FROM fulfillment_tasks ft
+ WHERE ft.order_item_id = oi.id AND ft.executor_key = 'affiliate_dash')
+ THEN 'affiliate_dash'
+ WHEN EXISTS (SELECT 1 FROM fulfillment_tasks ft
+ WHERE ft.order_item_id = oi.id
+ AND ft.executor_key IN ('kuaishou_ct_assisted', 'kuaishou_feifei', 'kuaishou-industry'))
+ THEN 'legacy'
+ WHEN EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id)
+ THEN 'work_order'
+ ELSE 'manual'
+ END,
+ carrier_decided_at = now(),
+ carrier_reason = 'backfill:2026-10-17'
+WHERE oi.carrier_type IS NULL
+ AND oi.id IN (
+ SELECT oi2.id
+ FROM order_items oi2 JOIN orders o ON o.id = oi2.order_id
+ WHERE o.pay_status = 'paid' AND o.paid_at < now() - interval '24 hours'
+ LIMIT 100000
+ );
+
+-- 3.3 索引(CONCURRENTLY 不能放在事务里,需单独执行;生产执行前确认无长事务)
+-- CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_order_items_carrier_type
+-- ON order_items (carrier_type);
+-- CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_fulfillment_tasks_order_item_executor
+-- ON fulfillment_tasks (order_item_id, executor_key);
+-- CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_work_orders_order_item
+-- ON work_orders (order_item_id);
+
+-- 3.4 回滚
+-- ALTER TABLE order_items DROP CONSTRAINT IF EXISTS chk_order_items_carrier_type;
+-- ALTER TABLE order_items DROP CONSTRAINT IF EXISTS chk_order_items_carrier_status;
+-- ALTER TABLE order_items
+-- DROP COLUMN IF EXISTS carrier_type,
+-- DROP COLUMN IF EXISTS carrier_status,
+-- DROP COLUMN IF EXISTS carrier_decided_at,
+-- DROP COLUMN IF EXISTS carrier_reason;
+
+
+-- ============================================================================
+-- §4 校验(每次重算后必须跑;差异不为 0 说明派生列与事实不一致)
+-- ============================================================================
+
+-- Q7 守恒校验:三类 + legacy 必须等于已支付订单项总数
+WITH w AS (
+ SELECT max(paid_at) AS hi FROM orders WHERE pay_status = 'paid' AND paid_at IS NOT NULL
+),
+paid AS (
+ SELECT oi.id AS item_id
+ FROM order_items oi JOIN orders o ON o.id = oi.order_id, w
+ WHERE o.pay_status = 'paid' AND o.paid_at >= w.hi - interval '30 days'
+)
+SELECT count(*) AS paid_items,
+ count(*) FILTER (WHERE oi.carrier_type = 'affiliate_dash') AS auto_items,
+ count(*) FILTER (WHERE oi.carrier_type = 'work_order') AS work_order_items,
+ count(*) FILTER (WHERE oi.carrier_type = 'legacy') AS legacy_items,
+ count(*) FILTER (WHERE oi.carrier_type = 'manual') AS manual_items,
+ count(*) FILTER (WHERE oi.carrier_type IS NULL) AS still_null,
+ count(*) FILTER (WHERE oi.carrier_type = 'manual'
+ AND EXISTS (SELECT 1 FROM fulfillment_tasks ft WHERE ft.order_item_id = oi.id)
+ OR oi.carrier_type <> 'manual'
+ AND NOT EXISTS (SELECT 1 FROM fulfillment_tasks ft WHERE ft.order_item_id = oi.id)
+ AND NOT EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id)
+ ) AS mismatched
+FROM paid p JOIN order_items oi ON oi.id = p.item_id;
+
+-- Q8 冲突与长期无载体(应进异常队列,不进报表)
+SELECT oi.id, oi.order_id, oi.carrier_type,
+ EXISTS (SELECT 1 FROM fulfillment_tasks ft WHERE ft.order_item_id = oi.id) AS has_task,
+ EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id) AS has_wo
+FROM order_items oi
+WHERE oi.carrier_type IN ('affiliate_dash', 'legacy')
+ AND EXISTS (SELECT 1 FROM work_orders wo WHERE wo.order_item_id = oi.id)
+LIMIT 100;
+
+
+-- ============================================================================
+-- §5 人工成本区间(按单成本 × 人工块件数)
+-- ----------------------------------------------------------------------------
+-- 人工块实测:40,519 项 / 30 天(91 38,451 + PDD 2,066 + 抖音 2),涉及金额 ¥5,214,708。
+-- 单均人工成本 X 元时的月度人工成本:
+-- X=3 -> ¥121,557 X=5 -> ¥202,595 X=8 -> ¥324,152 X=15 -> ¥607,785
+-- 说明:这是“人工履约投入”的量级估算,不含商品/货源成本;商品成本走 cost_versions。
+-- 建议先按单固定成本落地,同时记录 started_at/completed_at 以备后续按工时核算。
+-- ============================================================================
diff --git a/docs/履约配置/cloudtentacles发货平台.md b/docs/履约配置/cloudtentacles发货平台.md
index bda9dfcb..2507703a 100644
--- a/docs/履约配置/cloudtentacles发货平台.md
+++ b/docs/履约配置/cloudtentacles发货平台.md
@@ -1,5 +1,5 @@
> ⚠ 本仓库当前**未接入** cloudtentacles 通道:该履约通道已下线,`apps/backend/src/services/platforms/` 下已无对应适配器,`fulfillment/executors/registry.ts:18-19` 对历史通道只保留只读 legacy stub(`legacy-retired-executor.ts`)。本文是第三方(cloudtentacles)官方接口样例,仅作外部参考。
-> 现行自动履约通道是 affiliate-dash(`apps/backend/src/services/platforms/affiliate-dash/`,走 `/api/client/v1`);通道现状见 `docs/ affiliate_dash_发货平台/affiliate-dash对接.md`。
+> 现行自动履约通道是 affiliate-dash(`apps/backend/src/services/platforms/affiliate-dash/`,走 `/api/client/v1`);通道现状见 `docs/affiliate-dash-发货平台/affiliate-dash对接.md`。
查询SKU列表
```
diff --git a/docs/履约配置/多发货平台与电子凭证整体链路.md b/docs/履约配置/多发货平台与电子凭证整体链路.md
index c07f6feb..0f099c84 100644
--- a/docs/履约配置/多发货平台与电子凭证整体链路.md
+++ b/docs/履约配置/多发货平台与电子凭证整体链路.md
@@ -1,12 +1,15 @@
# 多发货平台与电子凭证整体链路
+> 状态:现行。最后校准:2026-10-17(`main @ 6850bc64`)。
+> 来源口径:本系统只有三个订单来源——`kuaishou-91`、`pdd`、`douyin-mashida`。**91 回调与快手 send-code 是同一个 `kuaishou-91` 来源的两条数据通道**(91=骨架权威源,快手 send-code=补全/兜底源),不是一个来源加另一个来源。
+
## 1. 当前结论
当前系统需要把四类能力拆开理解:
| 模块 | 定位 | 说明 |
| --- | --- | --- |
-| 91 卡券 | 订单来源 + 发链接通道 | 91 调用我方下单接口,我方生成领取链接;91 查询订单时拿到领取链接,并发送给用户。 |
+| `kuaishou-91` | 订单来源 | 91 回调提供订单骨架;快手 send-code 提供商品/SKU 补全与兜底。两者共享 `kuaishou-91` 来源与同一店铺键空间。 |
| affiliate-dash | 现行发货平台(自动履约) | 当前唯一自动履约通道,order_site 作为其商户走开放接口 `/api/client/v1`,发放皮肤、道具。 |
| kuaishou-cloud / kuaishou-feifei | 已下线发货平台 | 历史乐玩 / 飞飞通道已下线,executor 仅保留只读 legacy stub(`legacy-retired-executor.ts`),不再发货。 |
| 快手电子凭证 | 领取流程里的自动核销能力 | 用来替代用户在领取页手动输入核销码的步骤,不替代 91,也不替代发货平台。 |
@@ -14,16 +17,29 @@
因此整体设计不是“新增订单来源”,而是:
```text
-91 负责进单和把领取链接发给用户
+kuaishou-91(91 回调 + 快手 send-code)负责进单与补全
我方领取页负责统一承接用户
kuaishou-cloud / kuaishou-feifei 通道已下线,affiliate-dash 负责真实发货
快手电子凭证负责把领取页里的核销码输入步骤自动化
```
+### 1.1 执行载体三块
+
+真正决定成本和监管方式的是订单项的**执行载体**:
+
+| 载体 | 当前实现 | 说明 |
+| --- | --- | --- |
+| `affiliate_dash` | 自动履约任务 | 上游固定成本,适合店铺/日期/皮肤销量统计 |
+| `work_order` | 接单平台工单 | 变动成本,适合影子总账与打手结算 |
+| `manual` | 目前没有显式 case | 无自动任务且无工单的已支付订单项;当前最大监管缺口 |
+
+详细设计见 [../成本中心与三块履约模型.md](../成本中心与三块履约模型.md)。
+
## 2. 目标链路
```text
-91 卡券下单
+kuaishou-91 进单
+ (91 回调 = 订单骨架权威源;快手 send-code = 商品/SKU 补全与兜底)
-> 我方创建内部订单和履约任务
-> 按商品选择发货平台
-> affiliate-dash(自动履约)
diff --git a/docs/影子总账说明.md b/docs/影子总账说明.md
new file mode 100644
index 00000000..4caf0048
--- /dev/null
+++ b/docs/影子总账说明.md
@@ -0,0 +1,324 @@
+# 影子总账到底是什么意思
+
+> 状态:**财务系统设计说明**
+> 代码基线:`main @ 6850bc64`
+> 配套方案:[财务系统集成方案.md](财务系统集成方案.md)
+> 一句话:**影子总账是一套“跟着真实业务跑、但暂时不接管余额”的复式会计账。** 它先把每一笔业务翻译成借贷分录,和现有钱包/订单/提现数据逐日对账;确认长期对得平之后,再让它成为正式总账。
+>
+> 边界:影子总账解决“钱怎么记、怎么对”,不负责“商品是什么、成本是多少、人工谁处理”。商品与成本见 [成本中心与三块履约模型.md](成本中心与三块履约模型.md),人工履约见同一文档第八节。
+
+---
+
+## 一、为什么叫“影子”
+
+“影子”不是指数据是假的,而是指它的**地位**:
+
+- 现有 `worker_wallets` 仍然是运行时余额的权威来源;
+- 现有 `worker_wallet_ledgers` 仍然继续记录打手资金动作;
+- 影子总账在后台**并行**记录同一批业务事件,但暂时不决定用户能提现多少、能接多少单;
+- 它像影子一样跟着真实系统走,用来验证“如果我们用会计方式记账,能不能和现有系统对得上”。
+
+```text
+真实业务
+ ├─ 现有链路:改订单 / 改工单 / 改钱包 / 改提现单 ← 仍然权威
+ └─ 影子链路:业务事件 → 复式分录 → 总账余额 ← 只观察、不接管
+```
+
+当影子账连续对平、业务口径确认后,才进入“转正”阶段:总账成为权威,钱包变成总账的余额投影。
+
+---
+
+## 二、它和现在的钱包流水有什么区别
+
+| 维度 | 现在的 `worker_wallet_ledgers` | 影子总账 |
+| --- | --- | --- |
+| 记账方式 | 单边流水:`+50` / `-20` | 复式分录:每笔有借方和贷方,必须平衡 |
+| 覆盖范围 | 主要是打手资金 | 订单收入、成本、打手报酬、押金、提现、寄卖货款、平台费 |
+| 余额 | `worker_wallets` 直接维护可变余额 | 由分录汇总/投影得出,可独立重算 |
+| 目的 | 让打手看到余额与流水 | 让财务能出报表、对账、关账、算毛利 |
+| 可修改性 | 余额可更新,流水是业务记录 | 已过账分录不可改,只能反向冲正 |
+| 维度 | 主要按 worker / work_order | 同时按 order / work_order / worker / shop / provider / settlement |
+| 失败影响 | 直接影响打手余额 | 影子期不影响业务;转正后与业务同事务 |
+
+### 最关键的区别:单边 vs 复式
+
+假设打手被冻结 20 元押金:
+
+- 现在只记一条:`frozen_deposit +20`,`available -20`;
+- 复式账要记两条:
+ - 借:打手可提现余额 20
+ - 贷:打手押金负债 20
+
+这样财务才能回答:这 20 元不是“消失了”,而是从“可提现负债”重分类成了“押金负债”。押金退回到哪、没收进哪个收入科目,也都有依据。
+
+---
+
+## 三、一个具体例子
+
+假设一笔订单:
+
+- 买家支付 100 元;
+- 工单验收,打手报酬 50 元;
+- 打手抢单时冻结押金 20 元;
+- 后来提现 10 元。
+
+### 1. 买家支付 100 元
+
+```text
+借:1002 支付通道待结算 100
+贷:2201 预收账款 100
+```
+
+此时钱包不变。这笔钱还不是收入,而是预收/待结算。
+
+### 2. 工单验收,确认收入与履约成本
+
+```text
+借:2201 预收账款 100
+贷:4001 商品销售收入 100
+
+借:5101 履约成本-打手报酬 50
+贷:2001 应付打手报酬 50
+```
+
+此时钱包仍不一定变,因为报酬可能还在待解冻期。
+
+### 3. 报酬转为可提现
+
+```text
+借:2001 应付打手报酬 50
+贷:2004 打手可提现余额 50
+```
+
+此时钱包 `available_amount +50`。
+
+### 4. 抢单冻结押金 20 元
+
+```text
+借:2004 打手可提现余额 20
+贷:2003 打手押金 20
+```
+
+钱包:`available -20`,`frozen_deposit +20`。这只是负债内部重分类,不是费用。
+
+### 5. 提现 10 元
+
+```text
+借:2004 打手可提现余额 10
+贷:1001 银行存款 10
+```
+
+钱包:`available -10`。
+
+### 6. 最终核对
+
+```text
+钱包 available = 50 - 20 - 10 = 20
+钱包 frozen_deposit = 20
+```
+
+总账中对应的负债也应分别是 20 和 20;如果对不上,就说明有事件漏记、重复记或金额映射错了。
+
+---
+
+## 四、影子总账在本项目怎么跑
+
+```mermaid
+flowchart LR
+ A["业务事务
订单/工单/提现/寄卖/售后"] --> B["finance_business_events
幂等事件"]
+ B --> C["finance-posting-service
借贷规则"]
+ C --> D["finance_journal_entries"]
+ D --> E["finance_journal_lines"]
+ E --> F["科目余额 / 钱包投影"]
+ G["现有 worker_wallets / ledgers"] --> H["每日对账"]
+ F --> H
+ I["平台账单 / 支付流水"] --> H
+ H --> J["差异清单
finance_reconciliation_items"]
+```
+
+### 1. 业务事件先落“收件箱”
+
+每个业务动作生成唯一事件:
+
+```text
+order:123:paid
+work_order:456:settled
+work_order:456:deposit_freeze
+withdrawal:789:approved
+consignment:abc:payout
+```
+
+写入 `finance_business_events`,`business_event_id` 唯一。重复投递直接忽略,保证幂等。
+
+### 2. 按规则生成借贷分录
+
+`finance-posting-service` 只做一件事:把事件翻译成分录。
+
+```text
+事件:work_order:456:settled
+分录:
+ 借 5101 履约成本 50
+ 贷 2001 应付打手报酬 50
+维度:orderId / workOrderId / workerId / shopId / provider
+```
+
+规则必须集中管理,不能散落在各个业务 service 里。
+
+### 3. 写凭证与分录
+
+- `finance_journal_entries`:凭证头(事件、时间、期间、状态)。
+- `finance_journal_lines`:借贷行(科目、金额、维度)。
+- 已过账凭证不可 UPDATE / DELETE;冲正用反向凭证。
+
+### 4. 生成余额投影
+
+有两种方式:
+
+- **实时投影**:分录写入后同步更新 `finance_account_balances` / `worker_wallets`;
+- **定时重算**:按期间从分录汇总,生成余额快照。
+
+影子期建议两者都做:实时投影用于展示,定时重算用于校验。
+
+### 5. 每日对账
+
+至少做四组对账:
+
+| 对账 | 左 | 右 |
+| --- | --- | --- |
+| 钱包余额 | `worker_wallets.available_amount` | 总账中打手可提现余额 |
+| 押金余额 | `worker_wallets.frozen_deposit_amount` | 总账中打手押金负债 |
+| 待解冻 | `worker_wallets.pending_unfreeze_amount` | 总账中待解冻明细 |
+| 平台应收 | 平台账单/结算单 | 总账中应收账款 |
+
+差异写入 `finance_reconciliation_items`,必须能定位到具体订单、工单、提现单或事件。
+
+---
+
+## 五、影子期和转正期的区别
+
+这是最容易混淆的地方。
+
+### 5.1 影子期(观察)
+
+- 现有钱包仍是权威;
+- 影子总账写入失败,**不应阻断业务**;
+- 失败事件进入重试/死信队列;
+- 每日对账发现漏记、重复、金额不一致;
+- 目标是把“事件覆盖率”和“对账差异”压到 0。
+
+因此影子期可以做成:
+
+```text
+业务事务提交
+ → 尝试写影子总账(best-effort)
+ → 失败只记录 finance_business_events.status=failed
+ → 定时任务重试
+```
+
+### 5.2 转正期(接管)
+
+- 总账成为权威;
+- 分录写入与业务状态变更必须在**同一个数据库事务**;
+- 分录写失败 → 业务回滚;
+- 钱包余额从总账投影读取;
+- 旧钱包字段保留一个版本用于回滚。
+
+```text
+业务事务开始
+ → 改业务状态
+ → 写总账事件 + 分录
+ → 更新钱包投影
+ → 提交
+```
+
+**影子期允许“账慢一点”,转正期不允许“账少一笔”。**
+
+---
+
+## 六、历史数据怎么处理
+
+不建议一上来把 32 万+ 历史订单全部重放。更稳妥的做法是:
+
+### 方案 A:期初余额 + 未来流水(推荐)
+
+1. 选一个切换日(例如 2026-11-01)。
+2. 把切换日各科目余额作为“期初分录”导入:
+ - 银行存款
+ - 平台应收
+ - 应付打手
+ - 打手押金
+ - 打手可提现余额
+ - 应付卖家货款
+3. 切换日之后的所有业务走影子总账。
+4. 历史报表继续由旧系统/旧查询提供,或后续再做历史回放。
+
+优点:风险低、上线快、不会因为历史脏数据卡住。
+缺点:历史订单级损益需要另行回放。
+
+### 方案 B:有限回放(用于验证)
+
+选最近 1–3 个月做回放:
+
+- 从 `orders`、`work_orders`、`worker_wallet_ledgers`、`worker_finance_requests`、`consignment_*` 重建事件;
+- 与现有钱包/报表逐日比对;
+- 验证 posting 规则,不要求覆盖全部历史。
+
+### 方案 C:全量回放(不推荐先行)
+
+历史数据可能存在:
+
+- 已删除/改写的业务记录;
+- 旧口径押金;
+- 多渠道补单;
+- 手工调账没有事件来源。
+
+全量回放很容易得到“永远对不平”的结果,反而拖慢项目。
+
+---
+
+## 七、转正标准
+
+建议至少满足:
+
+1. 所有核心事件类型已接入:订单支付/退款、工单结算、押金冻结/释放/没收、提现、充值、寄卖押金/货款、售后扣款/欠款。
+2. 连续 30 天:
+ - 借贷不平凭证数 = 0;
+ - 钱包余额与总账重算差异 = 0;
+ - 押金/待解冻/提现冻结差异 = 0;
+ - 平台应收差异可解释;
+ - 事件漏记率低于约定阈值(例如 0.1%)。
+3. 财务/运营完成抽样核对与签字。
+4. 有明确回滚开关:关闭总账投影读,回到旧钱包字段。
+5. 关账、冲正、手工分录权限与审计已就绪。
+
+---
+
+## 八、常见误区
+
+| 误区 | 实际 |
+| --- | --- |
+| 影子总账 = 数据库备份 | 不是。备份是数据副本,影子总账是另一套会计模型 |
+| 有 `worker_wallet_ledgers` 就是总账 | 不是。它是单边业务流水,不能出借贷平衡报表 |
+| 先把钱包表改成借贷 | 风险极高。应先并行建账,再切换读模型 |
+| 影子账可以先随便改 | 不行。已过账分录必须不可变,否则无法审计 |
+| 对账就是总账 | 不是。对账是校验手段,总账是记账体系 |
+| 历史数据必须全部重算 | 不一定。期初余额 + 未来流水通常更稳 |
+| 影子期 Ledger 写失败要回滚业务 | 影子期不应阻断业务;转正期才必须同事务 |
+
+---
+
+## 九、在本项目落地的最小步骤
+
+1. **定科目与口径**:收入确认时点、打手报酬是成本还是代付、平台费归属。
+2. **建表**:`finance_accounts`、`finance_business_events`、`finance_journal_entries`、`finance_journal_lines`、`finance_periods`、`finance_reconciliations`。
+3. **接 4 个高频事件**:订单支付、工单结算、押金冻结/释放/没收、提现。
+4. **做日对账**:钱包余额 vs 总账余额;先只报差异,不自动改账。
+5. **补事件**:寄卖押金/货款、售后欠款、充值。
+6. **做后台页面**:凭证簿、试算平衡、差异清单。
+7. **连续对平后切换**:总账成为权威,钱包改为投影。
+
+---
+
+## 十、一句话总结
+
+**影子总账就是“先用会计的方式把业务再记一遍,但不让它管钱;等它和现有钱包、平台账单长期对得上,再让它转正”。** 它的价值不是多一张表,而是提前暴露“钱从哪里来、到哪里去、哪一笔没记、哪一笔记错了”,避免财务系统上线后才发现历史资金链路对不平。
diff --git a/docs/成本中心与三块履约模型.md b/docs/成本中心与三块履约模型.md
new file mode 100644
index 00000000..9371117d
--- /dev/null
+++ b/docs/成本中心与三块履约模型.md
@@ -0,0 +1,544 @@
+# 成本中心与三块履约模型
+
+> 状态:**现行分析 / 设计基线(未写代码)**
+> 代码基线:`main @ 6850bc64`
+> 数据基线:2026-10-17 生产库只读聚合(`default_transaction_read_only=on`,仅 SELECT,无写入)
+> 关联:
+> - [项目全量分析报告-2026-10-17.md](项目全量分析报告-2026-10-17.md)
+> - [影子总账说明.md](影子总账说明.md)
+> - [财务系统集成方案.md](财务系统集成方案.md)
+
+---
+
+## 一、先纠正一个口径:三块不是“订单来源”,是“执行载体”
+
+同一个来源的订单,可能走自动发货、接单平台或人工。正确的拆分维度是**订单项最终由谁履约**:
+
+| 块 | 执行载体 | 成本特征 | 监管重点 |
+| --- | --- | --- | --- |
+| ① 自动发货 | `affiliate_dash` 履约任务 | 固定成本,按皮肤/SKU,很少变化 | 上游成本、店铺/日期/皮肤销量 |
+| ② 接单平台 | `work_orders` + 拼单份额 | 变动成本,按模板/单价/固定价/拼单 | 发单价格、打手提现、售后追缴、毛利 |
+| ③ 人工客服 | 目前**无载体** | 人工工时/按单服务成本 | 谁处理、何时完成、证据、成本、异常 |
+
+硬不变式:
+
+```text
+一个订单项 = 订单骨架 + 买家凭证 + 一个执行载体
+执行载体 ∈ { affiliate_dash, work_order, manual }
+```
+
+当前代码对抖音有 `snapshot.classification` 守卫,但 91/PDD 仍然主要靠 `isConfigured=false` 隐式表达“人工”。这会导致人工单在系统里没有显式状态,也没有处理记录。
+
+---
+
+## 二、生产数据证据(2026-10-17 只读)
+
+### 2.1 最近 30 天无载体订单
+
+“无载体”定义:已支付订单项,既没有 `fulfillment_tasks`,也没有 `work_orders`。
+
+| 来源 | 无载体订单数 | 无载体金额 | 说明 |
+| --- | ---: | ---: | --- |
+| 91 | **42,391** | 约 ¥624 万 | 几乎全部 `snapshot.isConfigured=false` |
+| PDD | **2,258** | 约 ¥39 万 | 几乎全部 `isConfigured=false` |
+| 抖音 | **8** | 约 ¥595 | 仅 8 单未命中,抖音分流已基本收口 |
+
+补充:
+
+- 2026-09 无载体订单项约 **37,708**;2026-08 约 **46,322**。这不是历史遗留,是持续发生。
+- `manual_dispatch` 任务全库只有 **6 条**,全部为 `manual_review`。
+- 这些无载体订单的 `claim_status` 全部为 `active`,没有完成信号。
+- 已验收工单对应的订单,`orders.order_status` 仍然是 `paid`:**订单主状态不能代表履约完成**。
+
+结论:**人工块不是少量兜底,而是每天上千单、金额很大的主业务块,但基本没有进入系统任务与状态机。**
+
+### 2.2 接单成本占比
+
+最近 30 天已验收工单:
+
+| 来源 | 工单数 | 打手报酬 | 订单金额口径 | 报酬/订单金额 |
+| --- | ---: | ---: | ---: | ---: |
+| 91 | 60,902 | ¥235.9 万 | ¥274.4 万 | 86.0% |
+| PDD | 1,289 | ¥20.3 万 | ¥25.4 万 | 80.0% |
+| 抖音 | 478 | ¥2.14 万 | ¥2.21 万 | 96.5% |
+
+**注意:91 的 `orders.total_amount` 来自 `maxAmount`(91 给的可接受金额/结算口径),PDD 来自 `order_amount`,抖音来自 `pay_amount`。三者不是同一财务口径,不能直接相加当收入。**
+
+### 2.3 SKU 口径问题
+
+最近 30 天,同一个商品名会对应多个 `sku_code`:
+
+- 「指挥官密钥1个」:**27 个** `sku_code`
+- 「1个精致荣耀专属礼盒」:10 个
+- 「苏尔南钻石100颗」:35 个
+
+因此:
+
+- 不能直接按 `sku_code` 做“什么皮肤卖了多少”;
+- 必须先建 `product_catalog` + `channel_product_mappings`,把平台 SKU 归一到 `canonical_product_id`;
+- 数量也不能只读 `order_items.quantity`,要从 SKU 文本解析(如「指挥官密钥5个」)。
+
+### 2.4 当前订单结构
+
+最近 30 天,三个来源的订单基本都是 **1 个订单 1 个订单项**。这让第一版销售统计可以做简化,但模型必须按“订单项”设计,不能把“一单一品”写死。
+
+---
+
+## 三、统一商品维度:先解决“什么皮肤”
+
+### 3.1 商品主数据
+
+```text
+product_catalog
+ id
+ canonical_sku -- 内部标准编码,统计/成本/报表唯一口径
+ product_name -- 展示名
+ category -- 皮肤/礼包/密钥/改名卡/其他
+ game -- 和平精英等
+ status
+ attributes_json
+```
+
+### 3.2 渠道 SKU 映射
+
+```text
+channel_product_mappings
+ id
+ provider -- kuaishou-91 / pdd / douyin-mashida
+ shop_id
+ external_sku_code -- 平台原始 sku_code / productNo / code
+ external_sku_name
+ canonical_product_id
+ valid_from / valid_to
+ confidence
+ source -- manual / import / rule
+```
+
+匹配优先级:
+
+```text
+shop_id + external_sku_code
+ > provider + external_sku_code
+ > external_sku_name 归一化
+ > 人工映射
+```
+
+匹配不到的商品进入 `product_mapping_missing` 异常队列,**不能直接拿原始 `sku_code` 当报表维度**。
+
+### 3.3 店铺维度
+
+`shop_id` 是稳定键;`shop_name` 会变。报表以 `shop_id` 为准,展示名取最新或按时间快照。
+
+---
+
+## 四、统一成本中心
+
+### 4.1 当前成本散落点
+
+| 成本 | 当前来源 | 问题 |
+| --- | --- | --- |
+| 自动发货采购成本 | `affiliate-dash` 商品接口 `cost_amount` | 代码已映射,但没有落库/展示/快照 |
+| 工单报酬 | `work_product_rules.reward_amount` / `unit_price_fen` / `sharing_unit_reward` | 价格与匹配规则混在一张表,改价无版本 |
+| 工单实际成本 | `work_orders.reward_amount`、`share_reward` | 实际值有了,但没有统一成本项/版本 |
+| 人工成本 | 无 | 完全缺失 |
+| 平台费/支付费/售后损失 | 分散或未建模 | 无法算真实毛利 |
+
+### 4.2 成本类型
+
+| 类型 | 说明 | 典型单位 |
+| --- | --- | --- |
+| `auto_purchase` | 自动发货上游采购成本 | 按件 |
+| `work_order_reward` | 接单平台打手报酬 | 按件/按单/按份 |
+| `manual_service` | 人工客服成本 | 按单/按小时 |
+| `platform_fee` | 平台服务费/佣金 | 按单/按比例 |
+| `payment_fee` | 支付通道费 | 按单/按比例 |
+| `after_sales_loss` | 售后损失/赔付 | 按单 |
+| `shipping_other` | 其他履约费用 | 按单 |
+
+### 4.3 成本表
+
+```text
+cost_items
+ id
+ cost_key
+ cost_type
+ name
+ unit -- per_item / per_order / per_hour / percent
+ currency
+ status
+
+cost_versions
+ id
+ cost_item_id
+ amount_fen -- 固定金额;percent 类型用 rate_bps
+ rate_bps
+ effective_from
+ effective_to
+ scope_type -- global / provider / shop / product / rule
+ provider
+ shop_id
+ canonical_product_id
+ work_product_rule_id
+ source -- manual / upstream_sync / import
+ status -- draft / active / retired
+ change_reason
+ created_by
+ approved_by
+```
+
+### 4.4 成本快照
+
+```text
+cost_snapshots
+ id
+ order_id
+ order_item_id
+ work_order_id
+ manual_case_id
+ cost_item_id
+ cost_version_id
+ cost_type
+ quantity
+ unit_amount_fen
+ total_amount_fen
+ snapshot_stage -- estimated / actual / adjustment
+ resolved_at
+ metadata_json
+```
+
+规则:
+
+- 改价只新增 `cost_versions`,不覆盖历史;
+- 订单/工单/人工单创建或完成时写 `cost_snapshot`;
+- 匹配不到成本时写 `cost_missing`,**不能默认 0**;
+- 历史成本永远从快照读,不从当前成本表倒推。
+
+### 4.5 “只改一个地方”的过渡方案
+
+目标状态:
+
+- 金额只在 `cost_versions` 改;
+- `work_product_rules` 只保留匹配条件 + 计价方式 + `cost_item_id`;
+- `channel_product_mappings` 引用自动发货 `cost_item_id`;
+- 人工成本引用 `manual_service` 成本项;
+- 业务表只保存 `cost_snapshot_id`。
+
+过渡:
+
+1. 给 `work_product_rules` 增加可空 `cost_item_id`;
+2. 有 `cost_item_id` 走成本中心;
+3. 没有则回退旧 `reward_amount/unit_price_fen`,标记 `legacy_cost_source`;
+4. 659 条规则逐步迁移;
+5. 全部迁移后旧字段变只读/删除。
+
+这样不会一次性打断发单,又能逐步做到“成本改动只在一个地方”。
+
+---
+
+## 五、销售事实与店铺日报
+
+### 5.1 销售事实表
+
+```text
+sales_daily_facts
+ stat_date -- Asia/Shanghai
+ provider
+ shop_id
+ canonical_product_id
+ sales_channel -- auto / work_order / manual
+ order_count
+ item_count
+ quantity -- 归一化件数(SKU 文本解析后)
+ gross_revenue_fen
+ refund_fen
+ net_revenue_fen
+ auto_cost_fen
+ work_order_cost_fen
+ manual_cost_fen
+ platform_fee_fen
+ after_sales_loss_fen
+ gross_profit_fen
+ cost_missing_count
+```
+
+### 5.2 三个必须统一的字段
+
+1. **商品**:`canonical_product_id`,不是原始 `sku_code`。
+2. **数量**:`normalized_quantity`,不是简单的 `order_items.quantity`。
+3. **收入**:`revenue_amount_fen` + `revenue_basis`,不是直接 `orders.total_amount`。
+
+收入口径建议:
+
+| 来源 | 当前字段 | 建议 |
+| --- | --- | --- |
+| 91 | `maxAmount` | 明确是结算收入还是成本上限,再归一 |
+| PDD | `order_amount` | 作为订单收入候选 |
+| 抖音 | `pay_amount` | 作为买家支付收入候选 |
+
+### 5.3 退款日期
+
+现在只有 `pay_status=refunded` 和 `updated_at`,不足以做按日净销售。建议补 `refunded_at` / `refund_amount_fen`,或从退款事件表聚合。
+
+### 5.4 统计实现
+
+第一版可以用物化视图:
+
+```text
+orders + order_items + channel_product_mappings + cost_snapshots
+ -> sales_daily_facts
+```
+
+数据量目前约 35 万订单、每天 4k+ 单,物化视图 + 索引足够;后续再改增量表。
+
+---
+
+## 六、自动发货块
+
+```text
+订单项 -> 命中 affiliate_dash SKU 映射 -> fulfillment_task
+ -> 上游商品成本 cost_amount -> cost_snapshot
+ -> 履约完成 -> sales_daily_facts
+```
+
+要点:
+
+- 上游商品接口已有 `price_amount` / `cost_amount`,但当前只映射、不落库;需要同步到成本中心。
+- `price_amount` 是售价/积分,`cost_amount` 是成本,不能混用。
+- 成本变化生成新版本;变动超阈值告警/审批。
+- 店铺/日期/皮肤统计必须走 `canonical_product_id`,不能按原始 `sku_code`。
+- 统计件数要解析 SKU 文本中的数量。
+
+---
+
+## 七、接单平台块
+
+```text
+订单项 -> 命中接单模板 -> work_order
+ -> 创建/发布/抢单/验收 -> 工单报酬 -> cost_snapshot
+ -> 押金冻结/释放/没收 -> 影子总账(负债,不直接进成本)
+ -> 提现/售后/欠款 -> 影子总账
+```
+
+要点:
+
+- 工单报酬是主成本;押金是负债,不是成本。
+- 模板改价必须生成 `cost_versions`,不能直接改历史工单。
+- 工单实际结算金额写入 `cost_snapshot(actual)`。
+- 影子总账负责收入/成本/押金/提现/售后的复式记录;成本中心负责“金额是多少、用了哪个版本”。
+
+生产数据提示:91/PDD/抖音已验收工单的报酬/订单金额分别约 86%/80%/96.5%。在收入口径未确认前,这个比例只能作为量级参考,不能直接当毛利率。
+
+---
+
+## 八、人工客服块(当前最需要补的)
+
+> **口径确认(2026-10-17)**:业务明确人工块当前**不以 SLA 监管为目标**,第一优先级是
+> **人工支出可追溯** ——「主要是把流程搞清楚:哪个人处理了哪些东西、付了多少钱。现在人工完全监管不到,
+> 很多地方需要人工付款,也不知道付款项目、金额、订单号」。
+> 因此本章顺序调整为:**① 人工支出登记 → ② 处理人可见(轻量 case) → ③ SLA 与监管指标(延后)**。
+> DDL 见 [drafts/2026-10-17-人工支出登记-DDL草案.sql](drafts/2026-10-17-人工支出登记-DDL草案.sql)。
+
+### 8.1 问题本质
+
+不是“客服效率低”,而是**人工单没有进入系统状态机**,且**人工付款完全没有台账**:
+
+- 没有 case;
+- 没有处理人;
+- 没有开始/完成时间;
+- 没有交付证据;
+- 没有人工成本;
+- 订单完成状态无法回写;
+- **付款项目、金额、关联订单号、付款人、凭证全部缺失** —— 这是当前最直接的损失点。
+
+### 8.2 载体判定
+
+建议在订单项上增加显式字段:
+
+```text
+carrier_type: affiliate_dash / work_order / manual
+carrier_status: pending / active / completed / failed / cancelled
+carrier_decided_at
+carrier_reason
+```
+
+判定:
+
+1. 命中自动发货 -> `affiliate_dash`
+2. 命中接单模板 -> `work_order`
+3. 都没有 -> `manual`
+4. 宽限期后仍无载体 -> 自动创建人工 case
+
+### 8.3 人工 case 模型
+
+> 落地版见 [drafts/2026-10-17-人工支出登记-DDL草案.sql](drafts/2026-10-17-人工支出登记-DDL草案.sql):已去掉 `due_at`/优先级等 SLA 字段,先只保留「谁在处理哪个订单项 + 结果 + 证据 + 支出登记」。下表为完整形态(阶段③)。
+
+```text
+manual_fulfillment_cases
+ id
+ case_no
+ order_id
+ order_item_id
+ provider
+ shop_id
+ canonical_product_id
+ quantity
+ priority
+ status
+ assignee_admin_user_id
+ assigned_at
+ due_at
+ started_at
+ completed_at
+ outcome -- delivered / refunded / failed / cancelled
+ delivery_reference
+ evidence_json
+ cost_snapshot_id
+ notes
+
+manual_case_events
+ id
+ case_id
+ event_type
+ actor_type
+ actor_id
+ payload_json
+ created_at
+```
+
+状态:
+
+```text
+pending_assign -> assigned -> processing -> waiting_customer
+ -> pending_review -> completed / failed / cancelled
+```
+
+### 8.4 监管规则(阶段③,延后)
+
+- 每个无载体订单必须有 case;
+- 完成必须填 outcome + 交付凭证/参考号;
+- 失败必须填原因并决定重试/退款/升级;
+- 高价值订单主管复核;
+- 超时自动升级;
+- 所有状态变更写 `manual_case_events`,不可改;
+- 客服不能私下改价/退款,必须走审批。
+
+### 8.5 人工成本
+
+- 按单固定成本:简单,先落地;
+- 按小时:`agent_hourly_rate × (completed_at - started_at)`;
+- 建议两者都记录:固定服务成本 + 工时,便于后续校准;
+- 完成时写 `cost_snapshot`;缺失进入 `cost_missing`。
+
+### 8.6 监管指标(阶段③,延后)
+
+- 待处理量、平均等待、最长挂起;
+- 客服处理量、平均处理时长、完成率;
+- 退款率、投诉率、返工率;
+- 单均人工成本;
+- 无凭证完成率;
+- 同一买家/客服异常重复;
+- 高价值订单抽样复核。
+
+### 8.7 与现有任务系统的关系
+
+现有 `manual_dispatch` 任务只有 6 条,且缺少 assignee/SLA/证据/成本字段。建议:
+
+- 新人工 case 作为主模型;
+- 如果存在对应 `manual_dispatch` 任务,完成时桥接现有完成接口;
+- 不把所有人工作业强行塞进 `fulfillment_tasks`。
+
+---
+
+## 九、三块统一数据流
+
+```mermaid
+flowchart LR
+ O["订单项"] --> C["载体判定"]
+ C -->|命中自动| A["affiliate_dash 任务"]
+ C -->|命中模板| W["work_order"]
+ C -->|都没有| M["manual_case"]
+
+ A --> CS["cost_snapshots"]
+ W --> CS
+ M --> CS
+
+ CS --> F["sales_daily_facts / 毛利"]
+ A --> L["影子总账"]
+ W --> L
+ M --> L
+
+ P["product_catalog + channel_product_mappings"] --> F
+ V["cost_items + cost_versions"] --> CS
+```
+
+---
+
+## 十、分期落地
+
+### P0:商品与销售事实(先能做报表)
+
+- `product_catalog`
+- `channel_product_mappings`
+- `sales_daily_facts`(可先物化视图)
+- 数量归一、收入口径、退款日期补齐
+
+### P1:成本中心影子期
+
+- `cost_items` / `cost_versions` / `cost_snapshots`
+- 自动发货先接固定成本
+- 工单先接现有规则金额
+- 人工先接按单/按小时成本
+- 缺失成本进异常队列
+
+### P2:人工支出登记(先行)+ 轻量处理记录
+
+- 载体判定(`carrier_type`)
+- 无载体自动建轻量 case(谁在处理哪个订单项)
+- **人工支出登记**:付款项目 / 金额 / 订单号 / 付款人 / 收款人 / 付款方式 / 凭证 / 审批
+- 人工支出进成本快照与影子总账(借:履约成本-人工,贷:其他应付-垫付人)
+- 延后(阶段③):SLA、超时升级、主管复核阈值、监管指标
+
+### P3:影子总账
+
+- 订单收入、工单成本、押金、提现、寄卖、售后
+- 日对账、差异清单
+- 连续对平后转正
+
+---
+
+## 十一、业务口径确认结果(2026-10-17)
+
+### 11.1 已确认
+
+| # | 问题 | 结论 | 依据 |
+| --- | --- | --- | --- |
+| 1 | 91 的 `orders.total_amount` 是收入还是成本上限? | **收入**:`total_amount` 就是用户付款金额,与快手发码配合,计入收入 | 业务确认;数据核验:91 已支付 312,569 笔中 235,271 笔含快手 `paymentFen`,其中 235,268 笔与 `total_amount` **完全一致(100.00%)**,仅 3 笔差 ¥1.80~2.00 |
+| 2 | 打手报酬是成本还是代收代付? | **支出/成本**:工单结算后打手会提现,我方实际付款给他们 → 按**总额法**(收入记用户实付全额,报酬记履约成本) | 业务确认 |
+| 3 | 人工块的目标是什么? | **先做流程可见与支出可追溯**(谁处理了哪些、付了多少钱、什么项目、关联订单号、凭证),SLA/监管指标延后 | 业务确认 |
+| 4 | 退款/仲裁怎么冲减? | **暂未接入,先预留**:字段、反向冲正关系与事件类型先留好,本轮不实现逻辑 | 业务确认 |
+
+### 11.2 仍待确认
+
+1. 收入确认时点:付款 / 发货 / 验收 / 售后期结束?(金额口径已定 = 用户实付)
+2. 自动发货的 `cost_amount` 是否稳定、是否含平台积分/汇率?
+3. 人工支出按单还是按小时归集?(先按单登记,工时字段后续再加)
+4. 平台服务费/支付通道费由谁承担、结算周期与账单口径?
+5. 寄卖货款的采购成本确认时点?
+
+---
+
+## 十二、分析基线与克隆现状(2026-10-17 更新)
+
+生产只读聚合(`SET default_transaction_read_only = on`)已完成,无写入、无改表、无改数据。
+
+**现有基线**:本机已存在可反复使用的**生产快照克隆**(本地 docker `order_site-postgres-1`,`orders` 316,597 行,数据截至 2026-10-14),
+因此**后续 SQL 迭代一律打本地克隆,不必再连生产**;需要更新时再取新快照。
+
+本机备份目录已收敛:`backups/` 及子目录 `700`、两份 dump `600`,并删除了一个残留临时 dump(137 MB)。
+如需长期保留留档,建议只保留一份最新 dump,并确认磁盘加密。
+
+---
+
+## 十三、一句话结论
+
+**自动发货解决“固定成本 + 店铺/日期/皮肤销量”,接单平台解决“变动成本 + 影子总账”,人工客服解决“支出可追溯(谁付的、什么项目、多少钱、哪个订单)+ 处理人可见 + 人工成本”;三者共用一套商品主数据、成本中心和销售事实表,成本改价只发生在 `cost_versions` 一个地方。**
diff --git a/docs/抖音-码速达/00-总览.md b/docs/抖音-码速达/00-总览.md
index 60945dfb..d079c46e 100644
--- a/docs/抖音-码速达/00-总览.md
+++ b/docs/抖音-码速达/00-总览.md
@@ -1,17 +1,17 @@
# 抖音码速达 · 总览(进度 / 代码 / 配置 / 上线)
> 更新:2026-10-15 **状态:最小接入已跑通,且按 SKU 的三层分流已上线**(真实网关签名验证 / 真实推送订阅 / 真实 orderDetail 建单 / 真实 coupons.syncV2 同步成功 / 工单发布顺带核销抖音券),两端自检全绿
-> 本文是这套内容的**入口**,具体细节在 04~11,最新增量见 [§九点六](#九点六2026-10-14--10-15-增量)。
+> 本文是这套内容的**入口**,具体细节在 04~12,最新增量见 [§九点六](#九点六2026-10-14--10-15-增量)。
---
## 一、一句话现状
-按 [05-最小接入方案分析.md](05-最小接入方案分析.md) 落地的**最小闭环六步全部完成**,后端 636 测试 / 前端 115 测试全绿,迁移 `094` 已在本地临时库实测通过(建表 + 幂等 + 唯一约束行为均验证)。
+按 [05-最小接入方案分析.md](05-最小接入方案分析.md) 落地的**最小闭环六步全部完成**;2026-10-17 复核:后端 740 项测试(737 pass / 3 skipped)、前端 143 项测试全绿,迁移 `094` / `095` 已在本地临时库实测通过。
**没做的**:兑换后的实际交付物(卡密/充值)、`order.searchList` 对账、兑换页二次校验(手机后 4 位)。
-> ✅ **原「按 SKU 的三层分类」已上线**(2026-10-14):抖音与 91/快手 同构(命中映射 → `affiliate_dash` 自动发货;未命中 → 接单平台建工单;都不中 → 无载体=待人工),载体唯一有硬守卫。总设计 [12-按SKU分类改造总设计.md](12-按SKU分类改造总设计.md)、分层实测 [10-三层分流设计.md](10-三层分流设计.md)、运营动作 [11-接单模板待补清单.md](11-接单模板待补清单.md)。
+> ✅ **原「按 SKU 的三层分类」已上线**(2026-10-14):抖音与 `kuaishou-91` 来源(91 回调 + 快手 send-code)同构(命中映射 → `affiliate_dash` 自动发货;未命中 → 接单平台建工单;都不中 → 无载体=待人工),载体唯一有硬守卫。总设计 [12-按SKU分类改造总设计.md](12-按SKU分类改造总设计.md)、分层实测 [10-三层分流设计.md](10-三层分流设计.md)、运营动作 [11-接单模板待补清单.md](11-接单模板待补清单.md)。
**只走真实公网联调**(离线模拟已按要求删除):
@@ -143,50 +143,33 @@
## 五、文档索引
-| 文档 | 内容 | 是否入库 |
+| 文档 | 内容 | 状态 |
| --- | --- | --- |
-| [README.md](README.md) | 抓取说明 + 目录 | ✅ 已提交(`5978e923`) |
+| [README.md](README.md) | 抓取说明 + 目录 | ✅ 已入库 |
| [01-计费规则.md](01-计费规则.md) | 平台计费 | ✅ |
| [02-接口调用规范与签名.md](02-接口调用规范与签名.md) | 出网签名规则 | ✅ |
| [03-消息推送规范与签名.md](03-消息推送规范与签名.md) | 推送与验签 | ✅ |
| `接口API/`、`消息推送/`、`data/` | 全量抓取快照(46 个文件 / 488K) | ✅ |
-| [04-结合本项目的深度分析.md](04-结合本项目的深度分析.md) | 深读平台后结合仓库的分析 | ❌ 仅本地 |
-| [05-最小接入方案分析.md](05-最小接入方案分析.md) | **实施方案**(决策与取舍都在这) | ❌ 仅本地 |
-| [06-待确认问题清单.md](06-待确认问题清单.md) | 问题清单(Q1~Q4 已答,其余边做边测) | ❌ 仅本地 |
-| [07-最小接入实现说明.md](07-最小接入实现说明.md) | 实现说明 + 联调步骤 + 坑 | ❌ 仅本地 |
-| [08-公网联调.md](08-公网联调.md) | **真实公网联调**(ngrok + 一键切地址 + 前置自检 + checklist + 坑) | ❌ 仅本地 |
-| 本文 `00-总览.md` | 进度索引 | ❌ 仅本地 |
+| [04-结合本项目的深度分析.md](04-结合本项目的深度分析.md) | 深读平台后结合仓库的分析 | ✅ 已入库(历史分析) |
+| [05-最小接入方案分析.md](05-最小接入方案分析.md) | **实施方案**(决策与取舍都在这) | ✅ 已入库(历史方案,已实施) |
+| [06-待确认问题清单.md](06-待确认问题清单.md) | 问题清单(Q1~Q4 已答,其余边做边测) | ✅ 已入库(历史清单) |
+| [07-最小接入实现说明.md](07-最小接入实现说明.md) | 实现说明 + 联调步骤 + 坑 | ✅ 已入库(现行) |
+| [08-公网联调.md](08-公网联调.md) | **真实公网联调**(ngrok + 一键切地址 + 前置自检 + checklist + 坑) | ✅ 已入库(现行) |
+| 本文 `00-总览.md` | 进度索引 | ✅ 已入库(现行) |
---
-## 六、提交范围建议
+## 六、落地提交记录
-只提交本次抖音相关的 22 个文件(**不要 `git add -A`**,同一分支有并行会话在改寄售):
+| commit | 内容 |
+| --- | --- |
+| `5a73fb74` | 抖音券核销后台界面(列表/核销/冲正/同步/对账/作废) |
+| `319e0822` | 领取消息只发给有自动发货领取单元的单,人工单不再假发货 |
+| `4b081d88` | 抖音按 SKU 三层分类接入,对齐 `kuaishou-91` |
+| `10464fea` | 抽离接单模板匹配引擎(来源适配器/引擎/价格收口) |
+| `6850bc64` | 工单发布并核销支持抖音券(按来源分流) |
-```bash
-git add apps/backend/src/app.ts \
- apps/backend/src/config/app-config-keys.ts \
- apps/backend/src/routes/admin/platform-config.ts \
- apps/backend/src/routes/admin/platform-config/douyin-mashida.ts \
- apps/backend/src/routes/douyin-mashida.ts \
- apps/backend/src/routes/redeem.ts \
- apps/backend/src/db/migrations/094_redeem_certificates.sql \
- apps/backend/src/repositories/redeem-certificate-repo.ts \
- apps/backend/src/services/admin/platform-config/douyin-mashida-service.ts \
- apps/backend/src/services/platforms/douyin-mashida \
- apps/backend/src/services/redeem \
- apps/frontend/src/App.tsx \
- apps/frontend/src/styles/client.css \
- apps/frontend/src/pages/redeem \
- apps/frontend/src/services/redeem.ts \
- apps/frontend/src/types/redeem.ts \
- apps/frontend/src/utils/redeem.ts \
- apps/frontend/tests/redeem.test.ts
-```
-
-建议 commit message:`feat(douyin-mashida): 接入抖音码速达最小闭环(消息建单/券码同步/固定兑换页/核销回执)`
-
-> 04~07 与本文档默认**不入库**(分析类内容)。若要留档,建议单独一次 `docs(douyin-mashida): ...` 提交。
+> 本文档与 04~12 均已入库;历史分析文档按状态标注,不再使用“仅本地/不入库”的口径。
---
@@ -268,7 +251,7 @@ git add apps/backend/src/app.ts \
### 1)按 SKU 三层分流上线(`4b081d88`)
-抖音与 91/快手 同构,**只动抖音,91 不变**:
+抖音与 `kuaishou-91` 同构,**只动抖音,kuaishou-91 不变**:
| 层 | 判据 | 载体 |
| --- | --- | --- |
@@ -283,7 +266,7 @@ git add apps/backend/src/app.ts \
### 2)接单模板匹配引擎重构(`10464fea`,P0+P1)
-按 [接单模板匹配引擎重构设计.md](../接单模板匹配引擎重构设计.md) 抽出 `services/product-match/`(适配器按**来源**划分:`kuaishou-91` / `pdd` / `douyin-mashida`)。生产回放 6000 条订单项:91/PDD 决策 **0 差异**,抖音 5 条全为修正。旧 `work-product-match-mappers.ts` 变为门面,签名不变。
+按 [接单模板匹配引擎重构设计.md](../接单模板匹配引擎重构设计.md) 抽出 `services/product-match/`(适配器按**来源**划分:`kuaishou-91` / `pdd` / `douyin-mashida`,其中 `kuaishou-91` 同时覆盖 91 回调与快手 send-code 两条数据通道)。生产回放 6000 条订单项:`kuaishou-91`/PDD 决策 **0 差异**,抖音 5 条全为修正。旧 `work-product-match-mappers.ts` 变为门面,签名不变。
### 3)工单「发布并核销」支持抖音券(本次)
@@ -300,7 +283,7 @@ publishAdminWorkOrder / assignAdminWorkOrder / 寄售履约
│ ├─ 委托 verifyCertificate(redeem/certificate-service.ts)→ coupons.verifyV3
│ │ (幂等 / invalid 拒核 / 未同步拒核 / 平台券码优先 / 失败对账自愈)
│ └─ 券快照 merge 进工单 material_json.douyinMashidaVoucher + 事件 publish_voucher_consumed
- └─ 其它来源(91/快手)
+ └─ 其它来源(kuaishou-91)
└─ 原快手电子凭证路径(listKuaishouIndustryVouchersByOid + consumeKuaishouIndustryVoucher)
```
@@ -325,7 +308,7 @@ publishAdminWorkOrder / assignAdminWorkOrder / 寄售履约
---
-## 十、自检结果(本次)
+## 十、自检结果(2026-10-15 当时)
| 项 | 结果 |
| --- | --- |
diff --git a/docs/抖音-码速达/04-结合本项目的深度分析.md b/docs/抖音-码速达/04-结合本项目的深度分析.md
index ffa518f4..4008474d 100644
--- a/docs/抖音-码速达/04-结合本项目的深度分析.md
+++ b/docs/抖音-码速达/04-结合本项目的深度分析.md
@@ -2,8 +2,9 @@
> 分析对象:新平台「抖音码速达 - 开放平台」(下文简称 **码速达**),协议数据见同目录
> [README.md](README.md) / [02-接口调用规范与签名.md](02-接口调用规范与签名.md) / [03-消息推送规范与签名.md](03-消息推送规范与签名.md)。
-> 分析主体:本仓库 `order_site`(快手自动发货与履约管理系统)。
-> 写作日期:2026-10-08 | 分支:`dev` | 结论均标注「事实 / 推断」,「事实」都带代码位置。
+> 分析主体:本仓库 `order_site`(多渠道订单履约与打手众包平台)。
+> 写作日期:2026-10-08 | 分支:`dev` | 状态:**接入前历史分析**;现行实现与结论以 [00-总览.md](00-总览.md)、[07-最小接入实现说明.md](07-最小接入实现说明.md) 为准。
+> 结论均标注「事实 / 推断」,「事实」都带代码位置;跨版本行号可能已漂移。
---
diff --git a/docs/抖音-码速达/05-最小接入方案分析.md b/docs/抖音-码速达/05-最小接入方案分析.md
index 05fd4f0c..a5c09b00 100644
--- a/docs/抖音-码速达/05-最小接入方案分析.md
+++ b/docs/抖音-码速达/05-最小接入方案分析.md
@@ -3,7 +3,7 @@
> 目标:**用最小代价跑通一条真实闭环**(收单 → 发券 → 用户兑换 → 回执核销),先不做对账轮询、售后、商品映射后台、充值回执。
> 协议依据:[README.md](README.md)、[02-接口调用规范与签名.md](02-接口调用规范与签名.md)、[03-消息推送规范与签名.md](03-消息推送规范与签名.md)、[接口API/](接口API)。
> 本仓库依据:[04-结合本项目的深度分析.md](04-结合本项目的深度分析.md)(下称「深度分析」)。
-> 日期:2026-10-08 | 分支:`dev`
+> 日期:2026-10-08 | 分支:`dev` | 状态:**已实施的历史方案**;现行实现见 [07-最小接入实现说明.md](07-最小接入实现说明.md) 与 [00-总览.md](00-总览.md)。
---
diff --git a/docs/抖音-码速达/06-待确认问题清单.md b/docs/抖音-码速达/06-待确认问题清单.md
index 1840a437..eb61f8b4 100644
--- a/docs/抖音-码速达/06-待确认问题清单.md
+++ b/docs/抖音-码速达/06-待确认问题清单.md
@@ -2,7 +2,7 @@
> 用途:把「最小接入」开工前必须问清楚的问题**按对象拆好**,可直接复制给对方;每条都标注了「为什么问 / 卡住什么 / 什么算回答了」。
> 上下文:[05-最小接入方案分析.md](05-最小接入方案分析.md)(最小闭环:收单 → 发券 → 固定链接兑换 → 核销 + 发链接)。
-> 整理日期:2026-10-08 | 分支:`dev`
+> 整理日期:2026-10-08 | 分支:`dev` | 状态:**历史问题清单**;Q1~Q4 已答复,未决项以 [00-总览.md](00-总览.md) 和现行实现为准。
---
diff --git a/docs/抖音-码速达/07-最小接入实现说明.md b/docs/抖音-码速达/07-最小接入实现说明.md
index fb619844..be78061c 100644
--- a/docs/抖音-码速达/07-最小接入实现说明.md
+++ b/docs/抖音-码速达/07-最小接入实现说明.md
@@ -1,8 +1,8 @@
-# 抖音码速达 · 最小接入实现说明(本地文档)
+# 抖音码速达 · 最小接入实现说明
> 对应方案:[05-最小接入方案分析.md](05-最小接入方案分析.md)。本文记录**已经落地了什么、怎么配、怎么联调、有哪些坑**。
-> 状态:后端已实现并通过 `npm run check`(630 测试 / 627 通过 / 0 失败);数据库迁移 `094` 未执行过(本地/生产启动时自动 up)。
-> 文档本身**未入库**,与 04/05/06 一样只保留在本地。
+> 状态:已实现;2026-10-17 复核后端 740 项测试(737 pass / 3 skipped)、前端 143 项全绿,生产构建通过。迁移 `094` / `095` 已入库并在本地临时库验证,生产以启动时自动迁移记录为准。
+> 本文已入库;04/05/06 属接入前历史分析,现行结论以 [00-总览.md](00-总览.md) 与本文为准。
---
diff --git a/docs/抖音-码速达/10-三层分流设计.md b/docs/抖音-码速达/10-三层分流设计.md
index a66a292f..7a4d0c8a 100644
--- a/docs/抖音-码速达/10-三层分流设计.md
+++ b/docs/抖音-码速达/10-三层分流设计.md
@@ -1,6 +1,6 @@
# 抖音码速达 · 三层分流设计(皮肤自动发货 / 接单平台 / 人工)
-> 状态:**设计稿,未实施**(2026-10-14)
+> 状态:**已实施(抖音侧,2026-10-14);后续项见 §8 未决问题**。最后校准:2026-10-17(`main @ 6850bc64`)。
> 依据:生产库只读实测(接单模板 662 条)+ 全链路代码走查
> 关联:[09-皮肤领取流程.md](09-皮肤领取流程.md)、[11-接单模板待补清单.md](11-接单模板待补清单.md)
@@ -13,11 +13,11 @@
1. 内部 SKU 命中 `AFFILIATE_DASH_SKU_MAPPING` → `affiliate_dash` 皮肤自动发货;
2. 其余 → 一律 `manual_dispatch`(人工)。
-而 91/快手 是**三条路**:皮肤自动发货 / **接单平台自动发单(按接单模板建工单给打手)** / 人工兜底。本次目标:让抖音复用同一套分类,把中间那层补上。
+而 `kuaishou-91` 来源(91 回调 + 快手 send-code 两条数据通道)是**三条路**:皮肤自动发货 / **接单平台自动发单(按接单模板建工单给打手)** / 人工兜底。本次目标:让抖音复用同一套分类,把中间那层补上。
---
-## 二、91/快手 现在的三层机制(代码事实)
+## 二、kuaishou-91 现在的三层机制(代码事实)
```mermaid
flowchart TD
@@ -62,7 +62,7 @@ flowchart TD
### 关键结论:层① 与层② 是**同一个单子的两种执行载体**(业务上严格互斥)
-⚠️ 先纠正一个容易搞错的关系:**91 单、快手、工单不是三条独立的线,本质是一个单子**。
+⚠️ 先纠正一个容易搞错的关系:**91 回调、快手 send-code、工单不是三条独立的线,本质是一个单子**。前两者是同一个 `kuaishou-91` 来源的两条数据通道,工单只是这个单子的执行载体之一。
`sync-work-orders-from-send-code.ts:5-6` 的注释就是权威口径:「**91 回调是订单骨架权威源,快手 send-code 是数据补全源与兜底源**」。
@@ -188,7 +188,7 @@ flowchart TD
## 五、改动清单(文件级)
-> ✅ **实施状态(2026-10-14)**:第 **1、2、3、4** 项已落地并测试通过(类型检查 + 后端全量 712 例);第 **7、8、9、10** 项待实施。详见 [12-按SKU分类改造总设计.md](12-按SKU分类改造总设计.md)「§1.1 已实施 / §1.2 待实施」。
+> ✅ **实施状态(2026-10-14)**:第 **1、2、3、4** 项已落地并测试通过;2026-10-17 复核后端全量 **740 例**(737 pass / 3 skipped)。第 **7、8、9、10** 项待实施。详见 [12-按SKU分类改造总设计.md](12-按SKU分类改造总设计.md)「§1.1 已实施 / §1.2 待实施」。
> - 第 4 项的实际落法:抖音**未命中时不再兜底 `manual_dispatch`**(覆盖 `unmatchedExecutorKey`),因此「未命中 = 无执行载体」,由发单层先判接单模板 → 命中即工单、都不中即待人工;载体唯一由 `snapshot.classification.channel === 'affiliate_dash'` 守卫。
| # | 文件 | 改动 | 风险 |
diff --git a/docs/抖音-码速达/12-按SKU分类改造总设计.md b/docs/抖音-码速达/12-按SKU分类改造总设计.md
index 62f9f165..a2dc8a1d 100644
--- a/docs/抖音-码速达/12-按SKU分类改造总设计.md
+++ b/docs/抖音-码速达/12-按SKU分类改造总设计.md
@@ -1,15 +1,15 @@
-# 抖音码速达 · 按 SKU 分类改造总设计(对齐 91/快手 三层)
+# 抖音码速达 · 按 SKU 分类改造总设计(对齐 kuaishou-91 三层)
-> 状态:**设计稿,未实施**(2026-10-14)
-> 依据:全链路代码走查 + 生产库只读实测(模板 662 条 / 91 订单 32.9 万 / 抖音 11 笔)
+> 状态:**抖音侧已实施(2026-10-14);P2 / P3 后续项未实施**。最后校准:2026-10-17(`main @ 6850bc64`)。
+> 依据:全链路代码走查 + 生产库只读实测(模板 662 条 / `kuaishou-91` 订单 32.9 万 / 抖音 11 笔)
> 前置:[10-三层分流设计.md](10-三层分流设计.md)、[11-接单模板待补清单.md](11-接单模板待补清单.md)
-> 前提:**抖音侧实现可自由重构**(用户确认,无历史兼容包袱)
+> 前提:抖音侧实现可自由重构(无历史兼容包袱);**91 回调与快手 send-code 是同一个 `kuaishou-91` 来源的两条数据通道,不拆成两个来源**
---
## 一、目标与结论摘要
-**目标**:抖音与 91/快手 用同一套「按 SKU 分类」逻辑,产出三种结果:
+**目标**:抖音与 `kuaishou-91` 用同一套「按 SKU 分类」逻辑,产出三种结果:
| 层 | 触发条件 | 产物 |
| --- | --- | --- |
@@ -37,7 +37,7 @@
| 6 | 券 `cert_link` 按基址形态决定(`/h5` 一律按订单号拼;其它形态有 token 用 token、否则回退 `certLink`)——**不再依赖是否命中自动发货** | `apps/backend/src/services/redeem/certificate-service.ts` |
| 7 | **抖音券写入工单 `material_json.douyinMashidaVoucher`**(对齐快手把凭证挂工单):① 发券后回填;② 建工单时券已存在则直接带上(覆盖「补模板后重新匹配」) | 同上 + `apps/backend/src/services/worker-platform/sync-worker-orders-service.ts` |
-**验证**:`npm run typecheck` 通过;后端全量测试 **717 例 / 714 通过 / 0 失败 / 3 跳过**;本次新增 16 例(分类判定、sku_code 不覆写、券链接 5 例、券回填 4 例、载体守卫 3 例、91 回归)。
+**验证(2026-10-14 当时)**:`npm run typecheck` 通过;后端全量测试 **717 例 / 714 通过 / 0 失败 / 3 跳过**;本次新增 16 例(分类判定、sku_code 不覆写、券链接 5 例、券回填 4 例、载体守卫 3 例、91 回归)。2026-10-17 复核为 740 项(737 pass / 3 skipped)。
### 1.2 业务口径(2026-10-14 确认)与后续
@@ -53,7 +53,7 @@
## 二、现状全链路梳理
-### 2.1 91/快手 全链路
+### 2.1 kuaishou-91 全链路
```mermaid
flowchart TD
@@ -94,7 +94,7 @@ flowchart TD
#### 一个单子的构成(关键认知)
-**91 与快手是互相配合的两个数据源,工单不是独立的第三条线**——`sync-work-orders-from-send-code.ts:5-6` 注释即权威口径:「91 回调是订单骨架权威源,快手 send-code 是数据补全源与兜底源」。
+**91 回调与快手 send-code 是同一个 `kuaishou-91` 来源的两条数据通道,工单不是独立的第三条线**——`sync-work-orders-from-send-code.ts:5-6` 注释即权威口径:「91 回调是订单骨架权威源,快手 send-code 是数据补全源与兜底源」。两者共享同一店铺键空间与同一套匹配上下文,**不是两个来源**。
| 组成 | 说明 |
| --- | --- |
@@ -138,7 +138,7 @@ flowchart TD
### 2.3 逐项差异对照
-| 维度 | 91/快手 | 抖音(现状) | 改造方向 |
+| 维度 | `kuaishou-91` | 抖音(现状) | 改造方向 |
| --- | --- | --- | --- |
| 分类键 | 中文商品名(`productNo`) | 内部 SKU(`code`) | 统一为规范化 **canonical SKU** |
| `sku_code` 落库 | 命中映射后被覆写成英文 | 同左(未命中时才是内部 SKU) | **不再覆写**,英文 sku 只落 snapshot |
diff --git a/docs/抖音-码速达/README.md b/docs/抖音-码速达/README.md
index 4fa3cc1c..a9bd8fc0 100644
--- a/docs/抖音-码速达/README.md
+++ b/docs/抖音-码速达/README.md
@@ -29,7 +29,7 @@
| [01-计费规则.md](01-计费规则.md) | 免费/基础/高级/增值 四档功能单价与各接口分级 |
| [02-接口调用规范与签名.md](02-接口调用规范与签名.md) | 网关、公共参数、签名算法、返回结构、JAVA/.NET/PHP 请求示例 |
| [03-消息推送规范与签名.md](03-消息推送规范与签名.md) | 推送参数、data 提交方式、签名与验签、消息设置、示例代码 |
-| 04 ~ 06 分析文档 | 【本地文档,暂未入库】结合本项目的深度分析、最小接入方案分析、待确认问题清单 |
+| [04-结合本项目的深度分析.md](04-结合本项目的深度分析.md) ~ [12-按SKU分类改造总设计.md](12-按SKU分类改造总设计.md) | 本项目接入分析、最小方案、实现说明、三层分流与按 SKU 改造;04~06 为历史分析,07/00/12 为现行实现 |
| `接口API/` | 16 个接口逐条文档(请求参数、返回参数与示例、签名原串示例) |
| `消息推送/` | 14 条消息逐条文档(topic、data 字段、示例 JSON、签名原串示例) |
| `data/raw_data.json` | 页面内嵌原始数据(api / message / fee / menu 四块,完整未删减) |
diff --git a/docs/接单模板匹配引擎重构设计.md b/docs/接单模板匹配引擎重构设计.md
index 95d077bc..08fdcdac 100644
--- a/docs/接单模板匹配引擎重构设计.md
+++ b/docs/接单模板匹配引擎重构设计.md
@@ -1,8 +1,10 @@
# 接单模板匹配引擎重构设计
-> 目标读者:后端 / 后台运营。状态:**设计草案(未实施)**。日期:2026-10-14。
+> 目标读者:后端 / 后台运营。状态:**P0 + P1 已实施;P2 / P3 待办**。最后校准:2026-10-17(`main @ 6850bc64`)。
>
-> 一句话:把「订单信息 → 接单模板(价格/拼单/要求)」的匹配,从当前散落在 4 个消费者、3 处平台判断、1 张"店铺必填"的绑定表里的实现,收敛为**一个平台无关的引擎**:匹配条件完全由**订单信息**表达,**店铺只是可选条件之一**,新增来源只加一个适配器 + 一组配置。
+> 一句话:把「订单信息 → 接单模板(价格/拼单/要求)」的匹配,从散落的多个消费点、平台判断和"店铺必填"的绑定表里,收敛为**一个平台无关的引擎**:匹配条件完全由**订单信息**表达,**店铺只是可选条件之一**,新增来源只加一个适配器 + 一组配置。
+>
+> **来源口径(2026-10-17 确认)**:全系统只有三个匹配来源——`kuaishou-91`、`pdd`、`douyin-mashida`。其中 **91 回调与快手 send-code 是同一个 `kuaishou-91` 来源的两条数据通道**:91 是订单骨架权威源,快手 send-code 是补全/兜底源;它们共享店铺键空间,**不是两个来源,也不要拆成两个适配器**。
---
@@ -12,8 +14,7 @@
```mermaid
graph LR
- A["91 回调"] --> ORD[("orders / order_items")]
- B["快手 send-code"] --> ORD
+ A["kuaishou-91
91 回调(骨架)+ 快手 send-code(补全)"] --> ORD[("orders / order_items")]
C["PDD 同步"] --> ORD
D["抖音订单"] --> ORD
ORD --> CTX["匹配上下文构造
resolveKuaishouWorkProductMatchContext"]
@@ -25,7 +26,9 @@ graph LR
OTH["寄卖建议价 / 模板改价同步大厅"] -.-> CFG
```
-### 1.2 四个消费者、各自实现
+### 1.2 四个匹配消费点(不是四个来源)
+
+> 这里的“四个”是**代码里曾经各自实现匹配的消费点**,不是订单来源数量。订单来源始终只有 `kuaishou-91 / pdd / douyin-mashida` 三个。
| 消费者 | 位置 | 现在怎么匹配 | 问题 |
| --- | --- | --- | --- |
@@ -40,7 +43,7 @@ graph LR
2. **商品绑定表要求店铺非空**:`work_product_rule_mappings` 有 CHECK `jsonb_array_length(seller_ids_json) > 0`(迁移 034),且打分函数第一关就是店铺命中(`:179-185`)。→ **不配店铺就无法使用"商品绑定"这一层**,而它恰恰是唯一表达 SKU 精确/数量系列的地方。
3. **上下文只有一个平台实现**:`resolveKuaishouWorkProductMatchContext` 只读 `snapshot.kuaishouSendCode`(`:147-169`)。其它平台靠别名兜(`out_sku_sn / externalSkuName / goodsId / spec`,`:241-257、303-316`)。
4. **兜底算法会被静默启用**:`:103 useLegacyFallback = !context.sellerId || 开关`,后台 `legacyFallbackEnabled=false`(生产实测值)也拦不住。
-5. **匹配入口三处触发且不重算**:`syncWorkerOrdersForSourceOrder` 被 91 回调(`platforms/ninetyone/order-service.ts:187`)、订单通用入口(`order/order-service.ts:211/241`)、快手 send-code(`sync-work-orders-from-send-code.ts:141/224`)分别调用;服务内发现已有工单即 `already_exists` 跳过(`sync-worker-orders-service.ts:111-121`)→ **先到先匹配,后到的补全数据不会纠正错误结果**(抖音 3 张单即此形态)。
+5. **匹配入口三处触发且不重算**:`syncWorkerOrdersForSourceOrder` 被 91 回调(`platforms/ninetyone/order-service.ts:187`)、订单通用入口(`order/order-service.ts:211/241`)、快手 send-code(`sync-work-orders-from-send-code.ts:141/224`)分别调用;服务内发现已有工单即 `already_exists` 跳过(`sync-worker-orders-service.ts:97`)→ **先到先匹配,后到的补全数据不会纠正错误结果**。注意:91 与快手 send-code 属于同一个 `kuaishou-91` 来源,这里是同一来源的两条到达通道,不是两个来源。
### 1.4 平台耦合清单
@@ -55,7 +58,9 @@ graph LR
### 1.5 订单信息实际有什么(生产实测 2026-10-14)
-| 来源 provider | 订单项数 | 有 shop_id | 店铺数 | 有 kuaishouSendCode |
+> 下表的 `provider=91kaquan` 是订单通道标识;匹配层仍统一归到 `sourceKey=kuaishou-91`。不要把 `provider` 当成匹配来源数量。
+
+| 订单 provider | 订单项数 | 有 shop_id | 店铺数 | 有 kuaishouSendCode |
| --- | --- | --- | --- | --- |
| 91kaquan | 329,677 | 100% | 8 | 252,375(76.6%) |
| pdd | 4,443 | 100% | 4 | 0 |
@@ -330,7 +335,7 @@ apps/backend/src/services/product-match/
- 抖音 5 条命中变化,全部是修正:
- `指挥官密钥1个 / 5个`(订单 334081 / 334084 / 334088):`碎梦浮光珍宝礼盒 0.85 元/个、拼单关` → **`指挥官密钥 9.5 元/个、拼单随订单开 5 份`**;
- `突击兵密钥5个`(333380 / 333932):由引流标题误命中的 `指挥官密钥` → **`突击兵`**(与快手侧同商品的判定一致)。
-- `npm run check`:format / sql guard / lint / typecheck / 727 用例(0 失败)/ build 全绿。
+- `npm run check`(2026-10-14 当时):format / sql guard / lint / typecheck / 727 用例(0 失败)/ build 全绿;2026-10-17 复核后端 740 项(737 pass / 3 skipped)、前端 143 项。
### 8.3 待办(P2 / P3)
diff --git a/docs/财务系统集成方案.md b/docs/财务系统集成方案.md
new file mode 100644
index 00000000..b254a7c9
--- /dev/null
+++ b/docs/财务系统集成方案.md
@@ -0,0 +1,474 @@
+# 财务系统集成方案
+
+> 状态:**方案(待实施)**
+> 代码基线:`main @ 6850bc64`
+> 目标:在**不破坏现有抢单、押金、结算、寄卖、提现链路**的前提下,为 order_site 增加一套可对账、可关账、可追溯的财务系统。
+> 结论先说:**不要直接在 `worker_wallets` / `worker_wallet_ledgers` 上继续加字段。** 现有表是业务钱包与流水,不是会计总账;正确做法是新增复式记账底座,让现有钱包成为它的“余额投影”。
+>
+> 如果只想知道“影子总账”是什么、为什么先跑影子账:先读 [影子总账说明.md](影子总账说明.md)。
+
+---
+
+## 一、现有财务能力盘点
+
+### 1.1 已有表
+
+| 表 | 作用 | 现状 |
+| --- | --- | --- |
+| `worker_wallets` | 打手资金汇总 | `available_amount` / `frozen_deposit_amount` / `pending_unfreeze_amount` / `frozen_withdraw_amount` / `total_credited_amount` / `total_settled_amount` |
+| `worker_wallet_ledgers` | 打手资金流水 | `ledger_type` / `amount` / `balance_after` / `frozen_after` / `audit_status` / `payload_json` |
+| `worker_deposit_unfreezes` | 押金/报酬延迟解冻队列 | 到点后由调度任务入账 |
+| `worker_finance_requests` | 充值/提现申请 | `request_type` / `status` / `amount` / `frozen_amount` |
+| `worker_withdrawal_accounts` | 提现账户 | 多渠道、二维码、黑名单 |
+| `worker_after_sales_cases` / `worker_after_sales_debts` | 售后追责与欠款 | 欠款可从后续报酬抵扣 |
+| `worker_wallet_ledgers.payload_json` | 寄卖发布押金等扩展信息 | `kind` / `consignmentSubmissionId` 等 |
+| `consignment_submissions` / `consignment_inventories` | 寄卖发布押金与货款 | 押金冻结/释放/没收、供货货款结算 |
+
+### 1.2 已有后台财务页
+
+`apps/backend/src/services/admin/admin-finance-service.ts` 目前用一条大 `UNION ALL` 把订单收款、退款、打手报酬、提现、充值、售后追缴拼成“财务事件”,输出日汇总与流水。
+
+这不是总账:
+
+- 没有借贷方向,无法验证每笔业务是否平衡。
+- 没有会计科目、期间、关账。
+- 没有平台侧账单与对账差异。
+- 没有订单级收入/成本/毛利。
+- 没有业务事件幂等键,重放可能重复统计。
+- 余额是可变字段,无法从流水独立重算并校验。
+
+### 1.3 现有资金动作(必须全部纳入财务)
+
+| 业务动作 | 代码入口 | 财务性质 |
+| --- | --- | --- |
+| 订单支付 | `services/order/order-service.ts` | 收入/预收 + 平台应收 |
+| 订单退款 | 订单状态回流、售后 | 冲减收入/应收 |
+| 工单验收结算 | `repositories/worker-platform/work-order-settlement-repo.ts` | 履约成本 + 应付打手 |
+| 抢单冻结押金 | `work-order-grab-repo.ts` | 打手余额 → 押金负债 |
+| 验收释放押金 | `work-order-settlement-repo.ts` | 押金负债 → 打手余额 |
+| 取消/超时没收押金 | `work-order-cancel-repo.ts` | 押金负债 → 违约金收入 |
+| 拼单份额结算 | `work-order-settlement-repo.ts` | 按份额拆分成本/押金 |
+| 售后扣款/欠款抵扣 | `after-sales-repo.ts` | 冲减应付打手 / 其他应收 |
+| 充值审核通过 | `worker-finance-request-service.ts` | 银行存款 → 打手余额 |
+| 提现申请/审核 | 同上 | 打手余额 → 银行存款 |
+| 寄卖发布押金 | `consignment-deposit-repo.ts` | 打手余额 → 押金负债 |
+| 寄卖货款结算 | `consignment-deposit-repo.ts` | 采购成本/存货 → 应付卖家 |
+| 平台服务费/通道费 | 目前未单独建模 | 销售费用/财务费用 |
+
+### 1.4 三块履约与成本中心的前置关系
+
+财务总账不是孤立存在的,它需要先知道“这笔钱属于哪个商品、哪个店铺、哪个执行载体、用了哪个成本版本”:
+
+| 块 | 载体 | 成本来源 | 财务侧 |
+| --- | --- | --- | --- |
+| 自动发货 | `affiliate_dash` | 上游固定 `cost_amount` → `cost_versions` | 采购成本 |
+| 接单平台 | `work_orders` | 模板价格 → `cost_versions` / 工单实际报酬快照 | 履约成本 + 押金负债 |
+| 人工客服 | `manual_fulfillment_cases` | 按单/按小时人工成本 | 人工成本 + 履约完成 |
+
+因此建议顺序是:
+
+```text
+商品主数据 + 渠道SKU映射
+ -> 成本中心(cost_items / cost_versions / cost_snapshots)
+ -> 销售事实(sales_daily_facts)
+ -> 影子总账(finance_*)
+ -> 对账 / 转正
+```
+
+详细设计见 [成本中心与三块履约模型.md](成本中心与三块履约模型.md)。
+
+---
+
+## 二、建设原则
+
+1. **复式记账**:每笔业务至少两行分录,借方合计 = 贷方合计;不平不允许提交。
+2. **不可变**:已过账分录禁止 UPDATE / DELETE;冲正用反向分录。
+3. **业务事件幂等**:每个业务动作有唯一 `business_event_id`,重复投递只入账一次。
+4. **金额只用整数分**:数据库 `BIGINT`,禁止浮点。
+5. **钱包是投影**:`worker_wallets` 的余额必须能由总账分录重算出来;差异告警。
+6. **同一事务**:业务状态变更与会计分录在同一数据库事务内提交,避免“货发了、账没记”。
+7. **期间可关账**:关账后禁止补记;必须用调整分录并留原因。
+8. **对账优先**:先做影子账 + 对账,再切换钱包读模型,不做一次性大爆炸迁移。
+
+---
+
+## 三、目标架构
+
+```mermaid
+flowchart LR
+ BIZ["业务动作
订单/工单/寄卖/提现/售后"] --> OUTBOX["finance_business_events
幂等事件"]
+ OUTBOX --> POST["finance-posting-service
借贷规则"]
+ POST --> JE["finance_journal_entries"]
+ JE --> JL["finance_journal_lines"]
+ JL --> ACC["finance_accounts
科目表"]
+ JL --> PROJ["余额投影
worker_wallets / 平台应收应付"]
+ EXT["平台账单 / 支付流水"] --> REC["finance_reconciliations"]
+ PROJ --> REC
+ REC --> REPORT["试算平衡 / 利润表 / 订单损益"]
+```
+
+### 3.1 科目表(建议)
+
+| 编码 | 科目 | 类型 | 正常方向 |
+| --- | --- | --- | --- |
+| 1001 | 银行存款 | 资产 | 借 |
+| 1002 | 支付通道待结算 | 资产 | 借 |
+| 1101 | 应收账款-平台 | 资产 | 借 |
+| 1201 | 预付账款-供货平台 | 资产 | 借 |
+| 1301 | 其他应收-打手欠款 | 资产 | 借 |
+| 2001 | 应付打手报酬 | 负债 | 贷 |
+| 2002 | 应付卖家货款 | 负债 | 贷 |
+| 2003 | 打手押金 | 负债 | 贷 |
+| 2004 | 打手可提现余额 | 负债 | 贷 |
+| 2101 | 应付平台服务费 | 负债 | 贷 |
+| 2201 | 预收账款 | 负债 | 贷 |
+| 3001 | 本年利润 | 权益 | 贷 |
+| 4001 | 商品销售收入 | 收入 | 贷 |
+| 4101 | 平台服务费收入 | 收入 | 贷 |
+| 4201 | 违约金/没收押金收入 | 收入 | 贷 |
+| 5001 | 商品采购成本 | 成本 | 借 |
+| 5101 | 履约成本-打手报酬 | 成本 | 借 |
+| 5201 | 支付通道费 | 费用 | 借 |
+| 5301 | 售后损失 | 费用 | 借 |
+| 5401 | 平台服务费 | 费用 | 借 |
+
+> 科目可按实际业务裁剪,但“资产/负债/权益/收入/成本费用”五类与借贷方向必须先定死。
+
+### 3.2 核心表
+
+```sql
+-- 科目
+CREATE TABLE finance_accounts (
+ id BIGSERIAL PRIMARY KEY,
+ account_code TEXT NOT NULL UNIQUE,
+ account_name TEXT NOT NULL,
+ account_type TEXT NOT NULL CHECK (account_type IN ('asset','liability','equity','revenue','expense')),
+ normal_side TEXT NOT NULL CHECK (normal_side IN ('debit','credit')),
+ parent_id BIGINT REFERENCES finance_accounts(id),
+ status TEXT NOT NULL DEFAULT 'active',
+ created_at TIMESTAMPTZ NOT NULL,
+ updated_at TIMESTAMPTZ NOT NULL
+);
+
+-- 业务事件收件箱:幂等闸门
+CREATE TABLE finance_business_events (
+ id BIGSERIAL PRIMARY KEY,
+ business_event_id TEXT NOT NULL UNIQUE,
+ event_type TEXT NOT NULL,
+ source_type TEXT NOT NULL,
+ source_id TEXT NOT NULL,
+ occurred_at TIMESTAMPTZ NOT NULL,
+ payload_json JSONB NOT NULL DEFAULT '{}'::jsonb,
+ status TEXT NOT NULL DEFAULT 'received'
+ CHECK (status IN ('received','posted','failed','ignored')),
+ attempts INTEGER NOT NULL DEFAULT 0,
+ last_error TEXT NOT NULL DEFAULT '',
+ created_at TIMESTAMPTZ NOT NULL,
+ processed_at TIMESTAMPTZ
+);
+
+-- 结算单(打手报酬 / 卖家货款 / 平台服务费)
+CREATE TABLE finance_settlements (
+ id BIGSERIAL PRIMARY KEY,
+ settlement_no TEXT NOT NULL UNIQUE,
+ counterparty_type TEXT NOT NULL CHECK (counterparty_type IN ('worker','supplier','platform','other')),
+ counterparty_id TEXT NOT NULL DEFAULT '',
+ period_key TEXT NOT NULL,
+ amount_fen BIGINT NOT NULL DEFAULT 0,
+ status TEXT NOT NULL DEFAULT 'pending'
+ CHECK (status IN ('pending','approved','paid','cancelled')),
+ approved_at TIMESTAMPTZ,
+ paid_at TIMESTAMPTZ,
+ created_at TIMESTAMPTZ NOT NULL,
+ updated_at TIMESTAMPTZ NOT NULL
+);
+
+CREATE TABLE finance_settlement_items (
+ id BIGSERIAL PRIMARY KEY,
+ settlement_id BIGINT NOT NULL REFERENCES finance_settlements(id) ON DELETE CASCADE,
+ source_type TEXT NOT NULL,
+ source_id TEXT NOT NULL,
+ amount_fen BIGINT NOT NULL DEFAULT 0,
+ created_at TIMESTAMPTZ NOT NULL
+);
+
+-- 凭证头
+CREATE TABLE finance_journal_entries (
+ id BIGSERIAL PRIMARY KEY,
+ entry_no TEXT NOT NULL UNIQUE,
+ business_event_id TEXT NOT NULL UNIQUE REFERENCES finance_business_events(business_event_id),
+ entry_type TEXT NOT NULL,
+ occurred_at TIMESTAMPTZ NOT NULL,
+ posted_at TIMESTAMPTZ NOT NULL,
+ period_key TEXT NOT NULL,
+ currency TEXT NOT NULL DEFAULT 'CNY',
+ memo TEXT NOT NULL DEFAULT '',
+ status TEXT NOT NULL DEFAULT 'posted' CHECK (status IN ('draft','posted','reversed')),
+ reversal_of_id BIGINT REFERENCES finance_journal_entries(id),
+ created_by TEXT NOT NULL DEFAULT 'system',
+ created_at TIMESTAMPTZ NOT NULL
+);
+
+-- 凭证行
+CREATE TABLE finance_journal_lines (
+ id BIGSERIAL PRIMARY KEY,
+ entry_id BIGINT NOT NULL REFERENCES finance_journal_entries(id) ON DELETE RESTRICT,
+ line_no INTEGER NOT NULL,
+ account_id BIGINT NOT NULL REFERENCES finance_accounts(id),
+ debit_fen BIGINT NOT NULL DEFAULT 0,
+ credit_fen BIGINT NOT NULL DEFAULT 0,
+ order_id BIGINT,
+ work_order_id BIGINT,
+ worker_id BIGINT,
+ shop_id TEXT NOT NULL DEFAULT '',
+ provider TEXT NOT NULL DEFAULT '',
+ settlement_id BIGINT REFERENCES finance_settlements(id),
+ memo TEXT NOT NULL DEFAULT '',
+ dimensions_json JSONB NOT NULL DEFAULT '{}'::jsonb,
+ CHECK (debit_fen >= 0 AND credit_fen >= 0),
+ CHECK ((debit_fen > 0) <> (credit_fen > 0)),
+ UNIQUE (entry_id, line_no)
+);
+
+-- 会计期间
+CREATE TABLE finance_periods (
+ period_key TEXT PRIMARY KEY, -- 2026-10
+ starts_at TIMESTAMPTZ NOT NULL,
+ ends_at TIMESTAMPTZ NOT NULL,
+ status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open','closed')),
+ closed_at TIMESTAMPTZ,
+ closed_by TEXT NOT NULL DEFAULT ''
+);
+
+-- 对账批次 / 明细
+CREATE TABLE finance_reconciliations (
+ id BIGSERIAL PRIMARY KEY,
+ channel TEXT NOT NULL, -- 91 / kuaishou / pdd / douyin / affiliate_dash / alipay
+ period_key TEXT NOT NULL,
+ expected_fen BIGINT NOT NULL DEFAULT 0,
+ actual_fen BIGINT NOT NULL DEFAULT 0,
+ diff_fen BIGINT NOT NULL DEFAULT 0,
+ status TEXT NOT NULL DEFAULT 'pending'
+ CHECK (status IN ('pending','matched','diff','resolved')),
+ created_at TIMESTAMPTZ NOT NULL,
+ resolved_at TIMESTAMPTZ
+);
+
+CREATE TABLE finance_reconciliation_items (
+ id BIGSERIAL PRIMARY KEY,
+ reconciliation_id BIGINT NOT NULL REFERENCES finance_reconciliations(id) ON DELETE CASCADE,
+ source_type TEXT NOT NULL,
+ source_id TEXT NOT NULL,
+ external_no TEXT NOT NULL DEFAULT '',
+ expected_fen BIGINT NOT NULL DEFAULT 0,
+ actual_fen BIGINT NOT NULL DEFAULT 0,
+ diff_fen BIGINT NOT NULL DEFAULT 0,
+ resolution TEXT NOT NULL DEFAULT '',
+ created_at TIMESTAMPTZ NOT NULL
+);
+```
+
+### 3.3 业务事件 → 分录规则(建议)
+
+> 冲正不修改原凭证:新建一条 `business_event_id = <原ID>:reverse` 的反向凭证,并让 `reversal_of_id` 指向原凭证;原凭证保持 `posted` 不变。
+
+| 事件 | 借方 | 贷方 | 备注 |
+| --- | --- | --- | --- |
+| 订单支付 | 1002 支付通道待结算 / 1101 应收平台 | 2201 预收账款 | 金额 = 用户实付(已确认);确认时点待拍板 |
+| 履约完成/验收 | 2201 预收账款 | 4001 商品销售收入 | 或按交付时点确认 |
+| 工单验收结算 | 5101 履约成本 | 2001 应付打手报酬 | 按份额逐笔 |
+| 打手报酬转可提现 | 2001 应付打手报酬 | 2004 打手可提现余额 | 与现有 `reward_settlement` 对齐 |
+| 抢单冻结押金 | 2004 打手可提现余额 | 2003 打手押金 | 不产生损益 |
+| 释放押金 | 2003 打手押金 | 2004 打手可提现余额 | 不产生损益 |
+| 没收押金 | 2003 打手押金 | 4201 违约金收入 | 需原因与审计 |
+| 提现审核通过 | 2004 打手可提现余额 | 1001 银行存款 | 先冻结后扣减 |
+| 充值审核通过 | 1001 银行存款 | 2004 打手可提现余额 | 需支付流水号 |
+| 售后扣款 | 2001/2004 | 1301 其他应收-打手欠款 | 欠款不足转 `worker_after_sales_debts` |
+| 寄卖发布押金 | 2004 打手可提现余额 | 2003 打手押金 | 与普通押金分维度 |
+| 寄卖验收货款 | 5001 商品采购成本 | 2002 应付卖家货款 | 货款金额取 `cost_price_fen || asking_price_fen` |
+| 支付卖家货款 | 2002 应付卖家货款 | 1001 银行存款 | 需结算单 |
+| 平台服务费 | 5401 平台服务费 | 2101 应付平台服务费 | 从平台账单导入 |
+| 人工支出登记通过(垫付) | 5101 履约成本-人工(按 `cost_item_id` 映射科目) | 2005 其他应付-垫付人 | 付款项目/订单号/付款人必填;见 [人工支出登记草案](drafts/2026-10-17-人工支出登记-DDL草案.sql) |
+| 人工垫付报销 | 2005 其他应付-垫付人 | 1001 银行存款 | 需凭证与审批留痕 |
+| 退款冲正(预留) | 反向凭证 `reversal_of_id` 指向原凭证 | — | 退款/仲裁暂未接入,先预留不实现 |
+
+> **已确认(2026-10-17)**:收入金额口径 = 用户实付;打手报酬 = 支出(总额法);退款/仲裁先预留;人工支出以登记表为入口。
+> **仍待拍板**:收入**确认时点**(付款 / 发货 / 验收 / 售后期结束),它直接影响利润表与预收账款。
+
+---
+
+## 四、与现有代码的接入方式
+
+### 4.1 唯一记账入口
+
+新增 `apps/backend/src/services/finance/finance-posting-service.ts`:
+
+```ts
+postBusinessEvent({
+ businessEventId, // 业务唯一键,如 work_order:123:settle
+ eventType, // work_order_settled
+ sourceType, // work_order
+ sourceId, // 123
+ occurredAt,
+ lines: [...], // 借贷行
+})
+```
+
+约束:
+
+- 在业务事务内调用;使用同一个 `PoolClient`。
+- `finance_business_events` 先 `INSERT ... ON CONFLICT DO NOTHING`。
+- 已存在则直接返回,不重复入账。
+- 校验借贷平衡;不平抛错并回滚业务事务。
+- 写入 `finance_journal_entries` / `finance_journal_lines`。
+- 更新钱包投影(或由投影任务异步更新,但同一事务更安全)。
+
+### 4.2 需要改造的写入点
+
+| 位置 | 改造 |
+| --- | --- |
+| `services/order/order-service.ts` | 订单支付/退款发事件 |
+| `repositories/worker-platform/work-order-settlement-repo.ts` | 验收结算、押金释放、报酬入账发事件 |
+| `work-order-grab-repo.ts` | 抢单冻结押金发事件 |
+| `work-order-cancel-repo.ts` | 取消/超时没收押金发事件 |
+| `after-sales-repo.ts` | 售后扣款/欠款抵扣发事件 |
+| `worker-finance-request-service.ts` | 充值/提现审核发事件 |
+| `consignment-deposit-repo.ts` | 发布押金、货款结算发事件 |
+| `admin-finance-service.ts` | 改为读总账/试算平衡,不再用 `UNION ALL` 拼报表 |
+
+### 4.3 钱包投影一致性
+
+第一阶段保留现有钱包更新逻辑,同时写总账;第二阶段再切换:
+
+1. **影子期**:业务照旧改钱包,同时写总账;每日比较“钱包余额”与“总账重算余额”。
+2. **校验期**:差异为 0 连续 N 天后,后台展示“总账余额”为准。
+3. **切换期**:钱包读接口改为读投影;旧字段保留一个版本用于回滚。
+4. **清理期**:删除重复的余额更新代码,只保留总账 + 投影。
+
+---
+
+## 五、后台页面与 API
+
+### 5.1 页面
+
+| 页面 | 内容 |
+| --- | --- |
+| 财务总览 | 收入、成本、毛利、押金负债、可提现余额、待结算 |
+| 凭证簿 | 按期间/事件/科目/订单筛选;查看借贷行 |
+| 科目表 | 科目树、启用/停用、期初余额 |
+| 结算中心 | 打手报酬、卖家货款、平台服务费结算单 |
+| 对账中心 | 平台账单导入、差异明细、处理状态 |
+| 期间关账 | 月末检查、关账、反关账(需高权限) |
+| 订单损益 | 单笔订单收入、采购成本、打手报酬、退款、毛利 |
+| 试算平衡 / 利润表 | 按期间导出 |
+
+### 5.2 API
+
+建议新增前缀 `/api/v1/admin/finance`:
+
+```text
+GET /accounts
+POST /accounts
+GET /journal?period=&eventType=&orderId=&workOrderId=
+GET /journal/:id
+GET /trial-balance?period=
+GET /profit-loss?period=
+GET /order-profit/:orderId
+GET /settlements
+POST /settlements/:id/approve
+GET /reconciliations
+POST /reconciliations/import
+POST /reconciliations/:id/resolve
+GET /periods
+POST /periods/:key/close
+```
+
+权限建议新增:
+
+- `finance.post`:手工分录
+- `finance.close`:关账
+- `finance.reconcile`:对账处理
+- 现有 `finance.view / review / config / export` 继续使用
+
+所有写操作必须写 `admin_audit_logs`;关账、手工分录、冲正必须填原因。
+
+---
+
+## 六、分期落地
+
+### P0:影子总账(1–2 周)
+
+> 前置:先完成商品主数据、渠道 SKU 映射、成本中心与销售事实(见 [成本中心与三块履约模型.md](成本中心与三块履约模型.md))。否则影子总账只能记“总额”,无法按店铺/皮肤/成本版本出毛利。
+
+- 建 `finance_accounts / business_events / journal_entries / journal_lines / periods`。
+- 先接 4 类高频事件:订单支付、工单结算、押金冻结/释放/没收、提现。
+- 写总账但不改现有钱包逻辑。
+- 每日对账:钱包余额 vs 总账余额。
+- 补金额守恒与幂等测试。
+
+### P1:寄卖、售后与人工成本(1–2 周)
+
+- 接寄卖发布押金、货款结算、售后欠款/抵扣。
+- 接人工 case 的人工成本快照(按单/按小时)。
+- 建 `finance_settlements` / `finance_settlement_items`。
+- 后台增加凭证簿与试算平衡。
+
+### P2:平台对账(2–3 周)
+
+- 接 91 / 快手 / PDD / 抖音 / affiliate-dash 账单或导出。
+- 建 `finance_reconciliations` / `finance_reconciliation_items`。
+- 差异必须能定位到订单/工单/提现单。
+- 对账差异不自动改账,必须人工确认后生成调整分录。
+
+### P3:报表与关账(2 周)
+
+- 利润表、订单损益、现金流量简表。
+- 期间关账、反关账、期初余额。
+- 导出与权限审计。
+
+### P4:钱包切换(1 周,需灰度)
+
+- 钱包读模型切到总账投影。
+- 旧字段保留一个版本;差异告警。
+- 回滚方案:关闭投影读开关,回到旧钱包字段。
+
+---
+
+## 七、必须补的测试
+
+1. 每笔凭证借方合计 = 贷方合计。
+2. 同一 `business_event_id` 重复投递只入账一次。
+3. 冲正后原凭证 + 反向凭证净额为 0。
+4. 钱包余额 = 总账重算余额。
+5. 抢单冻结 / 验收释放 / 取消没收的押金守恒。
+6. 拼单多份额:份额报酬 + 押金 + 欠款抵扣 = 钱包变化。
+7. 提现:申请冻结、审核通过、拒绝解冻三条路径。
+8. 寄卖:发布押金、驳回释放、验收货款、撤销处置。
+9. 售后:扣款、欠款、后续报酬抵扣。
+10. 关账后禁止补记;调整分录必须留原因。
+11. 对账:平台金额、订单金额、钱包金额三方差异可定位。
+
+---
+
+## 八、需要业务拍板的问题
+
+> 2026-10-17 已确认 4 条口径,明细见 [成本中心与三块履约模型.md](成本中心与三块履约模型.md) §11。
+
+| # | 问题 | 状态 | 影响 |
+| --- | --- | --- | --- |
+| 1 | 收入金额口径 | ✅ **已确认:收入 = 用户实付(`orders.total_amount`)**,与快手 `paymentFen` 数据核验 100% 一致 | 收入基数 |
+| 2 | 收入确认时点:付款 / 发货 / 验收 / 售后期结束? | ⏳ 待定(金额口径已定) | 决定利润表与预收账款 |
+| 3 | 平台结算周期与账单口径(91、快手、PDD、抖音、affiliate-dash) | ⏳ 待定 | 决定对账与应收 |
+| 4 | 打手报酬是“成本”还是“代收代付”? | ✅ **已确认:支出/成本**(最终提现打款),按**总额法** | 收入总额法/净额法 |
+| 5 | 寄卖货款的采购成本确认时点 | ⏳ 待定 | 决定存货/成本 |
+| 6 | 提现通道费、平台服务费由谁承担 | ⏳ 待定 | 决定费用科目与结算 |
+| 7 | 退款、拒付、仲裁的冲减规则 | ✅ **已确认:暂未接入,先预留**(字段 + `reversal_of_id` + 事件类型,不实现逻辑) | 决定售后与收入冲回 |
+| 8 | 人工支出如何入账 | ✅ **已确认:以「人工支出登记」为入口**(付款项目/金额/订单号/付款人/凭证),SLA 监管延后 | 决定人工成本科目与总账事件 |
+| 9 | 是否需要发票/税务 | ⏳ 待定 | 决定发票表与税额 |
+| 10 | 是否多币种 | ⏳ 待定(当前 CNY) | 若否则需汇率与汇兑损益 |
+
+---
+
+## 九、一句话结论
+
+**财务系统应作为“总账层”接入,而不是继续扩展业务钱包。** 先把商品主数据、成本中心、人工 case 与销售事实补齐,再以影子总账 + 幂等业务事件 + 每日对账落地;确认钱包与总账长期一致后,再切换余额读模型。这样既能保护现有抢单/押金/寄卖链路,也能为平台结算、订单损益和关账留出正确底座。
diff --git a/docs/载体判定与成本中心落地-复核与草案-2026-10-17.md b/docs/载体判定与成本中心落地-复核与草案-2026-10-17.md
new file mode 100644
index 00000000..e121e90c
--- /dev/null
+++ b/docs/载体判定与成本中心落地-复核与草案-2026-10-17.md
@@ -0,0 +1,189 @@
+# 载体判定与成本中心落地:复核与草案(2026-10-17 第二轮)
+
+> 状态:现行(复核 + 草案);基线:`main @ ccd5ed2a`;代码未改动。
+> 数据来源:**本地只读克隆**(生产快照,`orders` 316,597 行,数据截至 2026-10-14 14:52),全程只读 SELECT,未连生产、未写入任何业务库。
+> 金额单位:**分**(`orders.total_amount` / `work_orders.reward_amount` / `source_order_amount_fen` 均为分)。
+> 上游文档:[成本中心与三块履约模型.md](成本中心与三块履约模型.md)、[项目全量分析报告-2026-10-17.md](项目全量分析报告-2026-10-17.md)。
+
+本轮任务是把上一轮「只分析」的结论**复核到底、量化到数、落成可执行草案**。三件事:① 提交并推送待提交文档;② 收敛本机生产备份暴露面;③ 用只读 SQL 量化载体分布与人工块,并产出载体判定 + 成本中心两份 SQL 草案。
+
+---
+
+## 一、本轮执行结果
+
+| # | 事项 | 结果 |
+| --- | --- | --- |
+| 1 | 提交并推送文档 | `ccd5ed2a docs: 补充三块执行载体与通用成本中心方案`,已推送 `6850bc64..ccd5ed2a main -> main`;pre-push 两端全量 check 通过 |
+| 2 | 备份目录加固 | `backups/` 与子目录 `chmod 700`,两份 dump `chmod 600`;删除残留临时 dump(见 §六) |
+| 3 | 数据量化 | 只读克隆上完成 8 组聚合(见 §二、§三) |
+| 4 | SQL 草案 | 载体判定 + 成本中心/销售事实两份,**已在沙箱库零错误应用验证**(见 §五) |
+
+---
+
+## 二、独立复核:上一轮结论逐条对照
+
+| 上一轮结论 | 本轮复核方式 | 结果 |
+| --- | --- | --- |
+| 后端 740 测试(737 pass / 3 skipped) | 本地实跑 `npm test` | ✅ `tests 740 / pass 737 / fail 0 / skipped 3` |
+| 前端 143 测试全绿 | 本地实跑 | ✅ `143 pass / 0 fail` |
+| `typecheck` 通过 | 实跑 `tsc --noEmit` | ✅ 通过 |
+| 99 个迁移 / 53 张表 | 数文件 + 数 `CREATE TABLE` + 查库 | ✅ 99 迁移、53 条 `CREATE TABLE`;本地库 55 张 = 53 业务表 + `schema_migrations` + `pg_stat_statements`×2 |
+| 后端非测试源码 371 文件 | `find` 实测 | ✅ 371(另有测试 109) |
+| 无 CI、实际单分支 | 查 `.github` / `.gitlab-ci.yml`;`git log main..dev` | ✅ 均不存在;`main..dev` 为 0 |
+| 巨文件行数(3686/2015/1499/1478) | `wc -l` | ✅ 完全一致 |
+
+**风险项抽查(读源码确认,非复述)**:发单金额后端只校验 `> 0`(`admin-work-order-management-service.ts:228`)✅;寄卖审核 `payload.workerId` 覆盖归属(`consignment-admin-service.ts:606`)✅;抢单先锁钱包、指派先锁工单(`work-order-grab-repo.ts:25` vs `:156`)✅ 锁序反转成立;自动工单押金硬编码 0(`sync-worker-orders-service.ts:245`)✅;Webhook 先落库去重后处理(`douyin-mashida/notify-service.ts:95` → `:112`)✅;`/collect/upload` 不校验订单存在、`/redeem` 仅凭订单号、`/files/object` 无鉴权、`CLAIM_TOKEN_TTL_HOURS=0` ✅;Caddy 无 CSP/HSTS ✅。
+
+---
+
+## 三、载体分布(近 30 天,只读克隆实测)
+
+判定口径:**已支付订单项**为单位,「人工块」= 既无 `fulfillment_tasks` 也无 `work_orders`。
+
+| 来源 | 已支付项 | 自动发货 `affiliate_dash` | 接单工单 | 人工块 | 双载体冲突 | 人工块金额 | 人工占比 |
+| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
+| `kuaishou-91` | 112,520 | 30,900 | 42,907 | **38,451** | 0 | ¥4,853,281 | 34.2% |
+| `pdd` | 3,714 | 0 | 1,648 | **2,066** | 0 | ¥361,364 | 55.6% |
+| `douyin-mashida` | 4 | 2 | 0 | 2 | 0 | ¥63 | 50.0% |
+| **合计** | **116,238** | **30,902** | **44,555** | **40,519** | **0** | **¥5,214,708** | 34.9% |
+
+补充说明(都已核对):
+
+- 91 的 `112,520` 减去上述三类后还差 **262 项**,经查是历史执行器 `kuaishou_ct_assisted`(= `KUAISHOU_CLOUD`,已下线,活到 2026-09-15)的存量任务。**这是载体建模必须单列 `legacy` 的原因**,否则这 262 项会被错算成人工。
+- **双载体冲突 = 0 条**,说明文档「一个订单项只有一个执行载体」的不变式在当前数据上成立。
+- 人工块 `order_status` 全部 `paid`、`pay_status` 全部 `paid`、`claim_status` 全部 `active` —— **零完成信号**,与上一轮结论一致。
+
+### 人工块的形状(监管收益判断)
+
+| 来源 | 涉及店铺 | 单均 | p50 | p90 | ≥¥100 | ≥¥500 |
+| --- | ---: | ---: | ---: | ---: | ---: | ---: |
+| 91 | **8** | ¥126.22 | ¥53 | ¥288 | 13,229 | 2,708 |
+| PDD | 4 | ¥174.91 | ¥141 | ¥306 | 1,353 | 150 |
+
+Top 店铺(人工块):向日葵游戏服务店 14,903 项 / ¥183.9 万;西瓜游戏交易店 7,768 项 / ¥118.0 万;小鱼儿游戏服务店 8,190 项 / ¥97.4 万;佳佳游戏交易店 7,460 项 / ¥84.5 万;PDD 向日葵网络游戏专营店 1,885 项 / ¥32.8 万。
+
+结论:**人工块店铺高度集中(91 只有 8 个店铺)**,意味着「先上线人工 case 队列」的监管收益集中在少数店铺上,容易先抓重点、快速看到效果。
+
+### 月度趋势(确认不是历史遗留)
+
+| 月份 | 已支付项 | 人工块 |
+| --- | ---: | ---: |
+| 2026-08 | 142,969 | 46,337 |
+| 2026-09 | 103,010 | 39,148 |
+| 2026-10(截至 14 日) | 55,647 | 17,469 |
+
+### 人工成本区间(单均人工成本 X 元的月度量级)
+
+| X | ¥3 | ¥5 | ¥8 | ¥15 |
+| --- | ---: | ---: | ---: | ---: |
+| 月人工成本 | ¥121,557 | ¥202,595 | ¥324,152 | ¥607,785 |
+
+这是「人工履约投入」的量级估算,不含商品/货源成本(那部分属于 `cost_versions`)。
+
+---
+
+## 四、两个口径硬问题(比上一轮描述更严重)
+
+### 1. 数量:`quantity` 不可用,低估约 7.3 倍
+
+| 指标 | 实测值(全量已支付 316,585 项) |
+| --- | ---: |
+| `quantity = 1` | 316,578(99.998%) |
+| `quantity > 1` | **7** |
+| 名称里带「N个」但 `quantity = 1` | **104,710** |
+| `sum(quantity)` | 316,593 |
+| 按 SKU 名称解析的数量合计 | **2,316,222** |
+
+用 `quantity` 统计「卖了多少件」会低估约 **7.3 倍**。必须在订单项固化 `normalized_quantity`(草案已加列)。
+
+### 2. SKU 多码一名:比上一轮更严重
+
+| 商品名 | 不同 `sku_code` 数 | 项数 |
+| --- | ---: | ---: |
+| 苏尔南钻石100颗 | **47** | 3,319 |
+| 99999 | 44 | 540 |
+| 指挥官密钥1个 | **42** | 26,748 |
+| 指挥官密钥5个 | 41 | 3,608 |
+| 指挥官密钥10个 | 37 | 3,059 |
+| 苏尔南钻石50颗 | 35 | 379 |
+
+(上一轮报的是近 30 天窗口的 27/35/10;本轮是全量已支付口径,两者不矛盾,但都指向同一个结论:**不做 `channel_product_mappings` 就无法统计销量**。)
+
+---
+
+## 五、对上一轮结论的 4 处修正
+
+| # | 上一轮说法 | 本轮修正 |
+| --- | --- | --- |
+| 1 | C5「订单项无金额字段」是当下风险 | **是潜在缺口**:实测每笔已支付订单恰好 1 个订单项(316,585 单 = 316,585 项),订单级金额暂时等价;但 schema 允许多项,应在做销售事实前补齐 |
+| 2 | 三载体互斥(设计不变式) | **数据上已验证**:近 30 天双载体 = 0 条 |
+| 3 | 自动发货执行器只剩 `affiliate_dash` | 还有 **262 项历史 `kuaishou_ct_assisted` 任务**(已下线,2026-09-15 停);载体建模需单列 `legacy`,且这类任务仍在领取页给出「已下线」提示 |
+| 4 | 手动履约 `manual_dispatch` 任务极少 | 补充:`affiliate_dash` 侧仍在途 552 项(`link_generated` 300 + `waiting_binding` 252),属运营待跟进而非人工块 |
+
+---
+
+## 六、两份 SQL 草案
+
+放在 `docs/drafts/`,**未接入** `apps/backend/src/db/migrations`(接入时再拆分编号并先跑 `npm run check:sql`,避免本地栈重启时自动迁移)。
+
+| 文件 | 内容 |
+| --- | --- |
+| [载体判定与人工块量化.sql](drafts/2026-10-17-载体判定与人工块量化.sql) | §1 八组只读量化查询(含实测值注释);§2 判定规则与优先级(含 `legacy` 与冲突处理);§3 加列 + 回填 + 索引 + 回滚;§4 守恒校验与异常队列查询;§5 人工成本区间 |
+| [成本中心与销售事实-DDL草案.sql](drafts/2026-10-17-成本中心与销售事实-DDL草案.sql) | `product_catalog`、`channel_product_mappings`、`cost_items`、`cost_versions`、`cost_snapshots`、`cost_exceptions`、`sales_daily_facts`;订单项口径补齐(`normalized_quantity`/`revenue_amount_fen`/`revenue_basis`/`refunded_at`);`work_product_rules.cost_item_id` 过渡列;§5 对平校验;§6 已确认口径与剩余待确认 |
+| [人工支出登记-DDL草案.sql](drafts/2026-10-17-人工支出登记-DDL草案.sql) | `manual_fulfillment_cases`(轻量处理记录)+ `manual_expense_records`(付款项目/金额/订单号/付款人/收款人/凭证/审批)+ `manual_expense_events` 审计;§3 校验查询;§4 与影子总账的分录约定 |
+
+**验证方式**:在本地副本上 `pg_dump --schema-only` 建了一个一次性沙箱库(55 张表),按依赖顺序(载体判定 → 成本中心 → 人工支出登记)用 `psql -v ON_ERROR_STOP=1` 应用**三份草案 —— 零错误**:生成 10 张新表(共 65 张)、`order_items` 由 9 列增至 19 列、27 个 `CHECK` 约束、169 个索引;校验查询与回填语句均可执行。验证后沙箱库已 `DROP`。
+
+关键设计约束(三份草案共同遵循):
+
+1. **改价只发生在 `cost_versions`**,历史成本靠 `cost_snapshots` 固化,永不回写;
+2. **匹配不到成本不默认 0**,写入 `cost_exceptions`,报表必须暴露 `open` 数量;
+3. `carrier_type`/`carrier_status` 是**派生列**,事实来源仍是任务/工单存在性,必须有重算作业与守恒校验;
+4. 报表维度统一 `canonical_product_id + shop_id + stat_date`,禁止直接用 `sku_code` 或 `orders.total_amount`。
+
+---
+
+## 七、本机卫生记录(第 2 项执行细节)
+
+| 项 | 处理前 | 处理后 |
+| --- | --- | --- |
+| `backups/`、`backups/postgres/`、`backups/douyin-platform-clear-20261014/` | `755` | **`700`** |
+| `order_site_latest.dump`(559 MB)、`order_site_remote.dump`(105 MB) | `644` | **`600`** |
+| `.order_site_readonly.dump.nKbeRp8kUA`(137,330,688 B) | `600`,残留 | **已删除**,`backups/` 由 765 MB 降至 634 MB |
+
+删除前已确认:① 解析后绝对路径为 `…/order_site/backups/postgres/.order_site_readonly.dump.nKbeRp8kUA`;② 是**完整可读**的 PostgreSQL 自定义 dump(`pg_restore --list` 795 条目 / 53 个 `TABLE DATA`),sha256 `1a666af11ae9df2058bc2a5afa2196baefb9c125daf0cdecf18dfd20c59cc075`;③ 全仓库无任何脚本引用该文件或其余两份 dump 名;④ 无进程占用;⑤ 与同分钟产出的 `order_site_latest.dump` 属同一批快照,且本地克隆数据(截至 10-14)比这两份 10-10 快照更新。
+
+**遗留建议**:本机仍存两份全量生产 dump(含买家信息与平台凭据)。建议确认磁盘已加密;若后续以本地克隆为唯一分析基线,可只保留一份最新 dump。
+
+**分析基线建议**:本地 docker 栈(`order_site-postgres-1`)已含 10-14 生产快照(`orders` 316,597),后续所有 SQL 迭代直接打本地克隆即可,无需再连生产。
+
+---
+
+## 八、业务口径确认(2026-10-17)
+
+业务已就四个阻塞问题给出结论;其中两条已用克隆数据独立核验。
+
+| # | 问题 | 结论 | 数据核验 |
+| --- | --- | --- | --- |
+| 1 | 91 的 `total_amount` 是收入还是成本上限? | **收入**:它就是**用户付款金额**,与快手发码配合 | ✅ 91 已支付 312,569 笔,其中 235,271 笔含快手 `kuaishouSendCode.paymentFen`;**235,268 笔与 `total_amount` 完全一致(100.00%)**,仅 3 笔差 ¥1.80 / ¥1.85 / ¥2.00(2026-09-05 / 09-18 / 10-04) |
+| 2 | 打手报酬是成本还是代收代付? | **支出/成本**:工单结算 → 打手可提现 → 提现打款,属我方真实支出 → **总额法** | — |
+| 3 | 人工块的目标? | **先把流程搞清楚:谁处理了哪些、付了多少钱**。现状「很多地方需要人工付款,但不知道付款项目、金额、订单号」→ 新增**人工支出登记**草案;SLA/监管指标延后 | — |
+| 4 | 退款/仲裁? | **暂未接入,先预留**:字段 + `reversal_of_id` + 事件类型,本轮不实现逻辑 | — |
+
+影响:
+
+- 收入口径统一为 `revenue_amount_fen = orders.total_amount`(`revenue_basis = 'user_payment'`),草案中原本枚举的三家字段名已简化;
+- 成本侧确认「打手报酬 = 支出」,`cost_items.cost_type = 'work_order_reward'` 计入履约成本,不做净额;
+- 人工块方案重心从「case + SLA」改为「**人工支出登记**(付款项目 / 金额 / 订单号 / 付款人 / 收款人 / 凭证 / 审批)」;
+- 退款相关列与冲正关系保留为预留,不写逻辑。
+
+---
+
+## 九、下一步
+
+1. **业务仍待拍板 1 个关键口径**:收入**确认时点**(付款 / 发货 / 验收 / 售后期结束)—— 它决定利润表与预收账款;金额口径已定。
+2. **工程可先做两件不改业务代码的事**:
+ - 建 `channel_product_mappings` 初始映射,把 47 个 `sku_code` 归到 1 个标准商品;
+ - 用现有 `resolveSkuNameQuantity` 解析并回填 `normalized_quantity`。
+3. **紧接着做人工支出登记**(业务当前最痛):`manual_fulfillment_cases` + `manual_expense_records`,先解决「谁付的、什么项目、多少钱、哪个订单」。
+4. 之后按「商品主数据 → 成本中心影子期 → 人工支出 → 影子总账 → 对账转正」推进。
diff --git a/docs/项目全量分析报告-2026-10-17.md b/docs/项目全量分析报告-2026-10-17.md
new file mode 100644
index 00000000..7ed6459a
--- /dev/null
+++ b/docs/项目全量分析报告-2026-10-17.md
@@ -0,0 +1,315 @@
+# order_site 项目全量分析报告(2026-10-17)
+
+> 状态:**现行**
+> 代码基线:`main @ 6850bc64`(工作区干净)
+> 复核范围:后端、前端、数据库迁移、部署脚本、现行文档
+> 复核方式:源码走查 + 类型检查 + Lint + 格式检查 + 全量测试 + 生产构建 + 2026-10-17 生产库只读聚合(`default_transaction_read_only=on`)
+> 说明:本次没有做全量数据克隆;历史数据量引用自生产库只读聚合,已在文中标注。
+
+---
+
+## 一、结论速览
+
+| # | 结论 | 影响 |
+| --- | --- | --- |
+| 1 | 系统已是**三来源订单履约 + 众包接单 + 寄卖仓库 + 凭证核销**平台,不再是单一“快手自动发货”工具 | 文档与新人认知必须先统一来源模型 |
+| 2 | 来源只有三个:`kuaishou-91`、`pdd`、`douyin-mashida`;**91 回调与快手 send-code 是同一个 `kuaishou-91` 来源的两条数据通道** | 代码已正确使用 `kuaishou-91` 适配器;文档曾把二者写成两个来源,现已校正 |
+| 3 | 工程纪律处于同类项目上游:严格 TS、99 个渐进迁移、SQL 护栏、回调验签、日志脱敏、生产配置校验、按需部署 | 具备继续承载财务系统的工程基础 |
+| 4 | 最大风险集中在四处:**资金门禁、Webhook 失败恢复、公开接口凭据、并发锁序** | 这四处会直接影响钱、单、凭证与账号安全 |
+| 5 | 财务系统不能直接长在 `worker_wallet_ledgers` 上;现有表是“业务钱包流水”,不是会计总账 | 需要先建复式记账与对账底座,再接现有资金动作 |
+| 6 | 前端与数据库事务层的测试仍偏薄;部分后端测试是源码字符串断言 | 财务系统必须补真实 PostgreSQL 集成测试与金额守恒测试 |
+
+---
+
+## 二、来源模型:三个来源,不是一个来源加另一个来源
+
+```mermaid
+flowchart LR
+ A["kuaishou-91
91 回调(订单骨架)"] --> O[("orders / order_items")]
+ B["kuaishou-91
快手 send-code(补全/兜底)"] --> O
+ C["pdd"] --> O
+ D["douyin-mashida"] --> O
+ O --> M["product-match 引擎
3 个来源适配器"]
+ M --> W[("work_orders")]
+ O --> F[("fulfillment_tasks")]
+```
+
+| sourceKey | 数据通道 | 角色 |
+| --- | --- | --- |
+| `kuaishou-91` | 91 回调 | 订单骨架权威源 |
+| `kuaishou-91` | 快手 send-code | 商品/SKU/店铺补全源与兜底源 |
+| `pdd` | 拼多多同步 | 两段式原始订单暂存后消费 |
+| `douyin-mashida` | 抖音码速达推送 | 抖店订单、DYC 券、H5/兑换页 |
+
+代码事实:
+
+- 来源键定义:`apps/backend/src/services/product-match/types.ts:7`
+- 适配器:`apps/backend/src/services/product-match/adapters/kuaishou-91.ts`
+- 注册表:`apps/backend/src/services/product-match/adapters/registry.ts:69`
+- 快手 send-code 的权威注释:`apps/backend/src/services/worker-platform/sync-work-orders-from-send-code.ts:5`
+
+**结论:91 与快手 send-code 必须继续共用一个 `kuaishou-91` 适配器。** 拆开会导致同一店铺键空间、同一订单骨架被重复匹配,直接破坏“一个订单项一个执行载体”的不变式。
+
+---
+
+## 三、架构与运行形态
+
+| 层 | 现状 |
+| --- | --- |
+| 后端 | Node 22(生产)/ Express 5 / TypeScript 6;`routes → services → repositories → pg` |
+| 前端 | React 19 / Vite 8 / Ant Design 6 / TanStack Query 5 |
+| 数据库 | PostgreSQL 16,`node-pg-migrate`,启动时自动迁移 |
+| 网关 | Caddy,统一压缩、静态缓存、反代 |
+| 部署 | Docker Compose;`ubuntu-deploy.sh` 按 Git 改动只重建变化的一端 |
+| 运行 | 单实例;限流、调度器、SSE 订阅、列表缓存均在进程内存 |
+| 存储 | 生产阿里云 OSS;开发 MinIO |
+
+后端入口与路由挂载:`apps/backend/src/app.ts:109`。抖音消息推送必须在全局 body parser 之前挂载,以保留原始报文验签:`apps/backend/src/app.ts:42`。
+
+前端是三个入口共用一套构建产物:
+
+- 后台:`HashRouter`
+- 打手端:独立域名 `worker.khhao.com`
+- 买家侧:`/claim`、`/h5`、`/redeem` 独立路径路由
+
+---
+
+## 四、当前规模(实测)
+
+| 指标 | 后端 | 前端 |
+| --- | --- | --- |
+| 源码文件 / 行数 | 371 / 77,811 | 179 / 52,812 |
+| CSS | — | 7,448 行 |
+| 测试文件 / 行数 | 109 / 16,249 | 26 / 2,221 |
+| 测试/源码比 | 约 20.9% | 约 4.2% |
+| 接口声明 | 270(后台 197、打手 51) | — |
+| 测试结果(本次) | 740 项:737 pass / 3 skipped / 0 fail | 143 pass / 0 fail |
+| 生产构建 | 通过 | 通过(主 chunk 524.69 kB,有 >500 kB 警告) |
+
+数据库:**99 个迁移、53 张表**。
+
+提交历史:1,069 commits,作者高度集中(`git shortlog` 前两位 824 / 250),bus factor 约等于 1。
+
+---
+
+## 五、核心业务链路
+
+### 5.1 进单与匹配
+
+1. 三个来源统一调用 `upsertOrderFromSource`。
+2. 按 `(provider, platform, platformOrderId)` 做 `pg_advisory_xact_lock` 去重。
+3. `resolveOrderItemForFulfillment` 解析履约载体:`affiliate_dash` 或 `manual_dispatch`。
+4. `syncWorkerOrdersForSourceOrder` 用 `product-match` 引擎匹配接单模板,生成工单。
+
+关键点:
+
+- 匹配引擎是纯函数,来源差异只在适配器。
+- 并列最高分返回 `ambiguous`,不猜。
+- 抖音 `sku_code` 恒为内部 SKU;引流标题只进 `snapshot.productTitle`,永不参与匹配。
+- 抖音命中 `affiliate_dash` 后不建工单(`apps/backend/src/services/worker-platform/sync-worker-orders-service.ts:65`)。
+
+### 5.2 工单与结算
+
+状态机:
+
+```text
+pending_material → unassigned → open → in_progress
+ → pending_acceptance → accepted
+ → problem → open / cancelled
+```
+
+- 抢单:条件更新 `WHERE status='open' AND assigned_worker_id IS NULL`。
+- 结算:`acceptWorkOrderAndSettle` 锁工单与份额,释放/延迟解冻押金,抵扣售后欠款,可能升级等级。
+- 拼单:母单可长期 `open`,份额 `joined → submitted → accepted/cancelled`。
+- 赠送单:确认加好友后取消/脱离不再退押金。
+
+### 5.3 寄卖仓库
+
+- 提报:`consignment_submissions`,冻结发布押金。
+- 入库:`consignment_inventories`,审核通过生成 `INV…`。
+- 匹配:品类强隔离、三态比对、低配不充高配、unknown 不判精确。
+- 履约:`internal_stock` 内部现货,或 `worker_deposit` 指派打手。
+- 结算:验收后释放发布押金,补发货款 `cost_price_fen || asking_price_fen`。
+- 撤销:打手押金与卖家押金可 `auto / refund / forfeit / keep_frozen`,显式覆盖必须填原因。
+
+### 5.4 凭证与券
+
+| 类型 | 表 | 核销方式 |
+| --- | --- | --- |
+| 快手行业电子凭证 | `kuaishou_industry_vouchers` | 快手 OpenAPI |
+| 抖音 DYC 券 | `redeem_certificates` | `coupons.verifyV3` |
+
+两套表、两套状态机,**不可混用**。发布/指派时按来源分流:`publish-voucher-service.ts`。
+
+### 5.5 三块履约与成本(2026-10-17 只读数据增补)
+
+真正决定成本和监管方式的是**执行载体**,不是订单来源:
+
+| 块 | 载体 | 成本 | 生产数据 |
+| --- | --- | --- | --- |
+| 自动发货 | `affiliate_dash` 任务 | 固定,按皮肤/SKU | 上游已有 `cost_amount`,但未落库 |
+| 接单平台 | `work_orders` | 变动,按模板/单价/拼单 | 最近 30 天已验收 62,669 单 |
+| 人工客服 | 目前无载体 | 人工工时/按单 | 最近 30 天 **44,657 个已支付订单项无任务也无工单** |
+
+关键数据:
+
+- 91 无载体订单 42,391、PDD 2,258、抖音 8;全部 `claim_status=active`。
+- `manual_dispatch` 任务全库只有 6 条,说明人工块基本没进任务系统。
+- 已验收工单的 `orders.order_status` 仍为 `paid`,订单主状态不能代表履约完成。
+- 最近 30 天已验收工单:91 报酬/订单金额约 86.0%、PDD 80.0%、抖音 96.5%。**口径已确认(2026-10-17):`total_amount` 即用户付款金额、计收入,打手报酬计支出(总额法)**;91 的 `total_amount` 与快手 `paymentFen` 数据核验 100% 一致。详见 [载体判定与成本中心落地-复核与草案-2026-10-17.md](载体判定与成本中心落地-复核与草案-2026-10-17.md) §九。
+- 同一个商品名对应多个 `sku_code`:「指挥官密钥1个」27 个、「苏尔南钻石100颗」35 个,不能直接按 `sku_code` 做销量统计。
+- 当前 30 天订单基本是一单一品,但模型必须按订单项设计。
+
+详细设计见 [成本中心与三块履约模型.md](成本中心与三块履约模型.md)。
+
+---
+
+## 六、工程质量亮点
+
+1. **严格类型**:`strict`、`noUncheckedIndexedAccess`、`exactOptionalPropertyTypes`。
+2. **测试纪律**:后端 740 项,覆盖匹配引擎、押金策略、退款、路径判定、日志脱敏、SQL 形状。
+3. **SQL 护栏**:`check:sql` 检查历史迁移不可修改、热点索引存在、禁止 `COUNT(*) OVER()`。
+4. **生产配置校验**:生产强制 session secret 长度、显式 CORS 白名单、存储凭据完整。
+5. **回调验签**:91、快手、affiliate-dash、抖音四条外部链路均有验签;91/affiliate 有时间戳容差。
+6. **账号安全**:后台/打手口令 scrypt + 盐;会话落库 + `session_version`;打手设备数限制。
+7. **日志脱敏**:敏感 key、URL 参数、JSON 字段统一脱敏,并有单测。
+8. **部署与备份**:健康检查、优雅关闭、日志轮转、迁移前备份、按需重建。
+9. **零 TODO/FIXME**:源码中没有遗留标记;文档密度高。
+
+---
+
+## 七、风险清单(按当前代码复核)
+
+### P0 · 资金与一致性
+
+| # | 发现 | 证据 | 影响 |
+| --- | --- | --- | --- |
+| 1 | **发单金额高于用户付款只有前端提醒,后端无强制校验** | 前端 `apps/frontend/src/pages/admin/panels/work-order-payment.ts:17`;后端发布只校验 `>0` `apps/backend/src/services/worker-platform/admin-work-order-management-service.ts:228` | 直接调 API 可以发出亏损单;改价、发布、指派三入口没有统一门禁 |
+| 2 | **寄卖审核可由请求体改 `workerId`,并按改后归属结算货款** | `apps/backend/src/services/worker-platform/consignment-admin-service.ts:606`;供货方反查优先库存归属 `apps/backend/src/repositories/worker-platform/consignment-supplier-repo.ts:67` | 钱可能结给错误的人;还会按手机号自动绑定 |
+| 3 | **寄卖审核详情全量返回游戏账号密码 `credentialsJson`,只需 `consignment.view`** | `apps/backend/src/services/worker-platform/consignment-admin-service.ts:183` | 查看权限即可拿到卖家凭证,权限粒度过粗 |
+| 4 | **抢单与指派加锁顺序相反** | 抢单先锁钱包 `apps/backend/src/repositories/worker-platform/work-order-grab-repo.ts:25`;指派先锁工单 `:156`,再锁钱包 `:171` | 并发下可形成死锁,接口 40P01 / 500 |
+| 5 | **自动工单硬编码零押金,模板押金字段不生效** | `apps/backend/src/services/worker-platform/sync-worker-orders-service.ts:245` | 自动发单没有押金约束;`deposit_threshold_amount` 也只读写/展示 |
+| 6 | **发布工单时凭证核销失败不回滚;指派却会拦截** | 发布 `apps/backend/src/services/worker-platform/admin-work-order-management-service.ts:250`;指派 `apps/backend/src/services/worker-platform/admin-work-order-assignment-service.ts:75` | 同一条业务规则两套行为;抖音券缺失时工单仍可能进入大厅 |
+
+### P1 · 可靠性与公开面
+
+| # | 发现 | 证据 | 影响 |
+| --- | --- | --- | --- |
+| 7 | **Webhook 先去重落库、后处理业务;失败后平台重推被永久跳过** | 抖音 `apps/backend/src/services/platforms/douyin-mashida/notify-service.ts:95`;affiliate-dash `apps/backend/src/services/platforms/affiliate-dash/notify-service.ts:70` | 建单/发券/同步任一步失败,事件已写入 `webhook_events`,重推返回 duplicate,订单可能永久丢失 |
+| 8 | **抖音回调同步串行调用上游,单次超时 20 秒** | `apps/backend/src/services/platforms/douyin-mashida/source-config-service.ts:136` | 回调响应可能超过平台超时,与 #7 叠加后无法恢复 |
+| 9 | **`/collect/*`、`/redeem/*` 只凭订单号,无第二因子** | `apps/backend/src/routes/collect.ts:22`、`:32`;`apps/backend/src/routes/redeem.ts:24`、`:34` | 可查询/写入待补全订单资料;知道订单号即可触发买家券核销 |
+| 10 | **`/collect/upload` 可匿名上传任意场景文件到 OSS** | `apps/backend/src/routes/collect.ts:42`、`:49`;场景名只做字符串清洗 `apps/backend/src/services/file-storage/file-storage-service.ts:360` | 不校验订单存在,换 `orderNo` 即可绕过限流桶,存在存储滥用风险 |
+| 11 | **`/files/object` 无鉴权;claim token 默认永久有效** | `apps/backend/src/routes/files.ts:12`;默认 `apps/backend/src/config/defaults.ts:65`,生产示例同样为 0 `.env.server.example:27` | 链接或对象 key 泄露即长期可访问 |
+| 12 | **无 CSP/HSTS/X-Frame-Options;后台 token 存 localStorage** | `deploy/caddy/Caddyfile` 只有缓存头;`apps/frontend/src/utils/admin-auth.ts:1`、`:101` | 任一 XSS 即可接管后台会话 |
+| 13 | **先到先匹配,后到的快手补全数据不会纠正既有工单** | 已有工单直接 `already_exists` 跳过 `apps/backend/src/services/worker-platform/sync-worker-orders-service.ts:97` | 价格/模板可能停留在首次粗匹配结果 |
+| 14 | **单实例耦合**:限流桶、调度器、SSE、列表缓存均在进程内 | `apps/backend/src/middleware/rate-limit.ts:26`;`services/realtime/realtime-event-service.ts` | 直接扩容会出现重复定时任务、限流失效、缓存不一致 |
+
+### P2 · 可维护性与运维
+
+| # | 发现 | 证据 |
+| --- | --- | --- |
+| 15 | 巨型文件:`WorkOrdersPanel.tsx` 3,686 行、`AdminTaskDetailPage.tsx` 2,015 行、`work-order-query-repo.ts` 1,499 行、`consignment-repo.ts` 1,478 行 | 实测 |
+| 16 | 前端无组件渲染测试、无 E2E;部分后端测试是源码字符串断言 | 测试目录与用例实现 |
+| 17 | 无 CI;质量门禁只靠本机 pre-push,可被 `SKIP_CHECKS=1` 绕过 | `.githooks/pre-push` |
+| 18 | `dev` 分支已完全落后 `main`,实际是单分支开发 | `git log main..dev` 为空 |
+| 19 | 迁移启动时自动执行且无 down;多副本会竞争迁移 | `apps/backend/src/startup/bootstrap.ts:32` |
+| 20 | `uncaughtException` 只记录不退出 | `apps/backend/src/index.ts:57` |
+| 21 | 本地 Node 24,生产镜像 Node 22;前端主 chunk 524.69 kB 警告 | 构建输出、`deploy/docker/backend.Dockerfile:11` |
+| 22 | 备份会把 `apps/backend/data` 配置打包上传 OSS,可能含 PDD RSA 私钥/平台 Cookie | `deploy/backup-db.sh:119` |
+
+### 三块履约与成本增补(2026-10-17)
+
+| # | 发现 | 证据 | 影响 |
+| --- | --- | --- | --- |
+| C1 | **人工块没有载体、没有 case、没有完成状态** | 最近 30 天 44,657 个已支付订单项无任务也无工单;`manual_dispatch` 仅 6 条;已验收工单的订单仍 `order_status=paid` | 人工履约不可监管、不可计量、不可对账 |
+| C2 | **成本数据散落且无版本/快照** | 自动发货 `cost_amount` 只映射不落库;工单价格在 `work_product_rules`;人工无成本表 | 改价无法追溯,历史毛利不可重算,三块成本口径不一致 |
+| C3 | **订单金额口径不统一** | 91 `totalAmount=maxAmount`(`platforms/ninetyone/order-service.ts:48`);PDD `order_amount`;抖音 `pay_amount` | 直接汇总 `orders.total_amount` 会把不同财务口径相加 |
+| C4 | **SKU 与数量口径不稳定** | 同一商品名对应 27/35 个 `sku_code`;`order_items.quantity` 对「5个」类商品不可靠 | 店铺/日期/皮肤销量统计会漏计、重计或错并 |
+| C5 | **无订单项级金额字段** | `order_items` 仅有 `sku_code/sku_name/quantity/spec_json/item_snapshot_json` | 多商品订单无法直接分摊收入,需要快照/分摊规则 |
+
+---
+
+## 八、文档现状与本次整理
+
+本次已完成:
+
+1. 新增 [docs/README.md](README.md):文档唯一入口、状态约定、三来源口径。
+2. 新增 [docs/archive/README.md](archive/README.md):历史文档说明。
+3. 将 6 份过期阶段性文档移入 `docs/archive/2026-10-08/`,不删除内容,保留追溯。
+4. 统一以下文档的来源口径:
+ - `README.md`
+ - `docs/接单模板匹配引擎重构设计.md`
+ - `docs/抖音-码速达/00-总览.md`
+ - `docs/抖音-码速达/10-三层分流设计.md`
+ - `docs/抖音-码速达/12-按SKU分类改造总设计.md`
+ - `docs/履约配置/多发货平台与电子凭证整体链路.md`
+ - `docs/91卡券/5.当前项目对接配置.md`
+ - `docs/91卡券/6.开发设计.md`
+5. 校正“设计稿/未实施”状态:
+ - 匹配引擎:P0 + P1 已实施,P2/P3 待办。
+ - 抖音三层分流:抖音侧已实施。
+6. 校正过期测试数:抖音总览中的 636/115 改为 2026-10-17 复核的 740/143。
+7. 2026-10-17 增补:新增 [成本中心与三块履约模型.md](成本中心与三块履约模型.md),把自动发货/接单平台/人工客服三块、统一成本中心、销售事实与人工 case 纳入现行设计;本报告同步补入生产只读数据与 C1~C5 风险。
+
+仍保留为“设计/待办”的内容:
+
+- `work_product_match_entries` 新表尚未落地(只在设计中),当前仍靠 `compile.ts` 编译旧模板/绑定。
+- 匹配触发点收敛、寄卖建议价/大厅改价统一走 `pricing.ts` 尚未完成。
+- 抖音“待人工队列”仍未做。
+- `product_catalog` / `channel_product_mappings` / `cost_items` / `cost_versions` / `cost_snapshots` / `sales_daily_facts` / `manual_fulfillment_cases` 均未落地。
+
+---
+
+## 九、财务系统集成结论(摘要)
+
+现有财务能力是**业务钱包 + 流水**,不是会计总账:
+
+- `worker_wallets`:可用余额、冻结押金、累计入账、累计结算。
+- `worker_wallet_ledgers`:冻结/释放/扣减/结算等流水。
+- `worker_finance_requests`:充值/提现申请。
+- 售后欠款、寄卖发布押金、工单报酬、货款结算分散在多个 repo。
+
+缺口:
+
+1. 没有复式记账(借贷不平无法发现)。
+2. 没有会计期间/月结/关账。
+3. 没有平台侧对账(91、快手、PDD、抖音、affiliate-dash)。
+4. 没有订单级收入/成本/毛利。
+5. 余额是可变字段,缺少“由流水重算”的校验。
+6. 资金事件缺少统一的 `business_event_id` 幂等键。
+
+建议:先建**影子总账 + 对账**,不要一开始替换钱包。成本金额与商品维度单独由成本中心负责,人工块由人工 case 负责;三者关系见 [成本中心与三块履约模型.md](成本中心与三块履约模型.md),总账细节见 [财务系统集成方案.md](财务系统集成方案.md) 与 [影子总账说明.md](影子总账说明.md)。
+
+---
+
+## 十、建议路线
+
+### 立即(1–3 天)
+
+1. 后端统一“发单金额 ≤ 用户付款”校验,改价/发布/指派共用。
+2. 修复 Webhook 去重时序,改为 `processing / succeeded / failed` 或失败删除。
+3. 统一抢单/指派锁序,或对 40P01 做有限重试。
+4. 寄卖审核禁止直接改 `worker_id`;确需改派走独立接口 + 原因 + 复核。
+
+### 短期(1–2 周)
+
+1. 发布与凭证核销统一为“失败即不发布”,或明确异步补偿。
+2. `/collect`、`/redeem` 增加一次性 token/二次校验;上传前校验订单存在。
+3. 生产强制 `CLAIM_TOKEN_TTL_HOURS > 0`;私有文件短期签名;补安全响应头。
+4. 先落商品主数据 + 销售事实:`product_catalog`、`channel_product_mappings`、`sales_daily_facts`,解决店铺/日期/皮肤销量与数量归一。
+5. 再落成本中心影子期:`cost_items / cost_versions / cost_snapshots`,自动发货接上游固定成本,工单接现有规则金额,人工接按单/按小时成本。
+
+### 中期(1 个月)
+
+1. 人工 case:载体判定、无载体自动建 case、分配/SLA/证据/主管复核、人工成本快照。
+2. 财务影子总账:`finance_journal_entries / lines` + 业务事件映射。
+3. CI + PostgreSQL 集成测试;补金额守恒、并发抢单、Webhook 失败恢复测试。
+4. 财务对账:平台账单/支付流水/钱包流水三方核对。
+5. 订单级损益:收入、履约成本、打手报酬、人工成本、平台费、退款。
+6. 拆分巨型文件;补前端组件测试与 1–2 条 E2E。
+
+---
+
+## 十一、一句话总结
+
+**代码与工程纪律足以支撑下一阶段的财务系统;但财务系统必须建在“商品主数据 + 成本中心 + 不可变复式分录 + 业务事件幂等 + 对账”之上,而不是继续给 `worker_wallets` 加字段。** 先修资金门禁、Webhook 恢复、锁序与公开接口;再把人工块纳入 case 监管,最后接财务总账,风险最低。