926 lines
23 KiB
JavaScript
926 lines
23 KiB
JavaScript
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 { hasConfiguredOrderItems } from './product-match-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'
|
||
|
||
export async function processAgisoTradeWebhook(requestLike) {
|
||
const parsed = parseAgisoTradeRequest(requestLike)
|
||
const webhookEvent = await 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 shouldProcess = await hasConfiguredOrderItems({
|
||
provider: parsed.provider,
|
||
platform: parsed.platform,
|
||
shopId: parsed.shopId,
|
||
items: parsed.items,
|
||
})
|
||
|
||
if (!shouldProcess) {
|
||
await updateWebhookEvent(webhookEventId, {
|
||
processed: true,
|
||
process_error: 'ignored_unconfigured_product',
|
||
related_order_id: null,
|
||
})
|
||
|
||
logWebhook('[webhook-service/agiso]', 'Webhook 已忽略:未命中任何已配置履约商品', {
|
||
requestId,
|
||
webhookEventId,
|
||
provider: parsed.provider,
|
||
platform: parsed.platform,
|
||
shopId: parsed.shopId,
|
||
shopName: parsed.shopName,
|
||
platformOrderId: parsed.platformOrderId,
|
||
rawItemCount: Array.isArray(parsed.items) ? parsed.items.length : 0,
|
||
})
|
||
|
||
return {
|
||
accepted: true,
|
||
ignored: true,
|
||
ignoreReason: 'unconfigured_product',
|
||
provider: parsed.provider,
|
||
platform: parsed.platform,
|
||
shopId: parsed.shopId,
|
||
shopName: parsed.shopName,
|
||
platformRaw: parsed.platformRaw,
|
||
eventType: parsed.eventType,
|
||
platformOrderId: parsed.platformOrderId,
|
||
orderId: null,
|
||
totalAmountFen: Number(parsed.totalAmount || 0),
|
||
enriched: false,
|
||
enrichReason: 'skipped_unconfigured_product',
|
||
taskCount: 0,
|
||
messageDeliveries: [],
|
||
tasks: [],
|
||
}
|
||
}
|
||
|
||
const enriched = await enrichTradeBeforeUpsert(parsed, {
|
||
requestId,
|
||
webhookEventId,
|
||
})
|
||
const result = await upsertOrderFromWebhook(enriched.parsed)
|
||
await updateWebhookEvent(webhookEventId, {
|
||
processed: true,
|
||
process_error: result.ignored ? 'ignored_unconfigured_product' : '',
|
||
related_order_id: result.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: enriched.parsed.platformOrderId,
|
||
orderId: result.order?.id || null,
|
||
ignored: Boolean(result.ignored),
|
||
ignoreReason: String(result.ignoreReason || ''),
|
||
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: 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 || null,
|
||
totalAmountFen: Number(enriched.parsed.totalAmount || 0),
|
||
ignored: Boolean(result.ignored),
|
||
ignoreReason: String(result.ignoreReason || ''),
|
||
enriched: enriched.enriched,
|
||
enrichReason: enriched.reason || '',
|
||
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) {
|
||
await 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
|
||
}
|
||
}
|
||
|
||
async function enrichTradeBeforeUpsert(parsed, { requestId = '', webhookEventId = null } = {}) {
|
||
if (parsed.provider !== 'agiso' || parsed.platform !== 'xianyu') {
|
||
return { parsed, enriched: false, reason: 'not_supported' }
|
||
}
|
||
|
||
if (!shouldEnrichAgisoXianyuTrade(parsed)) {
|
||
return { parsed, enriched: false, reason: 'enough_fields_present' }
|
||
}
|
||
|
||
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 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)
|
||
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 {}
|
||
}
|
||
|
||
return parseJsonObject(rawJson, {
|
||
preserveLargeIntegers: true,
|
||
throwOnError: true,
|
||
})
|
||
}
|
||
|
||
function normalizeOrderItems(payload) {
|
||
const items = extractOrderItemSources(payload)
|
||
|
||
return items.map((item) => {
|
||
const source = isPlainObject(item) ? item : {}
|
||
const skuDescriptor = parseExternalSkuDescriptor(
|
||
pickFirstNonEmpty([
|
||
source.sku,
|
||
source.Sku,
|
||
payload.sku,
|
||
payload.Sku,
|
||
payload._agisoTradeDetail?.sku,
|
||
]),
|
||
)
|
||
const rawSkuCandidates = [
|
||
skuDescriptor.externalSkuCode,
|
||
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)
|
||
const externalItemId = pickFirstNonEmpty([
|
||
source.item_id,
|
||
source.itemId,
|
||
source.goods_id,
|
||
source.goodsId,
|
||
payload.item_id,
|
||
payload.itemId,
|
||
])
|
||
const externalSkuName = pickFirstNonEmpty([
|
||
skuDescriptor.externalSkuName,
|
||
source.Title,
|
||
source.title,
|
||
source.sku_name,
|
||
source.skuName,
|
||
source.goods_name,
|
||
source.goodsName,
|
||
skuCode,
|
||
])
|
||
|
||
return {
|
||
skuCode,
|
||
skuName: externalSkuName,
|
||
externalItemId,
|
||
externalSkuCode: pickFirstNonEmpty([skuDescriptor.externalSkuCode, skuCode, externalItemId]),
|
||
externalSkuName,
|
||
quantity: Math.max(
|
||
1,
|
||
normalizeInteger(
|
||
pickFirstNonEmpty([
|
||
source.quantity,
|
||
source.num,
|
||
source.Num,
|
||
source.buy_amount,
|
||
payload.quantity,
|
||
payload.num,
|
||
payload.Num,
|
||
]),
|
||
) || 1,
|
||
),
|
||
spec: source,
|
||
snapshot: {
|
||
externalItemId,
|
||
externalSkuCode: pickFirstNonEmpty([skuDescriptor.externalSkuCode, skuCode, externalItemId]),
|
||
externalSkuName,
|
||
},
|
||
}
|
||
})
|
||
}
|
||
|
||
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 candidates = Array.isArray(rawKey) ? rawKey : [rawKey]
|
||
|
||
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 parseExternalSkuDescriptor(rawValue) {
|
||
const raw = String(rawValue || '').trim()
|
||
if (!raw) {
|
||
return {
|
||
raw,
|
||
externalSkuCode: '',
|
||
externalSkuName: '',
|
||
}
|
||
}
|
||
|
||
const parts = raw.split('|').map((part) => String(part || '').trim()).filter(Boolean)
|
||
let externalSkuCode = ''
|
||
let externalSkuName = ''
|
||
|
||
for (const part of parts) {
|
||
if (!externalSkuCode && !part.includes(':') && !part.includes(':')) {
|
||
externalSkuCode = part
|
||
continue
|
||
}
|
||
|
||
const separatorIndex = Math.max(part.indexOf(':'), part.indexOf(':'))
|
||
if (separatorIndex >= 0) {
|
||
const label = part.slice(0, separatorIndex).trim()
|
||
const value = part.slice(separatorIndex + 1).trim()
|
||
if (value && ['商品名称', '商品名', 'sku名称', '规格名称', '名称', '商品'].includes(label)) {
|
||
externalSkuName = value
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!externalSkuCode && parts[0]) {
|
||
externalSkuCode = parts[0]
|
||
}
|
||
|
||
if (!externalSkuName) {
|
||
externalSkuName = parts
|
||
.map((part) => {
|
||
const separatorIndex = Math.max(part.indexOf(':'), part.indexOf(':'))
|
||
return separatorIndex >= 0 ? part.slice(separatorIndex + 1).trim() : ''
|
||
})
|
||
.find(Boolean) || ''
|
||
}
|
||
|
||
return {
|
||
raw,
|
||
externalSkuCode,
|
||
externalSkuName,
|
||
}
|
||
}
|
||
|
||
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 {}
|
||
}
|
||
}
|