优化了一些文件 增加多店铺
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
import { assignReservedCdk, findFirstAvailableCdkBySkuCode } from '../../repositories/cdk-repo.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
|
||||
export function reserveCdkForTask(skuCode, taskId) {
|
||||
if (!skuCode) {
|
||||
return null
|
||||
}
|
||||
|
||||
const available = findFirstAvailableCdkBySkuCode(skuCode)
|
||||
|
||||
if (!available) {
|
||||
return null
|
||||
}
|
||||
|
||||
return assignReservedCdk(available.id, taskId, nowIso())
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { createTask, listTasksByOrderId, updateTask } from '../../repositories/task-repo.js'
|
||||
import { reserveCdkForTask } from './cdk-service.js'
|
||||
import { createTaskClaimToken } from '../claim/claim-service.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
import { randomId } from '../../utils/random.js'
|
||||
|
||||
export function syncDeliveryTasksForOrder(order, orderItems) {
|
||||
const existingTasks = listTasksByOrderId(order.id)
|
||||
|
||||
if (existingTasks.length > 0) {
|
||||
if (order.pay_status !== 'paid') {
|
||||
return existingTasks
|
||||
}
|
||||
|
||||
const itemMap = new Map(orderItems.map((item) => [item.id, item]))
|
||||
return existingTasks.map((task) => preparePaidTask({
|
||||
...task,
|
||||
skuCode: itemMap.get(task.order_item_id)?.sku_code || '',
|
||||
skuName: itemMap.get(task.order_item_id)?.sku_name || '',
|
||||
}))
|
||||
}
|
||||
|
||||
const tasks = []
|
||||
|
||||
for (const item of orderItems) {
|
||||
const quantity = Math.max(1, Number(item.quantity || 1))
|
||||
|
||||
for (let index = 0; index < quantity; index += 1) {
|
||||
const createdAt = nowIso()
|
||||
const initialStatus = order.pay_status === 'paid' ? 'paid' : 'pending_payment'
|
||||
|
||||
const task = createTask({
|
||||
orderId: order.id,
|
||||
orderItemId: item.id,
|
||||
platformOrderId: order.platform_order_id,
|
||||
taskNo: randomId('DT'),
|
||||
taskStatus: initialStatus,
|
||||
loginType: '',
|
||||
claimTokenId: null,
|
||||
reservedCdkId: null,
|
||||
browserSessionId: '',
|
||||
nickname: '',
|
||||
roleId: '',
|
||||
roleName: '',
|
||||
area: '',
|
||||
partitionName: '',
|
||||
resultCode: '',
|
||||
resultMessage: '',
|
||||
screenshotPath: '',
|
||||
artifactsJson: '{}',
|
||||
lastError: '',
|
||||
retryCount: 0,
|
||||
expiresAt: null,
|
||||
claimedAt: null,
|
||||
roleConfirmedAt: null,
|
||||
redeemedAt: null,
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
})
|
||||
|
||||
tasks.push({
|
||||
...task,
|
||||
skuCode: item.sku_code,
|
||||
skuName: item.sku_name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (order.pay_status !== 'paid') {
|
||||
return tasks
|
||||
}
|
||||
|
||||
return tasks.map((task) => preparePaidTask(task))
|
||||
}
|
||||
|
||||
function preparePaidTask(task) {
|
||||
const now = nowIso()
|
||||
|
||||
if (['link_generated', 'claimed', 'role_confirmed', 'redeeming', 'redeemed'].includes(task.task_status)) {
|
||||
return task
|
||||
}
|
||||
|
||||
if (!task.skuCode) {
|
||||
return updateTask(task.id, {
|
||||
task_status: 'manual_review',
|
||||
last_error: '未匹配到 SKU,无法为任务分配 CDK',
|
||||
updated_at: now,
|
||||
})
|
||||
}
|
||||
|
||||
if (task.reserved_cdk_id && task.claim_token_id) {
|
||||
return updateTask(task.id, {
|
||||
task_status: 'link_generated',
|
||||
last_error: '',
|
||||
updated_at: now,
|
||||
})
|
||||
}
|
||||
|
||||
const reserved = reserveCdkForTask(task.skuCode, task.id)
|
||||
|
||||
if (!reserved) {
|
||||
return updateTask(task.id, {
|
||||
task_status: 'waiting_inventory',
|
||||
last_error: '库存不足,等待可用 CDK',
|
||||
updated_at: now,
|
||||
})
|
||||
}
|
||||
|
||||
const claimToken = createTaskClaimToken(task.id)
|
||||
|
||||
return updateTask(task.id, {
|
||||
task_status: 'link_generated',
|
||||
reserved_cdk_id: reserved.id,
|
||||
claim_token_id: claimToken.id,
|
||||
last_error: '',
|
||||
expires_at: claimToken.expired_at,
|
||||
updated_at: now,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import { getDb, runInTransaction } from '../../db/client.js'
|
||||
import { enrichAgisoXianyuTradeOrder } from '../platforms/agiso/xianyu/order-detail-service.js'
|
||||
import { logInfo } from '../../utils/logger.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
import { parseJsonObject } from '../../utils/json.js'
|
||||
|
||||
export async function repairAgisoOrderData() {
|
||||
const rows = getDb().prepare(`
|
||||
SELECT
|
||||
o.id,
|
||||
o.provider,
|
||||
o.platform,
|
||||
o.shop_id,
|
||||
o.shop_name,
|
||||
o.platform_order_id,
|
||||
o.total_amount,
|
||||
o.raw_payload_json,
|
||||
o.paid_at,
|
||||
o.buyer_id,
|
||||
o.buyer_name,
|
||||
o.receiver_contact,
|
||||
(
|
||||
SELECT we.body_json
|
||||
FROM webhook_events we
|
||||
WHERE we.related_order_id = o.id
|
||||
ORDER BY we.id DESC
|
||||
LIMIT 1
|
||||
) AS latest_body_json
|
||||
FROM orders o
|
||||
WHERE o.provider = 'agiso' AND o.platform = 'xianyu'
|
||||
ORDER BY o.id ASC
|
||||
`).all()
|
||||
|
||||
if (rows.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
let repairedOrderIdCount = 0
|
||||
let repairedAmountCount = 0
|
||||
|
||||
for (const row of rows) {
|
||||
const latestRequestBody = parseJsonObject(row.latest_body_json)
|
||||
const rawJson = String(latestRequestBody.json || '').trim()
|
||||
const webhookPayload = rawJson ? parseJsonObject(rawJson, { preserveLargeIntegers: true }) : {}
|
||||
const rawPayload = parseJsonObject(row.raw_payload_json, { preserveLargeIntegers: true })
|
||||
const expectedOrderId = pickFirstNonEmpty([
|
||||
webhookPayload.biz_order_id,
|
||||
webhookPayload.order_id,
|
||||
webhookPayload.orderId,
|
||||
rawPayload.biz_order_id,
|
||||
rawPayload.order_id,
|
||||
rawPayload.orderId,
|
||||
])
|
||||
|
||||
const parsed = {
|
||||
provider: row.provider,
|
||||
platform: row.platform,
|
||||
shopId: String(row.shop_id || '').trim(),
|
||||
shopName: String(row.shop_name || '').trim(),
|
||||
platformOrderId: expectedOrderId || String(row.platform_order_id || '').trim(),
|
||||
totalAmount: Number(row.total_amount || 0),
|
||||
buyerId: pickFirstNonEmpty([rawPayload.buyer_id, rawPayload.buyerId, rawPayload.BuyerId, row.buyer_id]),
|
||||
buyerName: pickFirstNonEmpty([rawPayload.buyer_name, rawPayload.buyerName, rawPayload.BuyerName, row.buyer_name]),
|
||||
receiverContact: pickFirstNonEmpty([
|
||||
rawPayload.receiver_contact,
|
||||
rawPayload.receiverContact,
|
||||
rawPayload.receiver_mobile,
|
||||
rawPayload.receiverMobile,
|
||||
rawPayload.mobile,
|
||||
rawPayload.phone,
|
||||
row.receiver_contact,
|
||||
]),
|
||||
paidAt: row.paid_at,
|
||||
rawPayload: Object.keys(webhookPayload).length > 0 ? webhookPayload : rawPayload,
|
||||
items: [],
|
||||
}
|
||||
|
||||
const detailResult = await enrichAgisoXianyuTradeOrder(parsed, {
|
||||
requestId: `repair-order-${row.id}`,
|
||||
})
|
||||
|
||||
const nextOrderId = pickFirstNonEmpty([
|
||||
detailResult.parsed.platformOrderId,
|
||||
expectedOrderId,
|
||||
row.platform_order_id,
|
||||
])
|
||||
const nextAmount = Number(detailResult.parsed.totalAmount || 0)
|
||||
const nextRawPayloadJson = JSON.stringify(detailResult.parsed.rawPayload || rawPayload || {})
|
||||
const nextShopName = pickFirstNonEmpty([detailResult.parsed.shopName, row.shop_name])
|
||||
const nextBuyerId = pickFirstNonEmpty([detailResult.parsed.buyerId, row.buyer_id])
|
||||
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 orderIdChanged = nextOrderId && nextOrderId !== String(row.platform_order_id || '')
|
||||
const amountChanged = nextAmount > 0 && nextAmount !== Number(row.total_amount || 0)
|
||||
const payloadChanged = nextRawPayloadJson !== String(row.raw_payload_json || '{}')
|
||||
const metadataChanged = nextShopName !== String(row.shop_name || '')
|
||||
|| nextBuyerId !== String(row.buyer_id || '')
|
||||
|| nextBuyerName !== String(row.buyer_name || '')
|
||||
|| nextReceiverContact !== String(row.receiver_contact || '')
|
||||
|| nextPaidAt !== row.paid_at
|
||||
|
||||
if (!orderIdChanged && !amountChanged && !payloadChanged && !metadataChanged) {
|
||||
continue
|
||||
}
|
||||
|
||||
runInTransaction((db) => {
|
||||
db.prepare(`
|
||||
UPDATE orders
|
||||
SET
|
||||
shop_name = ?,
|
||||
platform_order_id = ?,
|
||||
buyer_id = ?,
|
||||
buyer_name = ?,
|
||||
receiver_contact = ?,
|
||||
total_amount = ?,
|
||||
raw_payload_json = ?,
|
||||
paid_at = ?,
|
||||
updated_at = ?
|
||||
WHERE id = ?
|
||||
`).run(
|
||||
nextShopName,
|
||||
nextOrderId,
|
||||
nextBuyerId,
|
||||
nextBuyerName,
|
||||
nextReceiverContact,
|
||||
nextAmount,
|
||||
nextRawPayloadJson,
|
||||
nextPaidAt,
|
||||
nowIso(),
|
||||
row.id,
|
||||
)
|
||||
|
||||
if (orderIdChanged) {
|
||||
db.prepare('UPDATE delivery_tasks SET platform_order_id = ? WHERE order_id = ?').run(nextOrderId, row.id)
|
||||
db.prepare(`
|
||||
UPDATE message_deliveries
|
||||
SET
|
||||
platform_order_id = ?,
|
||||
recipient_key = CASE
|
||||
WHEN recipient_key = ? THEN ?
|
||||
ELSE recipient_key
|
||||
END,
|
||||
updated_at = ?
|
||||
WHERE order_id = ?
|
||||
`).run(nextOrderId, row.platform_order_id, nextOrderId, nowIso(), row.id)
|
||||
}
|
||||
})
|
||||
|
||||
if (orderIdChanged) {
|
||||
repairedOrderIdCount += 1
|
||||
}
|
||||
|
||||
if (amountChanged) {
|
||||
repairedAmountCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
if (repairedOrderIdCount > 0 || repairedAmountCount > 0) {
|
||||
logInfo('[startup]', '已自动修复历史 Agiso 咸鱼订单数据', {
|
||||
repairedOrderIdCount,
|
||||
repairedAmountCount,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function pickFirstNonEmpty(values) {
|
||||
for (const value of values) {
|
||||
const normalized = String(value || '').trim()
|
||||
if (normalized) {
|
||||
return normalized
|
||||
}
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import { createOrder, findOrderByPlatformOrderId, updateOrder } from '../../repositories/order-repo.js'
|
||||
import { replaceOrderItems } from '../../repositories/order-item-repo.js'
|
||||
import { getClaimTokenById } from '../../repositories/claim-token-repo.js'
|
||||
import { buildClaimUrl } from '../claim/claim-service.js'
|
||||
import { syncDeliveryTasksForOrder } from './delivery-task-service.js'
|
||||
import { ensureAgisoXianyuClaimMessageDeliveredForTask } from '../platforms/agiso/xianyu/message-service.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
import { logWebhook } from '../../utils/logger.js'
|
||||
|
||||
export async function upsertOrderFromWebhook(event) {
|
||||
const now = nowIso()
|
||||
const existing = findOrderByPlatformOrderId({
|
||||
provider: event.provider,
|
||||
platform: event.platform,
|
||||
shopId: event.shopId,
|
||||
platformOrderId: event.platformOrderId,
|
||||
})
|
||||
|
||||
logWebhook('[order-service]', '开始处理 webhook 订单 upsert', {
|
||||
provider: event.provider,
|
||||
platform: event.platform,
|
||||
shopId: event.shopId,
|
||||
shopName: event.shopName,
|
||||
platformOrderId: event.platformOrderId,
|
||||
existingOrderId: existing?.id || null,
|
||||
})
|
||||
|
||||
const basePayload = {
|
||||
provider: event.provider,
|
||||
platform: event.platform,
|
||||
shopId: event.shopId,
|
||||
shopName: event.shopName,
|
||||
platformOrderId: event.platformOrderId,
|
||||
orderStatus: event.orderStatus,
|
||||
payStatus: event.payStatus,
|
||||
buyerId: event.buyerId,
|
||||
buyerName: event.buyerName,
|
||||
receiverContact: event.receiverContact,
|
||||
totalAmount: event.totalAmount,
|
||||
currency: event.currency,
|
||||
rawPayloadJson: JSON.stringify(event.rawPayload),
|
||||
paidAt: event.paidAt,
|
||||
}
|
||||
|
||||
const order = existing
|
||||
? updateOrder(existing.id, {
|
||||
...basePayload,
|
||||
updatedAt: now,
|
||||
})
|
||||
: createOrder({
|
||||
...basePayload,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
|
||||
const orderItems = replaceOrderItems(
|
||||
order.id,
|
||||
event.items.map((item) => ({
|
||||
skuCode: item.skuCode,
|
||||
skuName: item.skuName,
|
||||
quantity: item.quantity,
|
||||
specJson: JSON.stringify(item.spec || {}),
|
||||
deliveryMode: 'claim_link',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})),
|
||||
)
|
||||
|
||||
const tasks = syncDeliveryTasksForOrder(order, orderItems)
|
||||
const messageDeliveries = []
|
||||
|
||||
logWebhook('[order-service]', 'Webhook 订单 upsert 完成', {
|
||||
orderId: order.id,
|
||||
provider: order.provider,
|
||||
platform: order.platform,
|
||||
shopId: order.shop_id,
|
||||
shopName: order.shop_name,
|
||||
platformOrderId: order.platform_order_id,
|
||||
orderItemCount: orderItems.length,
|
||||
taskCount: tasks.length,
|
||||
})
|
||||
|
||||
for (const task of tasks) {
|
||||
if (
|
||||
event.provider !== 'agiso'
|
||||
|| event.platform !== 'xianyu'
|
||||
|| String(task.task_status || '') !== 'link_generated'
|
||||
|| !task.claim_token_id
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
const claimToken = getClaimTokenById(task.claim_token_id)
|
||||
if (!claimToken) {
|
||||
continue
|
||||
}
|
||||
|
||||
const result = await ensureAgisoXianyuClaimMessageDeliveredForTask({
|
||||
order,
|
||||
task,
|
||||
claimUrl: buildClaimUrl(claimToken.token),
|
||||
expiredAt: claimToken.expired_at,
|
||||
})
|
||||
messageDeliveries.push({
|
||||
taskId: task.id,
|
||||
...result,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
order,
|
||||
orderItems,
|
||||
tasks,
|
||||
messageDeliveries,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,722 @@
|
||||
import crypto from 'node:crypto'
|
||||
|
||||
import { runtimeConfig } from '../../config/runtime.js'
|
||||
import { createWebhookEvent, updateWebhookEvent } from '../../repositories/webhook-event-repo.js'
|
||||
import { upsertOrderFromWebhook } from './order-service.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { logWebhook } from '../../utils/logger.js'
|
||||
import { parseAmountToFen } from '../../utils/money.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
|
||||
export async function processAgisoTradeWebhook(requestLike) {
|
||||
const parsed = parseAgisoTradeRequest(requestLike)
|
||||
const webhookEvent = createWebhookEvent(buildWebhookEventInput(requestLike, parsed))
|
||||
|
||||
logWebhook('[webhook-service/agiso]', 'Webhook 已解析并写入 webhook_events', {
|
||||
requestId: requestLike.requestId || '',
|
||||
webhookEventId: webhookEvent.id,
|
||||
provider: parsed.provider,
|
||||
platform: parsed.platform,
|
||||
shopId: parsed.shopId,
|
||||
shopName: parsed.shopName,
|
||||
eventKey: parsed.eventKey,
|
||||
eventType: parsed.eventType,
|
||||
platformOrderId: parsed.platformOrderId,
|
||||
signatureValid: parsed.signatureValid,
|
||||
})
|
||||
|
||||
return executeAgisoTradeWebhook(parsed, webhookEvent.id, { requestId: requestLike.requestId || '' })
|
||||
}
|
||||
|
||||
export async function replayAgisoTradeWebhookEvent(webhookEvent) {
|
||||
const requestLike = {
|
||||
requestId: `replay-${webhookEvent.id}`,
|
||||
headers: safeParseJson(webhookEvent.headers_json),
|
||||
query: safeParseJson(webhookEvent.query_json),
|
||||
body: safeParseJson(webhookEvent.body_json),
|
||||
}
|
||||
const parsed = parseAgisoTradeRequest(requestLike)
|
||||
|
||||
logWebhook('[webhook-service/agiso]', '开始重放 webhook 事件', {
|
||||
requestId: requestLike.requestId,
|
||||
webhookEventId: webhookEvent.id,
|
||||
provider: parsed.provider,
|
||||
platform: parsed.platform,
|
||||
shopId: parsed.shopId,
|
||||
shopName: parsed.shopName,
|
||||
eventKey: parsed.eventKey,
|
||||
eventType: parsed.eventType,
|
||||
platformOrderId: parsed.platformOrderId,
|
||||
})
|
||||
|
||||
return executeAgisoTradeWebhook(parsed, webhookEvent.id, { requestId: requestLike.requestId })
|
||||
}
|
||||
|
||||
function buildWebhookEventInput(requestLike, parsed) {
|
||||
return {
|
||||
provider: parsed.provider,
|
||||
platform: parsed.platform,
|
||||
shopId: parsed.shopId,
|
||||
shopName: parsed.shopName,
|
||||
eventType: parsed.eventType,
|
||||
eventKey: parsed.eventKey,
|
||||
signatureValid: parsed.signatureValid,
|
||||
headersJson: JSON.stringify(requestLike.headers || {}),
|
||||
queryJson: JSON.stringify(requestLike.query || {}),
|
||||
bodyJson: JSON.stringify(requestLike.body || {}),
|
||||
processed: false,
|
||||
processError: '',
|
||||
relatedOrderId: null,
|
||||
createdAt: nowIso(),
|
||||
}
|
||||
}
|
||||
|
||||
async function executeAgisoTradeWebhook(parsed, webhookEventId, { requestId = '' } = {}) {
|
||||
try {
|
||||
if (!parsed.signatureValid) {
|
||||
throw createHttpError('验签失败', {
|
||||
statusCode: 400,
|
||||
errorCode: 'invalid_signature',
|
||||
})
|
||||
}
|
||||
|
||||
if (!parsed.platformOrderId) {
|
||||
throw createHttpError('缺少平台订单号', {
|
||||
statusCode: 400,
|
||||
errorCode: 'missing_platform_order_id',
|
||||
})
|
||||
}
|
||||
|
||||
const result = await upsertOrderFromWebhook(parsed)
|
||||
updateWebhookEvent(webhookEventId, {
|
||||
processed: true,
|
||||
process_error: '',
|
||||
related_order_id: result.order.id,
|
||||
})
|
||||
|
||||
logWebhook('[webhook-service/agiso]', 'Webhook 业务处理成功', {
|
||||
requestId,
|
||||
webhookEventId,
|
||||
provider: parsed.provider,
|
||||
platform: parsed.platform,
|
||||
shopId: parsed.shopId,
|
||||
shopName: parsed.shopName,
|
||||
eventKey: parsed.eventKey,
|
||||
eventType: parsed.eventType,
|
||||
platformOrderId: parsed.platformOrderId,
|
||||
orderId: result.order.id,
|
||||
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,
|
||||
orderId: result.order.id,
|
||||
taskCount: result.tasks.length,
|
||||
messageDeliveries: result.messageDeliveries || [],
|
||||
tasks: result.tasks.map((task) => ({
|
||||
taskId: task.id,
|
||||
taskNo: task.task_no,
|
||||
status: task.task_status,
|
||||
})),
|
||||
}
|
||||
} catch (error) {
|
||||
updateWebhookEvent(webhookEventId, {
|
||||
processed: false,
|
||||
process_error: error instanceof Error ? error.message : String(error || ''),
|
||||
related_order_id: null,
|
||||
})
|
||||
|
||||
logWebhook(
|
||||
'[webhook-service/agiso]',
|
||||
'Webhook 业务处理失败',
|
||||
{
|
||||
requestId,
|
||||
webhookEventId,
|
||||
provider: parsed.provider,
|
||||
platform: parsed.platform,
|
||||
shopId: parsed.shopId,
|
||||
shopName: parsed.shopName,
|
||||
eventKey: parsed.eventKey,
|
||||
eventType: parsed.eventType,
|
||||
platformOrderId: parsed.platformOrderId,
|
||||
signatureValid: parsed.signatureValid,
|
||||
error,
|
||||
},
|
||||
{ level: 'error' },
|
||||
)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
function parseAgisoTradeRequest(requestLike) {
|
||||
const query = normalizeRecord(requestLike.query)
|
||||
const body = normalizeRecord(requestLike.body)
|
||||
const rawJson = String(body.json || '').trim()
|
||||
const payload = rawJson ? parsePayloadJson(rawJson) : body
|
||||
const timestamp = String(query.timestamp || '').trim()
|
||||
const sign = String(query.sign || '').trim().toLowerCase()
|
||||
const provider = 'agiso'
|
||||
const platformRaw = pickFirstNonEmpty([
|
||||
query.fromPlatform,
|
||||
query.from_platform,
|
||||
body.fromPlatform,
|
||||
body.from_platform,
|
||||
payload.fromPlatform,
|
||||
payload.FromPlatform,
|
||||
payload.platform,
|
||||
payload.Platform,
|
||||
])
|
||||
const platform = resolveBusinessPlatform(platformRaw)
|
||||
const shop = resolveShop(payload)
|
||||
const eventType = resolveEventType(query.aopic, payload)
|
||||
const signatureValid = verifyAgisoSignature({ rawJson, timestamp, sign })
|
||||
const itemSources = extractOrderItemSources(payload)
|
||||
const firstItem = itemSources[0] || {}
|
||||
const platformOrderId = pickFirstNonEmpty([
|
||||
payload.biz_order_id,
|
||||
payload.Tid,
|
||||
payload.tid,
|
||||
payload.Oid,
|
||||
payload.oid,
|
||||
payload.order_id,
|
||||
payload.orderId,
|
||||
firstItem.Oid,
|
||||
firstItem.oid,
|
||||
])
|
||||
|
||||
return {
|
||||
provider,
|
||||
platform,
|
||||
platformRaw,
|
||||
eventType,
|
||||
eventKey: [provider, platform || 'unknown', shop.shopId || 'unknown', platformOrderId || 'unknown', eventType, timestamp || 'na'].join(':'),
|
||||
signatureValid,
|
||||
platformOrderId,
|
||||
shopId: shop.shopId,
|
||||
shopName: shop.shopName,
|
||||
orderStatus: resolveOrderStatus(eventType, payload),
|
||||
payStatus: resolvePayStatus(eventType, payload),
|
||||
buyerId: pickFirstNonEmpty([
|
||||
payload.buyer_id,
|
||||
payload.buyerId,
|
||||
payload.BuyerId,
|
||||
payload.openid,
|
||||
payload.BuyerOpenUid,
|
||||
payload.buyer_open_uid,
|
||||
payload.buyerOpenUid,
|
||||
]),
|
||||
buyerName: pickFirstNonEmpty([
|
||||
payload.buyer_name,
|
||||
payload.buyerName,
|
||||
payload.BuyerName,
|
||||
payload.nick,
|
||||
payload.BuyerNick,
|
||||
payload.buyer_nick,
|
||||
payload.buyerNick,
|
||||
]),
|
||||
receiverContact: pickFirstNonEmpty([
|
||||
payload.receiver_contact,
|
||||
payload.receiverContact,
|
||||
payload.mobile,
|
||||
payload.phone,
|
||||
payload.receiver_mobile,
|
||||
payload.receiverMobile,
|
||||
]),
|
||||
totalAmount: parseAmountToFen(
|
||||
pickFirstNonEmpty([
|
||||
payload.total_fee,
|
||||
payload.totalFee,
|
||||
payload.TotalFee,
|
||||
payload.pay_fee,
|
||||
payload.payFee,
|
||||
payload.Payment,
|
||||
payload.payment,
|
||||
]),
|
||||
),
|
||||
currency: pickFirstNonEmpty([payload.currency, 'CNY']) || 'CNY',
|
||||
paidAt: resolvePaidAt(eventType, payload),
|
||||
rawPayload: payload,
|
||||
items: normalizeOrderItems(payload),
|
||||
}
|
||||
}
|
||||
|
||||
function verifyAgisoSignature({ rawJson, timestamp, sign }) {
|
||||
const appSecret = String(runtimeConfig.platforms.agiso.appSecret || '').trim()
|
||||
|
||||
if (!appSecret) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!rawJson || !timestamp || !sign) {
|
||||
return false
|
||||
}
|
||||
|
||||
const documentedDigest = crypto
|
||||
.createHash('md5')
|
||||
.update(`${appSecret}json${rawJson}timestamp${timestamp}${appSecret}`, 'utf8')
|
||||
.digest('hex')
|
||||
.toLowerCase()
|
||||
|
||||
if (documentedDigest === sign) {
|
||||
return true
|
||||
}
|
||||
|
||||
const legacyDigest = crypto
|
||||
.createHash('md5')
|
||||
.update(`${appSecret}${rawJson}${timestamp}`, 'utf8')
|
||||
.digest('hex')
|
||||
.toLowerCase()
|
||||
|
||||
return legacyDigest === sign
|
||||
}
|
||||
|
||||
function resolveEventType(aopic, payload) {
|
||||
const normalized = String(aopic || '').trim()
|
||||
|
||||
if (normalized === '1') {
|
||||
return 'payment_success'
|
||||
}
|
||||
|
||||
if (normalized === '21') {
|
||||
return 'trade_create'
|
||||
}
|
||||
|
||||
if (normalized === '32') {
|
||||
return 'trade_create'
|
||||
}
|
||||
|
||||
if (normalized === '256') {
|
||||
return 'buyer_confirm_goods'
|
||||
}
|
||||
|
||||
if (normalized === '128') {
|
||||
return 'trade_closed'
|
||||
}
|
||||
|
||||
const status = resolveProviderStatus(payload)
|
||||
if (status === 'closed') {
|
||||
return 'trade_closed'
|
||||
}
|
||||
|
||||
return 'trade_event'
|
||||
}
|
||||
|
||||
function resolveOrderStatus(eventType, payload) {
|
||||
const providerStatus = resolveProviderStatus(payload)
|
||||
|
||||
if (providerStatus === 'paid') {
|
||||
return 'paid'
|
||||
}
|
||||
|
||||
if (providerStatus === 'closed') {
|
||||
return 'closed'
|
||||
}
|
||||
|
||||
if (providerStatus === 'refunded') {
|
||||
return 'refunded'
|
||||
}
|
||||
|
||||
if (eventType === 'payment_success') {
|
||||
return 'paid'
|
||||
}
|
||||
|
||||
if (eventType === 'trade_closed') {
|
||||
return 'closed'
|
||||
}
|
||||
|
||||
const rawStatus = normalizeInteger(
|
||||
pickFirstNonEmpty([payload.order_status, payload.orderStatus, payload.status]),
|
||||
)
|
||||
|
||||
if (rawStatus === 3 || rawStatus === 4) {
|
||||
return 'paid'
|
||||
}
|
||||
|
||||
if (rawStatus === 6) {
|
||||
return 'closed'
|
||||
}
|
||||
|
||||
if (rawStatus === 5) {
|
||||
return 'refunded'
|
||||
}
|
||||
|
||||
return 'created'
|
||||
}
|
||||
|
||||
function resolvePayStatus(eventType, payload) {
|
||||
const providerStatus = resolveProviderStatus(payload)
|
||||
|
||||
if (providerStatus === 'paid') {
|
||||
return 'paid'
|
||||
}
|
||||
|
||||
if (providerStatus === 'closed') {
|
||||
return 'unpaid'
|
||||
}
|
||||
|
||||
if (providerStatus === 'refunded') {
|
||||
return 'refunded'
|
||||
}
|
||||
|
||||
if (eventType === 'payment_success') {
|
||||
return 'paid'
|
||||
}
|
||||
|
||||
if (eventType === 'trade_closed') {
|
||||
return 'unpaid'
|
||||
}
|
||||
|
||||
const rawStatus = normalizeInteger(
|
||||
pickFirstNonEmpty([payload.order_status, payload.orderStatus, payload.status]),
|
||||
)
|
||||
|
||||
if (rawStatus === 3 || rawStatus === 4) {
|
||||
return 'paid'
|
||||
}
|
||||
|
||||
if (rawStatus === 5) {
|
||||
return 'refunded'
|
||||
}
|
||||
|
||||
return 'unpaid'
|
||||
}
|
||||
|
||||
function resolvePaidAt(eventType, payload) {
|
||||
const providerPaidAt = normalizeProviderDateTime(
|
||||
pickFirstNonEmpty([
|
||||
payload.paid_at,
|
||||
payload.paidAt,
|
||||
payload.pay_time,
|
||||
payload.payTime,
|
||||
payload.PayTime,
|
||||
]),
|
||||
)
|
||||
|
||||
if (providerPaidAt) {
|
||||
return providerPaidAt
|
||||
}
|
||||
|
||||
if (resolveProviderStatus(payload) === 'paid') {
|
||||
return nowIso()
|
||||
}
|
||||
|
||||
if (eventType === 'payment_success') {
|
||||
return nowIso()
|
||||
}
|
||||
|
||||
const rawStatus = normalizeInteger(
|
||||
pickFirstNonEmpty([payload.order_status, payload.orderStatus, payload.status]),
|
||||
)
|
||||
|
||||
if (rawStatus === 3 || rawStatus === 4) {
|
||||
return nowIso()
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function parsePayloadJson(rawJson) {
|
||||
if (!rawJson) {
|
||||
return {}
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(rawJson)
|
||||
return isPlainObject(parsed) ? parsed : {}
|
||||
} catch {
|
||||
throw createHttpError('json 参数不是合法 JSON', {
|
||||
statusCode: 400,
|
||||
errorCode: 'invalid_json_payload',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeOrderItems(payload) {
|
||||
const items = extractOrderItemSources(payload)
|
||||
|
||||
return items.map((item) => {
|
||||
const source = isPlainObject(item) ? item : {}
|
||||
const rawSkuCandidates = [
|
||||
source.OuterSkuId,
|
||||
source.outerSkuId,
|
||||
source.outer_sku_id,
|
||||
source.OuterIid,
|
||||
source.outerIid,
|
||||
source.outer_iid,
|
||||
source.sku_code,
|
||||
source.skuCode,
|
||||
source.goods_sku,
|
||||
source.item_id,
|
||||
source.itemId,
|
||||
source.goods_id,
|
||||
source.goodsId,
|
||||
source.NumIid,
|
||||
source.num_iid,
|
||||
payload.item_id,
|
||||
payload.itemId,
|
||||
]
|
||||
const skuCode = resolveSkuCode(rawSkuCandidates)
|
||||
|
||||
return {
|
||||
skuCode,
|
||||
skuName: pickFirstNonEmpty([
|
||||
source.Title,
|
||||
source.title,
|
||||
source.sku_name,
|
||||
source.skuName,
|
||||
source.goods_name,
|
||||
source.goodsName,
|
||||
skuCode,
|
||||
]),
|
||||
quantity: Math.max(
|
||||
1,
|
||||
normalizeInteger(
|
||||
pickFirstNonEmpty([
|
||||
source.quantity,
|
||||
source.num,
|
||||
source.Num,
|
||||
source.buy_amount,
|
||||
payload.quantity,
|
||||
payload.num,
|
||||
payload.Num,
|
||||
]),
|
||||
) || 1,
|
||||
),
|
||||
spec: source,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function extractOrderItemSources(payload) {
|
||||
const candidates = [
|
||||
payload.items,
|
||||
payload.Items,
|
||||
payload.orders,
|
||||
payload.Orders,
|
||||
payload.order_list,
|
||||
payload.OrderList,
|
||||
]
|
||||
|
||||
for (const current of candidates) {
|
||||
if (Array.isArray(current) && current.length > 0) {
|
||||
return current
|
||||
}
|
||||
}
|
||||
|
||||
return [payload]
|
||||
}
|
||||
|
||||
function resolveBusinessPlatform(rawPlatform) {
|
||||
const normalized = normalizePlatformKey(rawPlatform)
|
||||
|
||||
if (!normalized) {
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
const aliases = {
|
||||
xianyu: 'xianyu',
|
||||
idlefish: 'xianyu',
|
||||
idle: 'xianyu',
|
||||
aldsidle: 'xianyu',
|
||||
taobaoidle: 'xianyu',
|
||||
taobaoxianyu: 'xianyu',
|
||||
taobao: 'taobao',
|
||||
tb: 'taobao',
|
||||
pdd: 'pdd',
|
||||
pinduoduo: 'pdd',
|
||||
douyin: 'douyin',
|
||||
weidian: 'weidian',
|
||||
jd: 'jd',
|
||||
jingdong: 'jd',
|
||||
}
|
||||
|
||||
return aliases[normalized] || normalized
|
||||
}
|
||||
|
||||
function normalizePlatformKey(value) {
|
||||
return String(value || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[\s_-]+/g, '')
|
||||
}
|
||||
|
||||
function resolveShop(payload) {
|
||||
const shopName = pickFirstNonEmpty([
|
||||
payload.shop_name,
|
||||
payload.shopName,
|
||||
payload.ShopName,
|
||||
payload.seller_name,
|
||||
payload.sellerName,
|
||||
payload.seller_nick,
|
||||
payload.sellerNick,
|
||||
payload.SellerNick,
|
||||
])
|
||||
|
||||
return {
|
||||
shopId: pickFirstNonEmpty([
|
||||
payload.shop_id,
|
||||
payload.shopId,
|
||||
payload.ShopId,
|
||||
payload.seller_id,
|
||||
payload.sellerId,
|
||||
payload.SellerId,
|
||||
payload.PlatformUserId,
|
||||
payload.platformUserId,
|
||||
shopName,
|
||||
]),
|
||||
shopName,
|
||||
}
|
||||
}
|
||||
|
||||
function resolveProviderStatus(payload) {
|
||||
const rawStatus = normalizeInteger(
|
||||
pickFirstNonEmpty([payload.order_status, payload.orderStatus, payload.status]),
|
||||
)
|
||||
|
||||
if (rawStatus === 3 || rawStatus === 4) {
|
||||
return 'paid'
|
||||
}
|
||||
|
||||
if (rawStatus === 6) {
|
||||
return 'closed'
|
||||
}
|
||||
|
||||
if (rawStatus === 5) {
|
||||
return 'refunded'
|
||||
}
|
||||
|
||||
const statusText = pickFirstNonEmpty([
|
||||
payload.Status,
|
||||
payload.status,
|
||||
payload.order_status_text,
|
||||
payload.orderStatusText,
|
||||
]).toUpperCase()
|
||||
|
||||
if (!statusText) {
|
||||
return pickFirstNonEmpty([payload.PayTime, payload.payTime, payload.pay_time]) ? 'paid' : 'unknown'
|
||||
}
|
||||
|
||||
if (['WAIT_SELLER_SEND_GOODS', 'WAIT_BUYER_CONFIRM_GOODS', 'TRADE_FINISHED', 'SUCCESS', 'PAID'].includes(statusText)) {
|
||||
return 'paid'
|
||||
}
|
||||
|
||||
if (['TRADE_CLOSED', 'TRADE_CLOSED_BY_TAOBAO', 'CLOSED'].includes(statusText)) {
|
||||
return 'closed'
|
||||
}
|
||||
|
||||
if (['REFUND_SUCCESS', 'TRADE_REFUND', 'REFUNDED'].includes(statusText)) {
|
||||
return 'refunded'
|
||||
}
|
||||
|
||||
if (['WAIT_BUYER_PAY', 'CREATED', 'NEW'].includes(statusText)) {
|
||||
return 'unpaid'
|
||||
}
|
||||
|
||||
return pickFirstNonEmpty([payload.PayTime, payload.payTime, payload.pay_time]) ? 'paid' : 'unknown'
|
||||
}
|
||||
|
||||
function normalizeProviderDateTime(value) {
|
||||
const normalized = String(value || '').trim()
|
||||
|
||||
if (!normalized) {
|
||||
return null
|
||||
}
|
||||
|
||||
const isoText = normalized.replace(' ', 'T')
|
||||
const utcMatch = isoText.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})$/)
|
||||
|
||||
if (utcMatch) {
|
||||
const [, year, month, day, hours, minutes, seconds] = utcMatch
|
||||
return new Date(
|
||||
Date.UTC(
|
||||
Number(year),
|
||||
Number(month) - 1,
|
||||
Number(day),
|
||||
Number(hours) - 8,
|
||||
Number(minutes),
|
||||
Number(seconds),
|
||||
),
|
||||
).toISOString()
|
||||
}
|
||||
|
||||
const date = new Date(normalized)
|
||||
return Number.isNaN(date.getTime()) ? null : date.toISOString()
|
||||
}
|
||||
|
||||
function resolveSkuCode(rawKey) {
|
||||
const mappings = runtimeConfig.orders.skuMappings
|
||||
const candidates = Array.isArray(rawKey) ? rawKey : [rawKey]
|
||||
|
||||
if (mappings && typeof mappings === 'object') {
|
||||
for (const candidate of candidates) {
|
||||
const normalizedCandidate = String(candidate || '').trim()
|
||||
if (!normalizedCandidate) {
|
||||
continue
|
||||
}
|
||||
|
||||
const direct = mappings[normalizedCandidate]
|
||||
if (typeof direct === 'string' && direct.trim()) {
|
||||
return direct.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (typeof candidate === 'string' && candidate.trim()) {
|
||||
return candidate.trim()
|
||||
}
|
||||
|
||||
if (typeof candidate === 'number' && Number.isFinite(candidate)) {
|
||||
return String(candidate)
|
||||
}
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
function normalizeRecord(value) {
|
||||
return isPlainObject(value) ? value : {}
|
||||
}
|
||||
|
||||
function normalizeInteger(value) {
|
||||
const parsed = Number(value)
|
||||
return Number.isFinite(parsed) ? Math.round(parsed) : 0
|
||||
}
|
||||
|
||||
function pickFirstNonEmpty(values) {
|
||||
for (const value of values) {
|
||||
const normalized = String(value || '').trim()
|
||||
if (normalized) {
|
||||
return normalized
|
||||
}
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
function isPlainObject(value) {
|
||||
return Object.prototype.toString.call(value) === '[object Object]'
|
||||
}
|
||||
|
||||
function safeParseJson(rawValue) {
|
||||
const normalized = String(rawValue || '').trim()
|
||||
|
||||
if (!normalized) {
|
||||
return {}
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(normalized)
|
||||
return isPlainObject(parsed) ? parsed : {}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user