diff --git a/apps/backend/src/repositories/worker-platform/consignment-repo.ts b/apps/backend/src/repositories/worker-platform/consignment-repo.ts index 98cb210e..0d60914a 100644 --- a/apps/backend/src/repositories/worker-platform/consignment-repo.ts +++ b/apps/backend/src/repositories/worker-platform/consignment-repo.ts @@ -333,6 +333,27 @@ export async function getConsignmentInventoryById( return res.rows[0] || null } +export async function listConsignmentInventoriesByIds( + ids: Array, +): Promise { + 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(sql, [numericIds]) + return res.rows +} + export async function updateConsignmentInventory( id: number | string, input: { diff --git a/apps/backend/src/repositories/worker-platform/index.ts b/apps/backend/src/repositories/worker-platform/index.ts index 4941e6a3..a5e60432 100644 --- a/apps/backend/src/repositories/worker-platform/index.ts +++ b/apps/backend/src/repositories/worker-platform/index.ts @@ -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' 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 3a208df1..965829b6 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 @@ -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() + 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() + for (const inv of inventories) { + inventoriesById.set(Number(inv.id), inv) + } + const sharesByOrderId = new Map() 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, } } + diff --git a/apps/backend/src/services/worker-platform/consignment-fulfillment-service.ts b/apps/backend/src/services/worker-platform/consignment-fulfillment-service.ts index 87ab2dc3..5fabe350 100644 --- a/apps/backend/src/services/worker-platform/consignment-fulfillment-service.ts +++ b/apps/backend/src/services/worker-platform/consignment-fulfillment-service.ts @@ -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, 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 4259e0a7..8163da74 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 @@ -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) + : {} + 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 = { + 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, + ), }, } : {}), diff --git a/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx b/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx index 6769425f..bd1b6967 100644 --- a/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx @@ -177,6 +177,16 @@ function isInternalConsignmentOrder(row: WorkOrder) { return Boolean(consignment.inventoryId) && !row.worker } +function isConsignmentOrder(row: WorkOrder) { + const consignment = asRecord((row.material as Record | undefined)?.consignment) + return Boolean( + consignment.inventoryId || + consignment.inventoryNo || + consignment.fulfillmentType || + row.gift?.boosterMaterial?.fields?.inventoryNo, + ) +} + /** 工单 `material.consignment`(寄售履约信息);没有寄售履约信息时返回 null。 */ function getConsignmentFulfillment(row: WorkOrder): ConsignmentFulfillmentInfo | null { const consignment = asRecord((row.material as Record | undefined)?.consignment) @@ -2093,112 +2103,177 @@ export default function WorkOrdersPanel() { - {detailOrder.acceptanceMode === 'friend_gift' && detailOrder.gift ? ( - - {isInternalConsignmentOrder(detailOrder) ? ( - 内部寄售匹配 - ) : ( - {formatAcceptanceMode(detailOrder.acceptanceMode)} - )} - - {formatGiftPhase( - detailOrder.gift.phase, - isInternalConsignmentOrder(detailOrder), - )} - - {detailOrder.capabilities?.canConfirmFriend && canManageProblem ? ( - - ) : null} - {detailOrder.capabilities?.canCompleteGiftCooldown && canManageProblem ? ( - - ) : null} - {detailOrder.capabilities?.canResetGiftFlow && canManageProblem ? ( - - ) : null} - - } - > -
- - - {detailOrder.gift.cooldownMinutes > 0 - ? `${detailOrder.gift.cooldownMinutes} 分钟` - : `${detailOrder.gift.cooldownHours} 小时`} - - - {formatAdminDateTime(detailOrder.gift.boosterMaterial?.submittedAt)} - - - {formatAdminDateTime(detailOrder.gift.friendAddedAt)} - - - {detailOrder.gift.giftAvailableAt - ? formatAdminDateTime(detailOrder.gift.giftAvailableAt) - : '-'} - - - - {( - [ - ['gameNickname', '游戏昵称'], - ['gameId', '游戏ID'], - ['serverZone', '区服'], - ['system', '系统'], - ] as const - ).map(([key, label]) => ( - - - {String(detailOrder.gift?.boosterMaterial?.fields?.[key] || '').trim() || - '-'} - - - ))} - - - {String(detailOrder.gift?.boosterMaterial?.note || '').trim() || '-'} - - - -
- 打手主页截图 -
- {detailOrder.gift.boosterMaterial?.screenshots?.length ? ( - - ) : ( - 未提交 - )} -
-
-
-
- ) : null} + {detailOrder.acceptanceMode === 'friend_gift' && detailOrder.gift + ? (() => { + const isConsignment = + isInternalConsignmentOrder(detailOrder) || isConsignmentOrder(detailOrder) + const consignmentInfo = getConsignmentFulfillment(detailOrder) + const sellerFields = detailOrder.gift.boosterMaterial?.fields || {} + const sellerContact = + sellerFields.sellerContact || String(consignmentInfo?.sellerContact || '') + const sellerPhone = + sellerFields.sellerPhone || String(consignmentInfo?.sellerPhone || '') + const inventoryNo = + sellerFields.inventoryNo || String(consignmentInfo?.inventoryNo || '') + const sourceSubmissionNo = + sellerFields.sourceSubmissionNo || + String(consignmentInfo?.sourceSubmissionNo || '') + const effectiveNote = String( + detailOrder.gift?.boosterMaterial?.note || consignmentInfo?.remark || '', + ).trim() + + return ( + + {isInternalConsignmentOrder(detailOrder) ? ( + 内部寄售匹配 + ) : isConsignment ? ( + 寄售指派匹配 + ) : ( + + {formatAcceptanceMode(detailOrder.acceptanceMode)} + + )} + + {formatGiftPhase(detailOrder.gift.phase, isConsignment)} + + {detailOrder.capabilities?.canConfirmFriend && canManageProblem ? ( + + ) : null} + {detailOrder.capabilities?.canCompleteGiftCooldown && canManageProblem ? ( + + ) : null} + {detailOrder.capabilities?.canResetGiftFlow && canManageProblem ? ( + + ) : null} + + } + > +
+ + + {detailOrder.gift.cooldownMinutes > 0 + ? `${detailOrder.gift.cooldownMinutes} 分钟` + : `${detailOrder.gift.cooldownHours} 小时`} + + + {formatAdminDateTime(detailOrder.gift.boosterMaterial?.submittedAt) || + (isConsignment ? '已自动带入卖家现货资料' : '-')} + + + {formatAdminDateTime(detailOrder.gift.friendAddedAt)} + + + {detailOrder.gift.giftAvailableAt + ? formatAdminDateTime(detailOrder.gift.giftAvailableAt) + : '-'} + + + + + + {String( + sellerFields.gameNickname || consignmentInfo?.buyerName || '', + ).trim() || '-'} + + + + + {String( + sellerFields.gameId || consignmentInfo?.sellerGameId || '', + ).trim() || '-'} + + + + + {String( + sellerFields.serverZone || consignmentInfo?.serverZone || '', + ).trim() || '-'} + + + + + {String( + sellerFields.system || consignmentInfo?.system || '', + ).trim() || '-'} + + + {isConsignment && (sellerContact || sellerPhone) ? ( + + + {[ + sellerContact ? `微信/QQ: ${sellerContact}` : '', + sellerPhone ? `手机: ${sellerPhone}` : '', + ] + .filter(Boolean) + .join(' / ')} + + + ) : null} + {isConsignment && (inventoryNo || sourceSubmissionNo) ? ( + + + {[ + sourceSubmissionNo ? `提报单: ${sourceSubmissionNo}` : '', + inventoryNo ? `仓库单: ${inventoryNo}` : '', + ] + .filter(Boolean) + .join(' / ')} + + + ) : null} + + + {effectiveNote || '-'} + + + +
+ + {isConsignment ? '卖家上架/主页截图' : '打手主页截图'} + +
+ {detailOrder.gift.boosterMaterial?.screenshots?.length ? ( + + ) : ( + 未提交 + )} +
+
+
+
+ ) + })() + : null}