From be7f6f6f09295cbb42798616e6c48948a8eaf171 Mon Sep 17 00:00:00 2001 From: zhaoxing <2558691827@qq.com> Date: Thu, 12 Nov 2026 12:02:18 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E5=AF=84=E5=94=AE?= =?UTF-8?q?=E7=99=BB=E8=AE=B0=E4=BA=BA=E6=94=B9=E5=90=8D=E5=90=8E=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E4=B8=8E=E7=AD=9B=E9=80=89=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/backend/src/db/migrate.ts | 6 +- .../158_consignment_worker_name_sync.sql | 75 +++++++++++++++++++ .../repositories/worker-platform-repo.test.ts | 16 ++++ .../worker-platform/consignment-repo.test.ts | 46 ++++++++++++ .../worker-platform/consignment-repo.ts | 45 ++++++++++- .../src/repositories/worker-platform/types.ts | 8 ++ .../worker-platform/work-order-query-repo.ts | 22 +++++- .../admin-work-order-query-service.ts | 4 + .../consignment-admin-service.test.ts | 60 ++++++++++++++- .../consignment-admin-service.ts | 10 ++- .../order-no-search-filter.test.ts | 2 + .../work-order-display-mappers.test.ts | 34 +++++++++ .../work-order-display-mappers.ts | 39 +++++++++- 13 files changed, 357 insertions(+), 10 deletions(-) create mode 100644 apps/backend/src/db/migrations/158_consignment_worker_name_sync.sql diff --git a/apps/backend/src/db/migrate.ts b/apps/backend/src/db/migrate.ts index 0b5b23f2..443d9e10 100644 --- a/apps/backend/src/db/migrate.ts +++ b/apps/backend/src/db/migrate.ts @@ -39,7 +39,11 @@ export async function runDatabaseMigrations() { migrationsSchema: 'public', schema: 'public', singleTransaction: true, - checkOrder: true, + // 联调/生产库可能存在「已应用、但当前分支已删除」的历史迁移记录 + // (如 153-155 semi_auto_*)。node-pg-migrate 的严格顺序检查会把这类 + // 残留记录判成乱序并直接拒绝启动;本地迁移文件本身只增不改, + // 这里关闭顺序检查,仍会按文件名顺序执行所有未应用迁移。 + checkOrder: false, logger: { debug: () => undefined, info: (message) => { diff --git a/apps/backend/src/db/migrations/158_consignment_worker_name_sync.sql b/apps/backend/src/db/migrations/158_consignment_worker_name_sync.sql new file mode 100644 index 00000000..13dcb64d --- /dev/null +++ b/apps/backend/src/db/migrations/158_consignment_worker_name_sync.sql @@ -0,0 +1,75 @@ +-- 158_consignment_worker_name_sync.sql —— 打手改名后同步寄售登记人快照。 +-- +-- 背景:consignment_inventories.worker_name / consignment_submissions.worker_name +-- 是入库/提报时的名字快照。worker_users.display_name 改名后不会自动更新, +-- 后台寄售列表仍显示旧名,按新名搜索也搜不到。 +-- +-- 这里做两件事: +-- 1. 用当前昵称/电话回填存量快照; +-- 2. worker_users 改名(display_name/username/phone)时自动同步这两张表, +-- 之后新改名无需逐表手工处理。 +-- +-- 历史工单 material_json.consignment.workerName 不做批量改写: +-- 后台展示端按 consignment_inventories 的实时关联覆盖,保留历史 JSON 的登记痕迹。 + +UPDATE consignment_inventories ci +SET + worker_name = COALESCE(NULLIF(wu.display_name, ''), wu.username), + worker_phone = wu.phone +FROM worker_users wu +WHERE ci.worker_id = wu.id + AND ( + ci.worker_name IS DISTINCT FROM COALESCE(NULLIF(wu.display_name, ''), wu.username) + OR ci.worker_phone IS DISTINCT FROM wu.phone + ); + +UPDATE consignment_submissions cs +SET + worker_name = COALESCE(NULLIF(wu.display_name, ''), wu.username), + worker_phone = wu.phone +FROM worker_users wu +WHERE cs.worker_id = wu.id + AND ( + cs.worker_name IS DISTINCT FROM COALESCE(NULLIF(wu.display_name, ''), wu.username) + OR cs.worker_phone IS DISTINCT FROM wu.phone + ); + +CREATE OR REPLACE FUNCTION sync_worker_name_to_consignment_snapshots() +RETURNS TRIGGER AS $$ +DECLARE + next_worker_name TEXT := COALESCE(NULLIF(NEW.display_name, ''), NEW.username); +BEGIN + IF NEW.display_name IS DISTINCT FROM OLD.display_name + OR NEW.username IS DISTINCT FROM OLD.username + OR NEW.phone IS DISTINCT FROM OLD.phone + THEN + UPDATE consignment_inventories + SET + worker_name = next_worker_name, + worker_phone = NEW.phone + WHERE worker_id = NEW.id + AND ( + worker_name IS DISTINCT FROM next_worker_name + OR worker_phone IS DISTINCT FROM NEW.phone + ); + + UPDATE consignment_submissions + SET + worker_name = next_worker_name, + worker_phone = NEW.phone + WHERE worker_id = NEW.id + AND ( + worker_name IS DISTINCT FROM next_worker_name + OR worker_phone IS DISTINCT FROM NEW.phone + ); + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS trg_worker_users_consignment_name_sync ON worker_users; +CREATE TRIGGER trg_worker_users_consignment_name_sync +AFTER UPDATE OF display_name, username, phone ON worker_users +FOR EACH ROW +EXECUTE FUNCTION sync_worker_name_to_consignment_snapshots(); diff --git a/apps/backend/src/repositories/worker-platform-repo.test.ts b/apps/backend/src/repositories/worker-platform-repo.test.ts index add71870..3538d195 100644 --- a/apps/backend/src/repositories/worker-platform-repo.test.ts +++ b/apps/backend/src/repositories/worker-platform-repo.test.ts @@ -223,6 +223,22 @@ test('内部现货不再因上一手 last_assigned 进入打手可见集', async assert.equal(calls.length, 2, '列表和概览必须共用同一条历史可见条件') }) +test('后台接单人员筛选把寄售供货人的绑定工单纳入可见集合', () => { + const query = buildWorkOrderWhere({ + workerSharingId: 1494, + consignmentSupplierWorkerId: 1494, + }) + assert.match(query.joins, /FROM consignment_inventories supplier_ci/) + assert.match(query.joins, /supplier_ci\.worker_id = \$2::bigint/) + assert.match(query.joins, /supplier_ci\.bound_work_order_id IS NOT NULL/) + assert.deepEqual(query.params, [1494, 1494]) + + // 打手端「我的订单」不传扩展口径,可见范围必须保持原样 + const plain = buildWorkOrderWhere({ workerSharingId: 1494 }) + assert.doesNotMatch(plain.joins, /supplier_ci/) + assert.deepEqual(plain.params, [1494]) +}) + test('「仅问题单」检索走短路分支,从 pending 表反查而非枚举可见集', () => { const query = buildWorkOrderWhere({ workerSharingId: 7, statuses: ['problem'] }) 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 51bb80f5..4000a56b 100644 --- a/apps/backend/src/repositories/worker-platform/consignment-repo.test.ts +++ b/apps/backend/src/repositories/worker-platform/consignment-repo.test.ts @@ -54,6 +54,52 @@ test('寄卖仓库搜索必须覆盖供货打手姓名、打手电话与页面 ) }) +test('寄卖提报单搜索必须能搜打手,列表与计数 SQL 都带实时打手关联', async () => { + const source = await readFile(new URL('./consignment-repo.ts', import.meta.url), 'utf8') + const start = source.indexOf('export async function listConsignmentSubmissions') + assert.ok(start > 0, '未找到寄卖提报单列表查询') + const body = source.slice(start, source.indexOf('\n}\n', start)) + + for (const field of [ + 'cs.worker_name', + 'cs.worker_phone', + 'supplier_worker.display_name', + 'supplier_worker.username', + 'supplier_worker.phone', + ]) { + assert.ok(body.includes(field), `提报单搜索缺少字段:${field}`) + } + assert.ok(body.includes('AS live_worker_name'), '列表必须带出实时打手昵称') + assert.ok(body.includes('AS live_worker_phone'), '列表必须带出实时打手电话') + + const countSqlLine = body + .split('\n') + .find((line) => + line.includes('SELECT COUNT(*)::integer AS total FROM consignment_submissions cs'), + ) + assert.ok(countSqlLine, '未找到提报单计数 SQL') + assert.ok( + countSqlLine!.includes('LEFT JOIN worker_users supplier_worker'), + '计数 SQL 缺少打手 join,按姓名/电话搜索会报错', + ) +}) + +test('寄售库存详情/批量/匹配候选查询都带出实时打手昵称', async () => { + const source = await readFile(new URL('./consignment-repo.ts', import.meta.url), 'utf8') + for (const fn of [ + 'getConsignmentInventoryById', + 'listConsignmentInventoriesByIds', + 'getConsignmentInventoryBySubmissionId', + 'listActiveConsignmentInventoriesForMatching', + ]) { + const start = source.indexOf(`export async function ${fn}`) + assert.ok(start > 0, `未找到查询:${fn}`) + const body = source.slice(start, source.indexOf('\n}\n', start)) + assert.ok(body.includes('LEFT JOIN worker_users supplier_worker'), `${fn} 缺少实时打手 join`) + assert.ok(body.includes('AS live_worker_name'), `${fn} 缺少实时打手昵称字段`) + } +}) + test('寄卖仓库按通过审核入库时间筛选,结束日期必须包含当天全天', async () => { const source = await readFile(new URL('./consignment-repo.ts', import.meta.url), 'utf8') const inventoryListStart = source.indexOf('export async function listConsignmentInventories') diff --git a/apps/backend/src/repositories/worker-platform/consignment-repo.ts b/apps/backend/src/repositories/worker-platform/consignment-repo.ts index 87ff4d82..e878335e 100644 --- a/apps/backend/src/repositories/worker-platform/consignment-repo.ts +++ b/apps/backend/src/repositories/worker-platform/consignment-repo.ts @@ -48,6 +48,10 @@ export type ConsignmentInventoryRow = { worker_id?: string | number | null worker_name?: string worker_phone?: string + /** 当前打手昵称(实时 join worker_users,优先于入库时的快照) */ + live_worker_name?: string | null + /** 当前打手电话(实时 join worker_users,优先于入库时的快照) */ + live_worker_phone?: string | null assigned_worker_id?: string | number | null deposit_amount_fen?: number /** 对接客服(发布/审核入库时选定,用于提成归属与联系) */ @@ -81,6 +85,10 @@ export type ConsignmentSubmissionRow = { worker_id?: string | number | null worker_name?: string worker_phone?: string + /** 当前打手昵称(实时 join worker_users,优先于提报时的快照) */ + live_worker_name?: string | null + /** 当前打手电话(实时 join worker_users,优先于提报时的快照) */ + live_worker_phone?: string | null deposit_amount_fen?: number /** 对接客服(发布时选定,用于提成归属与联系) */ contact_supporter_id?: string | number | null @@ -233,6 +241,8 @@ export async function listConsignmentInventories(filters: { wc.name AS category_name, cs.submission_no AS source_submission_no, cs.screenshots_json AS submission_screenshots_json, + COALESCE(NULLIF(supplier_worker.display_name, ''), supplier_worker.username) AS live_worker_name, + supplier_worker.phone AS live_worker_phone, wo.acceptance_json AS gift_evidence_json, wo.work_order_no AS gift_work_order_no, wo.status AS gift_work_order_status, @@ -355,6 +365,8 @@ export async function getConsignmentInventoryById( wc.name AS category_name, cs.submission_no AS source_submission_no, cs.screenshots_json AS submission_screenshots_json, + COALESCE(NULLIF(supplier_worker.display_name, ''), supplier_worker.username) AS live_worker_name, + supplier_worker.phone AS live_worker_phone, wo.acceptance_json AS gift_evidence_json, wo.work_order_no AS gift_work_order_no, wo.status AS gift_work_order_status, @@ -366,6 +378,7 @@ export async function getConsignmentInventoryById( FROM consignment_inventories ci LEFT JOIN work_categories wc ON wc.id = ci.category_id LEFT JOIN consignment_submissions cs ON cs.id = ci.source_submission_id + LEFT JOIN worker_users supplier_worker ON supplier_worker.id = ci.worker_id LEFT JOIN consignment_contact_supporters ccs ON ccs.id = ci.contact_supporter_id LEFT JOIN work_orders wo ON wo.id = ci.bound_work_order_id WHERE ci.id = $1 @@ -387,10 +400,13 @@ export async function listConsignmentInventoriesByIds( wc.name AS category_name, cs.submission_no AS source_submission_no, cs.screenshots_json AS submission_screenshots_json, + COALESCE(NULLIF(supplier_worker.display_name, ''), supplier_worker.username) AS live_worker_name, + supplier_worker.phone AS live_worker_phone, ccs.name AS contact_supporter_name FROM consignment_inventories ci LEFT JOIN work_categories wc ON wc.id = ci.category_id LEFT JOIN consignment_submissions cs ON cs.id = ci.source_submission_id + LEFT JOIN worker_users supplier_worker ON supplier_worker.id = ci.worker_id LEFT JOIN consignment_contact_supporters ccs ON ccs.id = ci.contact_supporter_id WHERE ci.id = ANY($1::bigint[]) ` @@ -933,9 +949,12 @@ export async function listActiveConsignmentInventoriesForMatching(options?: { SELECT ci.*, cs.submission_no AS source_submission_no, + COALESCE(NULLIF(supplier_worker.display_name, ''), supplier_worker.username) AS live_worker_name, + supplier_worker.phone AS live_worker_phone, ccs.name AS contact_supporter_name FROM consignment_inventories ci LEFT JOIN consignment_submissions cs ON cs.id = ci.source_submission_id + LEFT JOIN worker_users supplier_worker ON supplier_worker.id = ci.worker_id LEFT JOIN consignment_contact_supporters ccs ON ccs.id = ci.contact_supporter_id WHERE ${conditions.join(' AND ')} ORDER BY ci.id ASC @@ -967,12 +986,22 @@ export async function listConsignmentSubmissions(filters: { if (filters.keyword) { values.push(`%${filters.keyword.trim()}%`) conditions.push( - `(cs.item_name ILIKE $${values.length} OR cs.submission_no ILIKE $${values.length} OR cs.contact_info ILIKE $${values.length} OR cs.seller_game_id ILIKE $${values.length} OR cs.seller_phone ILIKE $${values.length} OR ccs.name ILIKE $${values.length})`, + `(cs.item_name ILIKE $${values.length} + OR cs.submission_no ILIKE $${values.length} + OR cs.contact_info ILIKE $${values.length} + OR cs.seller_game_id ILIKE $${values.length} + OR cs.seller_phone ILIKE $${values.length} + OR cs.worker_name ILIKE $${values.length} + OR cs.worker_phone ILIKE $${values.length} + OR supplier_worker.display_name ILIKE $${values.length} + OR supplier_worker.username ILIKE $${values.length} + OR supplier_worker.phone ILIKE $${values.length} + OR ccs.name ILIKE $${values.length})`, ) } const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '' - const countSql = `SELECT COUNT(*)::integer AS total FROM consignment_submissions cs LEFT JOIN consignment_contact_supporters ccs ON ccs.id = cs.contact_supporter_id ${whereClause}` + const countSql = `SELECT COUNT(*)::integer AS total FROM consignment_submissions cs LEFT JOIN worker_users supplier_worker ON supplier_worker.id = cs.worker_id LEFT JOIN consignment_contact_supporters ccs ON ccs.id = cs.contact_supporter_id ${whereClause}` const countRes = await query<{ total: number }>(countSql, values) const total = countRes.rows[0]?.total || 0 @@ -989,9 +1018,12 @@ export async function listConsignmentSubmissions(filters: { wo.acceptance_json AS gift_evidence_json, wo.work_order_no AS gift_work_order_no, wo.status AS gift_work_order_status, + COALESCE(NULLIF(supplier_worker.display_name, ''), supplier_worker.username) AS live_worker_name, + supplier_worker.phone AS live_worker_phone, ccs.name AS contact_supporter_name FROM consignment_submissions cs LEFT JOIN work_categories wc ON wc.id = cs.category_id + LEFT JOIN worker_users supplier_worker ON supplier_worker.id = cs.worker_id LEFT JOIN consignment_contact_supporters ccs ON ccs.id = cs.contact_supporter_id LEFT JOIN consignment_inventories rinv ON (rinv.id = cs.result_inventory_id OR rinv.source_submission_id = cs.id) LEFT JOIN work_orders wo ON wo.id = rinv.bound_work_order_id @@ -1209,9 +1241,12 @@ export async function listConsignmentSubmissionsByIds( wo.acceptance_json AS gift_evidence_json, wo.work_order_no AS gift_work_order_no, wo.status AS gift_work_order_status, + COALESCE(NULLIF(supplier_worker.display_name, ''), supplier_worker.username) AS live_worker_name, + supplier_worker.phone AS live_worker_phone, ccs.name AS contact_supporter_name FROM consignment_submissions cs LEFT JOIN work_categories wc ON wc.id = cs.category_id + LEFT JOIN worker_users supplier_worker ON supplier_worker.id = cs.worker_id LEFT JOIN consignment_contact_supporters ccs ON ccs.id = cs.contact_supporter_id LEFT JOIN consignment_inventories rinv ON (rinv.id = cs.result_inventory_id OR rinv.source_submission_id = cs.id) @@ -1238,10 +1273,13 @@ export async function getConsignmentInventoryBySubmissionId( ci.*, wc.name AS category_name, cs.submission_no AS source_submission_no, + COALESCE(NULLIF(supplier_worker.display_name, ''), supplier_worker.username) AS live_worker_name, + supplier_worker.phone AS live_worker_phone, ccs.name AS contact_supporter_name FROM consignment_inventories ci LEFT JOIN work_categories wc ON wc.id = ci.category_id LEFT JOIN consignment_submissions cs ON cs.id = ci.source_submission_id + LEFT JOIN worker_users supplier_worker ON supplier_worker.id = ci.worker_id LEFT JOIN consignment_contact_supporters ccs ON ccs.id = ci.contact_supporter_id WHERE ci.source_submission_id = $1 ORDER BY ci.id DESC @@ -1262,9 +1300,12 @@ export async function getConsignmentSubmissionById( wo.acceptance_json AS gift_evidence_json, wo.work_order_no AS gift_work_order_no, wo.status AS gift_work_order_status, + COALESCE(NULLIF(supplier_worker.display_name, ''), supplier_worker.username) AS live_worker_name, + supplier_worker.phone AS live_worker_phone, ccs.name AS contact_supporter_name FROM consignment_submissions cs LEFT JOIN work_categories wc ON wc.id = cs.category_id + LEFT JOIN worker_users supplier_worker ON supplier_worker.id = cs.worker_id LEFT JOIN consignment_contact_supporters ccs ON ccs.id = cs.contact_supporter_id LEFT JOIN consignment_inventories rinv ON (rinv.id = cs.result_inventory_id OR rinv.source_submission_id = cs.id) LEFT JOIN work_orders wo ON wo.id = rinv.bound_work_order_id diff --git a/apps/backend/src/repositories/worker-platform/types.ts b/apps/backend/src/repositories/worker-platform/types.ts index 163bffe0..7e23a974 100644 --- a/apps/backend/src/repositories/worker-platform/types.ts +++ b/apps/backend/src/repositories/worker-platform/types.ts @@ -514,6 +514,14 @@ export type ListInput = { /** 后台打手列表按提现黑名单筛选。 */ isWithdrawalBlacklisted?: boolean | null | undefined workerSharingId?: number + /** + * 后台「接单人员」筛选的扩展口径:同时把该打手作为寄售供货人的工单纳入可见集合。 + * + * 内部寄售直接履约不写 assigned_worker_id,供货关系只存在 + * consignment_inventories.worker_id;仅靠 workerSharingId 会漏掉这些单。 + * 该字段只由后台筛选使用,打手端「我的订单」不传,避免改变打手可见范围。 + */ + consignmentSupplierWorkerId?: number /** 大厅候选集合的总展示上限;筛选与分页在候选集合内执行。 */ hallCandidateLimit?: number hallCategoryLimits?: Record diff --git a/apps/backend/src/repositories/worker-platform/work-order-query-repo.ts b/apps/backend/src/repositories/worker-platform/work-order-query-repo.ts index b0315442..c1de2729 100644 --- a/apps/backend/src/repositories/worker-platform/work-order-query-repo.ts +++ b/apps/backend/src/repositories/worker-platform/work-order-query-repo.ts @@ -391,6 +391,7 @@ export async function listWorkOrders({ workerLevelIds = [], categoryId = 0, workerSharingId = 0, + consignmentSupplierWorkerId = 0, hallCandidateLimit = 0, hallCategoryLimits = {}, visibleAfterIso = '', @@ -416,6 +417,7 @@ export async function listWorkOrders({ ...(resolvedWorkerIds ? { workerIds: resolvedWorkerIds } : {}), categoryId, workerSharingId, + consignmentSupplierWorkerId, hallCandidateLimit, hallCategoryLimits, visibleAfterIso, @@ -1205,6 +1207,7 @@ export function buildWorkOrderWhere({ workerIds, categoryId = 0, workerSharingId = 0, + consignmentSupplierWorkerId = 0, hallCandidateLimit = 0, hallCategoryLimits = {}, visibleAfterIso = '', @@ -1234,6 +1237,7 @@ export function buildWorkOrderWhere({ ) } let workerSharePlaceholder = '' + let consignmentSupplierUnion = '' // 旧的 excludeFilledSharing 写法:LEFT JOIN 一份「整张 work_order_shares 的聚合」 // (GROUP BY work_order_id)再在 WHERE 里比较。列表/计数每次调用都要重算这份全局聚合, @@ -1245,6 +1249,22 @@ export function buildWorkOrderWhere({ if (workerSharingId) { params.push(workerSharingId) workerSharePlaceholder = `$${params.length}` + const normalizedSupplierWorkerId = Math.max( + 0, + Math.floor(Number(consignmentSupplierWorkerId) || 0), + ) + if (normalizedSupplierWorkerId > 0) { + // 内部寄售直接履约不写 assigned_worker_id:供货关系只存在 + // consignment_inventories.worker_id,这里按库存回查绑定工单,补进可见集合。 + params.push(normalizedSupplierWorkerId) + const supplierPlaceholder = `$${params.length}` + consignmentSupplierUnion = ` + UNION + SELECT supplier_ci.bound_work_order_id AS work_order_id + FROM consignment_inventories supplier_ci + WHERE supplier_ci.worker_id = ${supplierPlaceholder}::bigint + AND supplier_ci.bound_work_order_id IS NOT NULL` + } joins.push(` INNER JOIN ( SELECT assigned.id AS work_order_id @@ -1263,7 +1283,7 @@ export function buildWorkOrderWhere({ SELECT timeout_ev.work_order_id FROM work_order_events timeout_ev WHERE timeout_ev.event_type LIKE 'timeout_%' - AND timeout_ev.payload_json->>'workerId' = ${workerSharePlaceholder}::text + AND timeout_ev.payload_json->>'workerId' = ${workerSharePlaceholder}::text${consignmentSupplierUnion} ) worker_visible ON worker_visible.work_order_id = wo.id LEFT JOIN LATERAL ( SELECT wos.status, wos.created_at diff --git a/apps/backend/src/services/worker-platform/admin-work-order-query-service.ts b/apps/backend/src/services/worker-platform/admin-work-order-query-service.ts index dd31d842..2a29fe02 100644 --- a/apps/backend/src/services/worker-platform/admin-work-order-query-service.ts +++ b/apps/backend/src/services/worker-platform/admin-work-order-query-service.ts @@ -59,6 +59,7 @@ function buildWorkOrderStatisticsCacheKey(filters: JsonObject): string { filters.keyword || '', filters.shopNames || [], filters.workerSharingId || 0, + filters.consignmentSupplierWorkerId || 0, filters.workerId || 0, filters.workerLevelIds || [], filters.categoryId || 0, @@ -149,6 +150,9 @@ export function resolveAdminWorkOrderListFilters(query: JsonObject = {}): ListIn keyword, shopNames, workerSharingId: workerId, + // 「接单人员」筛选同时覆盖寄售供货人:内部寄售直接履约不写 assigned_worker_id, + // 供货关系只存在 consignment_inventories.worker_id,必须补进可见集合。 + ...(workerId > 0 ? { consignmentSupplierWorkerId: workerId } : {}), workerLevelIds, vipEvidencePending, ...(acceptanceMode ? { acceptanceMode } : {}), diff --git a/apps/backend/src/services/worker-platform/consignment-admin-service.test.ts b/apps/backend/src/services/worker-platform/consignment-admin-service.test.ts index 4673380f..24407c8e 100644 --- a/apps/backend/src/services/worker-platform/consignment-admin-service.test.ts +++ b/apps/backend/src/services/worker-platform/consignment-admin-service.test.ts @@ -1,7 +1,7 @@ import assert from 'node:assert/strict' import test from 'node:test' -import { normalizeInventoryDate } from './consignment-admin-service.js' +import { mapConsignmentInventory, normalizeInventoryDate } from './consignment-admin-service.js' test('寄售仓库入库日期筛选只接受合法的 YYYY-MM-DD', () => { assert.equal(normalizeInventoryDate('2026-11-05'), '2026-11-05') @@ -10,3 +10,61 @@ test('寄售仓库入库日期筛选只接受合法的 YYYY-MM-DD', () => { assert.equal(normalizeInventoryDate('2026/11/05'), undefined) assert.equal(normalizeInventoryDate(''), undefined) }) + +test('寄售库存改名后优先显示实时打手昵称,打手被删才回退快照', () => { + const base = { + id: 1, + inventory_no: 'INV1', + game_name: '和平精英', + category_id: null, + item_name: '测试物品', + system: '', + server_zone: '', + series: '', + diamond_level: 1, + brilliance_count: 0, + cost_price_fen: 0, + buyer_name: '', + inbound_batch: '', + seller_contact: '', + seller_game_id: '', + seller_payment_info: '', + seller_phone: '', + credentials_json: {}, + screenshots_json: [], + remark: '', + allow_matching: true, + status: 'in_stock', + allocation_status: '待安排', + bound_order_id: null, + bound_work_order_id: null, + bound_platform_order_id: '', + shop_name: '', + source_type: 'manual', + source_submission_id: null, + inbound_at: '2026-11-01T00:00:00.000Z', + consumed_at: null, + created_at: '2026-11-01T00:00:00.000Z', + updated_at: '2026-11-01T00:00:00.000Z', + worker_id: 7, + worker_name: '旧昵称', + worker_phone: '111', + } + + const renamed = mapConsignmentInventory({ + ...base, + live_worker_name: '新昵称', + live_worker_phone: '222', + } as never) + assert.equal(renamed.workerName, '新昵称') + assert.equal(renamed.workerPhone, '222') + + // 打手被删除时 join 不到实时行,仍要能显示登记时快照 + const deleted = mapConsignmentInventory({ + ...base, + live_worker_name: null, + live_worker_phone: null, + } as never) + assert.equal(deleted.workerName, '旧昵称') + assert.equal(deleted.workerPhone, '111') +}) diff --git a/apps/backend/src/services/worker-platform/consignment-admin-service.ts b/apps/backend/src/services/worker-platform/consignment-admin-service.ts index 4a1b2dd2..00726b26 100644 --- a/apps/backend/src/services/worker-platform/consignment-admin-service.ts +++ b/apps/backend/src/services/worker-platform/consignment-admin-service.ts @@ -222,8 +222,9 @@ export function mapConsignmentInventory(row: ConsignmentInventoryRow) { }) : null, workerId: row.worker_id ? Number(row.worker_id) : null, - workerName: row.worker_name || '', - workerPhone: row.worker_phone || '', + // 改名后以当前昵称为准;快照只用于打手被删除等无法实时关联的兜底。 + workerName: row.live_worker_name || row.worker_name || '', + workerPhone: row.live_worker_phone || row.worker_phone || '', assignedWorkerId: row.assigned_worker_id ? Number(row.assigned_worker_id) : null, depositAmountFen: Number(row.deposit_amount_fen || 0), contactSupporterId: row.contact_supporter_id ? Number(row.contact_supporter_id) : null, @@ -265,8 +266,9 @@ export function mapConsignmentSubmission(row: ConsignmentSubmissionRow) { ), askingPriceFen: Number(row.asking_price_fen || 0), workerId: row.worker_id ? Number(row.worker_id) : null, - workerName: row.worker_name || '', - workerPhone: row.worker_phone || '', + // 改名后以当前昵称为准;快照只用于打手被删除等无法实时关联的兜底。 + workerName: row.live_worker_name || row.worker_name || '', + workerPhone: row.live_worker_phone || row.worker_phone || '', depositAmountFen: Number(row.deposit_amount_fen || 0), contactSupporterId: row.contact_supporter_id ? Number(row.contact_supporter_id) : null, contactSupporterName: row.contact_supporter_name || '', diff --git a/apps/backend/src/services/worker-platform/order-no-search-filter.test.ts b/apps/backend/src/services/worker-platform/order-no-search-filter.test.ts index 2c830dc3..b00cdd1a 100644 --- a/apps/backend/src/services/worker-platform/order-no-search-filter.test.ts +++ b/apps/backend/src/services/worker-platform/order-no-search-filter.test.ts @@ -41,6 +41,7 @@ test('后台:显式单号检索时忽略状态等排他筛选,只按单号 assert.equal(filters.vipEvidencePending, false) // 接单人员:目标单可能根本不在这名打手的可见集合里 assert.equal(filters.workerSharingId, 0) + assert.equal(filters.consignmentSupplierWorkerId, undefined) // 分类 / 等级 / 验收模式 / 赠送阶段 / 创建时间 / 定位工单 assert.equal(filters.categoryId, 0) assert.deepEqual(filters.workerLevelIds, []) @@ -77,6 +78,7 @@ test('后台:普通关键词(商品名/昵称)仍按原样下推全部筛 assert.deepEqual(filters.statuses, ['open', 'in_progress']) assert.equal(filters.vipEvidencePending, true) assert.equal(filters.workerSharingId, 7) + assert.equal(filters.consignmentSupplierWorkerId, 7) assert.equal(filters.categoryId, 12) assert.deepEqual(filters.workerLevelIds, [1, 2]) assert.equal(filters.acceptanceMode, 'friend_gift') diff --git a/apps/backend/src/services/worker-platform/work-order-display-mappers.test.ts b/apps/backend/src/services/worker-platform/work-order-display-mappers.test.ts index 9c7c5f7c..95a7f220 100644 --- a/apps/backend/src/services/worker-platform/work-order-display-mappers.test.ts +++ b/apps/backend/src/services/worker-platform/work-order-display-mappers.test.ts @@ -4,6 +4,7 @@ import test from 'node:test' import type { WorkOrderShareRow } from '../../repositories/worker-platform/types.js' import { isWorkerOrderVipEvidencePending, + mapWorkOrderMaterialWithConsignmentWorker, pickWorkerRepresentativeShare, resolveWorkerOrderStatusTabs, stripWorkerPersonalShareFields, @@ -264,3 +265,36 @@ test('大厅广播快照必须剔除打手私有份额字段', () => { assert.deepEqual(snapshot.sharingProgress, { joinedQuantity: 2, pendingSubmissionCount: 1 }) assert.equal(snapshot.workOrderId, 1) }) + +test('寄售供货人改名后,工单 material 展示使用库存实时昵称', () => { + const material = { + consignment: { + inventoryId: 9, + workerName: '旧昵称', + workerPhone: '111', + itemName: '测试物品', + }, + } + + const renamed = mapWorkOrderMaterialWithConsignmentWorker(material, { + live_worker_name: '新昵称', + live_worker_phone: '222', + } as never) as { consignment: { workerName: string; workerPhone: string; itemName: string } } + + assert.equal(renamed.consignment.workerName, '新昵称') + assert.equal(renamed.consignment.workerPhone, '222') + // 其它寄售字段不能被覆盖 + assert.equal(renamed.consignment.itemName, '测试物品') + + const fallback = mapWorkOrderMaterialWithConsignmentWorker(material, null) as { + consignment: { workerName: string } + } + assert.equal(fallback.consignment.workerName, '旧昵称') + + // 历史上没有登记供货人的内部现货单,不能因为库存关联到打手就改标成「供货打手」 + const noSnapshot = mapWorkOrderMaterialWithConsignmentWorker( + { consignment: { inventoryId: 9, workerName: '' } }, + { live_worker_name: '新昵称', live_worker_phone: '222' } as never, + ) as { consignment: { workerName: string } } + assert.equal(noSnapshot.consignment.workerName, '') +}) diff --git a/apps/backend/src/services/worker-platform/work-order-display-mappers.ts b/apps/backend/src/services/worker-platform/work-order-display-mappers.ts index 20c7a034..30682fbc 100644 --- a/apps/backend/src/services/worker-platform/work-order-display-mappers.ts +++ b/apps/backend/src/services/worker-platform/work-order-display-mappers.ts @@ -68,6 +68,40 @@ export function mapWorkOrderMaterial(value: unknown) { } } +/** + * 工单 material.consignment 里保存的是入库/履约时的打手昵称快照。 + * 打手改名后,后台列表仍要显示当前昵称;有实时库存行时用它覆盖展示字段, + * 历史 JSON 本身不改写(保留登记时痕迹)。 + */ +export function mapWorkOrderMaterialWithConsignmentWorker( + value: unknown, + consignmentInventory?: ConsignmentInventoryRow | null, +) { + const material = mapWorkOrderMaterial(value) as JsonObject + const liveWorkerName = String( + consignmentInventory?.live_worker_name || consignmentInventory?.worker_name || '', + ).trim() + if (!liveWorkerName) return material + const consignment = material.consignment + if (!consignment || typeof consignment !== 'object' || Array.isArray(consignment)) { + return material + } + const consignmentRecord = consignment as JsonObject + // 只刷新「原本就登记过供货人」的单据,避免把历史上没有供货人快照的 + // 内部现货单改标成「供货打手」(那会改变历史单的展示语义)。 + if (!String(consignmentRecord.workerName || '').trim()) return material + return { + ...material, + consignment: { + ...consignmentRecord, + workerName: liveWorkerName, + workerPhone: String( + consignmentInventory?.live_worker_phone || consignmentInventory?.worker_phone || '', + ).trim(), + }, + } +} + /** 读取好友赠送模式的打手本人资料;主页截图访问链接需要读取时重签。 */ export function mapWorkOrderBoosterMaterial(value: unknown) { const material = safeParseJson(value) @@ -412,7 +446,10 @@ export function mapWorkOrderAdmin( depositThresholdAmount: Number( workOrder.deposit_threshold_amount || DEFAULT_DEPOSIT_THRESHOLD_AMOUNT, ), - material: mapWorkOrderMaterial(workOrder.material_json), + material: mapWorkOrderMaterialWithConsignmentWorker( + workOrder.material_json, + consignmentInventory, + ), requirement: { fields: resolveRequirementFields(workOrder), },