- P0/P1:影子总账追加调整、关账/退款/资金/积分幂等、成本版本追溯、对账订单项维度 - 迁移收敛:原 103–114 合并为 103_finance_foundation、104_finance_controls、105_work_order_consignment - 数据:L1 钱包 BIGINT/CHECK/奖励幂等;重复奖励审计口径修正与人工核对清单 - 报表:销售毛利缺失成本收入提示,旧收支页明确现金流口径 - 文档:同步迁移基线、合并评估、代码复核与上线说明
52 KiB
order_site 全流程与数据库设计梳理(2026-10-23)
状态:梳理稿(待评审,尚未登记到
docs/README.md) 落笔日期:2026-10-23 代码基线:工作区当前 HEAD;数据库迁移001–110(共 110 个文件) 核对方式:源码走查 + 本地 PostgreSQL 实读表结构 / 约束 / 索引 + 本地库状态与数据量统计 说明:本地库与生产同量级(orders约 37.5 万行),本文引用的数据量均为本地库实测,不与生产承诺一致,仅用于判断结构合理性。
一、先看结论(10 条)
- 系统早已不是「快手自动发货工具」,而是三来源订单履约 + 打手众包接单 + 寄卖仓库 + 电子凭证核销 + 客服备用金台账的一体化平台。
- 订单来源只有三个:
kuaishou-91(91 回调 + 快手 send-code 两条通道,同一个来源)、pdd、douyin-mashida。代码层的来源键定义在apps/backend/src/services/product-match/types.ts。 - 核心不变式:一个订单 = 骨架(
orders+order_items)+ 买家凭证(快手电子凭证 / 抖音券)+ 一个执行载体。载体只有三种:affiliate_dash自动发货、work_orders工单、manual人工(当前无显式表,靠「无任务且无工单」判定)。 - 履约任务按「订单项 × 数量」拆单元:
fulfillment_tasks的UNIQUE(order_item_id, unit_index)是整套领取链接体系的基座。 - 领取链接 =
claimLinkBase + '/' + claim_tokens.token,一任务一 token(claim_tokens.task_id唯一);CLAIM_TOKEN_TTL_HOURS=0表示永久有效。 - 打手侧是一条独立业务线:工单状态机
pending_material → unassigned → open → in_progress → pending_acceptance → accepted,另有problem、cancelled;资金走「钱包 + 流水」模型,不是会计总账。 - 寄卖是第三个业务面:提报 → 审核入库(
INV…)→ 规格匹配(系统/区服/系列/钻级/璀璨)→ 履约(内部现货 or 指派打手)→ 结算与撤销押金策略。 - 备用金台账(迁移 100)已落地:
fund_accounts(主管池 / 客服账户)+fund_entries(只追加流水,余额只能由流水变动)+manual_expenses+fund_reconciliations。 - 数据库现状:60 张业务表 +
schema_migrations,113 个外键、21 个索引的work_orders是全库索引最多的表;配置不入表结构,统一存app_config_entries(本地 18 个配置键)。 - 主要架构性风险仍是 2026-10-17 报告列的那几处:资金门禁只在前端、Webhook 先落库后处理、
/collect/redeem无第二因子、抢单/指派锁序相反、单实例依赖进程内存。
二、系统地图
2.1 运行拓扑
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 总览(一条单的完整生命周期)
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 卡券(下单 + 查询)
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。 - 本地库实测:
CONSUMED376,179、DESTROYED26,815、UNUSED106 —— 说明核销链路是真实主路径。
3.2.3 拼多多(两段式)
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 抖音码速达(推送 → 建单 → 发券 → 领取)
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 载体判定(本系统最关键的一步)
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 履约任务与领取链接
syncDeliveryTasksForOrder对每个订单项按quantity补建unit_index = 1..n,重复执行只补缺口。- 执行器来自
snapshot.fulfillmentRoute:affiliate_dash→ 绑定fulfillment_profiles.profile_key='affiliate_dash',requires_claim=true,初始状态paid;manual_dispatch→profile_key='manual_review',初始状态manual_review;- 未支付 →
pending_payment。
- 已支付订单立即
preparePaidFulfillmentTask(执行器注册表:affiliate_dash/manual/legacy-retired只读桩)。 - 领取 token:
createTaskClaimToken写claim_tokens(task_id唯一、token唯一、max_use_count=1),claimUrl = claimBaseUrl + '/' + token。 - 抖音走独立基址
claimLinkBase(后台配置),刻意不回退orders.claimBaseUrl。 orders.claim_status(迁移 099)=active / paused / closed,用于暂停或关闭整单兑换,是后台处置链接失效问题的新开关。
3.5 affiliate-dash 自动履约
建任务(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 建单规则
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 工单生命周期
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。
3.8 寄卖仓库
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)
公司 → 主管池(fund_accounts.account_type='supervisor')
主管 → 客服账户('agent',responsible_supervisor_id 指向主管)
客服 → 支出明细(manual_expenses:平台充值 / 采购 / 赔付 / 运费 / 其他)
四条硬约束(写在建表注释里):
- 余额只能由
fund_entries变动,禁止手工改; - 发放 / 缴回必须同时写主管与客服两条方向相反的流水,共用
transfer_no; - 支出必须同时写客服负数流水与
manual_expenses明细(UNIQUE(fund_entry_id)); - 已记账流水不修改不删除,差异走
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 关系图(核心链路)
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 约束与索引策略(值得记住的几条)
- 唯一键即幂等键:
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)。 - DDL 层不枚举业务状态:
orders/fulfillment_tasks/work_orders的状态列没有 CHECK,状态机只存在于 TS(domain/*.ts)。好处是迁移成本低,坏处是脏状态只能靠代码兜底(pending_config就是遗留证据)。寄卖与资金表则加了 CHECK,口径不完全统一。 - 索引高度针对后台查询:迁移 070/071/073/074/082/084–090 全是后台列表/统计/模糊搜索的覆盖索引,其中 073 用
pg_trgmGIN 支撑工单关键词(游戏 ID)搜索。 - JSONB 不做 GIN:除搜索字段外,
snapshot/context的查询都走「先取行再在应用层判断」,这也是解析逻辑必须集中在少数几个 service 的原因。 - 软删/归档:业务表基本不物理删除(
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 |
两个值得注意的信号:
pending_material占工单 13.8%(21,509 条),说明接单素材缺口是常态,补素材是运营主要工作量之一。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/*的草案一致,草案尚未进迁移。
八、我看到的「三个业务面」边界图
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 是所有链路的锚点)与打手钱包(工单与寄卖都用它)。
九、需要你确认的问题
见对话中的提问(不在文档内固化,确认后我再补「口径确认」章节)。