fix(consignment): 补齐指派补偿与规格复检,封闭寄卖资金/货物不一致窗口
- 指派失败改为完整补偿:取消指派(解冻打手押金 + 工单退回大厅 + 解绑现货)→ 兜底释放占用 → 摘除工单寄售标记。此前只把现货放回在库池,一旦"指派已提交、写 material 失败",工单会停在 in_progress + 押金冻结却不带寄售记录,现货还回到可匹配池,同一件货会被第二张单占用。 - 电子凭证核销失败写 consignment_fulfill_voucher_consume_failed 事件,前端按 voucherConsume.ok 弹警告并提示人工核销,不再无条件报成功(核销走快手外部接口,无法与数据库事务合并回滚)。 - 匹配规格改为 match/mismatch/unknown 三态,钻级兜底由 || 1 改 || 0:订单规格信息不全一律不进 "精确匹配",降级 partial 并在原因里列出缺失字段,消除"任意现货都 100 分精确匹配"的错发根因。 - 履约与指派在占库前做服务端规格复检:冲突 409 consignment_spec_mismatch;非 exact 必须显式 传 allowPartial(前端在部分匹配候选上先危险确认),高配充低配保留但不可误触。 - 人工解绑守卫由"状态黑名单"收窄为只拦交付中三态(与撤销白名单对齐),修复 "工单未开始 + 现货仍绑定"没有任何人工出口的问题。 - 新增 detachConsignmentMaterialFromWorkOrderWithClient,解绑回库/人工退回复库/作废同步摘掉 工单寄售标记,消除"已回库却仍显示寄卖履约"的陈旧痕迹。 - 回归测试新增 8 例(后端 410 例 / 407 通过),附迁移 079 上库前检查脚本。
This commit is contained in:
+40
-11
@@ -18,7 +18,8 @@
|
||||
| `fd1eb581` | 内部现货占库进入赠送冷静期;发布押金冻结/驳回退还/验收释放闭环 | `consignment-fulfillment-service`、`work-order-gift-repo`、结算联动 |
|
||||
| `6b940796` | 样式修复(cooldownMinutes prefer-const) | — |
|
||||
| `e828e677` | 权限打通(19 接口改 `requireAdminPermission`)、指派占库改条件更新、撤销守卫 | 新增 4 个回归测试 |
|
||||
| *(工作区未提交)* | 本轮:H5 渠道整体下线 + 凭据字段修复 + 状态机封闭(撤销守卫 / 库存绑定唯一性 / 生命周期解绑)+ 押金归属取台账 | 迁移 `079`、`consignment-*` 五个文件、`work-order-*` 三个仓库、两个前端面板 |
|
||||
| `632dddc4` | 上一轮:H5 渠道整体下线 + 凭据字段修复 + 状态机封闭(撤销守卫 / 库存绑定唯一性 / 生命周期解绑)+ 押金归属取台账 | 迁移 `079`、`consignment-*` 五个文件、`work-order-*` 三个仓库、两个前端面板 |
|
||||
| *(工作区未提交)* | 本轮 · 批次 C+D:指派部分成功补偿、凭证核销如实反馈、空规格不放行 + 履约/指派服务端规格复检 | `consignment-fulfillment-service.ts`、`consignment-matching-service.ts`、`consignment-repo.ts`、履约路由、`WorkOrdersPanel.tsx` |
|
||||
|
||||
**结论**:功能主干 3 天连成闭环(库存 → 提报审核 → 匹配 → 内部现货 / 打手押金两条履约 → 赠送冷静期 → 验收结算 → 撤销回库),但最近三次提交全是**上线后回填正确性**(权限被静默清除、并发占库、撤销资金挂账)。
|
||||
当前主要矛盾不是"缺功能",而是:**状态机不封闭 + 库存绑定无唯一性约束 + 工单终态对寄卖零感知**(本轮已处理);入口侧"H5 不可提交 / 手工录入丢凭据"两条断链已通过"下线 H5 + 修字段名"解决。
|
||||
@@ -70,6 +71,19 @@
|
||||
| 台账幂等键(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 · 直接导致功能不可用或重复出货
|
||||
@@ -233,16 +247,31 @@
|
||||
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` 与台账一致性校验(审核改额的剩余风险)。
|
||||
### 批次 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)。
|
||||
2. 履约事务内复检规格,非 exact 一律 409。
|
||||
3. 匹配下推 SQL + 批量匹配接口 + 工单列表角标(P2-8)。
|
||||
### 批次 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)未做:当前仍是"取全量在库 → 内存比对"。
|
||||
|
||||
### 批次 E · 权限门控与运营体验(P1/P2)
|
||||
1. 前端四权限门控(P1-8 剩余部分:库存/提报面板尚无 `consignment.view/manage/review` 门控);打手端菜单收口(P1-9 剩余)。
|
||||
@@ -256,4 +285,4 @@
|
||||
1. **撤销边界**:✅ 定案 —— **已验收一律禁止撤销**,历史数据不做兼容(用户:"这是测试阶段,不用管历史记录")。买家退款/纠纷走售后流程;若日后确有"验收后退款"场景,另开"售后回收库存"流程(回库或作废 + 押金重算)。
|
||||
2. **`worker_id` 拆分**:✅ 定案 —— 直接新增 `assigned_worker_id` 并让 `worker_id` 固定为卖家(迁移 `079`,无兼容期,不回填历史)。
|
||||
3. **押金策略**:⏳ 未定 —— `pending_review` 超期自动驳回退押金的时限(如 7 天)、以及打手自助撤回是否允许,涉及运营对账口径,需业务确认。
|
||||
4. **批次顺序**:✅ 定案 —— 先 A(入口可用)→ 再 B(资金安全);两者本轮均已落地,C/D/E 待排期。
|
||||
4. **批次顺序**:✅ 定案 —— A(入口可用)→ B(资金安全)→ C/D(资金与货物一致性、防错发)均已落地;E(权限门控与运营体验)为下一批。
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
-- 迁移 079 上库前 / 上库后检查脚本(只读,可直接在生产库执行)
|
||||
--
|
||||
-- 迁移文件:apps/backend/src/db/migrations/079_drop_consignment_collect_links.sql
|
||||
-- 已在全新空库验证:001 → 079 可一次性跑通(79 条迁移全部记录在 schema_migrations)。
|
||||
-- 生产库上库前请先跑完「上库前」四段检查。
|
||||
|
||||
-- ============================ 上库前 ============================
|
||||
|
||||
-- 1. 同一张工单被多件现货同时绑定?
|
||||
-- 会让唯一索引 uniq_consignment_inventories_bound_work_order 创建失败。
|
||||
-- 迁移自带清理(保留工单 material 里记录的那一件,其余解绑回库),
|
||||
-- 但有数据时请先确认"保留哪一件"与工单 material.consignment.inventoryId 一致。
|
||||
-- 期望:0 行。
|
||||
SELECT
|
||||
ci.bound_work_order_id,
|
||||
COUNT(*) AS bound_rows,
|
||||
ARRAY_AGG(ci.id ORDER BY ci.id) AS inventory_ids,
|
||||
MAX(NULLIF(wo.material_json -> 'consignment' ->> 'inventoryId', '')) AS material_inventory_id
|
||||
FROM consignment_inventories ci
|
||||
JOIN work_orders wo ON wo.id = ci.bound_work_order_id
|
||||
WHERE ci.bound_work_order_id IS NOT NULL
|
||||
AND ci.status = 'consumed'
|
||||
GROUP BY ci.bound_work_order_id
|
||||
HAVING COUNT(*) > 1;
|
||||
|
||||
-- 2. 同一提报单重复冻结了发布押金?
|
||||
-- 会让唯一索引 uniq_worker_wallet_ledgers_consignment_publish_freeze 创建失败。
|
||||
-- 期望:0 行。若有行,需人工决定保留哪笔冻结(多冻的部分按 deposit_release 冲正)。
|
||||
SELECT
|
||||
payload_json ->> 'consignmentSubmissionId' AS submission_id,
|
||||
COUNT(*) AS freeze_rows,
|
||||
SUM(ABS(amount)) AS frozen_fen
|
||||
FROM worker_wallet_ledgers
|
||||
WHERE ledger_type = 'deposit_freeze'
|
||||
AND payload_json ->> 'kind' = 'consignment_publish'
|
||||
GROUP BY 1
|
||||
HAVING COUNT(*) > 1;
|
||||
|
||||
-- 3. 待删除对象的影响面(收集链接渠道下线)。
|
||||
-- 用 DO 块 + 动态 SQL,保证"表/列已被删掉"时脚本也能跑(否则会直接报错中断)。
|
||||
DO $$
|
||||
DECLARE
|
||||
n bigint;
|
||||
BEGIN
|
||||
IF to_regclass('public.consignment_collect_links') IS NOT NULL THEN
|
||||
EXECUTE 'SELECT COUNT(*) FROM consignment_collect_links' INTO n;
|
||||
RAISE NOTICE '[3.1] 收集链接表仍存在,共 % 行;删除后不可恢复', n;
|
||||
ELSE
|
||||
RAISE NOTICE '[3.1] 收集链接表不存在(079 已执行或从未创建)';
|
||||
END IF;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'consignment_submissions' AND column_name = 'link_id'
|
||||
) THEN
|
||||
EXECUTE 'SELECT COUNT(*) FROM consignment_submissions WHERE link_id IS NOT NULL' INTO n;
|
||||
RAISE NOTICE '[3.2] 提报单 link_id 仍存在,共 % 行有引用;删除后不可恢复', n;
|
||||
ELSE
|
||||
RAISE NOTICE '[3.2] 提报单 link_id 不存在(079 已执行或从未创建)';
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
-- 3.3 来源枚举分布(迁移会把 collection_link 归并为 worker_portal)
|
||||
SELECT source_type, COUNT(*) FROM consignment_inventories GROUP BY source_type ORDER BY 1;
|
||||
|
||||
-- 4. 历史遗留的"已出库但无工单绑定"现货(079 之前 worker_deposit 占库不写绑定)
|
||||
-- 这些行不会随撤单自动解绑,需要运营在寄卖仓库手动【退回复库】。
|
||||
SELECT COUNT(*) AS legacy_consumed_without_binding
|
||||
FROM consignment_inventories
|
||||
WHERE status = 'consumed' AND bound_work_order_id IS NULL;
|
||||
|
||||
-- ============================ 上库后 ============================
|
||||
|
||||
-- 5. 结构校验:新列 / 新索引 / 枚举约束是否就位
|
||||
SELECT
|
||||
EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'consignment_inventories' AND column_name = 'assigned_worker_id'
|
||||
) AS has_assigned_worker_id,
|
||||
EXISTS (
|
||||
SELECT 1 FROM pg_indexes WHERE indexname = 'uniq_consignment_inventories_bound_work_order'
|
||||
) AS has_bound_work_order_unique,
|
||||
EXISTS (
|
||||
SELECT 1 FROM pg_indexes
|
||||
WHERE indexname = 'uniq_worker_wallet_ledgers_consignment_publish_freeze'
|
||||
) AS has_ledger_freeze_unique,
|
||||
EXISTS (
|
||||
SELECT 1 FROM information_schema.tables WHERE table_name = 'consignment_collect_links'
|
||||
) AS collect_links_table_still_exists,
|
||||
EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'consignment_submissions' AND column_name = 'link_id'
|
||||
) AS submissions_link_id_still_exists;
|
||||
|
||||
SELECT pg_get_constraintdef(oid) AS source_type_check
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'consignment_inventories_source_type_check';
|
||||
|
||||
-- 6. 绑定一致性总览(上库后运营巡检用)
|
||||
SELECT
|
||||
ci.id,
|
||||
ci.inventory_no,
|
||||
ci.status,
|
||||
ci.bound_work_order_id,
|
||||
wo.status AS work_order_status,
|
||||
ci.allocation_status,
|
||||
ci.assigned_worker_id,
|
||||
ci.worker_id AS seller_worker_id
|
||||
FROM consignment_inventories ci
|
||||
LEFT JOIN work_orders wo ON wo.id = ci.bound_work_order_id
|
||||
WHERE ci.status = 'consumed'
|
||||
ORDER BY ci.updated_at DESC
|
||||
LIMIT 50;
|
||||
|
||||
-- 7. 发布押金挂账巡检:台账未释放金额是否超过钱包冻结额(按打手)
|
||||
WITH ledger AS (
|
||||
SELECT
|
||||
worker_id,
|
||||
COALESCE(SUM(CASE WHEN ledger_type = 'deposit_freeze' THEN ABS(amount) ELSE 0 END), 0)
|
||||
- COALESCE(SUM(CASE WHEN ledger_type = 'deposit_release' THEN ABS(amount) ELSE 0 END), 0)
|
||||
- COALESCE(SUM(CASE WHEN ledger_type = 'deposit_deduction' THEN ABS(amount) ELSE 0 END), 0)
|
||||
AS outstanding_fen
|
||||
FROM worker_wallet_ledgers
|
||||
WHERE payload_json ->> 'kind' = 'consignment_publish'
|
||||
GROUP BY worker_id
|
||||
)
|
||||
SELECT
|
||||
l.worker_id,
|
||||
l.outstanding_fen,
|
||||
w.frozen_deposit_amount AS wallet_frozen_fen
|
||||
FROM ledger l
|
||||
JOIN worker_wallets w ON w.worker_id = l.worker_id
|
||||
WHERE l.outstanding_fen > 0
|
||||
AND l.outstanding_fen > w.frozen_deposit_amount;
|
||||
Reference in New Issue
Block a user