fix(consignment): 下线 H5 收集链接渠道并封闭寄卖履约状态机
下线定向收集链接(H5 填报)整条线,提报统一走接单平台打手端: - 删除前端 ConsignmentCollectPage、consignment-public 服务与 /consignment-collect/:token 路由 - 删除后端公开路由 routes/consignment.ts、consignment-public-service 与管理端 /consignment/links 三接口 - 迁移 079 删表 consignment_collect_links、删列 consignment_submissions.link_id, source_type 的 collection_link 归并为 worker_portal 并替换 CHECK 修复手工入库/编辑的凭据与截图静默丢失:后端统一按 credentialsJson ?? credentials、 screenshotsJson ?? screenshots 读取(入库/编辑/审核入库三个入口)。 按"已验收一律禁止撤销"封闭状态机: - 撤销寄售履约加状态白名单(in_progress|pending_acceptance|problem)+ 事务内 FOR UPDATE 锁单 + UPDATE 带 status 条件(0 行 409)+ 原因必填 + 清空指派/时限/验收草稿痕迹 - 库存改状态一律带条件:撤销要求 bound_work_order_id 命中本工单;作废/回库要求未被 进行中工单占用(被挡时提示具体工单号);079 加部分唯一索引并清理历史重复绑定 - 工单终止统一解绑回库:打手撤单/取消指派/退回大厅/撤单/打手退单申请/超时处置接入 releaseConsignmentInventoryBindingForWorkOrderWithClient(售后不改工单状态,无需接入) - 回库路径保留运营的 allow_matching 开关设置(不再强制开启),前端文案同步更正 顺带修掉资金与归属的同源缺陷: - 押金归属与重冻金额改取不可变的冻结核销行,审核改派不再"退给错人/向错人扣款" - 新增 assigned_worker_id 拆开"卖家"与"被指派履约人",占库不再覆盖卖家 - PUT /inventories/:id 禁止直接改 status;发布接口补 requireActiveWorker - 押金冻结/释放后补推钱包实时事件;迁移 079 给发布冻结加台账幂等唯一索引 验证:后端 401 测试/398 通过/3 skip(新增 5 例回归),前端 16 通过; 两侧 typecheck、lint:check、format:check 与 check:sql、build 全绿。 迁移 079 未经真实数据库执行(本环境无 Postgres),上库前请先 dry-run。
This commit is contained in:
+25
-13
@@ -61,7 +61,10 @@
|
||||
|
||||
## 三、 三大核心业务场景与操作流
|
||||
|
||||
### 1. 定向收集链接生成与用户填报(自动化收货渠道)
|
||||
### 1. ~~定向收集链接生成与用户填报(自动化收货渠道)~~ —— **已下线**
|
||||
> 2026-09 起:寄卖提报统一走**接单平台打手端提报**(`/api/v1/worker/consignment/publish`),
|
||||
> 对外收集链接(H5 填报页、`consignment_collect_links`、`consignment_submissions.link_id`)
|
||||
> 已整条移除,详见迁移 `079_drop_consignment_collect_links.sql`。本节保留仅作历史记录。
|
||||
|
||||
```
|
||||
[客服后台] ──► 点击【新建收集链接】
|
||||
@@ -169,25 +172,35 @@
|
||||
```
|
||||
|
||||
### 1. 工单视角:【撤销寄售履约 / 重新选择】
|
||||
在工单列表或工单详情页,对已通过内部库存履约的工单(状态为 `completed` 且带有寄售出库标记),提供 **【撤销履约】** 操作:
|
||||
在工单列表或工单详情页,对「已通过内部库存履约、且**尚未验收**」的工单(状态为 `in_progress` / `pending_acceptance` / `problem`)提供 **【撤销履约】** 操作:
|
||||
* **前置约束(2027-02-21 加固)**:已验收(`accepted`)与已取消(`cancelled`)的工单**一律禁止撤销** —— 钱已结算、货已交付,撤销会造成同一件现货二次出货并重新冻结已退还的押金;买家退款/纠纷改走售后流程。服务端在事务内 `FOR UPDATE` 重新读单并校验状态白名单,撤销原因必填,前端按钮同样按状态 + `consignment.fulfill` 权限渲染。
|
||||
* **分支 A:【回退为未分配(重新选库存 或 发大厅)】**
|
||||
* **原库存处理**:状态从 `consumed` 自动恢复为 `in_stock`,清除绑定的订单号与售出店铺,重新进入待匹配池。
|
||||
* **工单处理**:状态从 `completed` 安全回退为 `unassigned`(未分配),清理本次发货卡密记录。
|
||||
* **原库存处理**:状态从 `consumed` 自动恢复为 `in_stock`,清除绑定的订单号与售出店铺,重新进入待匹配池(**仅当该行仍绑定本工单时**才允许改写,否则 409)。
|
||||
* **工单处理**:状态安全回退为 `unassigned`(未分配),清空指派人、时限、验收草稿与本次发货记录。
|
||||
* **操作结果**:客服可以立即再次点击「发布并核销」选择其他匹配库存,或者直接发布到大厅由打手抢单!
|
||||
* **分支 B:【买家申请退款(取消工单并退库)】**
|
||||
* **原库存处理**:安全解绑并自动回退为在库 `in_stock`,避免资产报废损失。
|
||||
* **工单处理**:状态流转为 `cancelled`(已取消),不再继续履约。
|
||||
* **分支 C:【物资账号异常(作废该库存并重新选)】**
|
||||
* **原库存处理**:该把手枪/账号密码失效或被封,直接变更为 `archived`(已作废),记录作废原因,不再参与匹配。
|
||||
* **原库存处理**:该把手枪/账号密码失效或被封,直接变更为 `archived`(已作废),记录作废原因,不再参与匹配,并释放发布押金。
|
||||
* **工单处理**:工单回退为 `unassigned`,供客服挑选另一把正常手枪继续履约。
|
||||
|
||||
### 2. 仓库视角:【退回复库 / 重新上架】
|
||||
在「寄售仓库」管理列表中,对状态为 `consumed`(已出库)的物品行,提供 **【退回复库】** 按钮:
|
||||
* 客服可手动将某件出库的物品解绑原订单,直接重置为 `in_stock` 在库待匹配状态。
|
||||
* 适合线下沟通中买家临时更换需求、跑单或线下对账异常时的人工修正。
|
||||
* 客服可手动将某件出库的物品解绑原订单,直接重置为 `in_stock` 在库待匹配状态;适合线下沟通中买家临时更换需求、跑单或线下对账异常时的人工修正。
|
||||
* **前置约束(2027-02-21 加固)**:若该物品仍被一张「进行中的工单」占用(`bound_work_order_id` 指向的工单状态不是 `accepted`/`cancelled`),【退回复库】与【下架作废】都会被拒绝,并提示先撤销履约或撤单 —— 避免把别的工单正在交付的现货抢走。
|
||||
* **匹配开关语义**:回库**不再**强制打开 `allow_matching`,而是保留运营的手动设置(该开关是"暂停匹配"的运营闸门,自动回库不应替运营做决定)。编辑接口也不再接受直接改 `status`,状态只能经由带守卫的动作变更。
|
||||
|
||||
### 2.1 工单终止时的自动解绑(2027-02-21 新增)
|
||||
工单进入「不再交付」的状态时,会在**同一事务内**把占用的现货放回在库池(`releaseConsignmentInventoryBindingForWorkOrderWithClient`),不再依赖运营手工退库:
|
||||
* 覆盖路径:打手撤单(`cancelled_by_worker`)、后台取消指派(`unassigned_by_admin`)、后台退回大厅(`returned_to_hall_by_admin`)、后台撤单取消订单(`cancelled_by_admin`)、打手退单申请审核通过、超时处置(`timeout_reopen` / `timeout_cancel_release` / `timeout_cancel_deduct`)、已取消工单重新启用(兜底清理历史绑定)。
|
||||
* **售后处置不在其列**:售后(`resolveAdminAfterSalesCase`)只对**已验收**工单做资金追缴/退赔,工单状态保持 `accepted`,货物已交付,无需解绑。若买家退回实物,由运营在寄售仓库执行【退回复库】(已验收工单不再是"进行中",守卫会放行)并填写原因。
|
||||
* 发布押金保持冻结:现货回到在库池等同于"重新挂卖",符合「发布冻结、驳回/验收释放」的既有口径;卖家要彻底取回物品,由运营执行【下架作废】后释放押金。
|
||||
|
||||
### 3. 数据安全与事务保障
|
||||
* 撤单过程全部置于数据库事务(`withTransaction`)中,确保库存状态与工单状态原子同步,绝不会出现“工单回退了但库存未恢复”的错乱。
|
||||
* 撤销时事务内对工单行 `SELECT ... FOR UPDATE`,库存 UPDATE 带 `status = 'consumed' AND bound_work_order_id = $工单` 条件,命中 0 行即 409,防止与「验收」并发时丢失更新或抢占其他工单的绑定。
|
||||
* 一件现货同一时刻最多绑定一张工单:迁移 `079` 增加部分唯一索引 `uniq_consignment_inventories_bound_work_order` 兜底。
|
||||
* 每次撤单操作均在工单事件(`work_order_events`)中留下完整审计轨迹(记录撤销人、撤回时间、原绑定单号与原因)。
|
||||
|
||||
---
|
||||
@@ -232,9 +245,8 @@
|
||||
* 弹出确认弹窗,选择“选错重选 / 买家退款 / 物资作废”;
|
||||
* 点击确认后原子回退状态,工单即刻变回未分配状态,支持重新选或发大厅。
|
||||
|
||||
### 3. 客服创建收集链接弹窗(`CreateCollectLinkModal`)
|
||||
* 表单包含系统、区服、系列、钻级、璀璨下拉选择,支持输入协商收货价与批次号。
|
||||
* 生成后提供链接复制与二维码展示。
|
||||
### 3. ~~客服创建收集链接弹窗(`CreateCollectLinkModal`)~~ —— **已下线**
|
||||
> 随收集链接渠道一并移除,客服不再生成对外链接;提报入口为接单平台打手端页面。
|
||||
|
||||
### 4. 寄卖提报审核面板(`ConsignmentSubmissionsPanel`)
|
||||
* 展示卖家提交的待审单据。
|
||||
@@ -254,8 +266,8 @@
|
||||
* **实现工单撤销履约与库存回退重选服务(`revokeConsignmentFulfillment`)**。
|
||||
3. **阶段三:前端管理后台**
|
||||
* 接单平台新增「寄售仓库」面板(包含多维筛选、彩色标签、Switch 开关、卡密查看、退回复库);
|
||||
* 新增「寄售审核与收集链接」面板;
|
||||
* 新增「寄售审核」面板(提报来源为接单平台打手端);
|
||||
* 改造工单发布拦截与履约弹窗;
|
||||
* 增加工单列表/详情中的「撤销寄售履约」弹窗与重选逻辑。
|
||||
4. **阶段四:移动端 H5 填报端**
|
||||
* 实现移动端极简收集页,规格只读展示,支持截图直传。
|
||||
4. **阶段四:~~移动端 H5 填报端~~ —— 已下线**
|
||||
* 原设计为移动端极简收集页;实际改为接单平台打手端提报,H5 页面已删除。
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
# 寄卖(寄存)系统 · 现状分析与下一步优化方案
|
||||
|
||||
> 基线:`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 个回归测试 |
|
||||
| *(工作区未提交)* | 本轮:H5 渠道整体下线 + 凭据字段修复 + 状态机封闭(撤销守卫 / 库存绑定唯一性 / 生命周期解绑)+ 押金归属取台账 | 迁移 `079`、`consignment-*` 五个文件、`work-order-*` 三个仓库、两个前端面板 |
|
||||
|
||||
**结论**:功能主干 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` |
|
||||
|
||||
**一处刻意偏离原始批次 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. claim + assign + material/reward + event 合并事务,失败改补偿式撤销(P1-2)。
|
||||
2. 凭证核销并入状态变更事务或显式失败(P1-5)。
|
||||
3. 押金没收路径(P2-2 的 `deposit_deduction` 死分支):补"卖家违约扣没发布押金"或移除该死分支。
|
||||
4. 编辑接口的 `depositAmountFen` 与台账一致性校验(审核改额的剩余风险)。
|
||||
|
||||
### 批次 D · 匹配准确性与防错发(P1)
|
||||
1. 空规格不放行 + 钻级不再兜底 1(P1-6)。
|
||||
2. 履约事务内复检规格,非 exact 一律 409。
|
||||
3. 匹配下推 SQL + 批量匹配接口 + 工单列表角标(P2-8)。
|
||||
|
||||
### 批次 E · 权限门控与运营体验(P1/P2)
|
||||
1. 前端四权限门控(P1-8 剩余部分:库存/提报面板尚无 `consignment.view/manage/review` 门控);打手端菜单收口(P1-9 剩余)。
|
||||
2. 报表按文档 §六补齐节省赏金等指标(P2-7);`allocation_status` CHECK 约束(P2-19 剩余)。
|
||||
3. 面板错误态、双提交、徽标、搜索、押金回显、文案/时区等体验项(P2-1~P2-18)。
|
||||
|
||||
---
|
||||
|
||||
## 六、策略问题(原待确认项 → 已定案)
|
||||
|
||||
1. **撤销边界**:✅ 定案 —— **已验收一律禁止撤销**,历史数据不做兼容(用户:"这是测试阶段,不用管历史记录")。买家退款/纠纷走售后流程;若日后确有"验收后退款"场景,另开"售后回收库存"流程(回库或作废 + 押金重算)。
|
||||
2. **`worker_id` 拆分**:✅ 定案 —— 直接新增 `assigned_worker_id` 并让 `worker_id` 固定为卖家(迁移 `079`,无兼容期,不回填历史)。
|
||||
3. **押金策略**:⏳ 未定 —— `pending_review` 超期自动驳回退押金的时限(如 7 天)、以及打手自助撤回是否允许,涉及运营对账口径,需业务确认。
|
||||
4. **批次顺序**:✅ 定案 —— 先 A(入口可用)→ 再 B(资金安全);两者本轮均已落地,C/D/E 待排期。
|
||||
Reference in New Issue
Block a user