83 KiB
寄卖(寄存)系统 · 现状分析与下一步优化方案
基线:
e828e677(main,工作区干净) 基线校验:后端 397 测试 / 394 通过 / 0 失败 / 3 skip;typecheck、lint:check通过 分析对象:1aa6dba9、4a6215e8、fd1eb581、6b940796、e828e677及当前代码 取证方式:逐行读代码(含 SQL 条件、前端调用点)+ 三路并行专项审计(状态机并发 / 前后端+RBAC / 资金闭环) 所有关键结论均已复核到文件:行本轮落地(2027-02-21,未提交)后校验:后端 401 测试 / 398 通过 / 0 失败 / 3 skip;前端 16 通过;两侧typecheck、lint:check、format:check、check:sql、build全绿
一、最近提交在做什么
| 提交 | 内容 | 落点 |
|---|---|---|
1aa6dba9 |
初版:寄售仓库 / 定向收集链接 / 玩家提报审核 / 工单智能匹配与内部履约 | 迁移 077、consignment-* 全套、RBAC 四权限 |
4a6215e8 |
打手端押金履约(打手发布 → 冻结押金 → 指派打手交货) | 迁移 078、consignment-worker-service、consignment-deposit-repo |
fd1eb581 |
内部现货占库进入赠送冷静期;发布押金冻结/驳回退还/验收释放闭环 | consignment-fulfillment-service、work-order-gift-repo、结算联动 |
6b940796 |
样式修复(cooldownMinutes prefer-const) | — |
e828e677 |
权限打通(19 接口改 requireAdminPermission)、指派占库改条件更新、撤销守卫 |
新增 4 个回归测试 |
632dddc4 |
上一轮:H5 渠道整体下线 + 凭据字段修复 + 状态机封闭(撤销守卫 / 库存绑定唯一性 / 生命周期解绑)+ 押金归属取台账 | 迁移 079、consignment-* 五个文件、work-order-* 三个仓库、两个前端面板 |
5d11bf20 |
批次 C+D:指派部分成功补偿、凭证核销如实反馈、空规格不放行 + 履约/指派服务端规格复检 | consignment-fulfillment-service.ts、consignment-matching-service.ts、consignment-repo.ts、履约路由、WorkOrdersPanel.tsx |
d6f6219f |
批次 F1:内部现货履约停在「待加好友」,客服确认后才进赠送冷却 + 备注快捷项 | work-order-gift-repo.ts、履约服务、WorkOrdersPanel.tsx、utils/gift-phase.ts |
| 本批提交(批次 Y,同一提交内含本文档,故不写自身 hash) | 批次 Y:修生产 079 迁移 23505——建冻结幂等索引前先去重历史重复冻结并退还多冻押金(服务器未应用过 079,经确认直接改原迁移) | 079_drop_consignment_collect_links.sql、consignment-payout.test.ts |
| 本批提交(批次 X,同一提交内含本文档,故不写自身 hash) | 批次 X:发布寄卖自动带建议价——规格选齐后按 系统+系列+钻级 从「接单模板」(work_product_rules)匹配价格自动填入(安卓暗潮1钻 → ¥120),多条命中取最高,手改过不覆盖 | consignment-matching-service.ts、product-match-repo.ts、consignment-worker-service.ts、routes/worker/consignment.ts、WorkerConsignmentPage.tsx |
| 本批提交(批次 W,同一提交内含本文档,故不写自身 hash) | 批次 W:规格口径定案——后台全量(系统 苹果/安卓/通用,区服 微信/QQ/互通),打手端按物品展示(苏尔南:系统二选一、区服唯一「互通」默认选中);「互通」为区服通配值,匹配引擎通配符对称化 | utils/consignment-spec.ts、WorkerConsignmentPage.tsx、两个面板、consignment-matching-service.ts、consignment-worker-service.ts |
| 本批提交(批次 V,同一提交内含本文档,故不写自身 hash) | 批次 V:复核遗留——拼单份额验收收官补寄卖钩子(押金释放+供货货款)、手动匹配弹窗 409 刷新/关键字透传/无打手禁指派、核销反馈抽共享出口 | work-order-settlement-repo.ts、ConsignmentManualMatchModal.tsx、utils/voucher-consume.ts |
| 本批提交(批次 U,同一提交内含本文档,故不写自身 hash) | 批次 U:复核修复——占库 allocation_status 撞 080 CHECK(真实库必 500)、指派非供货卖家时货款双付、手动匹配入口按 fulfill 权限门控 | consignment-repo.ts、consignment-fulfillment-service.ts、两个面板、三个测试文件 |
| 本批提交(批次 S,同一提交内含本文档,故不写自身 hash) | 批次 S:手动指定现货匹配(判定/差异/强制留痕 + 双向入口 + 共享弹窗) | consignment-matching-service.ts、consignment-fulfillment-service.ts、ConsignmentManualMatchModal.tsx |
| 本批提交(批次 R,同一提交内含本文档,故不写自身 hash) | 批次 R:售后立案回落到供货卖家;抽共享"从工单找供货卖家"查询 | consignment-supplier-repo.ts、after-sales-repo.ts、consignment-deposit-repo.ts |
| 本批提交(批次 Q,同一提交内含本文档,故不写自身 hash) | 批次 Q:验收结算补寄卖货款给供货卖家;修指派打手线报酬 100 倍单位错 | consignment-deposit-repo.ts、work-order-settlement-repo.ts、consignment-fulfillment-service.ts |
| 本批提交(批次 P,同一提交内含本文档,故不写自身 hash) | 批次 P:确认加好友/上传赠送截图恢复为卖家操作,客服兜底 | consignment-item-stage.ts、consignment-worker-service.ts、worker-acceptance-service.ts |
| 本批提交(批次 O,同一提交内含本文档,故不写自身 hash) | 批次 O:文件链接去掉签名与有效期(修客服端凭证裂图),寄卖出参统一归一化 | file-storage-service.ts、routes/files.ts、consignment-admin-service.ts |
| 本批提交(批次 N,同一提交内含本文档,故不写自身 hash) | 批次 N:删掉按库存号的卖家入口,卖家动作只按寄卖单号定位 | consignment-worker-service.ts、routes/worker/consignment.ts、WorkerConsignmentPage.tsx |
| 本批提交(批次 M,同一提交内含本文档,故不写自身 hash) | 批次 M:停用客服手动入库(前端入口删除 + 后端 409 守卫),寄卖货源只走平台提报 | consignment-admin-service.ts、consignment-repo.ts、ConsignmentInventoryPanel.tsx |
| 本批提交(批次 L,同一提交内含本文档,故不写自身 hash) | 批次 L:匹配弹窗两个动作一律二次确认 + 按钮分区/图标/后果说明,防点错 | WorkOrdersPanel.tsx |
| 本批提交(批次 K,同一提交内含本文档,故不写自身 hash) | 批次 K:匹配弹窗改用寄卖单号(候选查询补 join),客服界面不再展示内部仓库号 | consignment-repo.ts、WorkOrdersPanel.tsx、ConsignmentInventoryPanel.tsx |
| 本批提交(批次 J,同一提交内含本文档,故不写自身 hash) | 批次 J:凭证链可视化(提报凭证 + 赠送凭证),客服审核/仓库面板统一展示 | consignment-admin-service.ts、consignment-repo.ts、ConsignmentSubmissionsPanel.tsx、ConsignmentInventoryPanel.tsx |
| 本批提交(批次 I,同一提交内含本文档,故不写自身 hash) | 批次 I:提报单号为唯一寄卖单号(一件货一行)+ 统一状态机 + 卖家确认加好友/提交赠送截图 | consignment-item-stage.ts、consignment-worker-service.ts、routes/worker/consignment.ts、ConsignmentInventoryPanel.tsx、WorkerConsignmentPage.tsx |
| 本批提交(批次 H,同一提交内含本文档,故不写自身 hash) | 批次 H:修掉发布时乱弹内部库存提醒(部分匹配兜底过宽)+ 弹窗赏金分/元换算 | consignment-matching-service.ts、履约复检、WorkOrdersPanel.tsx |
| 本批提交(批次 G,同一提交内含本文档,故不写自身 hash) | 批次 G:确认加好友后现货作废不回池 + 撤销寄售履约押金可选(含修打手押金永久冻结、真实库 42804 500) | consignment-repo.ts、consignment-deposit-policy.ts、work-order-deposit-repo.ts、撤销履约服务与路由、WorkOrdersPanel.tsx |
| 本批提交(批次 F2,同一提交内含本文档,故不写自身 hash) | 批次 F2:确认加好友后卖家发布押金不退(deposit_deduction 落地)+ 二次冻结护栏 |
consignment-deposit-repo.ts、取消/退单仓库、撤销履约服务 |
| 本批提交(批次 E,同一提交内含本文档,故不写自身 hash) | 前端四权限门控收口、allocation_status 约束与权限文案修正、面板错误态/防双提交/真实待审计数 |
admin-auth.ts、两个寄卖面板、AdminWorkerPlatformPage.tsx、迁移 080 |
结论:功能主干 3 天连成闭环(库存 → 提报审核 → 匹配 → 内部现货 / 打手押金两条履约 → 赠送冷静期 → 验收结算 → 撤销回库),但最近三次提交全是上线后回填正确性(权限被静默清除、并发占库、撤销资金挂账)。 当前主要矛盾不是"缺功能",而是:状态机不封闭 + 库存绑定无唯一性约束 + 工单终态对寄卖零感知(本轮已处理);入口侧"H5 不可提交 / 手工录入丢凭据"两条断链已通过"下线 H5 + 修字段名"解决。
二、现状链路
[打手接单平台提报] ── 唯一入口(H5 定向收集链接已整体下线)
[客服手工录入] ── 凭据/截图字段名已对齐(本轮修复)
│
▼
提报审核 → consignment_inventories(in_stock, allow_matching)
│
工单发布前拦截 ── 命中候选(exact 100 / partial 80,仅前端展示)
│
├─► 内部现货履约 (internal_stock):同一事务原子占库(含工单绑定)→ 工单 in_progress + 赠送冷静期 → 验收释放押金
└─► 指派打手押金履约 (worker_deposit):先占库(独立事务,写 assigned_worker_id + 工单绑定)→ 再指派(另一事务冻结押金/核销凭证)→ 再改 material/reward/写事件
│
└─ 撤销寄售履约(仅内部现货,仅 in_progress/pending_acceptance/problem)→ 回库 / 作废 / 取消工单
│
└─ 工单终止(打手撤单 / 取消指派 / 退回大厅 / 撤单 / 打手退单申请 / 超时)→ 同事务解绑现货回库(本轮新增)
三、已验证问题清单
状态说明(2027-02-21 更新):下列清单为第一轮审计结论。本轮已落地批次 A/B 的核心内容, 逐条状态见每项标题后的标记;未完成项保留原文,作为下一轮输入。 已交付的改动见
docs/寄卖库存功能设计与实施方案.md§五(状态机语义)与迁移079_drop_consignment_collect_links.sql。
✅ 本轮已落地(2027-02-21)
| 项 | 内容 | 落点 |
|---|---|---|
| H5 收集链接整条下线 | 删公开路由/服务/页面/服务端封装 + 迁移 079 删表 consignment_collect_links、删列 link_id,source_type 归并为 worker_portal |
前后端各 4 处删除 + 079 |
| 凭据/截图静默丢弃(P0-2) | 后端改读 credentialsJson ?? credentials、screenshotsJson ?? screenshots |
consignment-admin-service.ts 三处入口 |
| 撤销状态机(P0-3 / P1-3 / P1-4) | 状态白名单(in_progress/pending_acceptance/problem)+ 事务内 FOR UPDATE + UPDATE ... AND status=$n + 原因必填 + 清空指派/时限/验收/问题痕迹 |
consignment-fulfillment-service.ts |
| 库存绑定唯一性(P0-4) | 撤销/退回复库/作废统一 AND status='consumed' AND bound_work_order_id=$工单(作废与回库额外要求"未被进行中工单占用"),命中 0 行 409 且提示具体工单;部分唯一索引兜底 |
consignment-repo.ts、consignment-admin-service.ts、079 |
| 生命周期收口(P0-5) | 新增 releaseConsignmentInventoryBindingForWorkOrderWithClient,接入撤单仓库 5 条链路 + 打手退单申请 + 超时处置 |
work-order-cancel-repo.ts、work-order-issue-repo.ts、worker-cancel-request-repo.ts |
| 占库写入工单绑定(P2-19) | 指派占库时写 bound_order_id/bound_work_order_id/bound_platform_order_id/shop_name |
consignment-fulfillment-service.ts、consignment-repo.ts |
| 卖家/履约人拆列(P1-1) | 新增 assigned_worker_id,占库只写指派列,回滚不再把卖家置 NULL |
079 + consignment-repo.ts |
| 押金归属/金额取台账(P1-2、P2-1) | 冻结/释放的归属与金额改为读取不可变的冻结核销行,审核改派不再导致"钱退给错人/向错人扣款" | consignment-deposit-repo.ts |
| 编辑接口不再能改状态(P1-7) | PUT /inventories/:id 显式拒绝 status,仓库层移除 status 入参 |
consignment-admin-service.ts、consignment-repo.ts |
| 打手端补审核门禁(P1-9) | POST /consignment/publish 加 requireActiveWorker |
routes/worker/consignment.ts |
| 押金变动推送钱包(P2-4) | 发布冻结、驳回释放、作废归档、退回复库成功后推送钱包实时事件 | 三个 consignment service |
| 台账幂等键(P2-3) | 同一提报单的发布冻结加部分唯一索引 | 079 |
| 回归测试 | 新增 5 例:占库绑定断言、撤销状态白名单、解绑范围、作废/回库守卫、终态链路接线计数 | worker-platform-service.test.ts |
✅ 本轮已落地(批次 C+D,接续上表)
| 项 | 内容 | 落点 |
|---|---|---|
| 指派部分成功补偿(P1-2) | compensateFailedConsignmentAssignment:取消指派(解冻押金 + 工单回大厅 + 解绑现货)→ 兜底释放占用 → 摘除工单寄售标记 |
consignment-fulfillment-service.ts |
| 凭证核销如实反馈(P1-5) | 失败写 consignment_fulfill_voucher_consume_failed 事件;前端按 voucherConsume.ok 弹警告并提示人工核销 |
consignment-fulfillment-service.ts、WorkOrdersPanel.tsx、services/admin/consignment.ts |
| 空规格不放行(P1-6) | compareSpec 三态判定 + 钻级不再兜底 1;未知项一律不进 exact,reason 列出缺失字段 |
consignment-matching-service.ts |
| 履约/指派服务端复检 | assertConsignmentSpecMatchedOrder:冲突 409,非 exact 需显式 allowPartial;前端危险确认后放行 |
consignment-fulfillment-service.ts、履约/指派路由、WorkOrdersPanel.tsx |
| 人工解绑守卫收窄 | NO_LIVE_BOUND_WORK_ORDER_SQL 由"状态黑名单"改为只拦交付中三态,与撤销白名单对齐 |
consignment-repo.ts |
| 陈旧寄售标记清理 | 新增 detachConsignmentMaterialFromWorkOrderWithClient,接入解绑回库、人工退回复库、作废三处 |
consignment-repo.ts |
| 迁移 079 顺序修复 | 先删旧 source_type CHECK 再回填,全新空库 001→079 已验证跑通 |
079_drop_consignment_collect_links.sql |
| 回归测试 | 新增 8 例:匹配三态/空规格/库存侧缺字段/通用件冲突 + 补偿接线 + 复检接线 + 凭证事件 + 摘标记 SQL(含旧绑定先锁行) | 两个测试文件 |
一处刻意偏离原始批次 B 表述:原始方案写"所有回库路径恢复 allow_matching",实际实现为保留运营的开关设置(e828e677 已按此方向改过撤销路径)。理由:该开关是运营"暂停匹配"的闸门,自动回库替运营打开会静默解除风控;两条路径(撤销回库、退回复库)现在语义一致,UI 文案也已同步更正(不再宣称"重新进入待匹配池")。若业务坚持"回库即上架",改一处常量即可。
P0 · 直接导致功能不可用或重复出货
P0-1 H5 定向收集页 100% 无法提交(截图 URL 取错层级)—— ✅ 已随渠道下线废弃
services/consignment-public.ts:14-33:uploadConsignmentImage声明返回{url, assetId},实际return result?.data || result。- 后端上传响应信封是
{code,msg,time,data:{file:{url,...}}}(utils/http.ts:21-28,services/file-storage/file-storage-service.ts:100-135返回{ file: … }),正确写法见services/files.ts:19(response.data.file)。 - 页面
ConsignmentCollectPage.tsx:120-122取f.response?.url || f.url;而 antd Upload 的customRequest只把结果塞进f.response(ConsignmentCollectPage.tsx:528-534传的是{file:{...}}),且从不设置f.url。 ⇒ 上传成功、缩略图能看,但screenshots恒为空数组,点提交永远弹「请上传游戏主页截图以供核验!」,且文件已落 OSS 变成孤儿资源。 - 结论:H5 渠道已整条移除,此缺陷连同页面、服务、公开路由、
consignment_collect_links表与link_id列一并作废。
P0-2 手工入库 / 编辑的账号密码与截图被静默丢弃(前后端字段名不一致)—— ✅ 已修复(本轮)
- 前端发
credentialsJson/screenshotsJson:ConsignmentInventoryPanel.tsx:211-217(新增)、:264-269(编辑)。 - 后端读
payload.credentials/payload.screenshots:consignment-admin-service.ts:255-259、:308-312;全仓无 camelCase→snake 映射中间件。 ⇒ 客服录入的登录方式/账号/密码/安全码永不落库,详情弹窗(ConsignmentInventoryPanel.tsx:1206-1215)永远是-;打手履约写入material.consignment.credentials只能是{}(consignment-fulfillment-service.ts:214),无法上号交付。 - 修复:新增
pickCredentialsJson/pickScreenshotsJson(payload.credentialsJson ?? payload.credentials、screenshotsJson ?? screenshots),统一用于手工入库、编辑、审核入库三个入口。
P0-3 撤销寄售履约没有状态守卫,已验收(钱已结算、货已交付)的工单仍可被撤销 → 同一件现货二次出货 —— ✅ 已修复(本轮)
- 撤销入口只校验 material 里有没有
inventoryId、是不是押金模式:consignment-fulfillment-service.ts:419-442。 - 事务内不重新加锁、不校验工单状态:
:456-463→:539-570(UPDATE work_orders ... WHERE id = $4,无 status 条件)。 - 库存写回
in_stock的 UPDATE 同样无 status 条件::497-518。 - 验收结算只释放押金,不清除
material.consignment:work-order-settlement-repo.ts:33-78、:92-103。 - 前端按钮无状态白名单:
WorkOrdersPanel.tsx:1457-1465;无二次确认;撤单原因标required但没挂rules(:3042-3050),空原因也能提交,后端也不校验。 - 修复:
assertConsignmentRevocableWorkOrderStatus(白名单in_progress|pending_acceptance|problem,事务内外各校验一次)+ 事务内SELECT ... FOR UPDATE锁单 +UPDATE ... WHERE id=$4 AND status=$5+ 原因必填(前端同步校验)+ 清空指派/时限/验收草稿/问题单痕迹;前端isConsignmentRevocableOrder+consignment.fulfill权限门控。
P0-4 库存被按 id 无条件改写 → 抢占其它工单的绑定、复活已作废件(一件现货被三张单履约)—— ✅ 已修复(本轮)
- 撤销分支
WHERE id = $3且无条件清空bound_order_id / bound_work_order_id / bound_platform_order_id:consignment-fulfillment-service.ts:475-494、:497-518。 - 仓库侧【退回复库】只校验
status='consumed':consignment-repo.ts:640-669;【作废】连状态都不校验::620-638+consignment-admin-service.ts:349-374。 - 修复:撤销分支加
AND status='consumed' AND bound_work_order_id = $工单(0 行 → 409consignment_inventory_binding_changed);【作废】/【退回复库】加NO_LIVE_BOUND_WORK_ORDER_SQL(绑定工单状态不在accepted/cancelled即拒绝,提示具体工单号);迁移079加部分唯一索引uniq_consignment_inventories_bound_work_order兜底,并清理历史重复绑定。
P0-5 工单生命周期对寄卖零感知:撤单 / 超时 / 售后 / 重开都不解绑 —— ✅ 已修复(本轮)
- 本轮新增
releaseConsignmentInventoryBindingForWorkOrderWithClient(client, workOrderId, {now, reason})作为唯一出口,并接入:work-order-cancel-repo.ts(打手撤单 / 取消指派 / 退回大厅 / 撤单取消订单 / 已取消工单重新启用 共 5 处)、worker-cancel-request-repo.ts(打手退单申请审核通过)、work-order-issue-repo.ts(超时处置reopen/cancel_release/cancel_deduct)。 - 押金口径:现货回库等同于"重新挂卖",发布押金保持冻结(不释放也不重复冻结),与「发布冻结、驳回/验收释放」一致;卖家彻底取回物品走运营【下架作废】→ 释放押金。
- 售后不需要接入:
resolveAdminAfterSalesCase只对已验收工单做资金追缴/退赔,不改工单状态(fromStatus/toStatus均为accepted),货物已交付、无待解绑库存;买家退回实物时由运营在仓库执行【退回复库】(已验收工单不再是"进行中",守卫放行)。 - 效果:撤单/超时后现货立刻回到在库池(
in_stock,保留运营的匹配开关设置),重开工单不会再重复消耗第二件库存(绑定在撤单事务内已清)。
P1 · 数据正确性 / 权限 / 资金安全
P1-1 consignment_inventories.worker_id 一列两义:既是"卖家"又是"被指派的打手" —— ✅ 已修复(本轮)
- 审核入库写卖家:
consignment-repo.ts:1095,1129;指派占库覆盖为受指派打手::557-570;回滚直接置NULL::588-609。 - 打手"我的寄卖"(
consignment-worker-service.ts:185)与后台筛选(consignment-repo.ts:146)都按这一列。 ⇒ 客服指派另一位打手时卖家丢件、被指派者凭空多件;指派失败回滚(置 NULL)同样让卖家永久丢件。 - 修复:迁移
079新增assigned_worker_id(含注释与索引);占库只写assigned_worker_id,回滚只清该列,worker_id语义固定为卖家且入库后不再变更。
P1-2 指派打手存在"部分成功"窗口:库存放回了,工单仍挂在该打手身上 —— ⏳ 未处理(下一轮批次 C)
consignment-fulfillment-service.ts:318-327先占库(独立事务已提交)→:331-335指派(另一独立事务:冻结打手押金work-order-grab-repo.ts:180-226、核销凭证admin-work-order-assignment-service.ts:67)→:368 updateWorkOrder、:374 createWorkOrderEvent、:390每一步都可能抛错。:397-409的 catch 只放回库存,不撤销指派/押金/凭证/资料/金额。 ⇒ 事件写入失败、死锁、超时或两次提交之间重启:工单in_progress+ 打手押金已冻结 + 凭证已核销,material.consignment从未写入,现货却回到可匹配池。- 本轮已缓解:占库现在会写入
bound_work_order_id,补偿回滚的放回 UPDATE 带WHERE status='consumed';但"指派事务化"本身未做。 - 修复(待做):claim + assign + material/reward + event 合并同一事务(共用
PoolClient);指派后失败改补偿式撤销(解绑 + 解冻)。
P1-2b 押金归属取自可变的 consignment_submissions.worker_id —— ✅ 已修复(本轮)
- 归属原本按提报单可变的
worker_id反查钱包(consignment-deposit-repo.ts:173-202),而审核可覆盖它(consignment-repo.ts:985,前端有打手下拉)。 ⇒ 审核改派后:释放按新 worker 算得 outstanding=0 → 静默返回 0,发布人那笔押金永久冻结且无补救入口;反向地,回库重冻会向从未发布过的打手扣款。 - 修复:新增
getConsignmentDepositLedgerSnapshot,押金归属与金额以不可变的冻结核销行(payload_json->>'consignmentSubmissionId'+ledger_type='deposit_freeze')为准;ensure...重冻金额也改用台账历史金额,避免审核改额后按新金额扣款;提报驳回释放改走releaseConsignmentPublishDepositBySubmissionWithClient。迁移079补部分唯一索引,防重复冻结。
P1-3 撤销的工单 UPDATE 无锁无条件,用陈旧快照写回 → 覆盖并发验收(丢失更新)+ 双击双提交 —— ✅ 已修复(本轮)
- 事务外读工单与 material(
:456-463),事务内整体覆写status+ 整个material_json+RESET_GIFT_FLOW_SQL(:555-570)。 ⇒ 与"验收"并发时把accepted、acceptance_json、submitted_at、押金释放全部抹掉并回退为unassigned。 - 修复:事务内
SELECT ... FOR UPDATE重新读单并以锁行为准(material 也取锁行),状态白名单校验两次,UPDATE ... WHERE id=$4 AND status=$5(0 行 → 409work_order_status_changed),库存 UPDATE 作为最终仲裁(0 行 → 409)。
P1-4 撤销只重置冷静期字段,验收/时限/奖金残留 —— ✅ 已修复(本轮)
- 原
:560只应用RESET_GIFT_FLOW_SQL(work-order-gift-repo.ts:34-39)。 - 修复:撤销的
UPDATE work_orders现同时清空assigned_worker_id / assigned_at / deadline_at / submitted_at / accepted_at / acceptance_json / draft_acceptance_json / problem_note / published_at / pinned_at,与reopenCancelledWorkOrder的清理口径一致。
P1-5 电子凭证在事务外核销、失败被吞
- 指派路径在状态变更之前核销(
admin-work-order-assignment-service.ts:67-85),逐张提交(publish-voucher-service.ts:64-113)⇒ 指派被拒时可能已有凭证被核销。 - 履约路径在事务之后核销(
consignment-fulfillment-service.ts:248-264),失败只写进响应体;前端WorkOrdersPanel.tsx:716-726完全不读voucherConsume,无条件弹"成功"。 - 修复:核销并入状态变更事务(或异步编排 + 补偿);
!ok时接口失败并提示。
P1-6 匹配引擎对空值放行,且履约时服务端不复检 → 错发武器
consignment-matching-service.ts:125-128:订单侧为空时sameSpec直接return true;:150系列只在双方都有值时才强制相等;:119钻级解析失败兜底为 1(invDiamond >= 1恒真)。 ⇒ 商品名没写"安卓/微信/暗潮/1钻"(例如只写"苏尔南手枪")时,任意在库库存都会被推荐为 100 分"精确匹配"。consignment-fulfillment-service.ts:109-153:履约接口只校验"在库 + 允许匹配",不校验规格(匹配结果纯前端展示,routes/admin/worker-platform/consignment.ts:239-274直接透传inventoryId)。- 修复:空规格不得给"精确匹配";履约事务内用
matchPeacekeeperOrderWithInventory复检,非 exact 一律 409(partial 需显式覆盖参数)。
P1-7 PUT /inventories/:id 可绕过整个状态机 —— ✅ 已修复(本轮)
- 原来透传
status / workerId / depositAmountFen;consignment-repo.ts:404-540直接拼 SET:可把consumed改回in_stock(不清绑定、不动押金、无审计),可改deposit_amount_fen。 - 修复:服务层显式拒绝
payload.status(400consignment_status_update_forbidden,前端编辑弹窗本就不提交 status),仓库层移除status入参 —— 状态只能经由占库/解绑/回库/作废/撤销这些带守卫的函数变更。
P1-8 前端对四个 consignment 权限零引用 → 权限一旦被取消,按钮照旧显示、点击必然 403
- 19 个管理端接口已全部挂
requireAdminPermission(无遗漏),但前端全仓 grep 不到consignment.view/manage/review/fulfill(引用数 0)。 - 关键路径:工单列表发布按钮只需
worker_order.publish即可打开拦截弹窗(WorkOrdersPanel.tsx:1391-1402、:698-714),弹窗内两个按钮无条件渲染并直接调/fulfill、/assign-worker(需consignment.fulfill,:2952-2968)。AdminUsersPage.tsx:456-471的角色权限页可以取消任一 consignment 权限(本次 commit 修白名单的正是这一页),取消后 UI 毫无变化 → 点击必 403(routes/admin/session.ts:83-102)。 - 同源:
ConsignmentInventoryPanel/ConsignmentSubmissionsPanel全部写按钮(:192/:245/:172/:293/:307、CSP:97/:131)也无校验;consignment.view这个只读权限在 UI 层无法体现。 - 修复:像
WorkOrdersPanel.tsx:182-221那样读fetchAdminSession().user.permissions,把 canView/canManage/canReview/canFulfill 透传到面板与弹窗按钮。
P1-9 未审核 / 已冻结打手仍可发布寄卖(绕过接单账号审核)—— ✅ 已修复(本轮)
- 前端菜单无条件渲染:
WorkerLayout.tsx:35,42,90(⏳ UI 未收口);后端POST /consignment/publish原只用getRequiredWorkerSession。 - 修复:发布接口已挂
requireActiveWorker(对齐routes/worker/hall.ts:17-20);GET /consignment/my-items保持仅需会话(只读自己的数据)。前端菜单项的后置处理留待批次 E。
P1-10 收集链接没有后台管理 UI,服务层是死代码 —— ✅ 已随渠道下线废弃
services/admin/consignment.ts:67-82(fetch/create/update link)零调用;后端三个接口(routes/admin/worker-platform/consignment.ts:136-167)无入口。- 结论:收集链接整条线已删除,不再需要后台管理 UI。
P2 · 运营体验 / 合规 / 性能(按影响排序)
- 搜索骗人:
ConsignmentInventoryPanel.tsx:678占位符承诺"物品名/编号/收购人/店铺/卖家",后端 ILIKE 只覆盖部分字段(consignment-repo.ts:184-189)→ 按收购人/店铺搜恒空。 - 统计每次刷新跑 5 条全表聚合(
consignment-repo.ts:215-303),且列表无排序/无列筛选。 - 凭据无独立权限:
mapConsignmentInventory直接返回credentialsJson(游戏账号/密码,consignment-admin-service.ts:77-80),列表接口全量下发,仅需consignment.view。 is_multi_use=false从未生效:字段只落库(consignment-repo.ts:788),submitPublicConsignment全文不读 ⇒ 单次链接可无限提报。✅ 随渠道下线废弃- 公开提交按手机号自动绑定打手(
consignment-public-service.ts:113-124)⇒ 知道打手手机号的第三方可让其被动冻结押金。✅ 随渠道下线废弃(现只有打手端本人发布) - 押金可能长期冻结无人收口:
pending_review无过期策略;打手端只有publish+my-items(routes/worker/consignment.ts),没有"撤回我的提报"。⏳ 未处理(本轮已补:冻结归属取台账,运营手动驳回即可释放) - 统计报表与设计文档不符:文档 §六/§七 的【累计节省赏金】【本月出库单数】【店铺节省赏金/人效】【批次回款】均未实现;
getConsignmentInventoryStats只返回数量与成本 + 4 个 GROUP BY,前端指标卡也只 4 项(ConsignmentInventoryPanel.tsx:559-587)。 - 列表无"匹配内部库存"角标 + 匹配懒加载 + 全表入内存:
checkWorkOrderConsignmentMatches仅在点发布时调用(WorkOrdersPanel.tsx:700),且异常被空 catch 吞掉(:710-712,缺 view 权限时拦截永不出现);listActiveConsignmentInventoriesForMatching(consignment-repo.ts:683-707)全表读内存再 JS 匹配。 - 待审徽标只统计当前页(
ConsignmentSubmissionsPanel.tsx:85+:316)。 - 编辑无法解除打手关联:
ConsignmentInventoryPanel.tsx:260的workerId || undefined撞后端!== undefined判定(consignment-admin-service.ts:316-321)。 - 编辑弹窗押金被静默改成 ¥100(
ConsignmentInventoryPanel.tsx:513),而该值会被"回库重新冻结"按金额使用(consignment-deposit-repo.ts:242-251)。 - 冷静期文案硬编码"默认 3 天"(
WorkOrdersPanel.tsx:2852-2853),与产品规则(work-order-gift-repo.ts:101-105)及同页自算的cooldownLabel(:1040-1042)自相矛盾。 - 冷静期倒计时时区错位:后端把时间统一格式化为 Asia/Shanghai 无时区串(
utils/http.ts:4,212-235),前端按浏览器本地时区Date.parse(worker-order-utils.tsx:344、DeadlineCountdown.tsx:18)⇒ 非 UTC+8 环境显示偏移 8 小时(真实门禁在 DB,仅显示错)。 - 接口失败静默成 0/空:两个面板只看
isLoading无isError(ConsignmentInventoryPanel.tsx:129-163、ConsignmentSubmissionsPanel.tsx:66-85);打手下拉(:117-120)失败后指派按钮消失。 - 批量入库最多 50 次串行 POST,无回滚无 loading(
ConsignmentInventoryPanel.tsx:191-222),失败重试会重复建单(无幂等键)。 - 破坏性弹窗缺
confirmLoading→ 双提交(:1235-1242、CSP:361-369/518-525)。 - 两条回库路径
allow_matching语义分叉:consignment-repo.ts:657强制 TRUE,撤销回库保留原值,但两处 UI 文案都宣称"重新进入待匹配池"。✅ 已统一(本轮):所有回库路径一律保留运营的开关设置,UI 文案同步更正(决策理由见本节开头的"刻意偏离"说明)。 - 类型/契约漂移:
(文件已删除);services/consignment-public.ts:5-11声明ConsignmentSubmissiontypes/consignment.ts与后端返回结构不符、services/worker.ts:375-391缺depositAmountFen。⏳ 部分未处理(本轮已将sourceType收敛为manual|worker_portal|recycle、新增assignedWorkerId)。 - 契约约束缺失:
allocation_status中文自由文本无 CHECK(077_consignment_inventory.sql:27);指派占库完全不写bound_work_order_id(consignment-repo.ts:552-572)。✅ 占库已写绑定(本轮);allocation_status的 CHECK 约束仍未加。 - 小项:
archive不记录操作人与原因;remark拼接式审计无长度上限;履约成功后广播hallChanged: true(consignment-fulfillment-service.ts:264)但工单从未进大厅(无效广播);RBAC 体系混用(寄卖下拉依赖的/worker-platform/workers仍是requireAdminRoles,workers.ts:20-22)。
四、已核实为"正确"的部分(改动时不要破坏)
- 内部现货履约事务是原子的:事务内重新锁单(
consignment-fulfillment-service.ts:157-168)+ 条件更新占库(:170-199),并发下不会两单占一件;占库同时写入工单绑定(本轮新增,含bound_work_order_id)。 - 打手指派占库是单条条件 UPDATE(
consignment-repo.tsclaim 函数),回滚带WHERE status='consumed'(有测试覆盖)。 - 押金账务并发安全:先锁钱包
FOR UPDATE再读流水,缺口时拒绝部分退款,重复冻结幂等(consignment-deposit-repo.ts;本轮另加"归属/金额以冻结核销行为准"与部分唯一索引)。 - 审核入库/驳回原子且带状态条件。
- 押金履约模式禁止走撤销(
resolveRevocableConsignmentInventoryId),且该模式下的库存已改为按工单绑定,撤单时由工单终态链路自动解绑。 - RBAC 权限链正确:四权限已在
077注册并授予 admin/operator/support,requireAdminPermission对admin直通;19 个接口迁移无遗漏,没有写接口只挂 view。 - 上传有服务端大小/类型限制(
routes/file-upload.ts10MB + magic-byte 白名单)。 - 订单级押金与寄卖发布押金互不误伤:前者按
related_work_order_id匹配,后者related_work_order_id IS NULL且按payload_json.consignmentSubmissionId匹配。 - 手工钱包调整只写
available_amount,不减少frozen_deposit_amount,不破坏冻结池不变量。
五、优化路线(分批,每批可独立发布)
批次 A · 修复两条断掉的入口链路 —— ✅ 已完成(性质已变)
H5 上传返回值取→ H5 渠道整体下线(表、路由、页面、服务、data.filelink_id列一并删除;迁移079),提报统一走接单平台打手端。- ✅ 手工入库/编辑兼容
credentialsJson/screenshotsJson,三处入口统一走pickCredentialsJson/pickScreenshotsJson。 - 验收:
npm test401 例(398 通过 / 3 skip)、typecheck/lint:check/format:check/check:sql全绿。
批次 B · 状态机封闭与库存绑定唯一性 —— ✅ 已完成
- ✅ 撤销:
FOR UPDATE锁单 + 状态白名单(in_progress|pending_acceptance|problem)+UPDATE ... AND status=$n+ 原因必填 + 前端按状态与consignment.fulfill权限渲染。 - ✅ 库存绑定收口:撤销加
bound_work_order_id = $工单;退回复库/作废加"未被进行中工单占用"守卫;0 行 409 且给出具体工单号;079加部分唯一索引兜底并清理历史重复绑定。 - ✅ 生命周期收口:
releaseConsignmentInventoryBindingForWorkOrderWithClient接入撤单仓库 5 条链路 + 打手退单申请 + 超时处置(重开/取消)。 - ✅ 回归测试 5 例(占库绑定、状态白名单、解绑范围、作废/回库守卫、终态链路接线)。
- 验收偏差:原始表述"回库路径一律恢复
allow_matching"改为"保留运营开关设置",理由见第三节"刻意偏离"。
批次 C · 指派事务化、资金/凭证一致性(P1)—— ✅ 已完成(本轮)
- ✅ 指派"部分成功"窗口(P1-2):指派失败不再只把现货放回在库池,改为
compensateFailedConsignmentAssignment三步补偿 —— ① 指派事务若已提交(工单in_progress+ 已指派打手)走管理端「取消指派」:解冻打手押金、工单退回大厅,并顺带解绑现货; ② 兜底释放库存占用(指派未提交时的主路径); ③ 摘掉工单上的material.consignment并推入consignmentHistory。 每步独立 try:补偿失败不掩盖原始错误,但会留下备注/解绑痕迹。 - ✅ 凭证核销不再假成功(P1-5):核销需调用快手外部接口,无法与数据库事务合并回滚,因此改为"显式失败 + 留痕 + 如实反馈"——
核销失败写事件
consignment_fulfill_voucher_consume_failed(对齐既有发布链路publish_voucher_consume_failed口径), 返回voucherConsume.ok=false;前端notifyVoucherConsume改为message.warning(含失败原因与"请人工核销"),不再无条件弹成功。 指派链路的凭证失败本来就由assignAdminWorkOrderToWorker抛 409 + 写assign_voucher_consume_failed(失败即回滚,不产生部分状态)。 - ⏳ 押金没收(P2-2 死分支)与编辑接口改额的台账一致性校验仍未做:前者需要业务先定"卖家违约扣没"规则。
- 意外收获(必修):迁移
079的语句顺序有 bug —— 旧source_typeCHECK 只允许manual/collection_link/recycle, 而回填worker_portal写在删旧约束之前,导致任何全新库在 079 必然失败(本地 dev 库就是这样启动失败的)。 已改为"先删旧约束 → 回填 → 挂新约束",并在全新空库验证001 → 079一次性跑通(79 条迁移全部记录)。 因为 079 尚未发布(所在提交仅本地未推送),修复按仓库check:sql守卫要求合入引入它的那个提交,而不是新增补救迁移。
批次 D · 匹配准确性与防错发(P1)—— ✅ 已完成(本轮)
- ✅ 空规格不再放行、钻级不再兜底 1(P1-6):
sameSpec(任一侧为空即视为相等)改为compareSpec三态判定 (match/mismatch/unknown),extractPeacekeeperSpecs的钻级兜底由|| 1改为|| 0(未知)。 规则:系统/区服/系列冲突 → 直接排除;订单侧没写清楚的字段计入unknownFields,只要有未知项就绝不判"精确匹配", 降级为partial并在reason里列出缺失项(如"订单规格信息不全(未写: 系统、区服、钻级),需人工确认")。 - ✅ 履约/指派服务端复检:新增
assertConsignmentSpecMatchedOrder,两条路径(内部现货履约、指派打手)在占库前都要过: 规格冲突 → 409consignment_spec_mismatch;非 exact 且未显式传allowPartial→ 409consignment_spec_partial_requires_confirm。 前端在"部分匹配"候选上先modal.confirm危险确认,再带allowPartial: true重试,高配充低配的合法玩法保留但不可误触。 - ⏳ 匹配下推 SQL + 批量匹配接口 + 工单列表角标(P2-8)未做:当前仍是"取全量在库 → 内存比对"。
批次 Y · 生产 079 迁移失败修复:先去重重复冻结再建幂等索引(本轮)
现象:生产部署在 079 的 uniq_worker_wallet_ledgers_consignment_publish_freeze 唯一索引上
23505(提报单 5 有两条 consignment_publish 冻结流水),迁移整体回滚、数据库无损。
重复冻结正是该索引要防缺陷的历史脏数据(批次 F2 之前发布链路允许二次冻结)。
修法(服务器从未成功应用 079,经确认直接改原迁移,与 079 自己"重复绑定工单先清后约束"同套路):
每组(打手+提报单)保留最早一条冻结流水;多余行仅改 payload 标记 consignment_publish_superseded
(移出 outstanding 口径与索引范围,原行留审计);多冻的钱退回钱包
(退额 = LEAST(多冻总额, 冻结池实有),绝不无中生有)并写 consignment_publish_dedup_refund
修正流水;同一打手多条多余流水先按钱包聚合再退;然后才建索引。静态测试钉住"去重退还必须先于建索引"。
验证:一次性 Postgres 完整复现生产路径——停在 078 + 注入提报单 5 双冻结脏数据 → 新 079/080 迁移成功;钱包可用 +2000(多冻退回)、冻结池回落单份 2000;修正流水留痕; 第三次冻结被索引拒绝。全新空库 001→080 亦验证通过。后端 484 测试全绿。
批次 X · 发布寄卖自动带「接单模板」建议价(本轮)
需求:接单平台已按规格配好模板价(生产库几十条,如 安卓暗潮1钻 ¥120 / 6钻 ¥500), 卖家发布寄卖时按规格自动带出,不用手填/猜价。
- ✅ 后端:纯函数
suggestConsignmentPriceFromRules—— 模板商品名(「安卓暗潮1钻」) 用与订单同一套extractPeacekeeperSpecs解析,系统+系列+钻级全对上才命中(系统按通配口径, 模板写「通用/双端」两端都算;模板没写系统则不命中,宁可不建议不给错价);多条命中取最高价。 新端点GET /worker/consignment/price-suggestion(requireActiveWorker)。 - ✅ 前端:发布表单规格选齐(系统+系列+钻级)后 300ms 防抖请求;同规格去重、过期响应丢弃; 命中则自动填入寄售价并提示「已按接单模板匹配 N 条规则,建议价 ¥X(可修改)」; 卖家手动改过价则只提示不覆盖,换规格后重新允许自动带出;提交成功后状态复位。 建议价接口失败静默(锦上添花,不影响发布)。本地没配模板时无建议价、保持手填,行为不变。
- ✅ 测试:建议价纯函数行为测试(命中/系统不符/钻级不符/系列不符/多命中取最高/通用模板两端命中/无系统模板不命中)。
验证:后端 483 / 前端 54 测试全绿;两侧 typecheck / lint:check / format:check / check:sql 通过。
批次 W · 规格口径:互通通配 + 后台全量/发布端分物品展示(本轮)
业务定案:
- 操作后台全量选项:系统 苹果/安卓/通用;区服 微信区/QQ区/互通。提报审核与库存编辑
的系统/区服从自由文本 Input 改为受控下拉(单一来源
utils/consignment-spec.ts)。 - 打手端发布按物品类型展示:苏尔南 = 系统只有 苹果/安卓(必须区分端,不给「通用」), 区服只展示「互通」一个选项且默认选中(互通 = QQ/微信都能收)。后续其他品类规则不同 (有的不分端),在共享常量里按品类加条目即可。
- 匹配引擎通配符对称化:
compareSpec支持按字段传通配值集合——区服通配 =「互通」 (订单文案里的「通用/全区/不限」归一化成通配别名),系统通配 =「通用」(「双端/全端」同理)。 以前只认库存侧是通用件;现在订单侧写通配值(买家不限)同样放行,只增不减匹配面。describeConsignmentSpecDifferences同口径:通配对不算差异,不再吓唬客服。 - 提报白名单校验(API 层):系统只收 苹果/安卓(400
consignment_system_invalid), 区服只收 微信区/QQ区/互通(400consignment_server_zone_invalid)——旧页面缓存/直调接口绕不过。 - 物品名拼接:区服为「互通」时不拼进默认物品名("安卓互通暗潮…"对买家无信息量), 前端自动拼名与后端默认拼名同口径。
验证:后端 481 测试(互通双向匹配/通配不算差异/真冲突仍排除/白名单校验均有用例)、
前端 54 测试(选项口径/表单引用/默认值静态钉住)全绿;两侧 typecheck / lint:check /
format:check / check:sql 通过。
批次 V · 复核遗留:份额验收钩子 + 手动匹配弹窗三处(本轮)
- ✅ 拼单份额全部验收(母单转已验收)补寄卖钩子:
acceptWorkOrderShareAndSettle里最后一个份额把母单转accepted的分支,此前既不释放发布押金也不结算供货货款(两个钩子只在统一验收里)。现在与acceptWorkOrderAndSettle同口径:有material.consignment.sourceSubmissionId就释放发布押金;供货货款固定传assignedWorkerId: 0(拼单母单没有单一接单人,份额打手已各自按 share_reward 结算,不会重复)。 - ✅ 手动匹配弹窗 · 数据过期的 409 不再死循环:
consignment_inventory_race_condition / consignment_inventory_unavailable / consignment_spec_mismatch / consignment_spec_partial_requires_confirm四个错误码清空已选并自动重拉列表,按最新判定重来,不再对着失效候选反复撞错。 - ✅ 手动匹配弹窗 · 库存侧搜索与指派死路:关键字 +
pageSize:100透传后端(原来只拉默认 50 条、keyword 是从未传出去的死参数,第 51 单以后搜不到);关键字 300ms 防抖触发后端搜索;无供货打手的现货禁用"指派供货打手履约"选项(原来选了必 400consignment_worker_required且弹窗里没有打手选择器,是死路),提交前再拦一道;库存侧选中订单时从响应 inventory 带出workerId/workerName回显。 - ✅ 凭证核销反馈抽共享出口:
utils/voucher-consume.ts统一notifyVoucherConsume(含"已提前核销 N 张"提示),WorkOrdersPanel本地实现删除、手动匹配弹窗弃用内联子集版,口径不再漂移。
验证:后端 477 测试(+4)全绿、前端 50(+1)全绿;两侧 typecheck / lint:check / format:check / check:sql 通过。
批次 U · 复核修复:占库 500 / 指派双付 / 手动匹配权限门控(本轮)
对 94328ac6..0476339e 做了一轮对抗复核(两个并行审查 + 人工验证),修三处:
- ✅ P0 指派打手占库在真实库必 500:
claimConsignmentInventoryForWorker的allocation_status默认值是自由文本「已指派打手(押金履约中)」,撞上迁移080的CHECK ('待安排','已安排')。假 client 单测不跑真 SQL 一直没暴露(与当年 42804 同病)。 默认值改为「已安排」,并加静态守卫测试(剥掉注释后断言默认值必须在枚举内, 防止注释里引用历史值误伤)。 - ✅ P1 指派非供货卖家时货款双付:
resolveConsignmentAssignRewardFen以前无条件把 工单报酬改写成结算价。指派了别的打手时(API 层允许,界面不会主动构造):打手按结算价 拿一份 + 卖家再拿一份供货货款(settleConsignmentSupplierPayout只在"接单人===卖家"时 跳过)= 平台双付。现在纯函数加assigneeIsSupplier参数:卖家本人履约 → 报酬=结算价 (货款走工单报酬渠道);别的打手 → 报酬保持原值(货款走供货货款渠道),两条渠道互斥。 补行为级测试 + 调用点静态钉住(判定基准必须是inventory.worker_id)。 - ✅ P1 手动匹配入口权限错位:库存面板「手动匹配订单」挂在
consignment.manage门下、 拦截弹窗 footer「手动指定现货匹配」没门控,而两个动作提交打到要求consignment.fulfill的/fulfill、/assign-worker—— 只有 manage 的运营能点进去、选完单、点确认后必 403。 两处入口统一按canFulfillConsignment门控(编辑/作废仍为 manage,与后端权限一一对应), mount 测试静态钉住。
验证:后端 473 测试(+3)全绿、前端 49(+1)全绿;两侧 typecheck / lint:check /
format:check / check:sql / build 通过。
复核中确认无需改动:手动指定现货的服务端复检与留痕、售后回落卖家、卖家操作归属校验、 删除类提交的残留引用 —— 均已核对无发现。遗留待办(拼单份额验收补寄卖钩子、文件接口登录态、 弹窗 409 后刷新)见复核报告,未在本批处理。
批次 T · 手动匹配的入口修复(本轮)
问题(用户):在库待匹配列表点「手动匹配订单」不弹窗。
根因:弹窗被挂到了「编辑寄售物品」弹窗内部(JSX 嵌套位置错), 外层弹窗没打开时它根本没被挂载 → 点击只改状态、什么都不发生。 这类"点了没反应"的问题在浏览器里才看得出来,所以顺手加了静态守卫。
| 修复 | 说明 |
|---|---|
| ✅ 挂载位置 | 弹窗移到面板顶层(与其它弹窗同级),点击即弹 |
| ✅ 静态守卫 | 新增 3 例测试:断言手动匹配弹窗前面不能有未闭合的 Modal,按钮必须真的 setState、open 由该状态驱动(已验证该守卫能拦下"嵌在别的弹窗里"的写法) |
| ✅ 入口补全 | 订单侧原来只在「检测到候选」的提醒弹窗里给入口,订单没有任何系统候选时反而进不去(而那正是最需要人工指定的时候)→ 工单行内新增常驻「手动匹配现货」(仅待指派/待补资料,与后端前置校验同口径) |
| ✅ 失败前收敛 | 已暂停匹配的现货点按钮会直接 409 → 改为按钮禁用 + 提示"先打开允许匹配开关" |
| ✅ 顺带 | destroyOnClose 换成 antd 6 的 destroyOnHidden(前者已废弃) |
验证:前端 typecheck / lint / format / 48 例测试 / build 全过。
批次 S · 客服手动指定现货匹配(本轮)
需求(用户):客服要能在后台人工指定现货与订单匹配起来,而不是只能用系统算出的候选。
先说清楚现状:后端其实早就留了"人工选定现货"的口径(allowPartial + 宽松规格复检),
只是没有界面入口,客服只能从"规格完全对得上"的自动候选里挑;遇到订单规格没写全、
订单录错、需要高配充低配时无从下手。
改法(三项决策均由用户确认:允许强制匹配但必须留痕 / 两种交付方式都保留 / 双向入口都加):
- ✅ 判定与解释:新增
listConsignmentMatchOptions()/classifyConsignmentMatch(), 把某订单的全部在库现货逐件判定 exact / partial / conflict,conflict 时给出字段级差异 (describeConsignmentSpecDifferences,例如「系统:订单「苹果」/ 现货「安卓」」)。 正反两个方向共用同一套判定,避免"界面说能选、接口却拒绝"。 - ✅ 强制匹配 + 留痕:履约/指派两条链路新增
allowSpecMismatch与matchNote。 规格真冲突默认仍拒绝(409consignment_spec_mismatch);显式确认后放行,但 必填原因(400consignment_match_note_required),原因与差异明细写入工单事件与material.consignment(manualMatch/matchType/matchNote/specConflicts)。 - ✅ 双向入口:订单侧
GET /consignment/matches/:workOrderId/options(全部在库现货+判定), 库存侧GET /consignment/inventories/:id/match-orders(该现货可匹配的待指派/待补资料订单+判定)。 - ✅ 前端一个共享弹窗:
ConsignmentManualMatchModal,订单侧(匹配弹窗里「手动指定现货匹配」) 与库存侧(寄售仓库行内「手动匹配订单」)共用,避免两套 UI 逻辑漂移。 交互:搜索 → 选中(带判定标签与差异说明)→ 选交付方式(内部现货履约 / 指派供货打手) → 非精确勾选确认 / 冲突勾选「确认规格不一致」+ 必填原因 → 落地。 判定规则收敛在纯函数consignment-manual-match.ts并有单测(4 例)。
真实库验证(订单要「苹果微信区暗潮2钻」,现货为「安卓QQ区苍龙4钻」):
| 步骤 | 结果 |
|---|---|
| 订单侧选项 | 该件判定 conflict,列出 4 项差异(系统/区服/系列/钻石等级) |
| 库存侧可匹配订单 | 该订单判定 conflict,要求规格解析正确 |
| 不填原因强制匹配 | ✅ 被拒 consignment_match_note_required |
| 不勾选强制、直接履约 | ✅ 被规格复检拒绝 consignment_spec_mismatch |
| 勾选 + 填原因 | ✅ 成功:订单 in_progress、现货 consumed,事件与 material 均留痕 |
| 接口鉴权 | 未登录访问两个新接口均 401(路由已挂载、权限位生效) |
临时数据已清理(残留 0)。
批次 R · 售后立案也认寄卖关系(本轮)
补货款时顺手排查"只认接单人"的地方,发现同类第三处:售后立案。
createAfterSalesCase 里责任人取 work_orders.assigned_worker_id,取不到就
failureReason: 'worker_missing' → 客服界面上是 409「请指定已验收的责任打手或拼单份额」。
内部现货履约的寄卖单没有接单人,于是这类单子连售后都立不了案;
而货款修好后,钱已经结算给供货卖家了,担责的人本来就该是他。
改法:把"从工单反查供货卖家"抽成共享查询
getConsignmentSupplierByWorkOrderWithClient()(consignment-supplier-repo.ts),
货款结算与售后立案共用同一份口径(工单 material_json.consignment.inventoryId
或库存 bound_work_order_id → 库存 → 提报单),售后在找不到接单人时回落到供货卖家,
并在验收快照里标记 consignmentSupplier: true 与提报单号留痕。
真实库验证(内部现货履约,接单人为空):
| 步骤 | 结果 |
|---|---|
| 从工单反查供货卖家 | workerId=3、结算价 12400、提报单号正确 ✅(不是无关的打手 4) |
| 售后立案 | ✅ 成功,case_no=VERIFY-AS-…,责任人 worker_id=3(供货卖家) |
临时数据已清理(残留 0)。至此"寄卖单没有接单人"这一类坑的三处(货款、售后、卖家按钮)都已收口。
批次 Q · 补上寄卖货款结算(本轮)
问题(用户对账发现):工单 #100171 验收完成时,卖家钱包只有两笔——发布押金 -20 又 +20。 货卖出去了,但货款的收益没进卖家钱包。
查证:
| 项 | 值 |
|---|---|
工单报酬 reward_amount |
14500 分(¥145.00,发单方付给平台) |
货的结算价 cost_price_fen |
12400 分(¥124.00,卖家应得) |
接单人 assigned_worker_id |
空(内部现货履约,客服/内部仓代交付) |
通用验收结算的规则是"把工单报酬付给接单人"。内部现货履约的单子没有接单人, 于是这笔钱谁也没拿到;而卖家交付了自己的货,只拿回了自己的押金。差价(¥21)本该是平台的。
改法:
- ✅ 新增
settleConsignmentSupplierPayoutByWorkOrderWithClient():验收完成时, 把货的结算价(cost_price_fen,缺省退回提报的asking_price_fen)补给供货卖家, 金额单位与通用报酬一致(分),并同样做售后欠款抵扣、按售后策略决定是否延迟解冻、 写reward_settlement/reward_pending_unfreeze流水(说明「寄卖货款结算 #单号」)。 - ✅ 不重复付款:接单人就是供货卖家本人时跳过——指派打手线里接单人正是卖家本人, 货款已经随工单报酬付过了(见下一条),只有"没人收"的那一份才在这里补。
- ✅ 顺带修掉一个 100 倍单位错:指派打手线把工单报酬改写成"货的结算价"时,
写的是
Number((cost_price_fen / 100).toFixed(2))→ ¥124 被写成 ¥1.24, 卖家只能拿到结算价的百分之一。reward_amount的单位是分(与钱包、流水一致, 已用真实数据核对:reward_amount=2500对应流水2500、界面 ¥25.00)。 现在抽成纯函数resolveConsignmentAssignRewardFen()并加测试钉住单位。
真实库端到端验证(内部现货履约,结算价 ¥124 / 发单方付 ¥145):
| 步骤 | 结果 |
|---|---|
| 验收前卖家钱包 | 可用 ¥205.50,累计结算 ¥297.50 |
| 走真实验收结算 | 订单转 accepted |
| 本次流水 | reward_settlement ¥124.00 寄卖货款结算 #100175 |
| 验收后卖家钱包 | 可用 ¥329.50(+¥124.00),累计结算 ¥421.50(+¥124.00)✅ 等于货的结算价 |
验证脚本改动的开发库数据已还原(钱包恢复 ¥205.50 / 累计结算 ¥297.50,等级未被触发变更),临时数据残留 0。
遗留:用户对账时看到的 #100171 是在本批修复之前结算的,所以它当时确实没发货款(¥124.00)。 要不要给这类历史单补一笔,等用户确认。
批次 P · 「确认加好友 / 上传赠送截图」恢复为卖家操作(本轮)
问题(用户截图:卖家端的货点不了确认加好友):卖家「我的寄卖」里那件"已匹配-待加好友"的货, 提示写着「等待客服确认加好友」,没有按钮。
原因:批次 I 把这两个动作的能力位绑在"订单接单人 = 当前卖家"上(selfFulfilling),
而内部现货履约的订单根本没有接单打手(assigned_worker_id 为空),于是:
状态机把按钮关掉 + 提示写成"等客服";后端归属校验也会 403(consignment_fulfillment_not_owned)。
业务定调(用户):卖家可以确认加好友,冷却到期后也能自己上传赠送成功截图,客服只是兜底。
改法:
- ✅ 状态机能力位改成按阶段给(不再看接单人):
material_submitted(赠送资料已交)→ 卖家可点「确认加好友」;- 内部现货履约没有"先交赠送资料"这一步 →
material_required阶段卖家也可直接确认(与客服侧能力一致); - 冷却结束(
gift_ready)→ 卖家可「提交赠送截图」。 - 提示统一改成卖家视角:「请先加上买家好友,加好后点『确认加好友』:确认即进入赠送冷却期, 未完成赠送押金不退(客服也可代为确认)」。
- ✅ 后端归属只认「这张提报单就是当前卖家的」(别人的单仍 404
consignment_item_not_found), 删掉"订单接单人必须是卖家"的 403 —— 内部现货单没有接单人,按接单人卡会把卖家自己的货挡在外面。 - ✅ 卖家提交赠送凭证走
submitWorkerOrderAcceptance(..., { asConsignmentSupplier: true }): 跳过"必须是接单打手"的校验(归属已由寄卖单保证),冷却是否结束、必须带图等校验全部保留。 - ✅ 客服侧按钮不变,谁先点谁生效,后点的会拿到明确的 409 提示。
真实库端到端验证(造一件内部现货履约的货,订单无接单打手):
| 步骤 | 结果 |
|---|---|
| 卖家列表 | 阶段「已匹配-待加好友」,确认加好友按钮=true,提示为卖家操作口径 |
| 卖家点确认加好友 | gift_phase=friend_countdown,冷却到 +72h |
| 冷却中传截图 | 被拒 work_order_gift_cooldown_active |
| 冷却到期 | 阶段「待赠送」,上传截图按钮=true |
| 卖家上传赠送截图 | 成功 → 工单 pending_acceptance(交客服验收) |
临时数据已清理(残留 0)。
批次 O · 去掉文件链接的签名/有效期(本轮)
问题(用户截图:客服看到的凭证是裂图):寄卖凭证的图片地址是带签名的临时链接:
/api/v1/files/object?key=consignment_screenshot/2026/09/15/xxx.webp&expires=1789463063&signature=...
签名有效期只有 15 分钟,而入库时会把当时的链接原样存进 screenshots_json。
工单验收那条线在出参时用 refreshStoredFileUrl() 重签,所以工单图片正常;
寄卖这条线(提报凭证 / 赠送凭证 / 卖家列表)漏了这一步 → 存下来的链接早过期 → 客服看到裂图。
图片本身没丢,是"取的时候没重签"。
业务定调(用户):签名这套意义不大,直接去掉,用普通链接。
改法:
- ✅
buildFileUrl()现在只产出不带签名、不带有效期的普通链接 (对象 key 带随机段,本身不可枚举),删掉签名生成/校验与 15 分钟 TTL 常量, 文件读取路由不再校验签名。 - ✅
refreshStoredFileUrl()保留但语义变成"归一化":把历史存的签名链接洗成普通链接, 老数据不用改也能看(normalizeStoredFileUrl保留为同义别名)。 - ✅ 寄卖侧出参统一走归一化:提报凭证、赠送凭证(
imageUrls+files[])、库存截图、卖家列表截图; 这样新老数据都稳定可看,不再依赖"每一处读取都记得重签"。
真实库验证:库里那条早在 2026-09-15 就过期的老链接,现在真实请求返回
HTTP 200 / image/webp / 52632 字节(改造前是 403);客服接口下发的地址已是不含签名的普通链接;
不存在的 key 仍然 404。
取舍说明:文件路由本身没有登录校验,原来唯一把关的就是签名。去掉签名后, 知道链接的人就能看图(key 里有 24 位随机段,实际不可枚举,等价于一条长口令)。 寄卖凭证可能包含游戏账号/主页信息,如需收紧,可以后续给该路由加"仅登录后台可读"的校验。
批次 N · 删掉"按库存号操作"的卖家入口(本轮)
批次 I 为了兜住"客服手工入库的现货没有提报单"这种情况,给卖家动作额外开了两个按库存号的入口
(/worker/consignment/inventories/:inventoryId/...)。手动入库停用后这类货已不可能再产生,
这两个入口就是纯负担(还要维护"一个 id 两义/分路由"的复杂度)。
改法(测试阶段,无历史数据包袱,直接删):
- ✅ 删掉两个按库存号的路由、两个服务函数、前端两个请求函数与页面里的锚点分派逻辑; 卖家动作只按提报单号(寄卖单号)定位。
- ✅ 卖家列表里"没有提报单"的兜底行不再暴露内部仓库号(标题改「客服手工入库」), 且一律不给操作能力位——不给用户一个点下去必然失败的按钮。
- ✅ 加静态守卫测试钉住这两点,防止以后又长出按库存号的入口。
批次 M · 停用客服手动入库(本轮)
背景:手动入库是唯一会造出「没有提报单的现货」的入口,而这类货没有寄卖单号, 于是全流程(统一状态机、凭证链、搜索、卖家端的单号锚定)都没法用一个号串起来—— 它只能在界面上回退显示内部仓库号 INV…,正是"一件货两个号"的残留。
业务定调:寄卖货源只走「打手在平台提报 → 客服审核入库」这一条路。
改法:
- ✅ 前端「寄售仓库」的「手动入库」入口整块删除(按钮 + 入库弹窗 + 提交逻辑 + 服务函数), 客服无法再建无提报单的货。
- ✅ 后端同时挡住,避免旧页面缓存或直接调接口绕过:
POST /worker-platform/consignment/inventories返回 409consignment_manual_inbound_disabled(保留明确的拒绝而不是静默删除,将来要恢复只需回退这个守卫)。死代码一并清掉: 服务层的原实现与仓库层的createConsignmentInventory都已删除(要恢复从 git 历史取)。 - ✅ 提报审核入库(正常路径)不受影响:它照旧生成内部仓库号 INV… 并写入
source_submission_id, 即每件货都必然挂着一个寄卖单号,INV 只作为内部编号存在、界面不展示。
真实库验证:调用手动入库接口被拒(409 consignment_manual_inbound_disabled),
且库里「没有提报单的现货行数」前后均为 0(没有偷偷写入)。
批次 L · 匹配弹窗防点错(本轮)
问题:匹配弹窗里「走接单平台指派打手(冻结押金交付)」与「内部现货履约」两个按钮 上下紧挨、样式相近,而且只有"部分匹配"才弹确认——精确匹配时两个按钮点下去都立刻执行, 点错就要走「撤销寄售履约」才能退回来。
改法:
- ✅ 两个动作一律二次确认(不再只有部分匹配才确认),确认框把后果写清楚: 寄卖单号(可复制)、动作(给谁/要不要冻结押金)、结果(立刻占用现货、其它订单不能再匹配它、 内部履约还会跳过「待打手提交资料」直接进冷却)、以及"退回要走撤销寄售履约"; 部分匹配时额外红字提示"不是精确匹配、无法自动回退",并把确认按钮降为 danger。
- ✅ 按钮区做成独立分区的「选择履约方式」小卡片:两个按钮各占一行、
block铺满(点击目标更大、 不再是小按钮挨着)、间距拉开到 14px、各加图标(打手=TeamOutlined,库存=PushpinOutlined), 每个按钮下方一句后果说明("由该供货打手接单,需冻结履约押金" / "不外派打手,直接用库存账号赠送")。
批次 K · 匹配弹窗也改用寄卖单号,内部仓库号只留内部(本轮)
问题:内部寄售库存匹配弹窗(发单时拦一下"有现货可直接履约"那个)显示的是
寄卖单号 INV26091711253394——因为候选现货走的是另一个查询 listActiveConsignmentInventoriesForMatching,
它没 join 提报单,sourceSubmissionNo 取不到,前端就回退显示内部仓库号。
结果是客服在弹窗里看到 INV,拿去寄卖仓库搜索又搜不到,还得自己换算。
改法:
- ✅ 匹配候选查询补
LEFT JOIN consignment_submissions带出source_submission_no。 注意这个查询原来是SELECT *+ 不带前缀的列名,而consignment_submissions同样有status / game_name / category_id——加 join 会让这些条件变成 ambiguous column 直接 500, 所以列名一律加ci.前缀并显式列出字段(已加静态守卫测试钉住这一点)。 - ✅ 客服界面只展示寄卖单号:匹配弹窗候选卡片、工单上的寄售标识、工单详情、寄卖仓库列表/详情 全部去掉内部仓库号;客服手工入库的现货明确写「客服手工入库(无提报单号)」, 历史老单写「老单无提报单号」,都不再抛内部编号。
- ✅ 内部仓库号 保留:仍在数据里、仍在接口里、按它搜索依然搜得到(后端两个关键字条件都覆盖), 撤销/解绑等链路一行没动。
真实库验证:仓库层候选查询与匹配接口(checkWorkOrderInventoryMatches)在构造数据上都带出了
寄卖单号(VERIFY-MATCH-INV → VERIFY-MATCH-SUB)。临时数据已清理。
批次 J · 凭证链可视化:提报凭证 + 赠送凭证(本轮)
问题:一件寄卖有两类凭证,但客服界面只看得见第一类,而且是 48×48 一张缩略图,看不出是物品还是主页:
- 提报凭证 = 卖家提报时的「游戏主页截图」(含物品/仓库详情),存在提报表
screenshots_json。 - 赠送凭证 = 履约方冷却结束后上传的赠送完成截图(证明"这件货真送出去了"),
存在绑定工单的验收资料
work_orders.acceptance_json里。
第一类只在寄卖审核界面可见;第二类只有在工单验收界面才看得到。客服要证明一件寄卖的赠送 是否完成,得自己在两个模块之间翻,且没有任何地方把两类凭证并排呈现。
改法:
- ✅ 后端把凭证链从客服两个列表/详情接口一起下发(提报表 → 结果库存 → 绑定工单):
submissionScreenshots(提报凭证)、giftEvidenceImages(赠送凭证)、giftEvidenceNote、giftEvidenceSubmittedAt、giftWorkOrderNo、giftWorkOrderStatus、giftEvidenceReviewStatus(none未提交 /pending待客服验收 /accepted已验收 /submitted其他在途)。 赠送凭证的图片从工单验收资料的imageUrls与files[].url|mediumUrl|thumbnailUrl两处取并去重。 - ✅ 提报凭证口径统一:有来源提报单就用提报单的主页截图,客服手工入库的现货回退用库存自己的截图,
前端只需要看
submissionScreenshots一个字段,不会再拿错。 - ✅ 前端新增共享组件
ConsignmentEvidenceGallery(缩略图 + 大图预览 + 共 N 张), 审核面板与仓库面板复用;审核面板原来那个「凭据截图」列拆成 「提报凭证(游戏主页截图)」与「赠送凭证(赠送完成截图,带待验收/已验收 Tag 与关联工单)」两列, 详情抽屉同样分组,并给出describeGiftEvidence()的一句话说明。
真实库验证:赠送凭证解析在人工构造的工单验收资料上正确取到 3 张图(imageUrls 1 张 + files 2 张)
并能识别 pending_acceptance → 待客服验收;提报凭证在真实数据上取到 1 张。临时数据已清理。
批次 I · 一个单号锚定 + 统一状态机(本轮)
问题:一件货在系统里有三层互不相通的状态——提报单(SUB…,审核态)、库存(INV…,仓库态)、
工单赠送(material_required / friend_countdown / pending_acceptance / accepted)。
my-items 同时返回两个数组、前端分两个列表渲染,于是同一件货两行、两个号;
更糟的是卖家端只映射了前两套状态,"已出库"之后流程就断在黑暗里,
卖家不知道已经匹配、不知道该加好友、不知道在冷却、不知道该赠送。
改法:
- ✅ 提报单号
SUB…作为唯一「寄卖单号」,从发布锚定到结算(零迁移,历史数据直接可用)。listWorkerConsignments改成一个提报单一条记录:库存、绑定工单的信息挂上去, 客服手工入库的现货(没有提报单)才用库存号当锚点。真实库验证:打手 3 的 2 件货从 4 行收敛成 2 行。 - ✅ 统一状态机
domain/consignment-item-stage.ts(纯函数、可单测),把三套底层状态推导成 卖家看得懂的一个阶段 + 一句"下一步该做什么": 待审核 → 已通过(在库待匹配)→ 已匹配-待加好友 → 加好友赠送冷却中 → 待赠送 → 已赠送待审核 → 已完成;另有 已驳回 / 已下架作废 / 已取消,以及标准验收单的 履约中 / 待验收。 列表筛选与 KPI 也改由它驱动(待审核 / 履约中 / 已完成)。 - ✅ 卖家可以在「我的寄卖」操作(以前只有 publish + my-items 两个接口,卖家只能干看):
POST /worker/consignment/items/:submissionId/confirm-friend:确认已加好买家好友 → 进入赠送冷却期;前端弹窗明确写「确认后进入赠送冷却期、未完成赠送押金不退」。POST /worker/consignment/items/:submissionId/gift-evidence:上传赠送截图给客服审核。- 客服手工入库的现货没有提报单,另开
POST /worker/consignment/inventories/:inventoryId/...两个路由走同一套动作(不做"一个 id 两义",避免提报单号与库存号撞号时误操作别人的货)。 - 两个动作都复用既有工单链路(
confirmWorkOrderFriendAdded/submitWorkerOrderAcceptance), 只加一道归属校验:不是本人履约的订单直接 403,卖家碰不到别人的单。 顺带白拿了既有的强校验:冷却没结束不许提交凭证、必须带图。
- ✅ 客服端同号:库存列表/详情接口补
sourceSubmissionNo(LEFT JOIN 提报单), 履约/指派写进工单的寄售信息也带上sourceSubmissionNo,客服面板与工单详情显示同一个寄卖单号 (历史老单回退显示仓库号)。
真实库端到端验证(提报单线与手工入库线各跑一遍):卖家「确认加好友」→
gift_phase=friend_countdown、friend_added_at 落库、gift_available_at 正好 +72 小时;
冷却期内提交赠送截图被拒(work_order_gift_cooldown_active);
换另一个打手来操作被拒(consignment_item_not_found)。临时数据已清理。
遗留:列表按 pageSize: 200 拉全量再聚合,单个卖家寄卖件数超过 200 时需改成游标分页;
pending_acceptance 仍在寄卖可撤销白名单内(见 P2-7);客服长时间不点「确认加好友」没有超时策略(按业务确认先不管)。
批次 H · 修掉"乱弹匹配提醒"与"赏金没换算成元"(本轮)
- ✅ 发布时不再乱弹「内部寄售库存匹配提醒」:
根因在
matchPeacekeeperOrderWithInventory——精确匹配判定失败后无条件 fall through 到 部分匹配。而对"订单没写系统/区服/系列/钻级"这类字段,compareSpec返回unknown(不是 mismatch)会直接放行,于是任何只写了游戏名的订单都会命中仓库里全部在库货, 表现为"随便发布点什么都会弹一下",提醒失去意义。 现在严格模式加两道门槛:① 系统/区服/系列必须都能确认一致;② 还要有正信号(库存配置更高, 或订单压根没提钻级)。"订单没写规格"不再算命中。 - ✅ 人工履约复检不被连带收紧:
matchPeacekeeperOrderWithInventory增加allowUnspecifiedSpecs选项。运营已人工选定现货时走宽松模式(标题没写全规格不硬拦, 但真冲突仍然拦),自动提醒永远走严格模式,两个语义互不污染。 - ✅ 接单赏金换算成元:
WorkOrdersPanel.tsx里弹窗直接用rewardAmount(单位是分) 渲染,把 ¥40 显示成 ¥4000。同文件其他 30 余处都是/100,只有这里漏了。已修正, 并核对弹窗内内部收购成本/履约押金本来就是/100(正确)。
批次 G · 现货作废口径 + 押金处置可选(本轮)
- ✅ 确认加好友后现货一律作废下架,不再回池(严口径):
确认加好友意味着买家好友已加上、货随时可能送出去,回池会被下一个工单再次占用 (等于同一件货二次出货)。这条口径收口在寄卖自己的releaseConsignmentInventoryBindingForWorkOrderWithClient里:解绑前先读工单的acceptance_mode / friend_added_at,命中则status='archived'+allow_matching=FALSE- 备注留痕,否则仍按原逻辑
in_stock回池。所有解绑路径(取消指派/撤单/退回大厅/退单/ 售后/撤销寄售履约)自动继承,不需要各自判断,发单系统的接口一行没动。
- 备注留痕,否则仍按原逻辑
- ✅ 撤销寄售履约弹窗支持两笔押金各自可选(只加在寄卖专属入口
POST /consignment/revoke上):- 打手履约押金:
auto(已确认加好友则扣除,否则全额退还)/refund/forfeit - 卖家发布押金:
auto(已确认加好友则没收;否则作废退还、回库保持冻结)/keep_frozen/forfeit默认全部auto= 原行为;覆盖后原因照旧必填,并把选项与金额写进工单事件 (workerDeposit/consignmentDeposit/inventoryArchived)便于对账追责。 策略判定抽成纯函数domain/consignment-deposit-policy.ts,可单测。
- 打手履约押金:
- ✅ 修掉一个资金漏洞:撤销寄售履约从不结算打手履约押金。
它以前只清
assigned_worker_id、只处理卖家发布押金,打手的履约押金没有任何出口,worker_deposit(指派打手)单撤销后押金会一直冻在钱包里。现在撤销时按上面的 选项调用settleWorkOrderDepositWithClient结算(退还或没收,金额取冻结台账与钱包冻结额的较小值)。 - ✅ 修掉一个真实库必炸的 500(42804):
detachConsignmentMaterialFromWorkOrderWithClient里$1既作$1::text(写进 jsonb 的detachedAt)又赋值给updated_at,Postgres 把它 整体推断成 text,赋值给 timestamptz 列直接报错。凡解绑现货时摘寄售标记的路径 (取消指派/撤单/退单/售后/撤销履约/人工回库)在真实库上全都会炸,而假 client 的单测不跑 真 SQL,一直没暴露。已显式$1::timestamptz,并补了一道类级静态护栏 (sql-param-type-safety.test.ts)扫描同类"占位符既当 text 又当时间戳"的写法。 - ⏳ 待你拍板:
撤销寄售履约的状态白名单仍含pending_acceptance(打手已提交赠送凭证)。 未确认加好友的单子在这一步撤销仍会回池——如果这类单也可能已经交付,需要一并收紧。
批次 F · 待加好友流程与押金不退(本轮)
- ✅ 内部现货履约不再自动进冷却:新增
parkWorkOrderWaitingFriendAddWithClient,占库落地时把工单停在acceptance_mode=friend_gift+gift_phase=material_required的待加好友阶段(friend_added_at/gift_available_at/deadline_at全部留空),客服线下通知加好友后手动点「确认加好友」 (复用已有POST /worker-platform/orders/:id/confirm-friend)才进入赠送冷却。 冷却时长按商品规则落到工单,两者都为 0 时补默认 72 小时(否则确认后会得到 0 秒冷却)。 同时删除已无调用方的forceFriendGift开关,避免再出现"自动确认加好友"的旁路。 - ✅ 前端文案:内部现货单在该阶段显示「待加好友」(打手线仍是「待打手提交资料」),
确认按钮统一为「确认加好友」;阶段文案抽到
utils/gift-phase.ts便于单测。 - ✅ 后台备注快捷项:「已通知加好友」一键追加「[已通知加好友] 操作人 时间」,仍可手动补充(复用
admin_note)。 - ✅ 确认加好友后押金不退(P2-2
deposit_deduction死分支落地):- 打手履约押金:沿用既有的
isWorkOrderGiftDepositForfeitable(friend_added_at有值即不退); - 寄卖卖家发布押金同口径新增:
forfeitConsignmentPublishDepositBySubmissionWithClient/...ByWorkOrderWithClient,写一条负数deposit_deduction流水(可用余额不动、只扣冻结额), 挂在 6 处取消/退单/脱离判定点 + 「撤销寄售履约」的作废/回库分支上。 因为台账outstanding会把deposit_deduction计入已核销,之后同一提报单的 release / 重新冻结都拿不到钱——天然幂等,退不回去。 - 顺带修掉一个会 500 的边角:押金已退还/已没收时
ensureConsignmentPublishDepositFrozenBySubmissionWithClient会撞"每提报单只允许一条冻结流水"的唯一索引,现改为显式抛consignment_deposit_already_settled(409),由运营决定重新提报或人工处理。
- 打手履约押金:沿用既有的
- ⏳ 待你拍板:客服一直不点「确认加好友」时订单会无限期停住(押金也一直冻着), 目前没有超时提醒或自动策略;另外没收后同一现货再被匹配、卖家要不要重新补押金,需要业务规则。
批次 E · 权限门控与运营体验(P1/P2)—— 🔶 部分完成(本轮)
- ✅ 前端四权限门控(P1-8 剩余部分):
- 新增
resolveAdminPermission(sessionPermissions, code)(utils/admin-auth.ts):会话接口的最新权限列表优先, 接口异常时才回退登录时的本地快照;空数组也判定为无权限,不回退(管理员刚收回权限的场景)。 - 寄售仓库面板:
consignment.manage控制"手动入库 / 编辑 / 作废 / 退单回库 / 匹配开关"(无权限时开关禁用并给 tooltip 说明)。 - 寄卖提报面板:
consignment.review控制"审核入库 / 驳回"。 AdminWorkerPlatformPage:无consignment.view时不渲染"寄售仓库 / 寄卖提报审核"两个 tab, 并把 URL 或 localStorage 里的旧选择拉回工单页(否则会落在空白 tab 上)。
- 新增
- ✅
allocation_status收口(P2-19 剩余):迁移080先把历史非法值归位到「待安排」(备注留痕)再加CHECK (allocation_status IN ('待安排','已安排'));编辑接口新增拒绝allocationStatus(400consignment_allocation_status_update_forbidden),与"编辑接口不可改 status"同口径。顺带修正 077 遗留的 "收集链接/玩家提交"权限描述文案(该渠道已在 079 下线)。 - ✅ 体验项(部分):两个面板加
isError错误态(含重试按钮);审核通过/驳回弹窗加confirmLoading与 提交中守卫(防双提交);待审徽标改用后端真实总数(原来只统计当前页,翻页就变);编辑弹窗押金不再兜底¥100(会把"无押金"悄悄写成 100 元);履约拦截弹窗的冷静期文案不再硬编码"3 天",改为"按商品规则设定(未配置时 72 小时)"。 - ✅ 核实并撤回一条误报:原审计提到"倒计时时区偏移"——
deadlineAt由 node-pg 解析timestamptz后 以带Z的 ISO 字符串下发,前端new Date(deadlineAt)无偏移问题,已用真实库数据确认,不需要改。 - ⏳ 未做:
- 报表补齐(P2-7:节省赏金 / 出库单数 / 批次回款);
- 搜索字段后端不支持(面板承诺按联系方式/游戏 ID 搜索,后端只搜部分字段);
- 打手端"按账号状态隐藏寄卖入口":后端
POST /consignment/publish已强制requireActiveWorker, 前端要隐藏入口需要/worker/me暴露账号状态字段,本次未动接口。
六、策略问题(原待确认项 → 已定案)
- 撤销边界:✅ 定案 —— 已验收一律禁止撤销,历史数据不做兼容(用户:"这是测试阶段,不用管历史记录")。买家退款/纠纷走售后流程;若日后确有"验收后退款"场景,另开"售后回收库存"流程(回库或作废 + 押金重算)。
worker_id拆分:✅ 定案 —— 直接新增assigned_worker_id并让worker_id固定为卖家(迁移079,无兼容期,不回填历史)。- 押金策略:⏳ 未定 ——
pending_review超期自动驳回退押金的时限(如 7 天)、以及打手自助撤回是否允许,涉及运营对账口径,需业务确认。 - 批次顺序:✅ 定案 —— A(入口可用)→ B(资金安全)→ C/D(资金与货物一致性、防错发)已完成;E(权限门控与运营体验)本轮完成权限与约束部分,报表/搜索/打手端入口为剩余项。