Files
order_site/docs/项目全流程与数据库设计梳理-2026-10-23.md
yml2213 9d5af359fa feat(finance): 完成财务控制、L1 钱包硬化与迁移收敛
- P0/P1:影子总账追加调整、关账/退款/资金/积分幂等、成本版本追溯、对账订单项维度
- 迁移收敛:原 103–114 合并为 103_finance_foundation、104_finance_controls、105_work_order_consignment
- 数据:L1 钱包 BIGINT/CHECK/奖励幂等;重复奖励审计口径修正与人工核对清单
- 报表:销售毛利缺失成本收入提示,旧收支页明确现金流口径
- 文档:同步迁移基线、合并评估、代码复核与上线说明
2026-10-27 16:08:16 +08:00

876 lines
52 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# order_site 全流程与数据库设计梳理(2026-10-23)
> 状态:**梳理稿(待评审,尚未登记到 `docs/README.md`)**
> 落笔日期:2026-10-23
> 代码基线:工作区当前 HEAD;数据库迁移 `001`–`110`(共 110 个文件)
> 核对方式:源码走查 + 本地 PostgreSQL 实读表结构 / 约束 / 索引 + 本地库状态与数据量统计
> 说明:本地库与生产同量级(`orders` 约 37.5 万行),本文引用的数据量均为**本地库实测**,不与生产承诺一致,仅用于判断结构合理性。
---
## 一、先看结论(10 条)
1. 系统早已不是「快手自动发货工具」,而是**三来源订单履约 + 打手众包接单 + 寄卖仓库 + 电子凭证核销 + 客服备用金台账**的一体化平台。
2. 订单来源只有三个:`kuaishou-91`(91 回调 + 快手 send-code 两条通道,**同一个来源**)、`pdd`、`douyin-mashida`。代码层的来源键定义在 `apps/backend/src/services/product-match/types.ts`。
3. **核心不变式:一个订单 = 骨架(`orders` + `order_items`)+ 买家凭证(快手电子凭证 / 抖音券)+ 一个执行载体**。载体只有三种:`affiliate_dash` 自动发货、`work_orders` 工单、`manual` 人工(当前无显式表,靠「无任务且无工单」判定)。
4. 履约任务按「订单项 × 数量」拆单元:`fulfillment_tasks` 的 `UNIQUE(order_item_id, unit_index)` 是整套领取链接体系的基座。
5. 领取链接 = `claimLinkBase + '/' + claim_tokens.token`,一任务一 token(`claim_tokens.task_id` 唯一);`CLAIM_TOKEN_TTL_HOURS=0` 表示永久有效。
6. 打手侧是一条**独立业务线**:工单状态机 `pending_material → unassigned → open → in_progress → pending_acceptance → accepted`,另有 `problem`、`cancelled`;资金走「钱包 + 流水」模型,不是会计总账。
7. 寄卖是第三个业务面:提报 → 审核入库(`INV…`)→ 规格匹配(系统/区服/系列/钻级/璀璨)→ 履约(内部现货 or 指派打手)→ 结算与撤销押金策略。
8. 备用金台账(迁移 100)已落地:`fund_accounts`(主管池 / 客服账户)+ `fund_entries`(只追加流水,余额只能由流水变动)+ `manual_expenses` + `fund_reconciliations`。
9. 数据库现状:**60 张业务表 + `schema_migrations`**,113 个外键、21 个索引的 `work_orders` 是全库索引最多的表;配置不入表结构,统一存 `app_config_entries`(本地 18 个配置键)。
10. 主要架构性风险仍是 2026-10-17 报告列的那几处:资金门禁只在前端、Webhook 先落库后处理、`/collect` `/redeem` 无第二因子、抢单/指派锁序相反、单实例依赖进程内存。
---
## 二、系统地图
### 2.1 运行拓扑
```mermaid
flowchart LR
U1["外部平台<br/>91卡券 / 快手 / 拼多多 / 抖音码速达"] --> C
U2["买家<br/>领取页 / H5 / 兑换页"] --> C
U3["打手<br/>worker.khhao.com"] --> C
U4["后台运营 / 客服 / 财务"] --> C
C["Caddy :80/443<br/>反代 + 静态缓存 + 压缩"] --> F["frontend<br/>React 19 + Vite(一套产物三个入口)"]
C --> B["backend<br/>Express 5 + TS<br/>routes → services → repositories → pg"]
B --> P[("PostgreSQL 16")]
B --> O["对象存储<br/>生产 OSS / 本地 MinIO"]
B --> X["外部 API<br/>affiliate-dash / 快手 OpenAPI / 抖音码速达 / PDD"]
B --> M["进程内存<br/>限流桶 / 调度器 / SSE / 列表缓存"]
```
单实例假设写在代码里:限流、定时任务、SSE 订阅、列表缓存全部在进程内存(`middleware/rate-limit.ts`、`services/realtime/`),扩容前必须先外置。
### 2.2 后端入口与路由挂载
入口 `apps/backend/src/app.ts`:
| 挂载点 | 说明 |
| --- | --- |
| `/health`、`/health/live`、`/health/ready` | 健康检查;未 ready 时其余请求统一 503 |
| `/api/v1/open/douyin-mashida` | **必须在全局 body parser 之前**挂载,签名基于原始 JSON 字符串 |
| `/api/v1/open/affiliate-dash` | 发货平台异步通知(验签 + 时间戳容差) |
| `/api/v1/open/91` | 91 卡券异步下单 / 查询(验签) |
| `/api/v1/open/kuaishou-industry` | 快手行业电子凭证 send-code / destroy / query / consume |
| `/api/v1/redeem` | 抖音券兑换页接口 |
| `/api/v1/claim` | 买家领取页(token 与 H5 两种入口) |
| `/api/v1/collect` | 待补全订单资料收集 |
| `/api/v1/pdd/extension` | 拼多多浏览器扩展上报 Cookie / 公钥 |
| `/api/v1/files` | 文件对象读取 |
| `/api/v1/worker` | 打手端(公开鉴权路由 + 会话路由) |
| `/api/dcauth` | 设备认证(dcauth 协议) |
| `/api/v1/admin` | 后台 API(auth 路由放行,其余全部 `requireAdminSession`) |
| `/s` | 短链跳转 |
### 2.3 分层与模块职责
| 层 | 目录 | 职责 |
| --- | --- | --- |
| 路由 | `src/routes/**` | 参数解析、限流、鉴权、调用 service |
| 领域 | `src/domain/**` | 状态机与纯策略(任务、工单、寄卖、押金、验收模式) |
| 服务 | `src/services/**` | 订单、履约、工单平台、平台对接、财务、寄卖、调度 |
| 仓储 | `src/repositories/**` | 全部 SQL(含 `worker-platform/*` 大仓储) |
| 迁移 | `src/db/migrations` | 001 基线 + 109 个增量 |
| 前端 | `apps/frontend/src/pages/**` | `admin` 后台 / `worker` 打手端 / `claim` 领取页 / `redeem` 兑换页 / `collect` 补资料 |
---
## 三、端到端主流程
### 3.1 总览(一条单的完整生命周期)
```mermaid
flowchart TD
A["来源进单<br/>91 / PDD / 抖音"] --> B["upsertOrderFromSource<br/>advisory lock 去重 + 状态合并"]
B --> C["order_items 落库<br/>resolveOrderItemForFulfillment 判定载体"]
C --> D{"isConfigured?"}
D -->|否| E["待补全<br/>91 返回 orderStatus=10"]
C -->|是| F["syncDeliveryTasksForOrder<br/>按 order_item × quantity 建任务单元"]
F --> G{"executor_key"}
G -->|affiliate_dash| H["自动发货<br/>建 affiliate-dash 单 + 领取链接"]
G -->|manual_dispatch| I["人工履约<br/>manual_review 待办"]
C --> J["syncWorkerOrdersForSourceOrder<br/>product-match 匹配接单模板"]
J -->|命中且 auto_create| K["work_orders<br/>待补素材 / 待发布"]
K --> L["打手大厅抢单 / 后台指派"]
L --> M["代练履约 → 提交验收"]
M --> N["验收 → 结算报酬 / 释放押金 / 等级进度"]
H --> O["买家领取页 claimUrl<br/>填 UID → 绑定 → 提交发货"]
O --> P["affiliate-dash delivered 回调"]
P --> Q["任务 completed / redeemed<br/>快手电子凭证核销闭环"]
N --> R["钱包流水 + 售后 / 欠款"]
Q --> S["后台报表 / 备用金台账 / 审计"]
R --> S
```
### 3.2 三个来源的进单通道
| sourceKey(业务口径) | 实际 provider / platform | 入口 | 数据形态 | 备注 |
| --- | --- | --- | --- | --- |
| `kuaishou-91` | `91kaquan` / `kuaishou` | `POST /api/v1/open/91/orders/create` | 一单一条,`productNo` 解析出商品名与店铺 ID | 91 是订单骨架权威源 |
| `kuaishou-91` | `91kaquan` / `kuaishou` | `POST /api/v1/open/kuaishou-industry/send-code` | 快手电子凭证补全/兜底 | 与 91 共享店铺键空间,**不是新来源** |
| `pdd` | `pdd` / `pdd` | 定时任务 `pdd_order_sync`(20s)拉取 | 两段式 `pdd_raw_orders → orders` | 浏览器扩展上报 Cookie |
| `douyin-mashida` | `douyin_mashida` / `doudian` | `POST /api/v1/open/douyin-mashida/message` | 消息推送,先落 `webhook_events` 去重 | 抖店订单 + DYC 券 |
本地库实测分布:91 368,355 单;PDD 5,563 单;抖音 1,541 单。
#### 3.2.1 91 卡券(下单 + 查询)
```mermaid
sequenceDiagram
participant K as 91卡券
participant API as backend /open/91
participant S as order-service
participant DB as PostgreSQL
K->>API: POST /orders/create(验签)
API->>API: buildOpen91SourceEvent<br/>provider=91kaquan, platform=kuaishou<br/>totalAmount=maxAmount(分)
API->>S: upsertOrderFromSource(event)
S->>DB: pg_advisory_xact_lock(hashtextextended('provider|platform|orderNo'))
S->>DB: upsert orders + replace order_items
S->>S: 载体判定 → 建任务 / 建工单
API-->>K: orderStatus=10(未生成 claimUrl)或成功
K->>API: POST /orders/query
API-->>K: orderStatus=20 + cards(claimUrl 加密卡密)<br/>或 10(处理中)/ 30(失败)
```
要点:
- 订单幂等键是 `UNIQUE(provider, platform, shop_id, platform_order_id)`,事务内先取 advisory lock,避免并发重复建单。
- 状态合并用优先级表:`created(0) < paid(1) < closed(2) < refunded(3)`,`pay_status` 同理;`pending_config` 这类历史状态优先级最低,会被新事件覆盖。
- 91 查询的三种状态:`10` 处理中/未就绪,`20` 已发货(`cards` 为加密后的 claimUrl),`30` 失败(`pending_config`、`manual_failed` 或任务失败)。
- 历史遗留:库中仍有 25 条 `order_status='pending_config'` 的 91 老单(最近一条 2026-08-11),当前代码不再写入该状态,只作为查询口径识别。
#### 3.2.2 快手行业电子凭证(send-code 四条接口 + 两条出站回调)
- 券号与 91 订单解耦:`voucher_code = 'KSV' + Crockford Base32(10 字节随机)`,`UNIQUE(voucher_code)`,冲突重试 5 次。
- **幂等键是 `UNIQUE(oid, unit_index)`**:重复 `send-code` 返回同一批 `eticket.id`;`unit_index = 1..num`。
- 未找到 91 任务时券先落库(`task_id` 为空),后续由 `voucher-binding-service` 绑定,支持「快手先发码、91 后建单」。
- 出站回调用 `send_callback_status/attempt_count/last_error/response_json/sent_at` 记录,失败可重推。
- 核销流水号幂等:`consume_serial_num = CONSUME-{voucher_code}`;状态只有 `UNUSED / CONSUMED / DESTROYED`。
- 本地库实测:`CONSUMED` 376,179、`DESTROYED` 26,815、`UNUSED` 106 —— 说明核销链路是真实主路径。
#### 3.2.3 拼多多(两段式)
```text
pdd_order_sync 定时任务(默认 20s / pageSize 10)
→ pdd-client 拉订单(Cookie + anti-content)
→ upsert pdd_raw_orders(UNIQUE(order_sn))
→ processPendingPddRawOrders 逐条消费
→ pdd-order-mapper 归一化
→ upsertOrderFromSource
→ sync_status: pending → synced / ignored / failed
→ 后台可对 failed 的原始单做 replay
```
设计意图:原始报文与业务订单分离,失败可重放,不会因为映射问题丢单。断路器与 Cookie 过期识别在 `pdd-sync-circuit-breaker.ts` / `isPddCookieExpiredError`。
#### 3.2.4 抖音码速达(推送 → 建单 → 发券 → 领取)
```mermaid
sequenceDiagram
participant D as 抖音码速达
participant N as notify-service
participant O as order-service
participant C as certificate-service
D->>N: POST /message?topic&pub_time&sign(原始 body)
N->>N: 验签(data 原始字符串)
N->>N: eventId = f(topic, pub_time, data)
N->>N: insertWebhookEventOnce(先落库去重)
alt topic = trade_TradePaid
N->>D: 拉订单详情(网关)
N->>O: upsertOrderFromSource
N->>C: 生成 / 同步 DYC 券(redeem_certificates)
N->>D: 推送给买家的领取链接
else trade_TradeCanceled / refund_RefundSuccess
N->>C: 券失效 + 订单状态收敛
end
```
- 抖音 `sku_order_list[].code` 是**内部 SKU**,`product_name` 是引流标题;`sku_code` 恒存内部 SKU,标题只进 `snapshot.productTitle`,永不参与匹配。
- 券表 `redeem_certificates` 自带状态机 `issued / synced / verified / invalid` 与同步状态 `pending / success / failed`,核销走 `coupons.verifyV3`。
- **已知缺陷**:`webhook_events` 先插入后处理,处理失败时平台重推会被判 `duplicate` 丢弃(详见风险清单)。
### 3.3 载体判定(本系统最关键的一步)
```mermaid
flowchart TD
A["order_item"] --> B{"来源"}
B -->|91 / 快手| C["productNo → affiliate-dash sku 映射<br/>常量表 + 后台 skuMapping 覆盖"]
B -->|抖音| D["内部 SKU → affiliate-dash sku 映射"]
C --> E["resolveFulfillmentRoute"]
D --> E
E --> F{"路由顺序"}
F --> G["1. 履约路由总开关<br/>关闭 = 停止自动履约"]
F --> H["2. 指定匹配(显式映射)"]
F --> I["3. 商品名规则 exact / contains"]
F --> J["4. 内部 SKU 规则"]
F --> K["5. 全局执行器优先级"]
F --> L["6. unmatchedExecutorKey(生产为空)"]
G --> M["snapshot.fulfillmentRoute<br/>selectedExecutorKey / reason / candidates / skipped"]
H --> M
I --> M
J --> M
K --> M
L --> M
M --> N{"selectedExecutorKey"}
N -->|affiliate_dash| O["isConfigured=true<br/>建自动履约任务"]
N -->|manual_dispatch| P["isConfigured=true<br/>建人工任务"]
N -->|空| Q["isConfigured=false<br/>待补全 / 交给发单层判定"]
```
判定结果全部写进 `order_items.item_snapshot_json`:
| 快照键 | 内容 |
| --- | --- |
| `fulfillmentRoute` | `selectedExecutorKey`、`reason`、`candidates`、`skipped`、`matchedRule` |
| `classification`(抖音) | `channel: affiliate_dash / unmatched`、`canonicalSku` |
| `affiliateDash` | 命中的 `sku`、`productNo`、`skuName`、`matchMode` |
| `isConfigured` | 是否已配置可执行载体 |
| `productTitle`(抖音) | 引流长标题(只读展示) |
| `kuaishouSendCode` | 快手 send-code 补全的卖家/SKU 上下文 |
**载体互斥守护**:`sync-worker-orders-service.isAffiliateDashCarrierSnapshot()` 会跳过 `classification.channel === 'affiliate_dash'` 的订单项,避免自动发货与工单双通道同时消费一个订单项。
### 3.4 履约任务与领取链接
1. `syncDeliveryTasksForOrder` 对每个订单项按 `quantity` 补建 `unit_index = 1..n`,重复执行只补缺口。
2. 执行器来自 `snapshot.fulfillmentRoute`:
- `affiliate_dash` → 绑定 `fulfillment_profiles.profile_key='affiliate_dash'`,`requires_claim=true`,初始状态 `paid`;
- `manual_dispatch` → `profile_key='manual_review'`,初始状态 `manual_review`;
- 未支付 → `pending_payment`。
3. 已支付订单立即 `preparePaidFulfillmentTask`(执行器注册表:`affiliate_dash` / `manual` / `legacy-retired` 只读桩)。
4. 领取 token:`createTaskClaimToken` 写 `claim_tokens`(`task_id` 唯一、`token` 唯一、`max_use_count=1`),`claimUrl = claimBaseUrl + '/' + token`。
5. 抖音走独立基址 `claimLinkBase`(后台配置),**刻意不回退** `orders.claimBaseUrl`。
6. `orders.claim_status`(迁移 099)= `active / paused / closed`,用于暂停或关闭整单兑换,是后台处置链接失效问题的新开关。
### 3.5 affiliate-dash 自动履约
```text
建任务(executor=affiliate_dash,requires_claim=true)
→ preparePaidFulfillmentTask:调 affiliate-dash 建单,拿到 orderNo / bindUrl / qrUrl
→ 生成 claim token 与 claimUrl(91 查询即可返回 20)
→ 建单成功时即核销快手电子凭证(consumeStatus=success;失败只告警)
→ 买家在领取页:查 delivery → 填 UID → bind → 轮询 bind-result → submit
→ affiliate-dash 回调 order.shipping.updated(delivered)
→ 任务 completed / redeemed;回调只做状态收敛与兜底补核销
```
任务上下文固定形状 `context_json.affiliateDash`(`AffiliateDashFlow`),包含 `sku / orderNo / clientOrderNo / orderStatus / canShip / bindUrl / qrUrl / bound / submitStatus / consumeStatus / lastSyncedAt`。
### 3.6 买家侧三个页面
| 入口 | 路由 | 用途 |
| --- | --- | --- |
| 主站领取页 | `/#/claim/:token` | 快手渠道主路径,按 `executorKey` 渲染不同步骤 |
| H5 领取页 | `/h5/:orderNo`(BrowserRouter 分支) | 抖音等按订单号承接,`/api/v1/claim/h5/...` |
| 兑换页 | `/redeem` | 抖音 DYC 券兑换/核销 |
| 补资料页 | `/collect` | 待补全订单资料收集(**仅凭订单号,无第二因子**) |
领取页快照与轮询逻辑在 `pages/claim/claim-snapshot.ts`、`claim-poll.ts`;`affiliate-dash` 的具体步骤在 `ClaimAffiliateDashSteps.tsx`(查 delivery → UID → bind → 轮询 → submit)。
### 3.7 打手众包:从匹配到结算
#### 3.7.1 接单模板匹配(product-match 引擎)
匹配引擎是**纯函数**(`services/product-match/engine.ts`),来源差异只在适配器:
| 概念 | 说明 |
| --- | --- |
| 条目 `entry` | 一条接单模板(`work_product_rules`)或其映射(`work_product_rule_mappings`)编译而来 |
| 作用域 | `sourceKeys`、`shopKeys`、`qtyMin/qtyMax`:配了就必须命中 |
| 条件组 | `all`(全部命中)、`any`(至少一条)、`bonus`(命中加分、不中不淘汰) |
| 匹配模式 | `exact`、`contains`、`series`(剥离「N个」后比系列,兼容秘钥/密钥)、`series-exact` |
| 权重 | `skuId/relSkuId 900 > skuText/skuNick 800 > series 600 > skuCode 500 > itemId 300 > itemTitle 200`(contains 降档,文本另有「具体度」加成上限 80) |
| 结果 | 唯一最高分 → `matched`;并列最高 → `ambiguous`(**不猜**);无候选 → `unmatched` |
每次匹配都会写 `work_product_match_logs`(含 `candidates_json`、`raw_payload_json`),后台有「匹配测试 / 匹配日志 / 日志清理」三个入口。
#### 3.7.2 建单规则
```text
for each order_item:
if snapshot.classification.channel == 'affiliate_dash' → skip(载体已被自动发货占用)
if 该订单项已有工单 → skip(already_exists,不纠正旧单)
match = 匹配引擎
无命中/歧义 → skip + 日志
命中但 auto_create=false 且 autoOnly → skip
platform_order_id 已挂在**其他**订单下 → skip(防撞号)
建单:
status = 素材必填项齐全 ? unassigned : pending_material
reward = 单价 × SKU 数量(unit_price_fen)或固定 reward_amount
sharing(拼单)按模板 sharing_total_quantity / unit_reward 计算
acceptance_mode = normal / friend_gift(好友赠送)
timeout_minutes + timeout_policy(reopen / cancel_release / cancel_deduct)
material = 来源摘要 +(抖音)已有券快照
pending_material → 生成后台「待补素材」通知
```
#### 3.7.3 工单生命周期
```mermaid
stateDiagram-v2
[*] --> pending_material
pending_material --> unassigned: 素材补全
pending_material --> cancelled: 取消
unassigned --> open: 发布到大厅
unassigned --> in_progress: 后台指派
open --> in_progress: 打手抢单 / 拼单参与
in_progress --> pending_acceptance: 打手提交验收
pending_acceptance --> accepted: 验收通过(结算)
pending_acceptance --> problem: 发起问题单
problem --> open: 退回大厅
problem --> cancelled: 取消
in_progress --> open: 超时按 reopen 策略
in_progress --> cancelled: 超时按 cancel 策略
open --> cancelled: 取消
accepted --> [*]
cancelled --> [*]
```
- **抢单**:单事务内 `ensureWallet → 校验活跃单上限 → 校验可用余额 ≥ 押金 → 条件更新 `WHERE status='open' AND assigned_worker_id IS NULL` → 冻结押金(`deposit_freeze` 流水)→ 写 `work_order_events`。
- **指派**:先 `SELECT … FOR UPDATE` 锁工单,再锁钱包(与抢单顺序相反,见风险 R4)。
- **拼单**:`work_order_shares` 一份一行,`UNIQUE(work_order_id, worker_id)`;份额状态 `joined → submitted → accepted / cancelled`;母单可长期 `open`,最后一个份额验收后母单转 `accepted`。
- **好友赠送**:`acceptance_mode='friend_gift'`,`gift_phase`:`material_required → material_submitted → friend_countdown → gift_ready`,冷却结束才能提交赠送凭证;确认加好友后不再退押金(防套利)。
- **超时**:定时任务 `work-order-timeout`(默认 60s,scanLimit 50)扫描 `deadline_at`,按 `timeout_policy`(`reopen` 退回大厅 / `cancel_release` 取消并退押金 / `cancel_deduct` 取消并扣押金)处置;已确认加好友的赠送单即使配 `cancel_release` 也会转为扣款。超时只写 `timeout_*` 事件并释放寄卖现货,**不直接改等级计数**(等级处罚来自退单审核通过次数,见 3.7.6)。
- **验收与结算**(`acceptWorkOrderAndSettle`):锁工单与份额 → 报酬入账(可延迟解冻)→ 释放/延迟解冻押金(`worker_deposit_unfreezes` 队列)→ 抵扣售后欠款 → 升级等级 → 写 `worker_wallet_ledgers`。
- **售后**:`worker_after_sales_cases`(问题类型、投诉、验收快照、处置动作、退款)+ `worker_after_sales_debts`(欠款余额)+ `worker_after_sales_case_events`(时间线)。
#### 3.7.4 打手资金模型
| 表 | 作用 |
| --- | --- |
| `worker_wallets` | 可用余额、冻结押金、累计入账、累计结算、待解冻、提现冻结 |
| `worker_wallet_ledgers` | 只追加流水,13 种类型(见下) |
| `worker_finance_requests` | 充值 / 提现申请与审核 |
| `worker_withdrawal_accounts` | 提现账户(微信 / 支付宝二维码) |
| `worker_deposit_unfreezes` | 延迟解冻队列(押金、报酬两类) |
实测流水类型分布(本地库):`reward_settlement 108,623`、`sharing_reward 34,295`、`deposit_freeze 8,283`、`deposit_release 6,705`、`withdraw_paid 6,381`、`withdraw_freeze 5,833`、`deposit_deduction 920`、`deposit_pending_unfreeze / deposit_unfreeze 各 621`、`manual_credit 398`、`withdraw_release 121`、`after_sales_balance_deduction 20`、`after_sales_debt_offset 9`。
#### 3.7.5 等级、权限与风控
- `worker_levels.permission_json` 承载:免押额度、最大同时接单量、大厅可见延迟、自动验收(VIP)。
- `worker_level_logs` 记录升降级;`worker_users.is_level_locked` 可锁定等级。
- `worker_sessions` 会话落库 + `session_version`,设备数与设备类型可限制。
- `worker_risk_events` 记录刷单/异常行为与处置(ban / unban / release),`middleware/worker-risk-control.ts` 做抢单前置限流与拦截。
- 提现黑名单:`worker_users.is_withdrawal_blacklisted` + 原因 + 时间。
> 抢单/拼单/好友赠送/验收/结算的逐行细节(含押金矩阵、资金等式、退单处罚规则)见 [打手工单深挖-抢单拼单好友赠送结算-2026-10-23.md](打手工单深挖-抢单拼单好友赠送结算-2026-10-23.md)。
### 3.8 寄卖仓库
```mermaid
flowchart TD
A["卖家提报 consignment_submissions<br/>游戏/系统/区服/系列/钻级/璀璨/要价/凭证"] --> B{"客服审核"}
B -->|驳回| C["rejected(可改后重提)"]
B -->|通过| D["入库 consignment_inventories<br/>inventory_no = INV…<br/>status=in_stock"]
D --> E{"订单匹配"}
E -->|规格匹配| F["占用 consumed<br/>bound_order_id / bound_work_order_id"]
E -->|不匹配| D
F --> G{"履约方式"}
G -->|internal_stock| H["内部现货交付<br/>客服操作"]
G -->|worker_deposit| I["指派打手代练<br/>冻结打手押金"]
H --> J["验收 → 释放发布押金 + 结货款"]
I --> J
F -.->|撤销| K["revoke:打手押金 + 卖家押金<br/>auto / refund / forfeit / keep_frozen"]
```
匹配口径(`consignment-matching-service.ts`):
- **系统 / 区服 / 系列必须一致**(区服「互通」、系统「通用」视为通配);
- **钻级与璀璨低配不充高配**:号主 2 钻不能配 1 钻库存;
- 三态比对:`match` / `mismatch` / `unknown`,`unknown`(任一侧没值)**不算精确匹配**,只能进部分匹配候选;
- 人工指定时用 `classifyConsignmentMatch` 返回 `conflicts`,把「差在哪」显式告诉客服;
- 建议价从接单模板反推(`work_product_rules` 商品名 → 规格解析 → 最接近档位兜底,命中多条取最高价)。
卖家视角的单一阶段由 `deriveConsignmentItemStage` 从「提报 + 库存 + 工单」三套状态推导:`pending_review → in_stock → matched_waiting_friend → gift_cooldown → gift_ready → gift_submitted → fulfilling → pending_acceptance → completed`,终态 `rejected / archived / cancelled`。
押金策略(`domain/consignment-deposit-policy.ts`):
| 场景 | `auto` 行为 |
| --- | --- |
| 打手履约押金撤销 | 全额退还;要扣必须显式 `forfeit` |
| 卖家发布押金撤销 | 作废(下架)→ 退还;回库(继续挂卖)→ 保持冻结 |
| 显式覆盖 | 必须填原因(`isDepositHandlingOverridden` 校验) |
### 3.9 客服备用金台账(迁移 100)
```text
公司 → 主管池(fund_accounts.account_type='supervisor')
主管 → 客服账户('agent',responsible_supervisor_id 指向主管)
客服 → 支出明细(manual_expenses:平台充值 / 采购 / 赔付 / 运费 / 其他)
```
四条硬约束(写在建表注释里):
1. 余额只能由 `fund_entries` 变动,禁止手工改;
2. 发放 / 缴回必须同时写主管与客服两条方向相反的流水,共用 `transfer_no`;
3. 支出必须同时写客服负数流水与 `manual_expenses` 明细(`UNIQUE(fund_entry_id)`);
4. 已记账流水不修改不删除,差异走 `adjustment`。
对账:`fund_reconciliations`(`pending / confirmed / discrepancy`),DB 级校验 `difference_fen = actual_balance_fen - system_balance_fen`。
权限点:`fund.view / issue / expense / reconcile / adjust / export`,按角色(admin / finance / operator / support)分别在迁移里授权。
### 3.10 后台治理
| 能力 | 实现 |
| --- | --- |
| 登录 | 用户名密码 + 短信二次(可选) + 设备认证(`admin_user_devices` / `admin_device_challenges`) |
| 会话 | `admin_sessions` 落库 + `session_version`,可远程踢设备 |
| RBAC | 4 个内置角色(admin / finance / operator / support)+ 29 个权限点 + 72 条角色授权 |
| 审计 | `admin_audit_logs`(actor / action / target / payload),实测 55 万行 |
| 通知 | `admin_notifications`(类型 / 优先级 / 去重键 / 提醒次数 / 可见性 / 声音) |
| 实时 | `realtime_events` + SSE,后台列表与工单详情增量刷新 |
| 定时任务 | 后台可视化管理(启用、间隔、立即执行、运行状态) |
### 3.11 定时任务
| id | 类型 | 默认间隔 | 作用 |
| --- | --- | --- | --- |
| `work-order-timeout` | `work_order_timeout` | 60s(scanLimit 50) | 工单超时处置 + 打手超时计数 |
| `deposit-unfreeze` | `deposit_unfreeze` | 60s(scanLimit 100) | 押金/报酬延迟解冻出队 |
| `pdd_order_sync` | `pdd_order_sync` | 20s(pageSize 10) | 拼多多订单拉取 |
| `data-retention` | `data_retention` | 24h | 清理原始报文、匹配日志、任务事件、webhook、审计;回填 `source_order_amount_fen` |
调度器在进程内(`services/scheduler/scheduler-service.ts`),同一任务运行中会跳过本轮;配置存 `app_config_entries.scheduled_jobs`。
---
## 四、状态机一页汇总
### 4.1 订单 `orders`
| 字段 | 取值 | 备注 |
| --- | --- | --- |
| `order_status` | `created / paid / closed / refunded`(另有历史值 `pending_config`、`manual_failed`) | 合并时按优先级只升不降 |
| `pay_status` | `unpaid / failed / paid / refunded` | 与 `paid_at` 联动 |
| `claim_status` | `active / paused / closed` | 兑换有效性开关(迁移 099) |
> 注意:`order_status` **不能代表履约完成**。本地库中 375,432 条 `paid` 订单里,绝大多数并没有「订单级完成」状态,履约进度只能看 `fulfillment_tasks.task_status` 或 `work_orders.status`。
### 4.2 履约任务 `fulfillment_tasks`
`pending_payment → paid → link_generated → claimed → pending_binding_prepare → waiting_binding → role_confirmed → redeeming → dispatched_pending_return → completed / redeemed`,旁支 `retry_pending`、`manual_review`、`failed`、`expired`、`closed`。
状态转移约束定义在 `domain/task-status.ts`(`TASK_TRANSITIONS`),终态 `completed / redeemed / expired / closed`。
### 4.3 接单工单 `work_orders` / 份额 `work_order_shares`
- 工单:`pending_material / unassigned / open / in_progress / pending_acceptance / problem / accepted / cancelled`(终态 accepted、cancelled)。
- 份额:`joined / submitted / accepted / cancelled`。
- 好友赠送额外有 `gift_phase`:`material_required / material_submitted / friend_countdown / gift_ready`。
### 4.4 寄卖
| 对象 | 取值 |
| --- | --- |
| `consignment_submissions.status` | `pending_review / approved / rejected` |
| `consignment_inventories.status` | `in_stock / consumed / archived` |
| `consignment_inventories.allocation_status` | `待安排 / 已安排`(中文枚举,历史遗留) |
| `source_type` | `manual / worker_portal / recycle` |
| 卖家可感知阶段 | `pending_review / in_stock / matched_waiting_friend / gift_cooldown / gift_ready / gift_submitted / fulfilling / pending_acceptance / completed / rejected / archived / cancelled` |
### 4.5 凭证与券
| 对象 | 取值 |
| --- | --- |
| `kuaishou_industry_vouchers.status` | `UNUSED / CONSUMED / DESTROYED` |
| `kuaishou_industry_vouchers.send_callback_status` | `success / failed / pending`(回调快手发码结果) |
| `redeem_certificates.status` | `issued / synced / verified / invalid` |
| `redeem_certificates.sync_status` | `pending / success / failed` |
| `kuaishou_cloud_task_states.ticket_status` | 快手云端取号/绑号状态(配合 `dispatch_status`、`consume_status`) |
### 4.6 资金与售后
| 对象 | 取值 |
| --- | --- |
| `worker_finance_requests.status` | `pending / approved / rejected / cancelled` |
| `worker_finance_requests.request_type` | `recharge / withdraw` |
| `worker_deposit_unfreezes.status` | 待解冻 / 已释放(`released_at` 落地) |
| `worker_after_sales_cases.status` | 待响应 / 已响应 / 已处置 / 已退款(配合 `resolution_action`) |
| `worker_after_sales_debts.status` | 未结清 / 已结清 |
| `fund_entries.entry_type` | `opening / funding / issue / expense / return / adjustment` |
| `fund_reconciliations.status` | `pending / confirmed / discrepancy` |
---
## 五、数据库设计
### 5.1 总览(本地库实读)
| 指标 | 数值 |
| --- | --- |
| 业务表 | **59** 张(+ `schema_migrations`) |
| 迁移文件 | 110 个(001 基线 + 002–110 增量) |
| 外键 | 91 |
| 唯一约束 | 29 |
| CHECK 约束 | 28 |
| 索引 | 268(含主键与唯一索引) |
| 主键 | 绝大多数是 `BIGSERIAL`;钱包、工单备注、工单已读等关系表用复合主键 |
| 时间类型 | 统一 `TIMESTAMPTZ`(不存本地时间) |
| 金额类型 | 打手侧 `INTEGER`(分);`orders.total_amount` 也是 `INTEGER`;资金台账、PDD 与工单来源金额用 `BIGINT`(分);**没有 `NUMERIC`,也没有浮点金额** |
| JSON | 变化快的扩展信息一律 `JSONB`(快照、上下文、凭证原文),不建宽表 |
设计风格非常一致:**关系表 + `*_json` 扩展 + 只追加流水**。代价是 JSONB 里的字段无法被 DB 约束保护(如 `context_json.affiliateDash` 全靠 TS 类型)。
### 5.2 分域表清单
#### A. 订单与履约主链路(12)
| 表 | 行数(估) | 说明 |
| --- | --- | --- |
| `orders` | 375,459 | 订单骨架;`UNIQUE(provider, platform, shop_id, platform_order_id)` |
| `order_items` | 375,263 | 订单项;`item_snapshot_json` 是载体判定的全部证据 |
| `fulfillment_tasks` | 107,111 | 履约单元;`UNIQUE(order_item_id, unit_index)`、`UNIQUE(task_no)` |
| `claim_tokens` | 107,083 | 领取 token;`UNIQUE(task_id)`、`UNIQUE(token)` |
| `task_events` | 1,310,742 | 任务事件流水(最大表) |
| `task_runtime_contexts` | 45,976 | 运行时上下文(登录态、角色、截图、产物) |
| `fulfillment_profiles` | 4 | 履约档案(affiliate_dash / manual_review / legacy 桩) |
| `fulfillment_profile_requirements` | 少量 | 档案的凭证要求 |
| `pdd_raw_orders` | 5,068 | PDD 原始单,`UNIQUE(order_sn)`,`sync_status` 四态 |
| `webhook_events` | 110,233 | 外部回调去重表(`provider + event_id`) |
| `short_links` | 95 | 短链(目标 URL 哈希 + 访问计数) |
| `file_assets` | 289,260 | 对象存储资产(缩略图/中图/URL/场景/上传者) |
#### B. 凭证(3)
| 表 | 行数(估) | 说明 |
| --- | --- | --- |
| `kuaishou_industry_vouchers` | 403,299 | 快手电子凭证;`UNIQUE(voucher_code)`、`UNIQUE(oid, unit_index)` |
| `kuaishou_cloud_task_states` | 46,894 | 快手云端取号/绑号/核销状态(一任务一行) |
| `redeem_certificates` | 1,541 | 抖音 DYC 券,含平台券号、平台状态、同步错误 |
#### C. 接单平台(25)
| 表 | 行数(估) | 说明 |
| --- | --- | --- |
| `worker_users` | 1,905 | 打手账号(30 列:等级、邀请、改名卡、冻结、黑名单、寄卖权限、等级锁) |
| `worker_levels` / `worker_level_logs` | 6 / 133 | 等级定义(`permission_json`)与升降级流水 |
| `worker_sessions` | 32,727 | 会话(设备、IP、UA、过期、吊销) |
| `worker_sms_codes` | 5,206 | 短信验证码(哈希、失败次数) |
| `worker_wallets` | 1,905 | 钱包(可用、冻结押金、待解冻、提现冻结、累计) |
| `worker_wallet_ledgers` | 172,830 | 钱包流水(13 种 `ledger_type`) |
| `worker_finance_requests` | 6,769 | 充值/提现申请(含冻结金额与审核人) |
| `worker_withdrawal_accounts` | 776 | 提现账户(微信/支付宝二维码 JSON) |
| `worker_deposit_unfreezes` | 621 | 延迟解冻队列(`fund_type` 区分押金/报酬) |
| `work_categories` | 30 | 工单分类(支持父级与图标) |
| `work_orders` | 156,043 | 工单主表(44 列,全库最宽) |
| `work_order_events` | 1,258,811 | 工单事件流水 |
| `work_order_shares` | 41,383 | 拼单份额 |
| `worker_work_order_notes` | 13,604 | 打手备注(一工单一打手一行) |
| `worker_work_order_views` | 147,356 | 已读/素材已读 |
| `worker_order_cancel_requests` | 953 | 打手取消申请与审核 |
| `worker_order_feedbacks` | 501 | 工单反馈/申诉 |
| `worker_after_sales_cases` | 39 | 售后问题单 |
| `worker_after_sales_debts` | 4 | 售后欠款 |
| `worker_after_sales_case_events` | 93 | 售后时间线 |
| `worker_risk_events` | 209 | 风控事件与处置 |
| `work_product_rules` | 710 | 接单模板(29 列:匹配、奖励、押金、拼单、时限、验收模式) |
| `work_product_rule_mappings` | 707 | 模板映射(卖家/SKU/标题,`sku_match_mode`) |
| `work_product_match_logs` | 10,000 | 匹配诊断日志(保留量受清理任务控制) |
#### D. 寄卖(2)
| 表 | 行数(估) | 说明 |
| --- | --- | --- |
| `consignment_submissions` | 877 | 提报单(`submission_no` 唯一、要价、押金、审核结果指向库存) |
| `consignment_inventories` | 784 | 库存(`INV…` 唯一、38 列:规格、凭证、绑定订单/工单、卖家、指派打手) |
#### E. 后台与权限(11)
`admin_users`(40)、`admin_sessions`(3,077)、`admin_login_logs`(4,936)、`admin_roles`(4)、`admin_permissions`(29)、`admin_role_permissions`(72)、`admin_audit_logs`(556,360)、`admin_notifications`(166,883)、`admin_sms_codes`、`admin_user_devices`、`admin_device_challenges`。
#### F. 配置与实时(2)
`app_config_entries`(18 行,全部平台/业务配置)、`realtime_events`(600,滚动保留)。
#### G. 资金台账(4)
`fund_accounts`、`fund_entries`、`manual_expenses`、`fund_reconciliations`(本地均为空表,属于新落地模块)。
### 5.3 核心表字段速查
#### `orders`
| 列 | 类型 | 说明 |
| --- | --- | --- |
| `id` | bigserial PK | |
| `provider` / `platform` | text NOT NULL | 例如 `91kaquan` / `kuaishou` |
| `shop_id` / `shop_name` | text | 店铺键空间(参与唯一键) |
| `platform_order_id` | text NOT NULL | 平台单号 |
| `order_status` / `pay_status` | text | 见 4.1 |
| `buyer_id` / `buyer_name` / `receiver_contact` | text | 买家信息 |
| `total_amount` | integer | **用户付款金额(分)**,三家口径已统一为「实付」;91 = `maxAmount` |
| `currency` | text | 默认 CNY |
| `raw_payload_json` | jsonb | 原始报文(保留期由 data-retention 控制) |
| `paid_at` | timestamptz | 支付/成团时间,建单时用作 `created_at` 优先值 |
| `claim_status` / `claim_status_reason` / `claim_status_updated_at` | text / text / timestamptz | 兑换有效性开关 |
索引(12 个)覆盖:唯一键、`(provider, platform, order_status, created_at)`、平台单号、店铺筛选、claim 状态等(迁移 070/074/086/099)。
#### `order_items`
`id`、`order_id`(FK CASCADE)、`sku_code`、`sku_name`、`quantity`、`spec_json`、`item_snapshot_json`。
没有金额字段,也没有 `canonical_product_id`:**多商品订单无法直接分摊收入**(缺口 C5)。
#### `fulfillment_tasks`
| 列 | 说明 |
| --- | --- |
| `order_id` / `order_item_id` / `unit_index` | 归属与单元序号;`UNIQUE(order_item_id, unit_index)` |
| `task_no` | `DT…` 唯一编号 |
| `provider / platform / shop_id / shop_name / platform_order_id` | 冗余来源字段(便于按来源查询,避免 join) |
| `profile_id` / `executor_key` | 履约档案与执行器 |
| `task_status` / `delivery_status` / `result_code` / `result_message` | 主状态机 + 交付状态 |
| `claim_token` / `claim_expires_at` | 历史冗余列;实际 token 在 `claim_tokens`(查询时 `LEFT JOIN` 出 `primary_claim_token`) |
| `automation_mode` / `requires_claim` / `user_action_status` | 自动/人工、是否需要用户领取 |
| `attempt_count` / `last_error` | 重试 |
| `context_json` | 执行器上下文(`affiliateDash`、`kuaishouIndustryVoucher` 等) |
| `claimed_at` / `role_confirmed_at` / `redeemed_at` | 关键时间点 |
#### `work_orders`(44 列,按用途分组)
| 组 | 列 |
| --- | --- |
| 标识 | `id`、`work_order_no`(唯一)、`order_id`、`order_item_id`、`task_id`(历史遗留,已注释废弃)、`platform_order_id`、`product_rule_id`、`category_id` |
| 商品 | `product_name` |
| 状态 | `status`、`acceptance_mode`、`gift_phase`、`admin_note`、`problem_note` |
| 金额 | `reward_amount`、`required_deposit_amount`、`deposit_threshold_amount`、`source_order_amount_fen`、`sharing_unit_reward` |
| 归属 | `assigned_worker_id`、`last_assigned_worker_id` |
| 拼单 | `sharing_enabled`、`sharing_total_quantity`、`sharing_unit_reward` |
| 时限 | `deadline_at`、`timeout_minutes`、`timeout_policy` |
| 好友赠送 | `friend_added_at`、`gift_available_at`、`gift_cooldown_hours`、`gift_cooldown_minutes` |
| 内容 | `material_json`、`requirement_json`、`acceptance_json`、`draft_acceptance_json`、`booster_material_json`、`worker_note` |
| 大厅 | `hall_queued_at`、`pinned_at`、`published_at` |
| 时间 | `assigned_at`、`submitted_at`、`accepted_at`、`created_at`、`updated_at` |
21 个索引,覆盖:状态+时间、分类+状态、店铺筛选、大厅队列、统计口径、关键词查询等(迁移 031/047/051/073/083/085/087/088/090/103)。
#### `worker_wallets` / `worker_wallet_ledgers`
`worker_wallets`:`worker_id` PK/FK、`available_amount`、`frozen_deposit_amount`、`pending_unfreeze_amount`、`frozen_withdraw_amount`、`total_credited_amount`、`total_settled_amount`。
`worker_wallet_ledgers`:`worker_id`、`ledger_type`、`amount`(有符号)、`balance_after`、`frozen_after`、`related_work_order_id`、`audit_status`、`note`、`payload_json`。
> 余额是**可变字段**,没有「由流水重算」的校验;这是财务化的第一道缺口(见第七节)。
#### `consignment_inventories`(38 列)
`inventory_no`、`game_name`、`category_id`、`item_name`、`system`、`server_zone`、`series`、`diamond_level`、`brilliance_count`、`cost_price_fen`、`seller_*`(联系人/游戏 ID/收款/手机)、`credentials_json`(**游戏账号密码**)、`screenshots_json`、`status`、`allocation_status`、`bound_order_id`、`bound_work_order_id`、`bound_platform_order_id`、`shop_name`、`source_type`、`source_submission_id`、`worker_id`、`assigned_worker_id`、`deposit_amount_fen`、`inbound_at`、`consumed_at`。
#### `fund_*`
- `fund_accounts`:`account_no` 唯一、`account_type`(supervisor/agent)、`owner_admin_user_id`、`responsible_supervisor_id`(自引用)、`balance_fen >= 0`、`status`。
- `fund_entries`:`entry_no` 唯一、`account_id`、`entry_type`(6 种)、`amount_fen <> 0`(有符号)、`balance_after_fen`、`transfer_no`、`counterparty_account_id`、`reconciliation_id`、`evidence_json`。
- `manual_expenses`:`expense_no` 唯一、`account_id`、`amount_fen > 0`、`category`(5 种)、`payee_type`(4 种)、`order_id`/`order_item_id`、`fund_entry_id` 唯一(一支出对一流水)。
- `fund_reconciliations`:`period_start/end`、`system_balance_fen`、`actual_balance_fen`、`difference_fen`(CHECK 等于差值)、`status`、`confirmed_by`。
### 5.4 关系图(核心链路)
```mermaid
erDiagram
orders ||--o{ order_items : "1:N"
orders ||--o{ fulfillment_tasks : "1:N"
order_items ||--o{ fulfillment_tasks : "1:N (unit_index)"
fulfillment_tasks ||--|| claim_tokens : "1:1 (task_id unique)"
fulfillment_tasks ||--o{ task_events : "1:N"
fulfillment_tasks ||--o| task_runtime_contexts : "1:0..1"
fulfillment_tasks ||--o| kuaishou_cloud_task_states : "1:0..1"
orders ||--o{ kuaishou_industry_vouchers : "1:N"
fulfillment_tasks |o--o{ kuaishou_industry_vouchers : "task_id 可空"
orders ||--o{ redeem_certificates : "local_order_id"
order_items ||--o| work_orders : "1:0..1"
work_orders ||--o{ work_order_shares : "1:N"
work_orders ||--o{ work_order_events : "1:N"
worker_users ||--|| worker_wallets : "1:1"
worker_users ||--o{ worker_wallet_ledgers : "1:N"
worker_users ||--o{ worker_sessions : "1:N"
worker_users ||--o{ worker_finance_requests : "1:N"
work_orders ||--o{ worker_deposit_unfreezes : "1:N"
work_orders ||--o{ worker_after_sales_cases : "1:N"
worker_after_sales_cases ||--o{ worker_after_sales_debts : "1:N"
consignment_submissions ||--o| consignment_inventories : "审核产出"
consignment_inventories |o--o{ work_orders : "bound_work_order_id"
admin_users ||--o{ fund_accounts : "owner"
fund_accounts ||--o{ fund_entries : "1:N"
fund_entries ||--|| manual_expenses : "1:1 (fund_entry_id)"
fund_accounts ||--o{ fund_reconciliations : "1:N"
pdd_raw_orders |o--|| orders : "synced_order_id"
```
### 5.5 约束与索引策略(值得记住的几条)
1. **唯一键即幂等键**:`orders(provider, platform, shop_id, platform_order_id)`、`fulfillment_tasks(order_item_id, unit_index)`、`kuaishou_industry_vouchers(oid, unit_index)`、`pdd_raw_orders(order_sn)`、`work_order_shares(work_order_id, worker_id)`、`fund_entries(entry_no)`、`manual_expenses(fund_entry_id)`。
2. **DDL 层不枚举业务状态**:`orders` / `fulfillment_tasks` / `work_orders` 的状态列**没有 CHECK**,状态机只存在于 TS(`domain/*.ts`)。好处是迁移成本低,坏处是脏状态只能靠代码兜底(`pending_config` 就是遗留证据)。寄卖与资金表则加了 CHECK,口径不完全统一。
3. **索引高度针对后台查询**:迁移 070/071/073/074/082/084–090 全是后台列表/统计/模糊搜索的覆盖索引,其中 073 用 `pg_trgm` GIN 支撑工单关键词(游戏 ID)搜索。
4. **JSONB 不做 GIN**:除搜索字段外,`snapshot/context` 的查询都走「先取行再在应用层判断」,这也是解析逻辑必须集中在少数几个 service 的原因。
5. **软删/归档**:业务表基本不物理删除(`ON DELETE CASCADE` 只用于任务→事件、订单→订单项等从属关系),保留靠 `status` 与归档字段。
### 5.6 配置存储(`app_config_entries`)
配置不在文件里、也不在环境变量里(环境变量只管连接串与密钥),全部走一张 key-value 表,后台保存后即时生效(部分需要重启或重载调度器):
| key | 内容 |
| --- | --- |
| `affiliate_dash` | 自动发货平台配置(baseUrl、商户号、密钥、skuMapping) |
| `kuaishou_industry_source` | 快手电子凭证接入(appId、secret、token、店铺) |
| `douyin_mashida` | 抖音码速达(uid/secret/baseUrl/claimLinkBase/handledTopics) |
| `pdd_source` | 拼多多(店铺、Cookie、同步开关) |
| `fulfillment_routing` | 履约路由(总开关、商品名/SKU 规则、全局优先级、unmatchedExecutorKey) |
| `scheduled_jobs` | 定时任务配置与间隔 |
| `notification` | 通用通知(Bark / WPush) |
| `worker_finance` | 打手财务策略(押金、解冻天数、提现规则) |
| `worker_product_match` | 匹配引擎开关(`legacyFallbackEnabled`) |
| `worker_hall` | 大厅可见性、容量、分类上限 |
| `worker_announcement` | 打手端公告 |
| `worker_platform_notifications` | 打手端通知模板 |
| `worker_risk_control` | 风控阈值与处罚 |
| `worker_mock_shops` | 开发/联调假店铺 |
| 历史键 | `cloudtentacles_*`、`kuaishou_feifei`(已下线通道,仅保留数据) |
### 5.7 迁移演进(001–110)
| 区段 | 主题 |
| --- | --- |
| 001 | 基线:后台、配置、履约档案、订单/订单项/任务/领取 token、任务事件、快手券、短链 |
| 002–003 | 登录日志、电子凭证类型 |
| 004–012 | **接单平台落地**:等级、打手、钱包、流水、分类、工单、工单事件、文件、商品规则、财务申请、拼单、超时、解冻、短信 |
| 013–028 | 回调去重、权限修补、永久 claim token、工单备注/置顶、提现账户、通知 |
| 029–040 | 会话、实时事件、搜索索引、匹配上下文/目录/SKU 模式、匹配日志、后台会话 |
| 041–058 | 排行榜、自动激活、取消申请、反馈、来源规则、冻结原因、性能护栏、大厅队列、售后、改名卡、赠送验收、解冻配置、素材已读 |
| 059–076 | **RBAC**、提现冻结、管理备注、取消措辞、赠送冷却、个人免押、数据保留、筛选索引、财务角色、搜索重建、分类父子、提现黑名单 |
| 077–082 | **寄卖**(库存、打手押金、解除收集链接、分配状态、白名单) |
| 081 | **PDD 原始单**两段式 |
| 083–093 | 工单来源金额、多组覆盖索引、风控事件、遗留列注释、autovacuum |
| 094–098 | **抖音券** `redeem_certificates`、平台券号、任务 claim 提交(后废弃)、等级锁 |
| 099–103 | 订单兑换有效性、**备用金台账**、后台显示名、短信/设备认证 |
| 104–108 | **成本中心与财务履约**:商品目录、成本中心、销售事实、退款、人工履约、成本异常归档 |
| 109–110 | **工单拼单多次参与**(份额模型重写、份额级问题单、撤销原因/份数)+ 大厅置顶候选集索引、后台店铺筛选索引 |
约定(README):不改历史迁移;结构变更一律新增;空库按序执行;后端启动自动 `up`。
### 5.8 实测状态分布(本地库,用于验证对业务的理解)
| 维度 | 分布 |
| --- | --- |
| 订单 | `paid/paid/active` 375,432;`pending_config` 25;`manual_failed` 1;`closed` 1 |
| 履约任务 | `redeemed+delivered` 49,527;`completed+delivered` 45,658;`closed+cancelled` 5,158;`closed+closed` 3,238;`waiting_binding+pending` 956 |
| 工单 | `accepted` 131,786;`pending_material` 21,509;`cancelled` 1,075;`unassigned` 760;`in_progress` 612;`pending_acceptance` 174;`open` 101;`problem` 26 |
| 快手券 | `CONSUMED` 376,179;`DESTROYED` 26,815;`UNUSED` 106 |
| 抖音券 | `verified` 1,337;`invalid` 123;`synced` 81 |
| 财务申请 | 提现 approve 6,381;充值 reject 151;提现 reject 129;充值 approve 91 |
两个值得注意的信号:
1. `pending_material` 占工单 13.8%(21,509 条),说明**接单素材缺口是常态**,补素材是运营主要工作量之一。
2. `unassigned` 只有 760,而 `open` 只有 101 —— 大量工单没有经过大厅,直接被后台指派或同步生成后长期停在待补素材。
---
## 六、外部集成与安全边界
| 链路 | 验签 | 幂等/去重 | 备注 |
| --- | --- | --- | --- |
| 91 卡券 | 签名 + 时间戳容差 | `orders` 唯一键 + advisory lock | 卡密用 `buildOpen91Cards` 加密返回 |
| 快手电子凭证 | 签名(`crypto.ts`) | `UNIQUE(oid, unit_index)` | 4 个入站接口 + 2 个出站回调 |
| affiliate-dash 通知 | 验签 + 时间戳 | `webhook_events` | 失败重推会被判 duplicate(风险 R2) |
| 抖音码速达 | 原始 body 验签 | `webhook_events`(先落库) | 同上 |
| PDD | Cookie / anti-content | `pdd_raw_orders(order_sn)` + 断路器 | 无回调,纯拉取 |
其他安全面:
- 后台/打手口令都是 scrypt + 盐,会话落库 + `session_version`。
- 日志统一脱敏(敏感 key、URL 参数、JSON 字段),有单测。
- 生产强制:session secret 长度、显式 CORS 白名单、存储凭据完整。
- 明显缺口:`/collect/*`、`/redeem/*` 只凭订单号;`/files/object` 无鉴权;Caddy 无 CSP/HSTS/X-Frame-Options;后台 token 存 localStorage。
---
## 七、风险与设计缺口(按优先级)
### P0 · 资金与一致性
| # | 问题 | 位置 | 影响 |
| --- | --- | --- | --- |
| R1 | 发单金额高于用户付款只有前端提醒,后端无统一门禁 | `frontend/pages/admin/panels/work-order-payment.ts`;`admin-work-order-management-service.ts` | 直接调 API 可发亏损单 |
| R2 | 寄卖审核可由请求体改 `workerId` 并按改后归属结算货款 | `consignment-admin-service.ts`、`consignment-supplier-repo.ts` | 钱结给错误的人 |
| R3 | 寄卖审核详情全量返回游戏账号密码,只需 `consignment.view` | `consignment-admin-service.ts` | 权限粒度过粗 |
| R4 | 抢单先锁钱包、指派先锁工单(锁序相反) | `work-order-grab-repo.ts` | 并发下 40P01 死锁 |
| R5 | 自动工单硬编码零押金,模板押金字段不生效 | `sync-worker-orders-service.ts` | 自动发单无押金约束 |
| R6 | 发布工单时凭证核销失败不回滚,指派却拦截 | `admin-work-order-management-service.ts` / `...assignment-service.ts` | 同一规则两套行为 |
### P1 · 可靠性
| # | 问题 |
| --- | --- |
| R7 | Webhook 先落库后处理,失败后平台重推被永久跳过 |
| R8 | 抖音回调同步串行调上游,单次超时 20s,与 R7 叠加不可恢复 |
| R9 | `/collect/*`、`/redeem/*` 无第二因子 |
| R10 | `/collect/upload` 可匿名上传任意文件到 OSS |
| R11 | `/files/object` 无鉴权;claim token 默认永久 |
| R12 | 无 CSP/HSTS;后台 token 在 localStorage |
| R13 | 先到先匹配,后到的快手补全数据不纠正既有工单 |
| R14 | 单实例耦合(限流/调度/SSE/缓存都在进程内) |
### P2 · 可维护性
巨型文件(`WorkOrdersPanel.tsx` 3,686 行、`AdminTaskDetailPage.tsx` 1,925 行、`work-order-query-repo.ts` 1,409 行、`consignment-repo.ts` 1,410 行、`work-order-settlement-repo.ts` 1,181 行);前端无组件测试/E2E;无 CI;迁移启动自动执行且无 down。
### 数据模型层面的四个缺口
| # | 缺口 | 后果 |
| --- | --- | --- |
| D1 | 没有「人工块」载体:无自动任务且无工单的已支付订单项只靠「无任务且无工单」推断 | 人工履约不可监管、不可计量 |
| D2 | 无商品主数据 `product_catalog` / `channel_product_mappings` | 同一商品名对应 27–35 个 `sku_code`,销量统计不可靠 |
| D3 | 无成本版本与快照 `cost_items/cost_versions/cost_snapshots` | 改价不可追溯,历史毛利不可重算;自动发货 `cost_amount` 只映射不落库 |
| D4 | `order_items` 无金额字段 | 多商品订单无法分摊收入 |
> 这几条与 `docs/成本中心与三块履约模型.md`、`docs/drafts/*` 的草案一致,草案尚未进迁移。
---
## 八、我看到的「三个业务面」边界图
```mermaid
flowchart LR
subgraph 订单面
O["orders / order_items"]
T["fulfillment_tasks / claim_tokens"]
V["vouchers / redeem_certificates"]
end
subgraph 工单面
W["work_orders / work_order_shares"]
WW["worker_users / wallets / ledgers"]
P["work_product_rules / mappings"]
end
subgraph 寄卖面
CS["consignment_submissions"]
CI["consignment_inventories"]
end
subgraph 财务面
FA["fund_accounts / fund_entries"]
ME["manual_expenses"]
end
O --> T
O --> V
O --> W
P --> W
W --> WW
CI --> O
CS --> CI
CI --> W
ME --> FA
W -. 影子总账未落地 .-> FA
```
三个面共享的只有两件事:**订单项**(`order_items.id` 是所有链路的锚点)与**打手钱包**(工单与寄卖都用它)。
---
## 九、需要你确认的问题
见对话中的提问(不在文档内固化,确认后我再补「口径确认」章节)。