优化了订单展示

This commit is contained in:
yml
2026-04-09 02:03:39 +08:00
parent e4ab1f559e
commit 98cc0c16fc
6 changed files with 321 additions and 29 deletions
+10
View File
@@ -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()
@@ -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
@@ -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)
+15
View File
@@ -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
@@ -23,6 +23,19 @@ onMounted(async () => {
loading.value = false
}
})
function formatJsonBlock(value: Record<string, unknown>) {
return JSON.stringify(value || {}, null, 2)
}
function formatSpecSummary(spec: Record<string, unknown>) {
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(' | ') : '-'
}
</script>
<template>
@@ -39,16 +52,49 @@ onMounted(async () => {
<template v-else-if="detail">
<section class="info-card">
<h2>{{ detail.order.platformOrderId }}</h2>
<p>支付状态<AdminStatusTag :status="detail.order.payStatus" /> · 订单状态<AdminStatusTag :status="detail.order.orderStatus" /></p>
<p>
系统绑定<AdminStatusTag :status="detail.order.bindingSummary.systemBindingStatus" />
· 完整绑定<AdminStatusTag :status="detail.order.bindingSummary.userBindingStatus" />
</p>
<p>
任务进度{{ detail.order.bindingSummary.completedBindingTaskCount }}/{{ detail.order.bindingSummary.totalTaskCount }} 完整绑定
· {{ detail.order.bindingSummary.systemBoundTaskCount }}/{{ detail.order.bindingSummary.totalTaskCount }} 系统绑定
</p>
<div class="info-card-header">
<div>
<h2>{{ detail.order.platformOrderId }}</h2>
<p class="subtitle">{{ detail.order.platform }} / {{ detail.order.shopName || detail.order.shopId || '未识别店铺' }}</p>
</div>
<div class="status-stack">
<AdminStatusTag :status="detail.order.payStatus" />
<AdminStatusTag :status="detail.order.orderStatus" />
<AdminStatusTag :status="detail.order.bindingSummary.systemBindingStatus" />
<AdminStatusTag :status="detail.order.bindingSummary.userBindingStatus" />
</div>
</div>
<div class="info-grid">
<article class="info-tile">
<span>订单金额</span>
<strong>{{ detail.order.totalAmount }} {{ detail.order.currency }}</strong>
<small>支付时间{{ formatAdminDateTime(detail.order.paidAt) }}</small>
</article>
<article class="info-tile">
<span>买家信息</span>
<strong>{{ detail.order.buyerName || '-' }}</strong>
<small>ID{{ detail.order.buyerId || '-' }}</small>
</article>
<article class="info-tile">
<span>联系方式</span>
<strong>{{ detail.order.receiverContact || '-' }}</strong>
<small>店铺{{ detail.order.shopName || detail.order.shopId || '-' }}</small>
</article>
<article class="info-tile">
<span>任务进度</span>
<strong>{{ detail.order.bindingSummary.completedBindingTaskCount }}/{{ detail.order.bindingSummary.totalTaskCount }} 完整绑定</strong>
<small>{{ detail.order.bindingSummary.systemBoundTaskCount }}/{{ detail.order.bindingSummary.totalTaskCount }} 系统绑定</small>
</article>
</div>
<table class="meta-table">
<tbody>
<tr><th>内部订单 ID</th><td>{{ detail.order.orderId }}</td><th>提供方</th><td>{{ detail.order.provider }}</td></tr>
<tr><th>平台</th><td>{{ detail.order.platform }}</td><th>店铺 ID</th><td>{{ detail.order.shopId || '-' }}</td></tr>
<tr><th>创建时间</th><td>{{ formatAdminDateTime(detail.order.createdAt) }}</td><th>更新时间</th><td>{{ formatAdminDateTime(detail.order.updatedAt) }}</td></tr>
</tbody>
</table>
</section>
<section class="table-card">
@@ -58,14 +104,18 @@ onMounted(async () => {
<tr>
<th>SKU</th>
<th>商品</th>
<th>发放方式</th>
<th>数量</th>
<th>规格摘要</th>
</tr>
</thead>
<tbody>
<tr v-for="item in detail.items" :key="item.orderItemId">
<td>{{ item.skuCode }}</td>
<td>{{ item.skuName }}</td>
<td>{{ item.deliveryMode }}</td>
<td>{{ item.quantity }}</td>
<td>{{ formatSpecSummary(item.spec) }}</td>
</tr>
</tbody>
</table>
@@ -80,6 +130,7 @@ onMounted(async () => {
<th>状态</th>
<th>系统绑定</th>
<th>完整绑定</th>
<th>登录方式</th>
<th>错误</th>
</tr>
</thead>
@@ -91,6 +142,7 @@ onMounted(async () => {
<td><AdminStatusTag :status="task.status" /></td>
<td><AdminStatusTag :status="task.systemBindingStatus" /></td>
<td><AdminStatusTag :status="task.userBindingStatus" /></td>
<td>{{ task.loginType || '-' }}</td>
<td>{{ task.lastError || '-' }}</td>
</tr>
</tbody>
@@ -104,6 +156,7 @@ onMounted(async () => {
<tr>
<th>事件 ID</th>
<th>类型</th>
<th>平台 / 店铺</th>
<th>验签</th>
<th>处理</th>
<th>时间</th>
@@ -115,16 +168,25 @@ onMounted(async () => {
<RouterLink :to="`/admin/webhook-events/${event.eventId}`">{{ event.eventId }}</RouterLink>
</td>
<td>{{ formatAdminWebhookEventType(event.eventType) }}</td>
<td>{{ event.platform }} / {{ event.shopName || event.shopId || '-' }}</td>
<td><AdminStatusTag :status="event.signatureValid ? 'success' : 'failed'" /></td>
<td><AdminStatusTag :status="event.processed ? 'success' : 'failed'" /></td>
<td>{{ formatAdminDateTime(event.createdAt) }}</td>
</tr>
<tr v-if="detail.webhookEvents.length === 0">
<td colspan="5">当前订单还没有关联的 webhook 记录</td>
<td colspan="6">当前订单还没有关联的 webhook 记录</td>
</tr>
</tbody>
</table>
</section>
<section class="table-card">
<h3>原始载荷</h3>
<details class="payload-box">
<summary>展开查看原始订单 JSON</summary>
<pre>{{ formatJsonBlock(detail.order.rawPayload) }}</pre>
</details>
</section>
</template>
</section>
</template>
@@ -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;
}
}
</style>
@@ -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 || '未识别商品'
}
</script>
<template>
@@ -80,20 +96,35 @@ onMounted(loadOrders)
<table class="data-table">
<thead>
<tr>
<th>订单</th>
<th>平台</th>
<th>订单信息</th>
<th>平台 / 店铺</th>
<th>商品摘要</th>
<th>订单进度</th>
<th>金额</th>
<th>金额 / 时间</th>
<th>任务进度</th>
<th>创建时间</th>
</tr>
</thead>
<tbody>
<tr v-for="item in items" :key="item.orderId">
<td>
<RouterLink :to="`/admin/orders/${item.orderId}`">{{ item.platformOrderId }}</RouterLink>
<div class="cell-stack">
<RouterLink :to="`/admin/orders/${item.orderId}`" class="primary-link">{{ item.platformOrderId }}</RouterLink>
<span class="cell-main">{{ resolveBuyerLabel(item) }}</span>
<span class="cell-sub">联系方式{{ resolveContactLabel(item) }}</span>
</div>
</td>
<td>
<div class="cell-stack">
<span class="cell-main">{{ item.platform }}</span>
<span class="cell-sub">{{ resolveShopLabel(item) }}</span>
</div>
</td>
<td>
<div class="cell-stack">
<span class="cell-main">{{ resolveItemSummary(item) }}</span>
<span class="cell-sub">{{ item.itemCount }} 合计 {{ item.totalQuantity }} </span>
</div>
</td>
<td>{{ item.platform }}</td>
<td>
<div class="status-stack">
<AdminStatusTag :status="item.payStatus" />
@@ -102,7 +133,13 @@ onMounted(loadOrders)
<AdminStatusTag :status="item.userBindingStatus" />
</div>
</td>
<td>{{ item.totalAmount }} {{ item.currency }}</td>
<td>
<div class="cell-stack">
<span class="cell-main amount-text">{{ item.totalAmount }} {{ item.currency }}</span>
<span class="cell-sub">支付{{ formatAdminDateTime(item.paidAt) }}</span>
<span class="cell-sub">创建{{ formatAdminDateTime(item.createdAt) }}</span>
</div>
</td>
<td>
<div class="progress-stack">
<span
@@ -119,7 +156,6 @@ onMounted(loadOrders)
</span>
</div>
</td>
<td>{{ formatAdminDateTime(item.createdAt) }}</td>
</tr>
</tbody>
</table>
@@ -142,6 +178,26 @@ onMounted(loadOrders)
align-items: center;
}
.cell-stack {
display: grid;
gap: 4px;
}
.primary-link,
.cell-main {
font-weight: 600;
color: #1d3555;
}
.cell-sub {
color: #64748b;
font-size: 13px;
}
.amount-text {
white-space: nowrap;
}
.progress-stack {
display: flex;
flex-wrap: wrap;