diff --git a/apps/backend/src/services/admin/admin-service.js b/apps/backend/src/services/admin/admin-service.js index 243d762b..93e9e98c 100644 --- a/apps/backend/src/services/admin/admin-service.js +++ b/apps/backend/src/services/admin/admin-service.js @@ -74,6 +74,7 @@ export function getAdminOrderDetail(orderId) { const items = listOrderItemsByOrderId(order.id) const tasks = listTasksByOrderId(order.id) const webhookEvents = listWebhookEventsByOrderId(order.id) + const itemSummary = summarizeOrderItems(items) return { order: { @@ -94,6 +95,7 @@ export function getAdminOrderDetail(orderId) { paidAt: order.paid_at, createdAt: order.created_at, updatedAt: order.updated_at, + itemSummary, rawPayload: safeParseJson(order.raw_payload_json), bindingSummary: buildOrderBindingSummary(tasks), }, @@ -101,6 +103,7 @@ export function getAdminOrderDetail(orderId) { orderItemId: item.id, skuCode: item.sku_code, skuName: item.sku_name, + itemTitle: resolveOrderItemTitle(item), quantity: item.quantity, deliveryMode: item.delivery_mode, spec: safeParseJson(item.spec_json), @@ -965,7 +968,8 @@ function summarizeOrderItems(items) { } const [firstItem] = normalizedItems - const firstLabel = String(firstItem?.sku_name || firstItem?.sku_code || '').trim() + const firstLabel = resolveOrderItemTitle(firstItem) + || String(firstItem?.sku_name || firstItem?.sku_code || '').trim() if (normalizedItems.length === 1) { return firstLabel @@ -974,6 +978,25 @@ function summarizeOrderItems(items) { return `${firstLabel} 等 ${normalizedItems.length} 项` } +function resolveOrderItemTitle(item) { + if (!item) { + return '' + } + + const spec = safeParseJson(item.spec_json) + + return pickFirstNonEmpty([ + spec.title, + spec.Title, + spec.itemTitle, + spec.item_title, + spec.goods_name, + spec.goodsName, + item.sku_name, + item.sku_code, + ]) +} + function buildOrderBindingSummary(tasks) { const normalizedTasks = Array.isArray(tasks) ? tasks : [] const totalTaskCount = normalizedTasks.length diff --git a/apps/backend/src/services/order/order-repair-service.js b/apps/backend/src/services/order/order-repair-service.js index 97c77629..cd7fdb8b 100644 --- a/apps/backend/src/services/order/order-repair-service.js +++ b/apps/backend/src/services/order/order-repair-service.js @@ -1,4 +1,5 @@ import { getDb, runInTransaction } from '../../db/client.js' +import { listOrderItemsByOrderId, replaceOrderItems } from '../../repositories/order-item-repo.js' import { enrichAgisoXianyuTradeOrder } from '../platforms/agiso/xianyu/order-detail-service.js' import { logInfo } from '../../utils/logger.js' import { nowIso } from '../../utils/time.js' @@ -37,6 +38,7 @@ export async function repairAgisoOrderData() { let repairedOrderIdCount = 0 let repairedAmountCount = 0 + let repairedOrderItemCount = 0 for (const row of rows) { const latestRequestBody = parseJsonObject(row.latest_body_json) @@ -91,6 +93,11 @@ export async function repairAgisoOrderData() { const nextBuyerName = pickFirstNonEmpty([detailResult.parsed.buyerName, row.buyer_name]) const nextReceiverContact = pickFirstNonEmpty([detailResult.parsed.receiverContact, row.receiver_contact]) const nextPaidAt = detailResult.parsed.paidAt || row.paid_at + const currentOrderItems = listOrderItemsByOrderId(row.id) + const repairedItems = Array.isArray(detailResult.parsed.items) ? detailResult.parsed.items : [] + const shouldRepairOrderItems = repairedItems.length > 0 + && (currentOrderItems.length === 0 || currentOrderItems.length === repairedItems.length) + && hasOrderItemChanges(currentOrderItems, repairedItems) const orderIdChanged = nextOrderId && nextOrderId !== String(row.platform_order_id || '') const amountChanged = nextAmount > 0 && nextAmount !== Number(row.total_amount || 0) @@ -101,7 +108,7 @@ export async function repairAgisoOrderData() { || nextReceiverContact !== String(row.receiver_contact || '') || nextPaidAt !== row.paid_at - if (!orderIdChanged && !amountChanged && !payloadChanged && !metadataChanged) { + if (!orderIdChanged && !amountChanged && !payloadChanged && !metadataChanged && !shouldRepairOrderItems) { continue } @@ -132,6 +139,22 @@ export async function repairAgisoOrderData() { row.id, ) + if (shouldRepairOrderItems) { + const itemNow = nowIso() + replaceOrderItems( + row.id, + repairedItems.map((item) => ({ + skuCode: item.skuCode, + skuName: item.skuName, + quantity: item.quantity, + specJson: JSON.stringify(item.spec || {}), + deliveryMode: 'claim_link', + createdAt: itemNow, + updatedAt: itemNow, + })), + ) + } + if (orderIdChanged) { db.prepare('UPDATE delivery_tasks SET platform_order_id = ? WHERE order_id = ?').run(nextOrderId, row.id) db.prepare(` @@ -155,16 +178,38 @@ export async function repairAgisoOrderData() { if (amountChanged) { repairedAmountCount += 1 } + + if (shouldRepairOrderItems) { + repairedOrderItemCount += 1 + } } - if (repairedOrderIdCount > 0 || repairedAmountCount > 0) { + if (repairedOrderIdCount > 0 || repairedAmountCount > 0 || repairedOrderItemCount > 0) { logInfo('[startup]', '已自动修复历史 Agiso 咸鱼订单数据', { repairedOrderIdCount, repairedAmountCount, + repairedOrderItemCount, }) } } +function hasOrderItemChanges(currentItems, repairedItems) { + if (!Array.isArray(currentItems) || !Array.isArray(repairedItems) || currentItems.length !== repairedItems.length) { + return repairedItems.length > 0 + } + + return repairedItems.some((item, index) => { + const current = currentItems[index] + const currentSpec = parseJsonObject(current?.spec_json, { preserveLargeIntegers: true }) + const nextSpec = item?.spec || {} + + return String(current?.sku_code || '').trim() !== String(item?.skuCode || '').trim() + || String(current?.sku_name || '').trim() !== String(item?.skuName || '').trim() + || Number(current?.quantity || 0) !== Number(item?.quantity || 0) + || JSON.stringify(currentSpec) !== JSON.stringify(nextSpec) + }) +} + function pickFirstNonEmpty(values) { for (const value of values) { const normalized = String(value || '').trim() diff --git a/apps/backend/src/services/order/webhook-service.js b/apps/backend/src/services/order/webhook-service.js index fa746a0f..603001ca 100644 --- a/apps/backend/src/services/order/webhook-service.js +++ b/apps/backend/src/services/order/webhook-service.js @@ -5,6 +5,7 @@ import { createWebhookEvent, updateWebhookEvent } from '../../repositories/webho import { enrichAgisoXianyuTradeOrder } from '../platforms/agiso/xianyu/order-detail-service.js' import { upsertOrderFromWebhook } from './order-service.js' import { createHttpError } from '../../utils/http.js' +import { parseJsonObject } from '../../utils/json.js' import { logWebhook } from '../../utils/logger.js' import { parseAmountToFen } from '../../utils/money.js' import { nowIso } from '../../utils/time.js' @@ -172,8 +173,8 @@ async function enrichTradeBeforeUpsert(parsed, { requestId = '', webhookEventId return { parsed, enriched: false, reason: 'not_supported' } } - if (Number(parsed.totalAmount || 0) > 0) { - return { parsed, enriched: false, reason: 'already_has_amount' } + if (!shouldEnrichAgisoXianyuTrade(parsed)) { + return { parsed, enriched: false, reason: 'enough_fields_present' } } const detailResult = await enrichAgisoXianyuTradeOrder(parsed, { requestId }) @@ -201,6 +202,54 @@ async function enrichTradeBeforeUpsert(parsed, { requestId = '', webhookEventId } } +function shouldEnrichAgisoXianyuTrade(parsed) { + if (!parsed || parsed.provider !== 'agiso' || parsed.platform !== 'xianyu') { + return false + } + + if (Number(parsed.totalAmount || 0) <= 0) { + return true + } + + if (!String(parsed.buyerName || '').trim()) { + return true + } + + if (!String(parsed.shopName || '').trim()) { + return true + } + + if (!hasDetailedOrderItems(parsed.items)) { + return true + } + + return false +} + +function hasDetailedOrderItems(items) { + const normalizedItems = Array.isArray(items) ? items : [] + + if (normalizedItems.length === 0) { + return false + } + + return normalizedItems.some((item) => { + const spec = isPlainObject(item?.spec) ? item.spec : {} + const title = pickFirstNonEmpty([ + spec.title, + spec.Title, + spec.itemTitle, + spec.item_title, + spec.goods_name, + spec.goodsName, + item?.skuName, + ]) + const skuCode = String(item?.skuCode || '').trim() + + return Boolean(title && title !== skuCode) + }) +} + function parseAgisoTradeRequest(requestLike) { const query = normalizeRecord(requestLike.query) const body = normalizeRecord(requestLike.body) @@ -473,15 +522,10 @@ function parsePayloadJson(rawJson) { return {} } - try { - const parsed = JSON.parse(rawJson) - return isPlainObject(parsed) ? parsed : {} - } catch { - throw createHttpError('json 参数不是合法 JSON', { - statusCode: 400, - errorCode: 'invalid_json_payload', - }) - } + return parseJsonObject(rawJson, { + preserveLargeIntegers: true, + throwOnError: true, + }) } function normalizeOrderItems(payload) { diff --git a/apps/backend/src/services/platforms/agiso/xianyu/order-detail-service.js b/apps/backend/src/services/platforms/agiso/xianyu/order-detail-service.js index 23793afd..77944c86 100644 --- a/apps/backend/src/services/platforms/agiso/xianyu/order-detail-service.js +++ b/apps/backend/src/services/platforms/agiso/xianyu/order-detail-service.js @@ -273,6 +273,7 @@ function mergeEnrichedTrade(parsed, detailPayload, { keepOriginalAmount = false _agisoTradeDetail: detailPayload, } const totalAmountFen = resolveTotalAmountFen(detailPayload) + const mergedItems = normalizeOrderItems(detailPayload, normalizeOrderItems(parsed.rawPayload, parsed.items)) return { ...parsed, @@ -317,7 +318,7 @@ function mergeEnrichedTrade(parsed, detailPayload, { keepOriginalAmount = false totalAmount: keepOriginalAmount ? parsed.totalAmount : (totalAmountFen || parsed.totalAmount), paidAt: parsed.paidAt || resolvePaidAt(detailPayload, parsed.paidAt), rawPayload: mergedPayload, - items: normalizeOrderItems(mergedPayload, parsed.items), + items: mergedItems, } } diff --git a/apps/frontend/src/types/admin.ts b/apps/frontend/src/types/admin.ts index a200d941..f424d9da 100644 --- a/apps/frontend/src/types/admin.ts +++ b/apps/frontend/src/types/admin.ts @@ -164,6 +164,7 @@ export interface AdminOrderDetail { paidAt: string | null createdAt: string updatedAt: string + itemSummary: string rawPayload: Record bindingSummary: { totalTaskCount: number @@ -177,6 +178,7 @@ export interface AdminOrderDetail { orderItemId: number skuCode: string skuName: string + itemTitle: string quantity: number deliveryMode: string spec: Record diff --git a/apps/frontend/src/views/admin/AdminOrderDetailView.vue b/apps/frontend/src/views/admin/AdminOrderDetailView.vue index a0736272..6e924233 100644 --- a/apps/frontend/src/views/admin/AdminOrderDetailView.vue +++ b/apps/frontend/src/views/admin/AdminOrderDetailView.vue @@ -66,6 +66,11 @@ function formatSpecSummary(spec: Record) {
+
+ 商品标题 + {{ detail.order.itemSummary || '-' }} + 来自订单详情 title / 商品标题字段 +
订单金额 {{ detail.order.totalAmount }} {{ detail.order.currency }} @@ -103,6 +108,7 @@ function formatSpecSummary(spec: Record) { SKU + 标题 商品 发放方式 数量 @@ -112,6 +118,7 @@ function formatSpecSummary(spec: Record) { {{ item.skuCode }} + {{ item.itemTitle || '-' }} {{ item.skuName }} {{ item.deliveryMode }} {{ item.quantity }}