fix(worker-platform): 接单平台工单详情抽屉完整展示寄售手动匹配与指派订单的卖家资料

1. 修复管理后台「接单平台 -> 接单工单」抽屉中,手动匹配或指派寄售库存履约的工单因 booster_material_json 为空导致卖家资料全部显示 '-' 的问题;
2. 后端在履约及指派时将卖家角色昵称、数字ID、联系方式、手机号、备注及上架凭证截图完整写入工单 material.consignment;
3. 后端工单查询及展示映射层增加自动回退与批量库存补充解析机制,确保历史与新增的寄售工单均能准确解析出卖家角色名、ID、区服、系统与截图;
4. 前端工单抽屉卡片优化寄售订单的资料展示与阶段标签,清晰展示卖家角色名、游戏ID、联系方式(微信/QQ/手机)、提报/仓库单号及上架截图。
This commit is contained in:
yml2213
2026-09-19 14:33:56 +08:00
parent 13a9028c66
commit 2c120bd3e5
6 changed files with 358 additions and 113 deletions
@@ -333,6 +333,27 @@ export async function getConsignmentInventoryById(
return res.rows[0] || null
}
export async function listConsignmentInventoriesByIds(
ids: Array<number | string>,
): Promise<ConsignmentInventoryRow[]> {
const numericIds = [...new Set(ids.map(Number).filter((n) => Number.isFinite(n) && n > 0))]
if (numericIds.length === 0) return []
const sql = `
SELECT
ci.*,
wc.name AS category_name,
cs.submission_no AS source_submission_no,
cs.screenshots_json AS submission_screenshots_json
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
WHERE ci.id = ANY($1::bigint[])
`
const res = await query<ConsignmentInventoryRow>(sql, [numericIds])
return res.rows
}
export async function updateConsignmentInventory(
id: number | string,
input: {
@@ -25,3 +25,4 @@ export * from './worker-cancel-request-repo.js'
export * from './after-sales-repo.js'
export * from './product-match-repo.js'
export * from './sms-code-repo.js'
export * from './consignment-repo.js'
@@ -1,15 +1,18 @@
import {
getWorkOrderStatistics,
listConsignmentInventoriesByIds,
listWorkOrderEventsByOrderId,
listWorkOrderSharesByOrderIds,
listWorkOrders,
sumPendingUnfreezeByOrderIds,
type ConsignmentInventoryRow,
} from '../../repositories/worker-platform/index.js'
import type { JsonObject } from '../../types/json.js'
import {
normalizeDateString,
normalizePage,
normalizePageSize,
safeParseJson,
} from '../admin/admin-query-utils.js'
import {
mapWorkOrderAdmin,
@@ -60,10 +63,29 @@ export async function listAdminWorkOrders(query: JsonObject = {}) {
getWorkOrderStatistics(filters),
])
const itemIds = items.map((item) => Number(item.id))
const [pendingUnfreezeByOrderId, shares] = await Promise.all([
const inventoryIds = new Set<number>()
for (const item of items) {
const mat = safeParseJson(item.material_json)
const invId = Number(mat.consignment?.inventoryId || 0)
if (invId > 0) {
inventoryIds.add(invId)
}
}
const [pendingUnfreezeByOrderId, shares, inventories] = await Promise.all([
sumPendingUnfreezeByOrderIds(itemIds),
listWorkOrderSharesByOrderIds(itemIds),
inventoryIds.size > 0
? listConsignmentInventoriesByIds([...inventoryIds])
: Promise.resolve([]),
])
const inventoriesById = new Map<number, ConsignmentInventoryRow>()
for (const inv of inventories) {
inventoriesById.set(Number(inv.id), inv)
}
const sharesByOrderId = new Map<number, typeof shares>()
for (const share of shares) {
const orderId = Number(share.work_order_id)
@@ -72,11 +94,17 @@ export async function listAdminWorkOrders(query: JsonObject = {}) {
sharesByOrderId.set(orderId, current)
}
return {
items: items.map((item) => ({
...mapWorkOrderAdmin(item, sharesByOrderId.get(Number(item.id)) || []),
pendingUnfreezeAmount: pendingUnfreezeByOrderId.get(Number(item.id)) || 0,
})),
items: items.map((item) => {
const mat = safeParseJson(item.material_json)
const invId = Number(mat.consignment?.inventoryId || 0)
const inv = invId > 0 ? inventoriesById.get(invId) || null : null
return {
...mapWorkOrderAdmin(item, sharesByOrderId.get(Number(item.id)) || [], inv),
pendingUnfreezeAmount: pendingUnfreezeByOrderId.get(Number(item.id)) || 0,
}
}),
pagination: { page, pageSize, total: statistics.totalCount },
statistics,
}
}
@@ -462,8 +462,15 @@ export async function fulfillWorkOrderWithConsignment(input: {
diamondLevel: inv.diamond_level,
brillianceCount: inv.brilliance_count,
buyerName: inv.buyer_name,
sellerGameId: inv.seller_game_id || '',
sellerContact: inv.seller_contact || '',
sellerPhone: inv.seller_phone || '',
workerName: inv.worker_name || '',
workerPhone: inv.worker_phone || '',
remark: inv.remark || '',
inboundBatch: inv.inbound_batch,
credentials: safeParseJson(inv.credentials_json),
screenshots: safeParseJson(inv.screenshots_json || inv.submission_screenshots_json || []),
costPriceFen: inv.cost_price_fen,
sourceSubmissionId: inv.source_submission_id ? Number(inv.source_submission_id) : null,
sourceSubmissionNo: inv.source_submission_no || '',
@@ -694,11 +701,22 @@ export async function assignWorkOrderWithConsignmentWorker(input: {
diamondLevel: claimed.diamond_level,
brillianceCount: claimed.brilliance_count,
costPriceFen: claimed.cost_price_fen,
buyerName: claimed.buyer_name,
sellerGameId: claimed.seller_game_id || '',
sellerContact: claimed.seller_contact || '',
sellerPhone: claimed.seller_phone || '',
workerId: Number(targetWorkerId),
workerName: claimed.worker_name || '',
workerPhone: claimed.worker_phone || '',
remark: claimed.remark || '',
credentials: safeParseJson(claimed.credentials_json),
screenshots: safeParseJson(
claimed.screenshots_json || claimed.submission_screenshots_json || [],
),
sourceSubmissionId: claimed.source_submission_id
? Number(claimed.source_submission_id)
: null,
sourceSubmissionNo: claimed.source_submission_no || '',
fulfillmentType: 'worker_deposit',
assignedAt: now,
assignedBy: input.actorName,
@@ -10,6 +10,7 @@ import {
WORK_ORDER_GIFT_PHASE,
} from '../../domain/work-order-acceptance-mode.js'
import {
type ConsignmentInventoryRow,
type WorkCategoryRow,
type WorkOrderCapabilities,
type WorkOrderRow,
@@ -76,9 +77,103 @@ export function mapWorkOrderBoosterMaterial(value: unknown) {
screenshots: normalizeUploadedFiles(material.screenshots).map(refreshUploadedFileUrls),
note: String(material.note || ''),
submittedAt: String(material.submittedAt || ''),
isConsignmentSellerMaterial: false,
}
}
/** 读取好友赠送模式的打手本人资料或寄售卖家资料;截图访问链接需要读取时重签。 */
export function resolveWorkOrderEffectiveBoosterMaterial(
workOrder: WorkOrderRow,
consignmentInventory?: ConsignmentInventoryRow | null,
) {
const existingBooster = safeParseJson(workOrder.booster_material_json)
const existingFields =
existingBooster &&
typeof existingBooster.fields === 'object' &&
!Array.isArray(existingBooster.fields)
? (existingBooster.fields as Record<string, string>)
: {}
const hasExistingFields = Object.values(existingFields).some(
(v) => String(v || '').trim().length > 0,
)
const existingScreenshots = normalizeUploadedFiles(existingBooster.screenshots).map(
refreshUploadedFileUrls,
)
const hasExistingScreenshots = existingScreenshots.length > 0
if (hasExistingFields || hasExistingScreenshots) {
return {
fields: existingFields,
screenshots: existingScreenshots,
note: String(existingBooster.note || ''),
submittedAt: String(existingBooster.submittedAt || ''),
isConsignmentSellerMaterial: false,
}
}
// Fallback to consignment seller information if this is a consignment-fulfilled order
const material = safeParseJson(workOrder.material_json)
const consignment = safeParseJson(material.consignment)
const hasConsignment =
(consignment && typeof consignment === 'object' && Object.keys(consignment).length > 0) ||
Boolean(consignmentInventory)
if (hasConsignment) {
const rawScreenshots =
consignment?.screenshots ||
consignmentInventory?.screenshots_json ||
consignmentInventory?.submission_screenshots_json ||
[]
const fields: Record<string, string> = {
gameNickname: String(
consignment?.buyerName ||
consignmentInventory?.buyer_name ||
consignment?.sellerGameNickname ||
'',
).trim(),
gameId: String(
consignment?.sellerGameId || consignmentInventory?.seller_game_id || '',
).trim(),
serverZone: String(
consignment?.serverZone || consignmentInventory?.server_zone || '',
).trim(),
system: String(consignment?.system || consignmentInventory?.system || '').trim(),
sellerContact: String(
consignment?.sellerContact || consignmentInventory?.seller_contact || '',
).trim(),
sellerPhone: String(
consignment?.sellerPhone || consignmentInventory?.seller_phone || '',
).trim(),
sourceSubmissionNo: String(
consignment?.sourceSubmissionNo || consignmentInventory?.source_submission_no || '',
).trim(),
inventoryNo: String(
consignment?.inventoryNo || consignmentInventory?.inventory_no || '',
).trim(),
sourceType: 'consignment',
}
return {
fields,
screenshots: normalizeUploadedFiles(rawScreenshots).map(refreshUploadedFileUrls),
note: String(
consignment?.remark || consignmentInventory?.remark || consignment?.matchNote || '',
).trim(),
submittedAt: String(
consignment?.fulfilledAt ||
consignment?.assignedAt ||
consignmentInventory?.inbound_at ||
consignmentInventory?.created_at ||
workOrder.created_at ||
'',
),
isConsignmentSellerMaterial: true,
}
}
return mapWorkOrderBoosterMaterial(workOrder.booster_material_json)
}
/** 统一计算后台使用的工单业务状态,避免拼单订单继续直接暴露母工单 open。 */
export function resolveWorkOrderOperationalStatus(workOrder: WorkOrderRow): string {
const activeShareQuantity = Math.max(0, Number(workOrder.active_share_quantity || 0))
@@ -131,7 +226,11 @@ function resolveWorkOrderCancelSource(cancelEventType: string | null | undefined
return eventType || '未知来源'
}
export function mapWorkOrderAdmin(workOrder: WorkOrderRow, shares?: WorkOrderShareRow[]) {
export function mapWorkOrderAdmin(
workOrder: WorkOrderRow,
shares?: WorkOrderShareRow[],
consignmentInventory?: ConsignmentInventoryRow | null,
) {
const activeShareQuantity = Math.max(0, Number(workOrder.active_share_quantity || 0))
const pendingShareCount = Math.max(0, Number(workOrder.pending_share_count || 0))
const sharingTotalQuantity = Math.max(1, Number(workOrder.sharing_total_quantity || 1))
@@ -245,7 +344,10 @@ export function mapWorkOrderAdmin(workOrder: WorkOrderRow, shares?: WorkOrderSha
cooldownMinutes: normalizeWorkOrderGiftCooldownMinutes(workOrder.gift_cooldown_minutes),
friendAddedAt: workOrder.friend_added_at,
giftAvailableAt: workOrder.gift_available_at,
boosterMaterial: mapWorkOrderBoosterMaterial(workOrder.booster_material_json),
boosterMaterial: resolveWorkOrderEffectiveBoosterMaterial(
workOrder,
consignmentInventory,
),
},
}
: {}),