- P0/P1:影子总账追加调整、关账/退款/资金/积分幂等、成本版本追溯、对账订单项维度 - 迁移收敛:原 103–114 合并为 103_finance_foundation、104_finance_controls、105_work_order_consignment - 数据:L1 钱包 BIGINT/CHECK/奖励幂等;重复奖励审计口径修正与人工核对清单 - 报表:销售毛利缺失成本收入提示,旧收支页明确现金流口径 - 文档:同步迁移基线、合并评估、代码复核与上线说明
876 lines
52 KiB
Markdown
876 lines
52 KiB
Markdown
# 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` 是所有链路的锚点)与**打手钱包**(工单与寄卖都用它)。
|
||
|
||
---
|
||
|
||
## 九、需要你确认的问题
|
||
|
||
见对话中的提问(不在文档内固化,确认后我再补「口径确认」章节)。
|