修复:寄售登记人改名后显示与筛选同步

This commit is contained in:
2026-11-12 12:02:18 +08:00
parent 4159a435d5
commit be7f6f6f09
13 changed files with 357 additions and 10 deletions
+5 -1
View File
@@ -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) => {
@@ -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();
@@ -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'] })
@@ -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')
@@ -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
@@ -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<string, number>
@@ -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
@@ -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 } : {}),
@@ -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')
})
@@ -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 || '',
@@ -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')
@@ -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, '')
})
@@ -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),
},