fix(worker-platform): 修复寄售匹配工单详情中卖家上架截图丢失问题,并收敛手动匹配为内部现货直接履约

1. 修复文件归一化器 normalizeUploadedFiles 遇到纯字符串 URL 数组被误过滤为空的问题,自动提取 objectKey 并构造规范 UploadedFile 结构;
2. 优化工单展示映射器 resolveWorkOrderEffectiveBoosterMaterial:当工单资料中截图为空时,自动回退并加载寄售卖家上架/主页截图;
3. 前端管理后台详情抽屉与打手端详情抽屉支持读取 detailOrder.gift.boosterMaterial.screenshots 与 consignmentInfo.screenshots 降级兜底渲染;
4. 手动匹配弹窗与发布拦截弹窗全面收敛为单一标准的【内部现货直接履约】,废除旧版指派打手带来的重复扣押金与二次填资料问题;
5. 新增 consignment-booster-material 单元测试,更新寄卖库存优化现状与进度追踪文档,前后端全套测试通过。
This commit is contained in:
yml2213
2026-09-21 11:35:38 +08:00
parent 040b3db84c
commit c80bd0e337
9 changed files with 372 additions and 102 deletions
@@ -214,7 +214,10 @@ export function refreshUploadedFileUrls(value: unknown) {
value && typeof value === 'object' && !Array.isArray(value)
? (value as Record<string, unknown>)
: {}
const objectKey = String(source.objectKey || source.object_key || '').trim()
const rawUrl = String(source.url || '').trim()
const objectKey =
String(source.objectKey || source.object_key || '').trim() ||
extractObjectKeyFromFileUrl(rawUrl)
if (!objectKey) return source
return {
...source,
@@ -274,7 +277,7 @@ function buildUnsignedFileUrl(objectKey: string) {
return `/api/v1/files/object?key=${encodeURIComponent(normalizeObjectKey(objectKey))}`
}
function extractObjectKeyFromFileUrl(value: string) {
export function extractObjectKeyFromFileUrl(value: string) {
if (!value) return ''
try {
const parsed = new URL(value, 'http://localhost')
@@ -0,0 +1,81 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { normalizeUploadedFiles } from './worker-input-normalizers.js'
import { resolveWorkOrderEffectiveBoosterMaterial } from './work-order-display-mappers.js'
test('normalizeUploadedFiles 兼容字符串 URL 数组并自动提取 objectKey', () => {
const input = [
'/api/v1/files/object?key=consignment_screenshot%2F2026%2F09%2F19%2Fb8b974206a8a0cf6c89a6296.webp',
]
const result = normalizeUploadedFiles(input)
assert.equal(result.length, 1)
assert.equal(
result[0].objectKey,
'consignment_screenshot/2026/09/19/b8b974206a8a0cf6c89a6296.webp',
)
assert.equal(
result[0].url,
'/api/v1/files/object?key=consignment_screenshot%2F2026%2F09%2F19%2Fb8b974206a8a0cf6c89a6296.webp',
)
})
test('resolveWorkOrderEffectiveBoosterMaterial 在寄售工单无打手截图时自动回退卖家主页截图', () => {
const row = {
id: '100140',
booster_material_json: {},
material_json: {
consignment: {
inventoryId: 23,
buyerName: '客服',
sellerGameId: '草莓酱板鸭 (1234551)',
serverZone: '互通',
system: '安卓',
screenshots: [
'/api/v1/files/object?key=consignment_screenshot%2F2026%2F09%2F19%2Fb8b974206a8a0cf6c89a6296.webp',
],
},
},
} as never
const material = resolveWorkOrderEffectiveBoosterMaterial(row)
assert.equal(material.isConsignmentSellerMaterial, true)
assert.equal(material.fields.gameNickname, '客服')
assert.equal(material.fields.gameId, '草莓酱板鸭 (1234551)')
assert.equal(material.screenshots.length, 1)
assert.equal(
material.screenshots[0].objectKey,
'consignment_screenshot/2026/09/19/b8b974206a8a0cf6c89a6296.webp',
)
})
test('resolveWorkOrderEffectiveBoosterMaterial 在已写入 fields 但无截图时依然回退卖家主页截图', () => {
const row = {
id: '100140',
booster_material_json: {
fields: {
gameNickname: '客服',
gameId: '1234551',
},
screenshots: [],
isConsignmentSellerMaterial: true,
},
material_json: {
consignment: {
inventoryId: 23,
screenshots: [
'/api/v1/files/object?key=consignment_screenshot%2F2026%2F09%2F19%2Fb8b974206a8a0cf6c89a6296.webp',
],
},
},
} as never
const material = resolveWorkOrderEffectiveBoosterMaterial(row)
assert.equal(material.isConsignmentSellerMaterial, true)
assert.equal(material.fields.gameNickname, '客服')
assert.equal(material.screenshots.length, 1)
assert.equal(
material.screenshots[0].objectKey,
'consignment_screenshot/2026/09/19/b8b974206a8a0cf6c89a6296.webp',
)
})
@@ -101,16 +101,6 @@ export function resolveWorkOrderEffectiveBoosterMaterial(
)
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)
@@ -118,12 +108,38 @@ export function resolveWorkOrderEffectiveBoosterMaterial(
(consignment && typeof consignment === 'object' && Object.keys(consignment).length > 0) ||
Boolean(consignmentInventory)
if (hasConsignment) {
const rawScreenshots =
consignment?.screenshots ||
const rawConsignmentScreenshots = hasConsignment
? consignment?.screenshots ||
consignmentInventory?.screenshots_json ||
consignmentInventory?.submission_screenshots_json ||
[]
: []
const consignmentScreenshots =
normalizeUploadedFiles(rawConsignmentScreenshots).map(refreshUploadedFileUrls)
const effectiveScreenshots = hasExistingScreenshots
? existingScreenshots
: hasConsignment
? consignmentScreenshots
: []
const isConsignmentSeller = Boolean(existingBooster.isConsignmentSellerMaterial || hasConsignment)
if (hasExistingFields || hasExistingScreenshots) {
return {
fields: existingFields,
screenshots: effectiveScreenshots,
note: String(
existingBooster.note || consignment?.remark || consignmentInventory?.remark || '',
),
submittedAt: String(
existingBooster.submittedAt || consignment?.fulfilledAt || consignment?.assignedAt || '',
),
isConsignmentSellerMaterial: isConsignmentSeller,
}
}
if (hasConsignment) {
const fields: Record<string, string> = {
gameNickname: String(
consignment?.buyerName ||
@@ -153,7 +169,7 @@ export function resolveWorkOrderEffectiveBoosterMaterial(
return {
fields,
screenshots: normalizeUploadedFiles(rawScreenshots).map(refreshUploadedFileUrls),
screenshots: consignmentScreenshots,
note: String(
consignment?.remark || consignmentInventory?.remark || consignment?.matchNote || '',
).trim(),
@@ -1,5 +1,8 @@
import type { JsonObject } from '../../types/json.js'
import { normalizeStoredFileUrl } from '../file-storage/file-storage-service.js'
import {
extractObjectKeyFromFileUrl,
normalizeStoredFileUrl,
} from '../file-storage/file-storage-service.js'
import { normalizeInteger } from './worker-normalizers.js'
export function normalizeSubmittedFields(value: unknown) {
@@ -33,19 +36,38 @@ export function normalizeUploadedFiles(value: unknown) {
if (!Array.isArray(value)) return []
return value
.map((item) => {
if (typeof item === 'string') {
const rawUrl = item.trim()
if (!rawUrl) return null
const objectKey = extractObjectKeyFromFileUrl(rawUrl)
const normalizedUrl = normalizeStoredFileUrl(rawUrl, objectKey)
return {
objectKey,
url: normalizedUrl,
thumbnailUrl: normalizedUrl,
mediumUrl: normalizedUrl,
filename: '',
contentType: '',
size: 0,
}
}
const source =
item && typeof item === 'object' && !Array.isArray(item) ? (item as JsonObject) : {}
const objectKey = String(source.objectKey || source.object_key || '').trim()
const url = String(source.url || '').trim()
if (!objectKey && !url) return null
const rawObjectKey = String(source.objectKey || source.object_key || '').trim()
const rawUrl = String(source.url || '').trim()
const objectKey = rawObjectKey || extractObjectKeyFromFileUrl(rawUrl)
if (!objectKey && !rawUrl) return null
return {
objectKey,
url: normalizeStoredFileUrl(url, objectKey),
url: normalizeStoredFileUrl(rawUrl, objectKey),
thumbnailUrl: normalizeStoredFileUrl(
source.thumbnailUrl || source.thumbnail_url || url,
source.thumbnailUrl || source.thumbnail_url || rawUrl,
objectKey,
),
mediumUrl: normalizeStoredFileUrl(
source.mediumUrl || source.medium_url || rawUrl,
objectKey,
),
mediumUrl: normalizeStoredFileUrl(source.mediumUrl || source.medium_url || url, objectKey),
filename: String(source.filename || '').trim(),
contentType: String(source.contentType || source.content_type || '').trim(),
size: normalizeInteger(source.size, 0),