diff --git a/apps/backend/src/repositories/worker-platform/consignment-repo.test.ts b/apps/backend/src/repositories/worker-platform/consignment-repo.test.ts index ee7cefb7..179fcc8e 100644 --- a/apps/backend/src/repositories/worker-platform/consignment-repo.test.ts +++ b/apps/backend/src/repositories/worker-platform/consignment-repo.test.ts @@ -18,3 +18,22 @@ test('寄卖仓库按关键字搜索必须能搜到"寄卖单号"(SUB…), '计数 SQL 缺少提报单 join,会导致按寄卖单号搜索时报 500', ) }) + +test('匹配候选要带出寄卖单号,并且 join 之后所有列名都必须加 ci. 前缀', async () => { + const source = await readFile(new URL('./consignment-repo.ts', import.meta.url), 'utf8') + const start = source.indexOf('export async function listActiveConsignmentInventoriesForMatching') + assert.ok(start > 0, '未找到匹配候选查询') + const body = source.slice(start, source.indexOf('\n}\n', start)) + + // 客服界面只展示寄卖单号(SUB…),候选里必须带出来,否则又回退显示内部仓库号 + assert.ok(body.includes('cs.submission_no AS source_submission_no')) + assert.ok(body.includes('LEFT JOIN consignment_submissions cs')) + // consignment_submissions 也有 status/game_name/category_id:不加前缀会 ambiguous column 直接 500 + assert.ok( + !/WHERE[^`]*[^.]\bstatus = /.test(body.replace(/ci\.status/g, 'X')), + '存在未加前缀的 status', + ) + assert.ok(body.includes("ci.status = 'in_stock'")) + assert.ok(body.includes('ci.allow_matching = TRUE')) + assert.ok(!body.includes('SELECT *'), 'SELECT * 会把两表同名列搞乱,必须显式列出') +}) diff --git a/apps/backend/src/repositories/worker-platform/consignment-repo.ts b/apps/backend/src/repositories/worker-platform/consignment-repo.ts index 987c248a..ca896f96 100644 --- a/apps/backend/src/repositories/worker-platform/consignment-repo.ts +++ b/apps/backend/src/repositories/worker-platform/consignment-repo.ts @@ -902,23 +902,29 @@ export async function listActiveConsignmentInventoriesForMatching(options?: { gameName?: string categoryId?: number | null }): Promise { - const conditions: string[] = ["status = 'in_stock'", 'allow_matching = TRUE'] + // 客服看到的匹配候选要显示「寄卖单号」(提报单号),所以必须带出提报单号; + // 注意列名一律加 ci. 前缀:consignment_submissions 也有 status/game_name/category_id, + // 加了 join 之后不加前缀会直接撞成 ambiguous column。 + const conditions: string[] = ["ci.status = 'in_stock'", 'ci.allow_matching = TRUE'] const values: unknown[] = [] if (options?.gameName) { values.push(options.gameName) - conditions.push(`game_name = $${values.length}`) + conditions.push(`ci.game_name = $${values.length}`) } if (options?.categoryId) { values.push(options.categoryId) - conditions.push(`category_id = $${values.length}`) + conditions.push(`ci.category_id = $${values.length}`) } const sql = ` - SELECT * - FROM consignment_inventories + SELECT + ci.*, + cs.submission_no AS source_submission_no + FROM consignment_inventories ci + LEFT JOIN consignment_submissions cs ON cs.id = ci.source_submission_id WHERE ${conditions.join(' AND ')} - ORDER BY id ASC + ORDER BY ci.id ASC ` const res = await query(sql, values) return res.rows diff --git a/apps/frontend/src/pages/admin/panels/ConsignmentInventoryPanel.tsx b/apps/frontend/src/pages/admin/panels/ConsignmentInventoryPanel.tsx index 51bf7bee..bff2b352 100644 --- a/apps/frontend/src/pages/admin/panels/ConsignmentInventoryPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/ConsignmentInventoryPanel.tsx @@ -96,10 +96,13 @@ const EVIDENCE_CAPTION_STYLE = { fontSize: 12, color: '#8c8c8c' } as const /** * 寄卖单号 = 来源提报单号(卖家提报时生成的提报单号就是唯一寄卖单号)。 - * 客服手工入库的现货没有来源提报,回退显示内部仓库号,保证客服对单口径一致。 + * + * 对外只展示这个号:内部仓库号 INV… 仍然保留在数据与搜索里(按它搜得到), + * 但客服界面上不再出现,避免"一件货两个号"再次把人对晕。 + * 客服手工入库的现货没有提报单,就明确写出来,不给一串内部编号。 */ function resolveConsignmentNo(record: ConsignmentInventory) { - return record.sourceSubmissionNo || record.inventoryNo + return record.sourceSubmissionNo || '客服手工入库' } /** 关联工单状态文案(后端 giftWorkOrderStatus:in_progress / pending_acceptance / accepted 等) */ @@ -393,13 +396,6 @@ export default function ConsignmentInventoryPanel() { {resolveConsignmentNo(record)} - {record.sourceSubmissionNo ? ( -
- - 仓库号 {record.inventoryNo} - -
- ) : null}
{record.sourceType === 'worker_portal' ? ( @@ -1300,11 +1296,6 @@ export default function ConsignmentInventoryPanel() { )} - - - {viewingItem.inventoryNo} - - {viewingItem.status === 'in_stock' ? ( 在库待匹配 diff --git a/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx b/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx index 18102575..e9a03322 100644 --- a/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx @@ -181,11 +181,13 @@ function getConsignmentFulfillment(row: WorkOrder): ConsignmentFulfillmentInfo | } /** - * 寄卖单号 = 来源提报单号(提报单号即寄卖单号)。 - * 历史老单的 consignment 没有 sourceSubmissionNo,回退到内部仓库号 inventoryNo。 + * 寄卖单号 = 来源提报单号(提报单号即寄卖单号),客服界面只认这一个号。 + * + * 内部仓库号 INV… 仍留在数据里(`info.inventoryNo`,撤销/解绑链路还在用), + * 只是不再展示;历史老单没有 sourceSubmissionNo 时明确写出来,不给内部编号。 */ function resolveConsignmentNo(info: ConsignmentFulfillmentInfo) { - return info.sourceSubmissionNo || info.inventoryNo || '' + return info.sourceSubmissionNo || (info.inventoryNo ? '老单无提报单号' : '') } /** @@ -3025,13 +3027,10 @@ export default function WorkOrdersPanel() { 【部分匹配 / 高配充低配】 )} - 寄卖单号 {inv.sourceSubmissionNo || inv.inventoryNo} + {inv.sourceSubmissionNo + ? `寄卖单号 ${inv.sourceSubmissionNo}` + : '客服手工入库(无提报单号)'} - {inv.sourceSubmissionNo ? ( - - 仓库号 {inv.inventoryNo} - - ) : null}
diff --git a/docs/寄卖系统优化方案.md b/docs/寄卖系统优化方案.md index 5e91b791..7c68eb1b 100644 --- a/docs/寄卖系统优化方案.md +++ b/docs/寄卖系统优化方案.md @@ -21,6 +21,7 @@ | `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` | @@ -280,6 +281,28 @@ 前端在"部分匹配"候选上先 `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 一张缩略图,看不出是物品还是主页: