diff --git a/apps/backend/src/index.js b/apps/backend/src/index.js index 1feb1570..43826966 100644 --- a/apps/backend/src/index.js +++ b/apps/backend/src/index.js @@ -7,6 +7,7 @@ import adminRouter from './routes/admin.js' import claimsRouter from './routes/claims.js' import webhooksRouter from './routes/webhooks.js' import { ensureAdminUsersBootstrapped } from './services/admin/admin-auth-service.js' +import { repairAgisoOrderData } from './services/order/order-repair-service.js' import { closeLocalOcrWorker, warmupLocalOcrWorker } from './services/session/ocr.js' import { closeAllTencentBrowserSessions, warmupTencentBrowser } from './services/session/session.js' import { logError, logInfo, logWarn } from './utils/logger.js' @@ -17,6 +18,7 @@ const port = Number(runtimeConfig.server.port || 3000) runDatabaseMigrations() await ensureAdminUsersBootstrapped() +await repairHistoricalOrders() app.use((req, res, next) => { res.setHeader('Access-Control-Allow-Origin', '*') @@ -89,6 +91,14 @@ async function bootstrapOcr() { } } +async function repairHistoricalOrders() { + try { + await repairAgisoOrderData() + } catch (error) { + logWarn('[startup]', `历史 Agiso 咸鱼订单修复跳过: ${formatStartupError(error)}`) + } +} + function formatStartupError(error) { if (error instanceof Error && error.message) { return error.message.split('\n')[0].trim() diff --git a/apps/backend/src/services/admin/admin-service.js b/apps/backend/src/services/admin/admin-service.js index 8c30597e..243d762b 100644 --- a/apps/backend/src/services/admin/admin-service.js +++ b/apps/backend/src/services/admin/admin-service.js @@ -924,7 +924,9 @@ function mapAdminTaskListItem(task) { function mapAdminOrderListItem(item) { const tasks = listTasksByOrderId(item.id) + const orderItems = listOrderItemsByOrderId(item.id) const bindingSummary = buildOrderBindingSummary(tasks) + const itemSummary = summarizeOrderItems(orderItems) return { orderId: item.id, @@ -935,12 +937,18 @@ function mapAdminOrderListItem(item) { platformOrderId: item.platform_order_id, orderStatus: item.order_status, payStatus: item.pay_status, + buyerId: item.buyer_id || '', buyerName: item.buyer_name, + receiverContact: item.receiver_contact || '', totalAmount: formatFenToAmount(item.total_amount), totalAmountFen: normalizeFen(item.total_amount), currency: item.currency, + paidAt: item.paid_at, createdAt: item.created_at, updatedAt: item.updated_at, + itemCount: orderItems.length, + totalQuantity: orderItems.reduce((sum, orderItem) => sum + Math.max(1, Number(orderItem.quantity || 1)), 0), + itemSummary, taskCount: Number(item.task_count || tasks.length || 0), systemBindingStatus: bindingSummary.systemBindingStatus, userBindingStatus: bindingSummary.userBindingStatus, @@ -949,6 +957,23 @@ function mapAdminOrderListItem(item) { } } +function summarizeOrderItems(items) { + const normalizedItems = Array.isArray(items) ? items : [] + + if (normalizedItems.length === 0) { + return '' + } + + const [firstItem] = normalizedItems + const firstLabel = String(firstItem?.sku_name || firstItem?.sku_code || '').trim() + + if (normalizedItems.length === 1) { + return firstLabel + } + + return `${firstLabel} 等 ${normalizedItems.length} 项` +} + function buildOrderBindingSummary(tasks) { const normalizedTasks = Array.isArray(tasks) ? tasks : [] const totalTaskCount = normalizedTasks.length diff --git a/apps/backend/src/services/order/webhook-service.js b/apps/backend/src/services/order/webhook-service.js index 086cddc7..fa746a0f 100644 --- a/apps/backend/src/services/order/webhook-service.js +++ b/apps/backend/src/services/order/webhook-service.js @@ -2,6 +2,7 @@ import crypto from 'node:crypto' import { runtimeConfig } from '../../config/runtime.js' import { createWebhookEvent, updateWebhookEvent } from '../../repositories/webhook-event-repo.js' +import { enrichAgisoXianyuTradeOrder } from '../platforms/agiso/xianyu/order-detail-service.js' import { upsertOrderFromWebhook } from './order-service.js' import { createHttpError } from '../../utils/http.js' import { logWebhook } from '../../utils/logger.js' @@ -87,7 +88,11 @@ async function executeAgisoTradeWebhook(parsed, webhookEventId, { requestId = '' }) } - const result = await upsertOrderFromWebhook(parsed) + const enriched = await enrichTradeBeforeUpsert(parsed, { + requestId, + webhookEventId, + }) + const result = await upsertOrderFromWebhook(enriched.parsed) updateWebhookEvent(webhookEventId, { processed: true, process_error: '', @@ -103,22 +108,28 @@ async function executeAgisoTradeWebhook(parsed, webhookEventId, { requestId = '' shopName: parsed.shopName, eventKey: parsed.eventKey, eventType: parsed.eventType, - platformOrderId: parsed.platformOrderId, + platformOrderId: enriched.parsed.platformOrderId, orderId: result.order.id, + enriched: enriched.enriched, + enrichReason: enriched.reason || '', + totalAmountFen: Number(enriched.parsed.totalAmount || 0), taskCount: result.tasks.length, messageDeliveryCount: Array.isArray(result.messageDeliveries) ? result.messageDeliveries.length : 0, }) return { accepted: true, - provider: parsed.provider, - platform: parsed.platform, - shopId: parsed.shopId, - shopName: parsed.shopName, - platformRaw: parsed.platformRaw, - eventType: parsed.eventType, - platformOrderId: parsed.platformOrderId, + provider: enriched.parsed.provider, + platform: enriched.parsed.platform, + shopId: enriched.parsed.shopId, + shopName: enriched.parsed.shopName, + platformRaw: enriched.parsed.platformRaw, + eventType: enriched.parsed.eventType, + platformOrderId: enriched.parsed.platformOrderId, orderId: result.order.id, + totalAmountFen: Number(enriched.parsed.totalAmount || 0), + enriched: enriched.enriched, + enrichReason: enriched.reason || '', taskCount: result.tasks.length, messageDeliveries: result.messageDeliveries || [], tasks: result.tasks.map((task) => ({ @@ -156,6 +167,40 @@ async function executeAgisoTradeWebhook(parsed, webhookEventId, { requestId = '' } } +async function enrichTradeBeforeUpsert(parsed, { requestId = '', webhookEventId = null } = {}) { + if (parsed.provider !== 'agiso' || parsed.platform !== 'xianyu') { + return { parsed, enriched: false, reason: 'not_supported' } + } + + if (Number(parsed.totalAmount || 0) > 0) { + return { parsed, enriched: false, reason: 'already_has_amount' } + } + + const detailResult = await enrichAgisoXianyuTradeOrder(parsed, { requestId }) + const nextParsed = detailResult?.parsed || parsed + + logWebhook('[webhook-service/agiso]', 'Agiso webhook 订单补查结束', { + requestId, + webhookEventId, + provider: nextParsed.provider, + platform: nextParsed.platform, + shopId: nextParsed.shopId, + platformOrderId: nextParsed.platformOrderId, + enriched: Boolean(detailResult?.enriched), + reason: String(detailResult?.reason || ''), + totalAmountFen: Number(nextParsed.totalAmount || 0), + paidAt: nextParsed.paidAt || null, + errorMessage: String(detailResult?.errorMessage || ''), + }) + + return { + parsed: nextParsed, + enriched: Boolean(detailResult?.enriched), + reason: String(detailResult?.reason || ''), + errorMessage: String(detailResult?.errorMessage || ''), + } +} + function parseAgisoTradeRequest(requestLike) { const query = normalizeRecord(requestLike.query) const body = normalizeRecord(requestLike.body) diff --git a/apps/frontend/src/types/admin.ts b/apps/frontend/src/types/admin.ts index 7df6fdb1..a200d941 100644 --- a/apps/frontend/src/types/admin.ts +++ b/apps/frontend/src/types/admin.ts @@ -80,16 +80,25 @@ export interface AdminDashboardSummary { export interface AdminOrderListItem { orderId: number + provider: string platform: string + shopId: string + shopName: string platformOrderId: string orderStatus: string payStatus: string + buyerId: string buyerName: string + receiverContact: string totalAmount: string totalAmountFen: number currency: string + paidAt: string | null createdAt: string updatedAt: string + itemCount: number + totalQuantity: number + itemSummary: string taskCount: number systemBindingStatus: string userBindingStatus: string @@ -139,7 +148,10 @@ export interface AdminTaskActionResponse { export interface AdminOrderDetail { order: { orderId: number + provider: string platform: string + shopId: string + shopName: string platformOrderId: string orderStatus: string payStatus: string @@ -187,7 +199,10 @@ export interface AdminOrderDetail { }> webhookEvents: Array<{ eventId: number + provider: string platform: string + shopId: string + shopName: string eventType: string eventKey: string signatureValid: boolean diff --git a/apps/frontend/src/views/admin/AdminOrderDetailView.vue b/apps/frontend/src/views/admin/AdminOrderDetailView.vue index d3cbd622..a0736272 100644 --- a/apps/frontend/src/views/admin/AdminOrderDetailView.vue +++ b/apps/frontend/src/views/admin/AdminOrderDetailView.vue @@ -23,6 +23,19 @@ onMounted(async () => { loading.value = false } }) + +function formatJsonBlock(value: Record) { + return JSON.stringify(value || {}, null, 2) +} + +function formatSpecSummary(spec: Record) { + const entries = Object.entries(spec || {}) + .filter(([, value]) => value !== null && value !== undefined && String(value).trim() !== '') + .slice(0, 4) + .map(([key, value]) => `${key}: ${String(value)}`) + + return entries.length > 0 ? entries.join(' | ') : '-' +} @@ -137,6 +199,85 @@ onMounted(async () => { padding: 18px; border-radius: 20px; background: rgba(255,255,255,.94); border: 1px solid rgba(86,108,138,.1); } .error-copy { color: #b42318; } +.info-card-header { + display: flex; + justify-content: space-between; + gap: 16px; + align-items: flex-start; + margin-bottom: 16px; +} +.info-card-header h2 { + margin: 0; + color: #1d3555; +} +.subtitle { + margin: 8px 0 0; + color: #64748b; +} +.status-stack { + display: flex; + flex-wrap: wrap; + gap: 6px; + justify-content: flex-end; +} +.info-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; + margin-bottom: 16px; +} +.info-tile { + padding: 14px 16px; + border-radius: 16px; + background: linear-gradient(180deg, rgba(247, 250, 255, 0.98), rgba(241, 245, 249, 0.92)); + border: 1px solid rgba(86,108,138,.08); + display: grid; + gap: 6px; +} +.info-tile span, +.info-tile small { + color: #64748b; +} +.info-tile strong { + color: #1d3555; +} +.meta-table, .data-table { width: 100%; border-collapse: collapse; } -.data-table th,.data-table td { padding: 12px 10px; text-align: left; border-bottom: 1px solid rgba(86,108,138,.08); } +.meta-table th, +.meta-table td, +.data-table th, +.data-table td { padding: 12px 10px; text-align: left; border-bottom: 1px solid rgba(86,108,138,.08); } +.meta-table th, +.data-table th { color: #64748b; width: 120px; } +.payload-box summary { + cursor: pointer; + color: #1d4ed8; + font-weight: 600; +} +.payload-box pre { + margin: 12px 0 0; + padding: 14px; + overflow: auto; + border-radius: 16px; + background: #0f172a; + color: #dbeafe; + font-size: 12px; + line-height: 1.6; +} +@media (max-width: 1200px) { + .info-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} +@media (max-width: 720px) { + .info-card-header { + flex-direction: column; + } + .status-stack { + justify-content: flex-start; + } + .info-grid { + grid-template-columns: 1fr; + } +} diff --git a/apps/frontend/src/views/admin/AdminOrdersView.vue b/apps/frontend/src/views/admin/AdminOrdersView.vue index c37bf4dd..54ec5972 100644 --- a/apps/frontend/src/views/admin/AdminOrdersView.vue +++ b/apps/frontend/src/views/admin/AdminOrdersView.vue @@ -46,6 +46,22 @@ async function loadOrders(page = pagination.value.page) { } onMounted(loadOrders) + +function resolveBuyerLabel(item: AdminOrderListItem) { + return item.buyerName || item.buyerId || '未识别' +} + +function resolveContactLabel(item: AdminOrderListItem) { + return item.receiverContact || '未提供' +} + +function resolveShopLabel(item: AdminOrderListItem) { + return item.shopName || item.shopId || '未识别店铺' +} + +function resolveItemSummary(item: AdminOrderListItem) { + return item.itemSummary || '未识别商品' +}