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:
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user