Files
order_site/docs/寄卖系统优化方案.md
T
yml2213 54f8a27e97 fix(consignment): 匹配弹窗改用寄卖单号,客服界面不再展示内部仓库号
问题:内部寄售库存匹配弹窗显示的是「寄卖单号 INV26091711253394」——候选现货走的
是另一个查询 listActiveConsignmentInventoriesForMatching,它没 join 提报单,
sourceSubmissionNo 取不到,前端就回退显示内部仓库号;客服拿这个号去寄卖仓库又搜不到。

- 匹配候选查询补 LEFT JOIN consignment_submissions 带出 source_submission_no;
  该查询原本是 SELECT * 且列名无前缀,而 consignment_submissions 同样有
  status/game_name/category_id,加 join 会让条件变成 ambiguous column 直接 500,
  故列名统一加 ci. 前缀并显式列字段(加静态守卫测试钉住)
- 客服界面只展示寄卖单号:匹配弹窗候选卡片、工单寄售标识、工单详情、寄卖仓库列表与详情
  全部去掉内部仓库号;手工入库现货写「客服手工入库(无提报单号)」,
  历史老单写「老单无提报单号」,不再抛内部编号
- 内部仓库号保留:数据/接口/按它搜索都还在,撤销解绑等链路未动

真实库验证:仓库层候选查询与 checkWorkOrderInventoryMatches 都带出了寄卖单号。临时数据已清理。
2026-09-17 11:31:01 +08:00

53 KiB
Raw Blame History

寄卖(寄存)系统 · 现状分析与下一步优化方案

基线: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
本批提交(批次 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 行 → 409 consignment_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 行 → 409 work_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(400 consignment_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 · 运营体验 / 合规 / 性能(按影响排序)

  1. 搜索骗人:ConsignmentInventoryPanel.tsx:678 占位符承诺"物品名/编号/收购人/店铺/卖家",后端 ILIKE 只覆盖部分字段(consignment-repo.ts:184-189)→ 按收购人/店铺搜恒空。
  2. 统计每次刷新跑 5 条全表聚合(consignment-repo.ts:215-303),且列表无排序/无列筛选。
  3. 凭据无独立权限:mapConsignmentInventory 直接返回 credentialsJson(游戏账号/密码,consignment-admin-service.ts:77-80),列表接口全量下发,仅需 consignment.view。
  4. is_multi_use=false 从未生效:字段只落库(consignment-repo.ts:788),submitPublicConsignment 全文不读 ⇒ 单次链接可无限提报。✅ 随渠道下线废弃
  5. 公开提交按手机号自动绑定打手(consignment-public-service.ts:113-124)⇒ 知道打手手机号的第三方可让其被动冻结押金。✅ 随渠道下线废弃(现只有打手端本人发布)
  6. 押金可能长期冻结无人收口:pending_review 无过期策略;打手端只有 publish + my-items(routes/worker/consignment.ts),没有"撤回我的提报"。⏳ 未处理(本轮已补:冻结归属取台账,运营手动驳回即可释放)
  7. 统计报表与设计文档不符:文档 §六/§七 的【累计节省赏金】【本月出库单数】【店铺节省赏金/人效】【批次回款】均未实现;getConsignmentInventoryStats 只返回数量与成本 + 4 个 GROUP BY,前端指标卡也只 4 项(ConsignmentInventoryPanel.tsx:559-587)。
  8. 列表无"匹配内部库存"角标 + 匹配懒加载 + 全表入内存:checkWorkOrderConsignmentMatches 仅在点发布时调用(WorkOrdersPanel.tsx:700),且异常被空 catch 吞掉(:710-712,缺 view 权限时拦截永不出现);listActiveConsignmentInventoriesForMatching(consignment-repo.ts:683-707)全表读内存再 JS 匹配。
  9. 待审徽标只统计当前页(ConsignmentSubmissionsPanel.tsx:85 + :316)。
  10. 编辑无法解除打手关联:ConsignmentInventoryPanel.tsx:260 的 workerId || undefined 撞后端 !== undefined 判定(consignment-admin-service.ts:316-321)。
  11. 编辑弹窗押金被静默改成 ¥100(ConsignmentInventoryPanel.tsx:513),而该值会被"回库重新冻结"按金额使用(consignment-deposit-repo.ts:242-251)。
  12. 冷静期文案硬编码"默认 3 天"(WorkOrdersPanel.tsx:2852-2853),与产品规则(work-order-gift-repo.ts:101-105)及同页自算的 cooldownLabel(:1040-1042)自相矛盾。
  13. 冷静期倒计时时区错位:后端把时间统一格式化为 Asia/Shanghai 无时区串(utils/http.ts:4,212-235),前端按浏览器本地时区 Date.parse(worker-order-utils.tsx:344、DeadlineCountdown.tsx:18)⇒ 非 UTC+8 环境显示偏移 8 小时(真实门禁在 DB,仅显示错)。
  14. 接口失败静默成 0/空:两个面板只看 isLoading 无 isError(ConsignmentInventoryPanel.tsx:129-163、ConsignmentSubmissionsPanel.tsx:66-85);打手下拉(:117-120)失败后指派按钮消失。
  15. 批量入库最多 50 次串行 POST,无回滚无 loading(ConsignmentInventoryPanel.tsx:191-222),失败重试会重复建单(无幂等键)。
  16. 破坏性弹窗缺 confirmLoading → 双提交(:1235-1242、CSP:361-369/518-525)。
  17. 两条回库路径 allow_matching 语义分叉:consignment-repo.ts:657 强制 TRUE,撤销回库保留原值,但两处 UI 文案都宣称"重新进入待匹配池"。✅ 已统一(本轮):所有回库路径一律保留运营的开关设置,UI 文案同步更正(决策理由见本节开头的"刻意偏离"说明)。
  18. 类型/契约漂移:services/consignment-public.ts:5-11 声明 ConsignmentSubmission(文件已删除);types/consignment.ts 与后端返回结构不符、services/worker.ts:375-391 缺 depositAmountFen。⏳ 部分未处理(本轮已将 sourceType 收敛为 manual|worker_portal|recycle、新增 assignedWorkerId)。
  19. 契约约束缺失:allocation_status 中文自由文本无 CHECK(077_consignment_inventory.sql:27);指派占库完全不写 bound_work_order_id(consignment-repo.ts:552-572)。✅ 占库已写绑定(本轮);allocation_status 的 CHECK 约束仍未加。
  20. 小项: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.ts claim 函数),回滚带 WHERE status='consumed'(有测试覆盖)。
  • 押金账务并发安全:先锁钱包 FOR UPDATE 再读流水,缺口时拒绝部分退款,重复冻结幂等(consignment-deposit-repo.ts;本轮另加"归属/金额以冻结核销行为准"与部分唯一索引)。
  • 审核入库/驳回原子且带状态条件。
  • 押金履约模式禁止走撤销(resolveRevocableConsignmentInventoryId),且该模式下的库存已改为按工单绑定,撤单时由工单终态链路自动解绑。
  • RBAC 权限链正确:四权限已在 077 注册并授予 admin/operator/support,requireAdminPermission 对 admin 直通;19 个接口迁移无遗漏,没有写接口只挂 view。
  • 上传有服务端大小/类型限制(routes/file-upload.ts 10MB + magic-byte 白名单)。
  • 订单级押金与寄卖发布押金互不误伤:前者按 related_work_order_id 匹配,后者 related_work_order_id IS NULL 且按 payload_json.consignmentSubmissionId 匹配。
  • 手工钱包调整只写 available_amount,不减少 frozen_deposit_amount,不破坏冻结池不变量。

五、优化路线(分批,每批可独立发布)

批次 A · 修复两条断掉的入口链路 —— ✅ 已完成(性质已变)

  1. H5 上传返回值取 data.file → H5 渠道整体下线(表、路由、页面、服务、link_id 列一并删除;迁移 079),提报统一走接单平台打手端。
  2. ✅ 手工入库/编辑兼容 credentialsJson/screenshotsJson,三处入口统一走 pickCredentialsJson / pickScreenshotsJson。
  3. 验收:npm test 401 例(398 通过 / 3 skip)、typecheck/lint:check/format:check/check:sql 全绿。

批次 B · 状态机封闭与库存绑定唯一性 —— ✅ 已完成

  1. ✅ 撤销:FOR UPDATE 锁单 + 状态白名单(in_progress|pending_acceptance|problem)+ UPDATE ... AND status=$n + 原因必填 + 前端按状态与 consignment.fulfill 权限渲染。
  2. ✅ 库存绑定收口:撤销加 bound_work_order_id = $工单;退回复库/作废加"未被进行中工单占用"守卫;0 行 409 且给出具体工单号;079 加部分唯一索引兜底并清理历史重复绑定。
  3. ✅ 生命周期收口:releaseConsignmentInventoryBindingForWorkOrderWithClient 接入撤单仓库 5 条链路 + 打手退单申请 + 超时处置(重开/取消)。
  4. ✅ 回归测试 5 例(占库绑定、状态白名单、解绑范围、作废/回库守卫、终态链路接线)。
  5. 验收偏差:原始表述"回库路径一律恢复 allow_matching"改为"保留运营开关设置",理由见第三节"刻意偏离"。

批次 C · 指派事务化、资金/凭证一致性(P1)—— ✅ 已完成(本轮)

  1. ✅ 指派"部分成功"窗口(P1-2):指派失败不再只把现货放回在库池,改为 compensateFailedConsignmentAssignment 三步补偿 —— ① 指派事务若已提交(工单 in_progress + 已指派打手)走管理端「取消指派」:解冻打手押金、工单退回大厅,并顺带解绑现货; ② 兜底释放库存占用(指派未提交时的主路径); ③ 摘掉工单上的 material.consignment 并推入 consignmentHistory。 每步独立 try:补偿失败不掩盖原始错误,但会留下备注/解绑痕迹。
  2. ✅ 凭证核销不再假成功(P1-5):核销需调用快手外部接口,无法与数据库事务合并回滚,因此改为"显式失败 + 留痕 + 如实反馈"—— 核销失败写事件 consignment_fulfill_voucher_consume_failed(对齐既有发布链路 publish_voucher_consume_failed 口径), 返回 voucherConsume.ok=false;前端 notifyVoucherConsume 改为 message.warning(含失败原因与"请人工核销"),不再无条件弹成功。 指派链路的凭证失败本来就由 assignAdminWorkOrderToWorker 抛 409 + 写 assign_voucher_consume_failed(失败即回滚,不产生部分状态)。
  3. ⏳ 押金没收(P2-2 死分支)与编辑接口改额的台账一致性校验仍未做:前者需要业务先定"卖家违约扣没"规则。
  4. 意外收获(必修):迁移 079 的语句顺序有 bug —— 旧 source_type CHECK 只允许 manual/collection_link/recycle, 而回填 worker_portal 写在删旧约束之前,导致任何全新库在 079 必然失败(本地 dev 库就是这样启动失败的)。 已改为"先删旧约束 → 回填 → 挂新约束",并在全新空库验证 001 → 079 一次性跑通(79 条迁移全部记录)。 因为 079 尚未发布(所在提交仅本地未推送),修复按仓库 check:sql 守卫要求合入引入它的那个提交,而不是新增补救迁移。

批次 D · 匹配准确性与防错发(P1)—— ✅ 已完成(本轮)

  1. ✅ 空规格不再放行、钻级不再兜底 1(P1-6):sameSpec(任一侧为空即视为相等)改为 compareSpec 三态判定 (match / mismatch / unknown),extractPeacekeeperSpecs 的钻级兜底由 || 1 改为 || 0(未知)。 规则:系统/区服/系列冲突 → 直接排除;订单侧没写清楚的字段计入 unknownFields,只要有未知项就绝不判"精确匹配", 降级为 partial 并在 reason 里列出缺失项(如"订单规格信息不全(未写: 系统、区服、钻级),需人工确认")。
  2. ✅ 履约/指派服务端复检:新增 assertConsignmentSpecMatchedOrder,两条路径(内部现货履约、指派打手)在占库前都要过: 规格冲突 → 409 consignment_spec_mismatch;非 exact 且未显式传 allowPartial → 409 consignment_spec_partial_requires_confirm。 前端在"部分匹配"候选上先 modal.confirm 危险确认,再带 allowPartial: true 重试,高配充低配的合法玩法保留但不可误触。
  3. ⏳ 匹配下推 SQL + 批量匹配接口 + 工单列表角标(P2-8)未做:当前仍是"取全量在库 → 内存比对"。

批次 K · 匹配弹窗也改用寄卖单号,内部仓库号只留内部(本轮)

问题:内部寄售库存匹配弹窗(发单时拦一下"有现货可直接履约"那个)显示的是 寄卖单号 INV26091711253394——因为候选现货走的是另一个查询 listActiveConsignmentInventoriesForMatching, 它没 join 提报单,sourceSubmissionNo 取不到,前端就回退显示内部仓库号。 结果是客服在弹窗里看到 INV,拿去寄卖仓库搜索又搜不到,还得自己换算。

改法:

  1. ✅ 匹配候选查询补 LEFT JOIN consignment_submissions 带出 source_submission_no。 注意这个查询原来是 SELECT * + 不带前缀的列名,而 consignment_submissions 同样有 status / game_name / category_id——加 join 会让这些条件变成 ambiguous column 直接 500, 所以列名一律加 ci. 前缀并显式列出字段(已加静态守卫测试钉住这一点)。
  2. ✅ 客服界面只展示寄卖单号:匹配弹窗候选卡片、工单上的寄售标识、工单详情、寄卖仓库列表/详情 全部去掉内部仓库号;客服手工入库的现货明确写「客服手工入库(无提报单号)」, 历史老单写「老单无提报单号」,都不再抛内部编号。
  3. ✅ 内部仓库号 保留:仍在数据里、仍在接口里、按它搜索依然搜得到(后端两个关键字条件都覆盖), 撤销/解绑等链路一行没动。

真实库验证:仓库层候选查询与匹配接口(checkWorkOrderInventoryMatches)在构造数据上都带出了 寄卖单号(VERIFY-MATCH-INV → VERIFY-MATCH-SUB)。临时数据已清理。

批次 J · 凭证链可视化:提报凭证 + 赠送凭证(本轮)

问题:一件寄卖有两类凭证,但客服界面只看得见第一类,而且是 48×48 一张缩略图,看不出是物品还是主页:

  1. 提报凭证 = 卖家提报时的「游戏主页截图」(含物品/仓库详情),存在提报表 screenshots_json。
  2. 赠送凭证 = 履约方冷却结束后上传的赠送完成截图(证明"这件货真送出去了"), 存在绑定工单的验收资料 work_orders.acceptance_json 里。

第一类只在寄卖审核界面可见;第二类只有在工单验收界面才看得到。客服要证明一件寄卖的赠送 是否完成,得自己在两个模块之间翻,且没有任何地方把两类凭证并排呈现。

改法:

  1. ✅ 后端把凭证链从客服两个列表/详情接口一起下发(提报表 → 结果库存 → 绑定工单): submissionScreenshots(提报凭证)、giftEvidenceImages(赠送凭证)、giftEvidenceNote、 giftEvidenceSubmittedAt、giftWorkOrderNo、giftWorkOrderStatus、 giftEvidenceReviewStatus(none 未提交 / pending 待客服验收 / accepted 已验收 / submitted 其他在途)。 赠送凭证的图片从工单验收资料的 imageUrls 与 files[].url|mediumUrl|thumbnailUrl 两处取并去重。
  2. ✅ 提报凭证口径统一:有来源提报单就用提报单的主页截图,客服手工入库的现货回退用库存自己的截图, 前端只需要看 submissionScreenshots 一个字段,不会再拿错。
  3. ✅ 前端新增共享组件 ConsignmentEvidenceGallery(缩略图 + 大图预览 + 共 N 张), 审核面板与仓库面板复用;审核面板原来那个「凭据截图」列拆成 「提报凭证(游戏主页截图)」与「赠送凭证(赠送完成截图,带待验收/已验收 Tag 与关联工单)」两列, 详情抽屉同样分组,并给出 describeGiftEvidence() 的一句话说明。

真实库验证:赠送凭证解析在人工构造的工单验收资料上正确取到 3 张图(imageUrls 1 张 + files 2 张) 并能识别 pending_acceptance → 待客服验收;提报凭证在真实数据上取到 1 张。临时数据已清理。

批次 I · 一个单号锚定 + 统一状态机(本轮)

问题:一件货在系统里有三层互不相通的状态——提报单(SUB…,审核态)、库存(INV…,仓库态)、 工单赠送(material_required / friend_countdown / pending_acceptance / accepted)。 my-items 同时返回两个数组、前端分两个列表渲染,于是同一件货两行、两个号; 更糟的是卖家端只映射了前两套状态,"已出库"之后流程就断在黑暗里, 卖家不知道已经匹配、不知道该加好友、不知道在冷却、不知道该赠送。

改法:

  1. ✅ 提报单号 SUB… 作为唯一「寄卖单号」,从发布锚定到结算(零迁移,历史数据直接可用)。 listWorkerConsignments 改成一个提报单一条记录:库存、绑定工单的信息挂上去, 客服手工入库的现货(没有提报单)才用库存号当锚点。真实库验证:打手 3 的 2 件货从 4 行收敛成 2 行。
  2. ✅ 统一状态机 domain/consignment-item-stage.ts(纯函数、可单测),把三套底层状态推导成 卖家看得懂的一个阶段 + 一句"下一步该做什么": 待审核 → 已通过(在库待匹配)→ 已匹配-待加好友 → 加好友赠送冷却中 → 待赠送 → 已赠送待审核 → 已完成;另有 已驳回 / 已下架作废 / 已取消,以及标准验收单的 履约中 / 待验收。 列表筛选与 KPI 也改由它驱动(待审核 / 履约中 / 已完成)。
  3. ✅ 卖家可以在「我的寄卖」操作(以前只有 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,卖家碰不到别人的单。 顺带白拿了既有的强校验:冷却没结束不许提交凭证、必须带图。
  4. ✅ 客服端同号:库存列表/详情接口补 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 · 修掉"乱弹匹配提醒"与"赏金没换算成元"(本轮)

  1. ✅ 发布时不再乱弹「内部寄售库存匹配提醒」: 根因在 matchPeacekeeperOrderWithInventory——精确匹配判定失败后无条件 fall through 到 部分匹配。而对"订单没写系统/区服/系列/钻级"这类字段,compareSpec 返回 unknown(不是 mismatch)会直接放行,于是任何只写了游戏名的订单都会命中仓库里全部在库货, 表现为"随便发布点什么都会弹一下",提醒失去意义。 现在严格模式加两道门槛:① 系统/区服/系列必须都能确认一致;② 还要有正信号(库存配置更高, 或订单压根没提钻级)。"订单没写规格"不再算命中。
  2. ✅ 人工履约复检不被连带收紧:matchPeacekeeperOrderWithInventory 增加 allowUnspecifiedSpecs 选项。运营已人工选定现货时走宽松模式(标题没写全规格不硬拦, 但真冲突仍然拦),自动提醒永远走严格模式,两个语义互不污染。
  3. ✅ 接单赏金换算成元:WorkOrdersPanel.tsx 里弹窗直接用 rewardAmount(单位是分) 渲染,把 ¥40 显示成 ¥4000。同文件其他 30 余处都是 /100,只有这里漏了。已修正, 并核对弹窗内 内部收购成本 / 履约押金 本来就是 /100(正确)。

批次 G · 现货作废口径 + 押金处置可选(本轮)

  1. ✅ 确认加好友后现货一律作废下架,不再回池(严口径): 确认加好友 意味着买家好友已加上、货随时可能送出去,回池会被下一个工单再次占用 (等于同一件货二次出货)。这条口径收口在寄卖自己的 releaseConsignmentInventoryBindingForWorkOrderWithClient 里:解绑前先读工单的 acceptance_mode / friend_added_at,命中则 status='archived' + allow_matching=FALSE
    • 备注留痕,否则仍按原逻辑 in_stock 回池。所有解绑路径(取消指派/撤单/退回大厅/退单/ 售后/撤销寄售履约)自动继承,不需要各自判断,发单系统的接口一行没动。
  2. ✅ 撤销寄售履约弹窗支持两笔押金各自可选(只加在寄卖专属入口 POST /consignment/revoke 上):
    • 打手履约押金:auto(已确认加好友则扣除,否则全额退还)/ refund / forfeit
    • 卖家发布押金:auto(已确认加好友则没收;否则作废退还、回库保持冻结)/ keep_frozen / forfeit 默认全部 auto = 原行为;覆盖后原因照旧必填,并把选项与金额写进工单事件 (workerDeposit / consignmentDeposit / inventoryArchived)便于对账追责。 策略判定抽成纯函数 domain/consignment-deposit-policy.ts,可单测。
  3. ✅ 修掉一个资金漏洞:撤销寄售履约从不结算打手履约押金。 它以前只清 assigned_worker_id、只处理卖家发布押金,打手的履约押金没有任何出口, worker_deposit(指派打手)单撤销后押金会一直冻在钱包里。现在撤销时按上面的 选项调用 settleWorkOrderDepositWithClient 结算(退还或没收,金额取冻结台账与钱包冻结额的较小值)。
  4. ✅ 修掉一个真实库必炸的 500(42804):detachConsignmentMaterialFromWorkOrderWithClient 里 $1 既作 $1::text(写进 jsonb 的 detachedAt)又赋值给 updated_at,Postgres 把它 整体推断成 text,赋值给 timestamptz 列直接报错。凡解绑现货时摘寄售标记的路径 (取消指派/撤单/退单/售后/撤销履约/人工回库)在真实库上全都会炸,而假 client 的单测不跑 真 SQL,一直没暴露。已显式 $1::timestamptz,并补了一道类级静态护栏 (sql-param-type-safety.test.ts)扫描同类"占位符既当 text 又当时间戳"的写法。
  5. ⏳ 待你拍板:撤销寄售履约 的状态白名单仍含 pending_acceptance(打手已提交赠送凭证)。 未确认加好友的单子在这一步撤销仍会回池——如果这类单也可能已经交付,需要一并收紧。

批次 F · 待加好友流程与押金不退(本轮)

  1. ✅ 内部现货履约不再自动进冷却:新增 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 开关,避免再出现"自动确认加好友"的旁路。
  2. ✅ 前端文案:内部现货单在该阶段显示「待加好友」(打手线仍是「待打手提交资料」), 确认按钮统一为「确认加好友」;阶段文案抽到 utils/gift-phase.ts 便于单测。
  3. ✅ 后台备注快捷项:「已通知加好友」一键追加「[已通知加好友] 操作人 时间」,仍可手动补充(复用 admin_note)。
  4. ✅ 确认加好友后押金不退(P2-2 deposit_deduction 死分支落地):
    • 打手履约押金:沿用既有的 isWorkOrderGiftDepositForfeitable(friend_added_at 有值即不退);
    • 寄卖卖家发布押金同口径新增:forfeitConsignmentPublishDepositBySubmissionWithClient / ...ByWorkOrderWithClient,写一条负数 deposit_deduction 流水(可用余额不动、只扣冻结额), 挂在 6 处取消/退单/脱离判定点 + 「撤销寄售履约」的作废/回库分支上。 因为台账 outstanding 会把 deposit_deduction 计入已核销,之后同一提报单的 release / 重新冻结都拿不到钱——天然幂等,退不回去。
    • 顺带修掉一个会 500 的边角:押金已退还/已没收时 ensureConsignmentPublishDepositFrozenBySubmissionWithClient 会撞"每提报单只允许一条冻结流水"的唯一索引,现改为显式抛 consignment_deposit_already_settled(409),由运营决定重新提报或人工处理。
  5. ⏳ 待你拍板:客服一直不点「确认加好友」时订单会无限期停住(押金也一直冻着), 目前没有超时提醒或自动策略;另外没收后同一现货再被匹配、卖家要不要重新补押金,需要业务规则。

批次 E · 权限门控与运营体验(P1/P2)—— 🔶 部分完成(本轮)

  1. ✅ 前端四权限门控(P1-8 剩余部分):
    • 新增 resolveAdminPermission(sessionPermissions, code)(utils/admin-auth.ts):会话接口的最新权限列表优先, 接口异常时才回退登录时的本地快照;空数组也判定为无权限,不回退(管理员刚收回权限的场景)。
    • 寄售仓库面板:consignment.manage 控制"手动入库 / 编辑 / 作废 / 退单回库 / 匹配开关"(无权限时开关禁用并给 tooltip 说明)。
    • 寄卖提报面板:consignment.review 控制"审核入库 / 驳回"。
    • AdminWorkerPlatformPage:无 consignment.view 时不渲染"寄售仓库 / 寄卖提报审核"两个 tab, 并把 URL 或 localStorage 里的旧选择拉回工单页(否则会落在空白 tab 上)。
  2. ✅ allocation_status 收口(P2-19 剩余):迁移 080 先把历史非法值归位到「待安排」(备注留痕)再加 CHECK (allocation_status IN ('待安排','已安排'));编辑接口新增拒绝 allocationStatus(400 consignment_allocation_status_update_forbidden),与"编辑接口不可改 status"同口径。顺带修正 077 遗留的 "收集链接/玩家提交"权限描述文案(该渠道已在 079 下线)。
  3. ✅ 体验项(部分):两个面板加 isError 错误态(含重试按钮);审核通过/驳回弹窗加 confirmLoading 与 提交中守卫(防双提交);待审徽标改用后端真实总数(原来只统计当前页,翻页就变);编辑弹窗押金不再兜底 ¥100 (会把"无押金"悄悄写成 100 元);履约拦截弹窗的冷静期文案不再硬编码"3 天",改为"按商品规则设定(未配置时 72 小时)"。
  4. ✅ 核实并撤回一条误报:原审计提到"倒计时时区偏移"——deadlineAt 由 node-pg 解析 timestamptz 后 以带 Z 的 ISO 字符串下发,前端 new Date(deadlineAt) 无偏移问题,已用真实库数据确认,不需要改。
  5. ⏳ 未做:
    • 报表补齐(P2-7:节省赏金 / 出库单数 / 批次回款);
    • 搜索字段后端不支持(面板承诺按联系方式/游戏 ID 搜索,后端只搜部分字段);
    • 打手端"按账号状态隐藏寄卖入口":后端 POST /consignment/publish 已强制 requireActiveWorker, 前端要隐藏入口需要 /worker/me 暴露账号状态字段,本次未动接口。

六、策略问题(原待确认项 → 已定案)

  1. 撤销边界:✅ 定案 —— 已验收一律禁止撤销,历史数据不做兼容(用户:"这是测试阶段,不用管历史记录")。买家退款/纠纷走售后流程;若日后确有"验收后退款"场景,另开"售后回收库存"流程(回库或作废 + 押金重算)。
  2. worker_id 拆分:✅ 定案 —— 直接新增 assigned_worker_id 并让 worker_id 固定为卖家(迁移 079,无兼容期,不回填历史)。
  3. 押金策略:⏳ 未定 —— pending_review 超期自动驳回退押金的时限(如 7 天)、以及打手自助撤回是否允许,涉及运营对账口径,需业务确认。
  4. 批次顺序:✅ 定案 —— A(入口可用)→ B(资金安全)→ C/D(资金与货物一致性、防错发)已完成;E(权限门控与运营体验)本轮完成权限与约束部分,报表/搜索/打手端入口为剩余项。