优化了一些文件 增加多店铺
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
## Agiso Platform Services
|
||||
|
||||
Keep Agiso-wide shared helpers in this directory.
|
||||
|
||||
Per-platform integrations live in subdirectories:
|
||||
|
||||
- `xianyu/`
|
||||
- `pdd/`
|
||||
- `taobao/`
|
||||
@@ -0,0 +1,3 @@
|
||||
## PDD
|
||||
|
||||
Put all Agiso PDD-specific service integrations here.
|
||||
@@ -0,0 +1,103 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { PROJECT_ROOT, runtimeConfig } from '../../../config/runtime.js'
|
||||
|
||||
const AGISO_SHOPS_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'agiso-shops.json')
|
||||
|
||||
export function getAgisoShopsFilePath() {
|
||||
return AGISO_SHOPS_FILE_PATH
|
||||
}
|
||||
|
||||
export function getAgisoShopConfigMap() {
|
||||
const envConfig = normalizeAgisoShopConfigMap(runtimeConfig.platforms?.agiso?.messaging?.shops || {})
|
||||
const fileConfig = loadAgisoShopConfigMapFromFile()
|
||||
|
||||
return {
|
||||
...envConfig,
|
||||
...fileConfig,
|
||||
}
|
||||
}
|
||||
|
||||
export function getAgisoShopConfig(shopId) {
|
||||
const normalizedShopId = String(shopId || '').trim()
|
||||
if (!normalizedShopId) {
|
||||
return null
|
||||
}
|
||||
|
||||
return getAgisoShopConfigMap()[normalizedShopId] || null
|
||||
}
|
||||
|
||||
export function saveAgisoShopConfigMap(rawValue) {
|
||||
const normalized = normalizeAgisoShopConfigMap(rawValue)
|
||||
fs.mkdirSync(path.dirname(AGISO_SHOPS_FILE_PATH), { recursive: true })
|
||||
fs.writeFileSync(AGISO_SHOPS_FILE_PATH, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8')
|
||||
return normalized
|
||||
}
|
||||
|
||||
function loadAgisoShopConfigMapFromFile() {
|
||||
if (!fs.existsSync(AGISO_SHOPS_FILE_PATH)) {
|
||||
return {}
|
||||
}
|
||||
|
||||
try {
|
||||
const rawText = fs.readFileSync(AGISO_SHOPS_FILE_PATH, 'utf8')
|
||||
const parsed = JSON.parse(rawText)
|
||||
return normalizeAgisoShopConfigMap(parsed)
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeAgisoShopConfigMap(rawValue) {
|
||||
const output = {}
|
||||
|
||||
for (const [shopId, config] of Object.entries(rawValue || {})) {
|
||||
const normalizedShopId = String(shopId || '').trim()
|
||||
if (!normalizedShopId || !isPlainObject(config)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const next = {}
|
||||
const enabled = normalizeBooleanLike(config.enabled)
|
||||
if (enabled !== null) {
|
||||
next.enabled = enabled
|
||||
}
|
||||
|
||||
for (const key of ['shopName', 'accessToken', 'messageTemplate', 'appSecret', 'apiVersion', 'sendMessageEndpoint', 'tradeDetailEndpoint']) {
|
||||
const value = String(config[key] || '').trim()
|
||||
if (value) {
|
||||
next[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
output[normalizedShopId] = next
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
function normalizeBooleanLike(value) {
|
||||
if (typeof value === 'boolean') {
|
||||
return value
|
||||
}
|
||||
|
||||
const normalized = String(value || '').trim().toLowerCase()
|
||||
if (!normalized) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (['1', 'true', 'yes', 'on'].includes(normalized)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (['0', 'false', 'no', 'off'].includes(normalized)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function isPlainObject(value) {
|
||||
return Object.prototype.toString.call(value) === '[object Object]'
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
## Taobao
|
||||
|
||||
Put all Agiso Taobao-specific service integrations here.
|
||||
@@ -0,0 +1,3 @@
|
||||
## Xianyu
|
||||
|
||||
Put all Agiso Xianyu-specific service integrations here.
|
||||
@@ -0,0 +1,262 @@
|
||||
import crypto from 'node:crypto'
|
||||
|
||||
import { runtimeConfig } from '../../../../config/runtime.js'
|
||||
import { getAgisoShopConfigMap } from '../shop-config-service.js'
|
||||
import {
|
||||
createMessageDelivery,
|
||||
findLatestSuccessfulMessageDeliveryByTask,
|
||||
updateMessageDelivery,
|
||||
} from '../../../../repositories/message-delivery-repo.js'
|
||||
import { nowIso } from '../../../../utils/time.js'
|
||||
|
||||
const AGISO_XIANYU_MESSAGE_CHANNEL = 'agiso_im'
|
||||
|
||||
export async function ensureAgisoXianyuClaimMessageDeliveredForTask({ order, task, claimUrl, expiredAt }) {
|
||||
if (!order || !task || !claimUrl) {
|
||||
return { sent: false, skipped: true, reason: 'missing_message_context' }
|
||||
}
|
||||
|
||||
const config = resolveAgisoXianyuMessagingConfig(order)
|
||||
if (!config.enabled) {
|
||||
return { sent: false, skipped: true, reason: 'messaging_disabled' }
|
||||
}
|
||||
|
||||
const endpoint = String(config.sendMessageEndpoint || '').trim()
|
||||
const accessToken = String(config.accessToken || '').trim()
|
||||
const appSecret = String(config.appSecret || runtimeConfig.platforms?.agiso?.appSecret || '').trim()
|
||||
if (!endpoint || !accessToken || !appSecret) {
|
||||
return { sent: false, skipped: true, reason: 'missing_endpoint_or_access_token_or_app_secret' }
|
||||
}
|
||||
|
||||
const successful = findLatestSuccessfulMessageDeliveryByTask(task.id, AGISO_XIANYU_MESSAGE_CHANNEL)
|
||||
if (successful) {
|
||||
return { sent: false, skipped: true, reason: 'already_sent', deliveryId: successful.id }
|
||||
}
|
||||
|
||||
const messageContent = renderAgisoClaimMessage({
|
||||
order,
|
||||
task,
|
||||
claimUrl,
|
||||
expiredAt,
|
||||
template: config.messageTemplate,
|
||||
shopName: config.shopName,
|
||||
})
|
||||
const url = buildRequestUrl(endpoint)
|
||||
const requestBody = buildRequestBody({
|
||||
tid: String(order.platform_order_id || ''),
|
||||
msg: messageContent,
|
||||
appSecret,
|
||||
})
|
||||
const requestHeaders = buildRequestHeaders({
|
||||
accessToken,
|
||||
apiVersion: String(config.apiVersion || '1').trim() || '1',
|
||||
})
|
||||
const createdAt = nowIso()
|
||||
const delivery = createMessageDelivery({
|
||||
provider: 'agiso',
|
||||
platform: String(order.platform || '').trim() || 'unknown',
|
||||
shopId: String(order.shop_id || '').trim(),
|
||||
shopName: String(order.shop_name || '').trim(),
|
||||
channel: AGISO_XIANYU_MESSAGE_CHANNEL,
|
||||
orderId: order.id,
|
||||
taskId: task.id,
|
||||
platformOrderId: order.platform_order_id,
|
||||
recipientKey: order.platform_order_id,
|
||||
messageContent,
|
||||
claimUrl,
|
||||
status: 'pending',
|
||||
requestUrl: url,
|
||||
requestHeadersJson: JSON.stringify(maskHeadersForStorage(requestHeaders)),
|
||||
requestBodyJson: JSON.stringify(maskBodyForStorage(requestBody)),
|
||||
responseStatus: 0,
|
||||
responseJson: '{}',
|
||||
errorMessage: '',
|
||||
sentAt: null,
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
})
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: requestHeaders,
|
||||
body: new URLSearchParams(requestBody).toString(),
|
||||
})
|
||||
const rawText = await response.text()
|
||||
const parsed = safeParseJson(rawText)
|
||||
const success = isAgisoSendSuccess(response.status, parsed)
|
||||
const updated = updateMessageDelivery(delivery.id, {
|
||||
status: success ? 'success' : 'failed',
|
||||
response_status: response.status,
|
||||
response_json: JSON.stringify(parsed ?? { rawText }),
|
||||
error_message: success ? '' : resolveAgisoErrorMessage(parsed, rawText, response.status),
|
||||
sent_at: success ? nowIso() : null,
|
||||
updated_at: nowIso(),
|
||||
})
|
||||
|
||||
return {
|
||||
sent: success,
|
||||
skipped: false,
|
||||
deliveryId: updated?.id || delivery.id,
|
||||
responseStatus: response.status,
|
||||
response: parsed ?? { rawText },
|
||||
errorMessage: success ? '' : resolveAgisoErrorMessage(parsed, rawText, response.status),
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error || '发送消息失败')
|
||||
updateMessageDelivery(delivery.id, {
|
||||
status: 'failed',
|
||||
response_status: 0,
|
||||
response_json: '{}',
|
||||
error_message: message,
|
||||
sent_at: null,
|
||||
updated_at: nowIso(),
|
||||
})
|
||||
|
||||
return {
|
||||
sent: false,
|
||||
skipped: false,
|
||||
deliveryId: delivery.id,
|
||||
errorMessage: message,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resolveAgisoXianyuMessagingConfig(order) {
|
||||
const baseConfig = runtimeConfig.platforms?.agiso?.messaging || {}
|
||||
const shopId = String(order?.shop_id || '').trim()
|
||||
const shopConfigs = getAgisoShopConfigMap()
|
||||
const shopConfig = shopId && isPlainObject(shopConfigs[shopId]) ? shopConfigs[shopId] : {}
|
||||
|
||||
return {
|
||||
...baseConfig,
|
||||
...shopConfig,
|
||||
}
|
||||
}
|
||||
|
||||
function buildRequestUrl(endpoint) {
|
||||
return new URL(endpoint).toString()
|
||||
}
|
||||
|
||||
function buildRequestHeaders({ accessToken, apiVersion }) {
|
||||
return {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
ApiVersion: apiVersion,
|
||||
'content-type': 'application/x-www-form-urlencoded; charset=utf-8',
|
||||
}
|
||||
}
|
||||
|
||||
function buildRequestBody({ tid, msg, appSecret }) {
|
||||
const timestamp = String(Math.floor(Date.now() / 1000))
|
||||
const payload = {
|
||||
tid,
|
||||
msg,
|
||||
timestamp,
|
||||
}
|
||||
|
||||
payload.sign = generateSign(payload, appSecret)
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
function generateSign(params, appSecret) {
|
||||
const sortedEntries = Object.entries(params).sort(([left], [right]) => left.localeCompare(right))
|
||||
let raw = appSecret
|
||||
|
||||
for (const [key, value] of sortedEntries) {
|
||||
raw += `${key}${value}`
|
||||
}
|
||||
|
||||
raw += appSecret
|
||||
|
||||
return crypto.createHash('md5').update(raw, 'utf8').digest('hex').toLowerCase()
|
||||
}
|
||||
|
||||
function renderAgisoClaimMessage({ order, task, claimUrl, expiredAt, template, shopName = '' }) {
|
||||
const source = String(template || '').trim() || '您的订单 {platformOrderId} 已创建领取链接,请在 {expiredAt} 前完成领取:{claimUrl}'
|
||||
const resolvedShopName = String(shopName || order.shop_name || order.shop_id || '').trim()
|
||||
|
||||
return source
|
||||
.replaceAll('{platformOrderId}', String(order.platform_order_id || ''))
|
||||
.replaceAll('{taskNo}', String(task.task_no || ''))
|
||||
.replaceAll('{shopName}', resolvedShopName)
|
||||
.replaceAll('{shopId}', String(order.shop_id || ''))
|
||||
.replaceAll('{claimUrl}', claimUrl)
|
||||
.replaceAll('{expiredAt}', String(expiredAt || '尽快'))
|
||||
}
|
||||
|
||||
function isAgisoSendSuccess(statusCode, payload) {
|
||||
if (statusCode < 200 || statusCode >= 300) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (payload.IsSuccess === true) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (Number(payload.Error_Code) === 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (Number(payload.code) === 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function resolveAgisoErrorMessage(payload, rawText, statusCode) {
|
||||
if (payload && typeof payload === 'object') {
|
||||
for (const value of [payload.Error_Msg, payload.msg, payload.message, payload.error]) {
|
||||
const normalized = String(value || '').trim()
|
||||
if (normalized) {
|
||||
return normalized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const text = String(rawText || '').trim()
|
||||
return text || `Agiso 咸鱼发消息失败,HTTP ${statusCode}`
|
||||
}
|
||||
|
||||
function safeParseJson(rawText) {
|
||||
const normalized = String(rawText || '').trim()
|
||||
|
||||
if (!normalized) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(normalized)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function maskHeadersForStorage(headers) {
|
||||
const output = { ...headers }
|
||||
|
||||
if (output.Authorization) {
|
||||
output.Authorization = '[masked]'
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
function maskBodyForStorage(body) {
|
||||
const output = { ...body }
|
||||
|
||||
if (output.sign) {
|
||||
output.sign = '[masked]'
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
function isPlainObject(value) {
|
||||
return Object.prototype.toString.call(value) === '[object Object]'
|
||||
}
|
||||
@@ -0,0 +1,497 @@
|
||||
import crypto from 'node:crypto'
|
||||
|
||||
import { runtimeConfig } from '../../../../config/runtime.js'
|
||||
import { getAgisoShopConfig } from '../shop-config-service.js'
|
||||
import { logWebhook } from '../../../../utils/logger.js'
|
||||
import { nowIso } from '../../../../utils/time.js'
|
||||
import { parseAmountToFen } from '../../../../utils/money.js'
|
||||
import { parseJsonObject } from '../../../../utils/json.js'
|
||||
|
||||
const DEFAULT_DETAIL_TIMEOUT_MS = 5000
|
||||
|
||||
export async function enrichAgisoXianyuTradeOrder(parsed, { requestId = '' } = {}) {
|
||||
if (!shouldHydrateAgisoXianyuTradeOrder(parsed)) {
|
||||
return { parsed, enriched: false, reason: 'not_needed' }
|
||||
}
|
||||
|
||||
const config = resolveAgisoXianyuTradeDetailConfig(parsed.shopId)
|
||||
if (!config.endpoint || !config.accessToken || !config.appSecret) {
|
||||
logWebhook('[agiso/xianyu/order-detail]', '跳过 Agiso 咸鱼订单详情补查:缺少必要配置', {
|
||||
requestId,
|
||||
platform: parsed.platform,
|
||||
shopId: parsed.shopId,
|
||||
platformOrderId: parsed.platformOrderId,
|
||||
hasEndpoint: Boolean(config.endpoint),
|
||||
hasAccessToken: Boolean(config.accessToken),
|
||||
hasAppSecret: Boolean(config.appSecret),
|
||||
}, { level: 'warn' })
|
||||
|
||||
return { parsed, enriched: false, reason: 'missing_config' }
|
||||
}
|
||||
|
||||
const requestBody = buildRequestBody({
|
||||
platformOrderId: parsed.platformOrderId,
|
||||
appSecret: config.appSecret,
|
||||
})
|
||||
const requestHeaders = buildRequestHeaders({
|
||||
accessToken: config.accessToken,
|
||||
apiVersion: config.apiVersion,
|
||||
})
|
||||
|
||||
logWebhook('[agiso/xianyu/order-detail]', '开始补查 Agiso 咸鱼订单详情', {
|
||||
requestId,
|
||||
platform: parsed.platform,
|
||||
shopId: parsed.shopId,
|
||||
platformOrderId: parsed.platformOrderId,
|
||||
endpoint: config.endpoint,
|
||||
})
|
||||
|
||||
try {
|
||||
const response = await fetchWithTimeout(config.endpoint, {
|
||||
method: 'POST',
|
||||
headers: requestHeaders,
|
||||
body: new URLSearchParams(requestBody).toString(),
|
||||
}, config.timeoutMs)
|
||||
const rawText = await response.text()
|
||||
const payload = parseJsonObject(rawText, { preserveLargeIntegers: true })
|
||||
const detailPayload = extractAgisoDetailPayload(payload)
|
||||
const detailAmountFen = resolveTotalAmountFen(detailPayload)
|
||||
|
||||
if (response.status < 200 || response.status >= 300) {
|
||||
const message = resolveAgisoDetailErrorMessage(payload, rawText, response.status)
|
||||
logWebhook('[agiso/xianyu/order-detail]', 'Agiso 咸鱼订单详情补查失败', {
|
||||
requestId,
|
||||
platform: parsed.platform,
|
||||
shopId: parsed.shopId,
|
||||
platformOrderId: parsed.platformOrderId,
|
||||
responseStatus: response.status,
|
||||
errorMessage: message,
|
||||
}, { level: 'warn' })
|
||||
|
||||
return { parsed, enriched: false, reason: 'http_error', errorMessage: message }
|
||||
}
|
||||
|
||||
if (!isAgisoDetailSuccess(payload)) {
|
||||
const message = resolveAgisoDetailErrorMessage(payload, rawText, response.status)
|
||||
logWebhook('[agiso/xianyu/order-detail]', 'Agiso 咸鱼订单详情补查返回业务失败', {
|
||||
requestId,
|
||||
platform: parsed.platform,
|
||||
shopId: parsed.shopId,
|
||||
platformOrderId: parsed.platformOrderId,
|
||||
responseStatus: response.status,
|
||||
errorMessage: message,
|
||||
response: payload,
|
||||
}, { level: 'warn' })
|
||||
|
||||
return { parsed, enriched: false, reason: 'business_error', errorMessage: message }
|
||||
}
|
||||
|
||||
if (!detailPayload || typeof detailPayload !== 'object' || Object.keys(detailPayload).length === 0) {
|
||||
logWebhook('[agiso/xianyu/order-detail]', 'Agiso 咸鱼订单详情补查未返回可用订单体', {
|
||||
requestId,
|
||||
platform: parsed.platform,
|
||||
shopId: parsed.shopId,
|
||||
platformOrderId: parsed.platformOrderId,
|
||||
responseStatus: response.status,
|
||||
}, { level: 'warn' })
|
||||
|
||||
return { parsed, enriched: false, reason: 'empty_detail_payload' }
|
||||
}
|
||||
|
||||
if (detailAmountFen <= 0) {
|
||||
logWebhook('[agiso/xianyu/order-detail]', 'Agiso 咸鱼订单详情已返回,但仍未解析出金额', {
|
||||
requestId,
|
||||
platform: parsed.platform,
|
||||
shopId: parsed.shopId,
|
||||
platformOrderId: parsed.platformOrderId,
|
||||
responseStatus: response.status,
|
||||
detailKeys: Object.keys(detailPayload),
|
||||
}, { level: 'warn' })
|
||||
|
||||
return {
|
||||
parsed: mergeEnrichedTrade(parsed, detailPayload, { keepOriginalAmount: true }),
|
||||
enriched: false,
|
||||
reason: 'amount_still_missing',
|
||||
}
|
||||
}
|
||||
|
||||
const merged = mergeEnrichedTrade(parsed, detailPayload)
|
||||
logWebhook('[agiso/xianyu/order-detail]', 'Agiso 咸鱼订单详情补查成功', {
|
||||
requestId,
|
||||
platform: parsed.platform,
|
||||
shopId: parsed.shopId,
|
||||
platformOrderId: parsed.platformOrderId,
|
||||
totalAmountFen: merged.totalAmount,
|
||||
paidAt: merged.paidAt,
|
||||
})
|
||||
|
||||
return {
|
||||
parsed: merged,
|
||||
enriched: true,
|
||||
totalAmountFen: merged.totalAmount,
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error || 'Agiso 咸鱼订单详情补查失败')
|
||||
logWebhook('[agiso/xianyu/order-detail]', 'Agiso 咸鱼订单详情补查异常', {
|
||||
requestId,
|
||||
platform: parsed.platform,
|
||||
shopId: parsed.shopId,
|
||||
platformOrderId: parsed.platformOrderId,
|
||||
errorMessage: message,
|
||||
}, { level: 'warn' })
|
||||
|
||||
return { parsed, enriched: false, reason: 'request_failed', errorMessage: message }
|
||||
}
|
||||
}
|
||||
|
||||
function shouldHydrateAgisoXianyuTradeOrder(parsed) {
|
||||
return parsed?.provider === 'agiso'
|
||||
&& parsed?.platform === 'xianyu'
|
||||
&& String(parsed?.platformOrderId || '').trim()
|
||||
&& Number(parsed?.totalAmount || 0) <= 0
|
||||
}
|
||||
|
||||
function resolveAgisoXianyuTradeDetailConfig(shopId) {
|
||||
const baseConfig = runtimeConfig.platforms?.agiso?.tradeDetail || {}
|
||||
const shopConfig = getAgisoShopConfig(shopId) || {}
|
||||
|
||||
return {
|
||||
endpoint: String(shopConfig.tradeDetailEndpoint || baseConfig.endpoint || '').trim(),
|
||||
apiVersion: String(shopConfig.tradeDetailApiVersion || baseConfig.apiVersion || '1').trim() || '1',
|
||||
accessToken: String(shopConfig.accessToken || runtimeConfig.platforms?.agiso?.messaging?.accessToken || '').trim(),
|
||||
appSecret: String(shopConfig.appSecret || runtimeConfig.platforms?.agiso?.appSecret || '').trim(),
|
||||
timeoutMs: normalizePositiveInteger(shopConfig.tradeDetailTimeoutMs || baseConfig.timeoutMs, DEFAULT_DETAIL_TIMEOUT_MS),
|
||||
}
|
||||
}
|
||||
|
||||
function buildRequestHeaders({ accessToken, apiVersion }) {
|
||||
return {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
ApiVersion: apiVersion,
|
||||
'content-type': 'application/x-www-form-urlencoded; charset=utf-8',
|
||||
}
|
||||
}
|
||||
|
||||
function buildRequestBody({ platformOrderId, appSecret }) {
|
||||
const timestamp = String(Math.floor(Date.now() / 1000))
|
||||
const payload = {
|
||||
tid: String(platformOrderId || '').trim(),
|
||||
timestamp,
|
||||
}
|
||||
|
||||
payload.sign = generateSign(payload, appSecret)
|
||||
return payload
|
||||
}
|
||||
|
||||
function generateSign(params, appSecret) {
|
||||
const sortedEntries = Object.entries(params).sort(([left], [right]) => left.localeCompare(right))
|
||||
let raw = appSecret
|
||||
|
||||
for (const [key, value] of sortedEntries) {
|
||||
raw += `${key}${value}`
|
||||
}
|
||||
|
||||
raw += appSecret
|
||||
|
||||
return crypto.createHash('md5').update(raw, 'utf8').digest('hex').toLowerCase()
|
||||
}
|
||||
|
||||
function extractAgisoDetailPayload(payload) {
|
||||
if (!isPlainObject(payload)) {
|
||||
return {}
|
||||
}
|
||||
|
||||
for (const key of ['Data', 'data', 'Result', 'result', 'Trade', 'trade', 'Order', 'order']) {
|
||||
if (isPlainObject(payload[key])) {
|
||||
return payload[key]
|
||||
}
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
function resolveTotalAmountFen(payload) {
|
||||
const fenAmount = normalizeFenInteger(pickFirstNonEmpty([
|
||||
payload.payment,
|
||||
payload.Payment,
|
||||
payload.post_fee,
|
||||
payload.postFee,
|
||||
payload.item?.price,
|
||||
]))
|
||||
|
||||
if (fenAmount > 0) {
|
||||
return fenAmount
|
||||
}
|
||||
|
||||
return parseAmountToFen(pickFirstNonEmpty([
|
||||
payload.total_fee,
|
||||
payload.totalFee,
|
||||
payload.TotalFee,
|
||||
payload.pay_fee,
|
||||
payload.payFee,
|
||||
payload.PayFee,
|
||||
payload.actual_fee,
|
||||
payload.actualFee,
|
||||
payload.ActualFee,
|
||||
payload.total_amount,
|
||||
payload.totalAmount,
|
||||
payload.Amount,
|
||||
payload.amount,
|
||||
]))
|
||||
}
|
||||
|
||||
function resolvePaidAt(payload, fallbackValue) {
|
||||
const providerPaidAt = normalizeProviderDateTime(
|
||||
pickFirstNonEmpty([
|
||||
payload.paid_at,
|
||||
payload.paidAt,
|
||||
payload.pay_time,
|
||||
payload.payTime,
|
||||
payload.PayTime,
|
||||
fallbackValue,
|
||||
]),
|
||||
)
|
||||
|
||||
if (providerPaidAt) {
|
||||
return providerPaidAt
|
||||
}
|
||||
|
||||
const rawStatus = normalizeInteger(
|
||||
pickFirstNonEmpty([payload.order_status, payload.orderStatus, payload.status]),
|
||||
)
|
||||
|
||||
if (rawStatus === 2 || rawStatus === 3 || rawStatus === 4) {
|
||||
return nowIso()
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function mergeEnrichedTrade(parsed, detailPayload, { keepOriginalAmount = false } = {}) {
|
||||
const mergedPayload = {
|
||||
...parsed.rawPayload,
|
||||
_agisoTradeDetail: detailPayload,
|
||||
}
|
||||
const totalAmountFen = resolveTotalAmountFen(detailPayload)
|
||||
|
||||
return {
|
||||
...parsed,
|
||||
buyerId: pickFirstNonEmpty([
|
||||
parsed.buyerId,
|
||||
detailPayload.encryption_buyer_id,
|
||||
detailPayload.buyer_id,
|
||||
detailPayload.buyerId,
|
||||
detailPayload.BuyerId,
|
||||
detailPayload.BuyerOpenUid,
|
||||
detailPayload.buyer_open_uid,
|
||||
]),
|
||||
buyerName: pickFirstNonEmpty([
|
||||
parsed.buyerName,
|
||||
detailPayload.buyer_name,
|
||||
detailPayload.buyerName,
|
||||
detailPayload.BuyerName,
|
||||
detailPayload.buyer_nick,
|
||||
detailPayload.nick,
|
||||
detailPayload.BuyerNick,
|
||||
]),
|
||||
receiverContact: pickFirstNonEmpty([
|
||||
parsed.receiverContact,
|
||||
detailPayload.receiver_contact,
|
||||
detailPayload.receiverContact,
|
||||
detailPayload.receiver_mobile,
|
||||
detailPayload.receiverMobile,
|
||||
detailPayload.mobile,
|
||||
detailPayload.phone,
|
||||
]),
|
||||
shopName: pickFirstNonEmpty([
|
||||
parsed.shopName,
|
||||
detailPayload.shop_name,
|
||||
detailPayload.shopName,
|
||||
detailPayload.ShopName,
|
||||
detailPayload.seller_name,
|
||||
detailPayload.sellerName,
|
||||
detailPayload.seller_nick,
|
||||
detailPayload.sellerNick,
|
||||
detailPayload.SellerNick,
|
||||
]),
|
||||
totalAmount: keepOriginalAmount ? parsed.totalAmount : (totalAmountFen || parsed.totalAmount),
|
||||
paidAt: parsed.paidAt || resolvePaidAt(detailPayload, parsed.paidAt),
|
||||
rawPayload: mergedPayload,
|
||||
items: normalizeOrderItems(mergedPayload, parsed.items),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeOrderItems(payload, fallbackItems = []) {
|
||||
const candidates = [
|
||||
payload.item ? [payload.item] : null,
|
||||
payload.items,
|
||||
payload.Items,
|
||||
payload.orders,
|
||||
payload.Orders,
|
||||
payload.order_list,
|
||||
payload.OrderList,
|
||||
]
|
||||
const items = candidates.find((item) => Array.isArray(item) && item.length > 0)
|
||||
|
||||
if (!Array.isArray(items) || items.length === 0) {
|
||||
return Array.isArray(fallbackItems) ? fallbackItems : []
|
||||
}
|
||||
|
||||
return items.map((item) => {
|
||||
const source = isPlainObject(item) ? item : {}
|
||||
const skuCode = pickFirstNonEmpty([
|
||||
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,
|
||||
])
|
||||
|
||||
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 isAgisoDetailSuccess(payload) {
|
||||
if (!payload || typeof payload !== 'object' || Object.keys(payload).length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (payload.IsSuccess === true) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (Number(payload.Error_Code) === 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (Number(payload.code) === 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (Number(payload.success) === 1 || payload.success === true) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function resolveAgisoDetailErrorMessage(payload, rawText, statusCode) {
|
||||
if (payload && typeof payload === 'object') {
|
||||
for (const value of [payload.Error_Msg, payload.msg, payload.message, payload.error]) {
|
||||
const normalized = String(value || '').trim()
|
||||
if (normalized) {
|
||||
return normalized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const text = String(rawText || '').trim()
|
||||
return text || `Agiso 咸鱼订单详情接口失败,HTTP ${statusCode}`
|
||||
}
|
||||
|
||||
async function fetchWithTimeout(url, options, timeoutMs) {
|
||||
const controller = new AbortController()
|
||||
const timer = setTimeout(() => controller.abort(new Error('Agiso 咸鱼订单详情请求超时')), timeoutMs)
|
||||
|
||||
try {
|
||||
return await fetch(url, {
|
||||
...options,
|
||||
signal: controller.signal,
|
||||
})
|
||||
} finally {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeProviderDateTime(value) {
|
||||
const normalized = String(value || '').trim()
|
||||
|
||||
if (!normalized) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (/^\d{10,13}$/.test(normalized)) {
|
||||
const timestamp = normalized.length === 13 ? Number(normalized) : Number(normalized) * 1000
|
||||
|
||||
if (Number.isFinite(timestamp)) {
|
||||
return new Date(timestamp).toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
const isoLike = normalized.replace(' ', 'T')
|
||||
const parsed = Date.parse(isoLike)
|
||||
|
||||
if (Number.isNaN(parsed)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return new Date(parsed).toISOString()
|
||||
}
|
||||
|
||||
function normalizeInteger(value) {
|
||||
const parsed = Number(value)
|
||||
return Number.isFinite(parsed) ? Math.round(parsed) : 0
|
||||
}
|
||||
|
||||
function normalizePositiveInteger(value, fallbackValue) {
|
||||
const parsed = Number(value)
|
||||
return Number.isFinite(parsed) && parsed > 0 ? Math.round(parsed) : fallbackValue
|
||||
}
|
||||
|
||||
function normalizeFenInteger(value) {
|
||||
const parsed = Number(value)
|
||||
return Number.isFinite(parsed) && parsed > 0 ? 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]'
|
||||
}
|
||||
Reference in New Issue
Block a user