优化了一些文件 增加多店铺

This commit is contained in:
yml
2026-04-09 01:31:47 +08:00
parent c2ec3aa6d2
commit e4ab1f559e
43 changed files with 1839 additions and 91 deletions
@@ -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 {}
}
}