开始多平台 多店铺整改
This commit is contained in:
@@ -4,29 +4,59 @@ 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 { nowIso } from '../utils/time.js'
|
||||
|
||||
export async function processAgisoTradeWebhook(requestLike) {
|
||||
const parsed = parseAgisoTradeRequest(requestLike)
|
||||
const webhookEvent = createWebhookEvent(buildWebhookEventInput(requestLike, parsed))
|
||||
|
||||
return executeAgisoTradeWebhook(parsed, webhookEvent.id)
|
||||
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)
|
||||
|
||||
return executeAgisoTradeWebhook(parsed, webhookEvent.id)
|
||||
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 {
|
||||
platform: 'agiso',
|
||||
provider: parsed.provider,
|
||||
platform: parsed.platform,
|
||||
shopId: parsed.shopId,
|
||||
shopName: parsed.shopName,
|
||||
eventType: parsed.eventType,
|
||||
eventKey: parsed.eventKey,
|
||||
signatureValid: parsed.signatureValid,
|
||||
@@ -40,7 +70,7 @@ function buildWebhookEventInput(requestLike, parsed) {
|
||||
}
|
||||
}
|
||||
|
||||
async function executeAgisoTradeWebhook(parsed, webhookEventId) {
|
||||
async function executeAgisoTradeWebhook(parsed, webhookEventId, { requestId = '' } = {}) {
|
||||
try {
|
||||
if (!parsed.signatureValid) {
|
||||
throw createHttpError('验签失败', {
|
||||
@@ -63,8 +93,28 @@ async function executeAgisoTradeWebhook(parsed, webhookEventId) {
|
||||
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,
|
||||
@@ -82,6 +132,25 @@ async function executeAgisoTradeWebhook(parsed, webhookEventId) {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -93,34 +162,83 @@ function parseAgisoTradeRequest(requestLike) {
|
||||
const payload = rawJson ? parsePayloadJson(rawJson) : body
|
||||
const timestamp = String(query.timestamp || '').trim()
|
||||
const sign = String(query.sign || '').trim().toLowerCase()
|
||||
const eventType = resolveEventType(query.aopic)
|
||||
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 {
|
||||
platform: 'agiso',
|
||||
provider,
|
||||
platform,
|
||||
platformRaw,
|
||||
eventType,
|
||||
eventKey: `${platformOrderId || 'unknown'}:${eventType}:${timestamp || 'na'}`,
|
||||
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.openid]),
|
||||
buyerName: pickFirstNonEmpty([payload.buyer_name, payload.buyerName, payload.nick]),
|
||||
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: normalizeInteger(
|
||||
pickFirstNonEmpty([payload.total_fee, payload.totalFee, payload.pay_fee, payload.payFee]),
|
||||
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),
|
||||
@@ -159,13 +277,17 @@ function verifyAgisoSignature({ rawJson, timestamp, sign }) {
|
||||
return legacyDigest === sign
|
||||
}
|
||||
|
||||
function resolveEventType(aopic) {
|
||||
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'
|
||||
}
|
||||
@@ -178,10 +300,29 @@ function resolveEventType(aopic) {
|
||||
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'
|
||||
}
|
||||
@@ -210,6 +351,20 @@ function resolveOrderStatus(eventType, payload) {
|
||||
}
|
||||
|
||||
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'
|
||||
}
|
||||
@@ -234,6 +389,24 @@ function resolvePayStatus(eventType, payload) {
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
@@ -266,13 +439,17 @@ function parsePayloadJson(rawJson) {
|
||||
}
|
||||
|
||||
function normalizeOrderItems(payload) {
|
||||
const items = Array.isArray(payload.items) && payload.items.length > 0
|
||||
? payload.items
|
||||
: [payload]
|
||||
const items = extractOrderItemSources(payload)
|
||||
|
||||
return items.map((item) => {
|
||||
const source = isPlainObject(item) ? item : {}
|
||||
const rawSkuKey = pickFirstNonEmpty([
|
||||
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,
|
||||
@@ -280,13 +457,18 @@ function normalizeOrderItems(payload) {
|
||||
source.itemId,
|
||||
source.goods_id,
|
||||
source.goodsId,
|
||||
source.NumIid,
|
||||
source.num_iid,
|
||||
])
|
||||
const skuCode = resolveSkuCode(rawSkuKey)
|
||||
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,
|
||||
@@ -295,25 +477,204 @@ function normalizeOrderItems(payload) {
|
||||
]),
|
||||
quantity: Math.max(
|
||||
1,
|
||||
normalizeInteger(pickFirstNonEmpty([source.quantity, source.num, source.buy_amount])) || 1,
|
||||
normalizeInteger(
|
||||
pickFirstNonEmpty([
|
||||
source.quantity,
|
||||
source.num,
|
||||
source.Num,
|
||||
source.buy_amount,
|
||||
payload.quantity,
|
||||
payload.num,
|
||||
payload.Num,
|
||||
]),
|
||||
) || 1,
|
||||
),
|
||||
spec: source,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function resolveSkuCode(rawKey) {
|
||||
const mappings = runtimeConfig.orders.skuMappings
|
||||
function extractOrderItemSources(payload) {
|
||||
const candidates = [
|
||||
payload.items,
|
||||
payload.Items,
|
||||
payload.orders,
|
||||
payload.Orders,
|
||||
payload.order_list,
|
||||
payload.OrderList,
|
||||
]
|
||||
|
||||
if (rawKey && mappings && typeof mappings === 'object') {
|
||||
const direct = mappings[String(rawKey)]
|
||||
if (typeof direct === 'string' && direct.trim()) {
|
||||
return direct.trim()
|
||||
for (const current of candidates) {
|
||||
if (Array.isArray(current) && current.length > 0) {
|
||||
return current
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof rawKey === 'string' && rawKey.trim()) {
|
||||
return rawKey.trim()
|
||||
return [payload]
|
||||
}
|
||||
|
||||
function resolveBusinessPlatform(rawPlatform) {
|
||||
const normalized = normalizePlatformKey(rawPlatform)
|
||||
|
||||
if (!normalized) {
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
const aliases = {
|
||||
xianyu: 'xianyu',
|
||||
idlefish: 'xianyu',
|
||||
idle: '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 ''
|
||||
|
||||
Reference in New Issue
Block a user