优化了自动发货和发货后的消息

This commit is contained in:
yml
2026-04-14 15:54:13 +08:00
parent 962867981e
commit ed45eab26c
13 changed files with 413 additions and 77 deletions
+2 -1
View File
@@ -53,7 +53,7 @@ module.exports = {
}, },
autoDelivery: { autoDelivery: {
enabled: true, enabled: true,
endpoint: 'https://gw-api.agiso.com/aldsIdle/Alds/AldsSend', endpoint: 'https://gw-api.agiso.com/aldsIdle/Order/DummySend',
apiVersion: '1', apiVersion: '1',
aldsType: 1, aldsType: 1,
ignoreAldsLog: false, ignoreAldsLog: false,
@@ -72,6 +72,7 @@ module.exports = {
accessToken: '', accessToken: '',
appSecret: '', appSecret: '',
messageTemplate: '您的订单 {platformOrderId} 已创建领取链接,请在 {expiredAt} 前完成领取:{claimUrl}', messageTemplate: '您的订单 {platformOrderId} 已创建领取链接,请在 {expiredAt} 前完成领取:{claimUrl}',
autoDeliveryMessageTemplate: '您的订单 {platformOrderId} 已完成自动发货,请注意查收。',
shops: {}, shops: {},
}, },
}, },
+6 -1
View File
@@ -270,6 +270,11 @@ function applyEnvOverrides(baseConfig) {
nextConfig.platforms.agiso.messaging.messageTemplate = agisoMessageTemplate nextConfig.platforms.agiso.messaging.messageTemplate = agisoMessageTemplate
} }
const agisoAutoDeliveryMessageTemplate = String(process.env.AGISO_AUTO_DELIVERY_MESSAGE_TEMPLATE || '').trim()
if (agisoAutoDeliveryMessageTemplate) {
nextConfig.platforms.agiso.messaging.autoDeliveryMessageTemplate = agisoAutoDeliveryMessageTemplate
}
const agisoShops = parseJsonObject(process.env.AGISO_SHOPS_JSON) const agisoShops = parseJsonObject(process.env.AGISO_SHOPS_JSON)
if (agisoShops) { if (agisoShops) {
nextConfig.platforms.agiso.messaging.shops = normalizeAgisoMessagingShops(agisoShops) nextConfig.platforms.agiso.messaging.shops = normalizeAgisoMessagingShops(agisoShops)
@@ -402,7 +407,7 @@ function normalizeAgisoMessagingShops(rawValue) {
next.enabled = enabled next.enabled = enabled
} }
for (const key of ['shopName', 'accessToken', 'messageTemplate', 'appSecret', 'apiVersion', 'sendMessageEndpoint']) { for (const key of ['shopName', 'accessToken', 'messageTemplate', 'autoDeliveryMessageTemplate', 'appSecret', 'apiVersion', 'sendMessageEndpoint']) {
const value = String(config[key] || '').trim() const value = String(config[key] || '').trim()
if (value) { if (value) {
next[key] = value next[key] = value
@@ -52,6 +52,7 @@ export async function getAdminAgisoShopConfigs() {
accessTokenMasked: maskSecret(config.accessToken), accessTokenMasked: maskSecret(config.accessToken),
enabled: typeof config.enabled === 'boolean' ? config.enabled : null, enabled: typeof config.enabled === 'boolean' ? config.enabled : null,
messageTemplate: String(config.messageTemplate || '').trim(), messageTemplate: String(config.messageTemplate || '').trim(),
autoDeliveryMessageTemplate: String(config.autoDeliveryMessageTemplate || '').trim(),
appSecretConfigured: Boolean(String(config.appSecret || '').trim()), appSecretConfigured: Boolean(String(config.appSecret || '').trim()),
apiVersion: String(config.apiVersion || '').trim(), apiVersion: String(config.apiVersion || '').trim(),
sendMessageEndpoint: String(config.sendMessageEndpoint || '').trim(), sendMessageEndpoint: String(config.sendMessageEndpoint || '').trim(),
@@ -70,6 +71,7 @@ export async function getAdminAgisoShopConfigs() {
/** @param {AdminAgisoShopConfigSaveInput} [payload] */ /** @param {AdminAgisoShopConfigSaveInput} [payload] */
export function updateAdminAgisoShopConfigs(payload = /** @type {AdminAgisoShopConfigSaveInput} */ ({})) { export function updateAdminAgisoShopConfigs(payload = /** @type {AdminAgisoShopConfigSaveInput} */ ({})) {
const rawItems = Array.isArray(payload.shops) ? payload.shops : [] const rawItems = Array.isArray(payload.shops) ? payload.shops : []
const currentMap = getAgisoShopConfigMap()
const nextMap = {} const nextMap = {}
for (const item of rawItems) { for (const item of rawItems) {
@@ -78,22 +80,33 @@ export function updateAdminAgisoShopConfigs(payload = /** @type {AdminAgisoShopC
continue continue
} }
const next = {} const current = currentMap[shopId] || {}
const next = { ...current }
const shopName = String(item?.shopName || '').trim() const shopName = String(item?.shopName || '').trim()
const accessToken = String(item?.accessToken || '').trim() const accessToken = String(item?.accessToken || '').trim()
const messageTemplate = String(item?.messageTemplate || '').trim() const messageTemplate = String(item?.messageTemplate || '').trim()
const autoDeliveryMessageTemplate = String(item?.autoDeliveryMessageTemplate || '').trim()
const appSecret = String(item?.appSecret || '').trim() const appSecret = String(item?.appSecret || '').trim()
const apiVersion = String(item?.apiVersion || '').trim() const apiVersion = String(item?.apiVersion || '').trim()
const sendMessageEndpoint = String(item?.sendMessageEndpoint || '').trim() const sendMessageEndpoint = String(item?.sendMessageEndpoint || '').trim()
if (shopName) { if (shopName) {
next.shopName = shopName next.shopName = shopName
} else {
delete next.shopName
} }
if (accessToken) { if (accessToken) {
next.accessToken = accessToken next.accessToken = accessToken
} }
if (messageTemplate) { if (messageTemplate) {
next.messageTemplate = messageTemplate next.messageTemplate = messageTemplate
} else if (typeof item?.messageTemplate === 'string') {
delete next.messageTemplate
}
if (autoDeliveryMessageTemplate) {
next.autoDeliveryMessageTemplate = autoDeliveryMessageTemplate
} else if (typeof item?.autoDeliveryMessageTemplate === 'string') {
delete next.autoDeliveryMessageTemplate
} }
if (appSecret) { if (appSecret) {
next.appSecret = appSecret next.appSecret = appSecret
@@ -128,6 +141,7 @@ export function updateAdminAgisoShopConfigs(payload = /** @type {AdminAgisoShopC
accessTokenMasked: maskSecret(config.accessToken), accessTokenMasked: maskSecret(config.accessToken),
enabled: typeof config.enabled === 'boolean' ? config.enabled : null, enabled: typeof config.enabled === 'boolean' ? config.enabled : null,
messageTemplate: String(config.messageTemplate || '').trim(), messageTemplate: String(config.messageTemplate || '').trim(),
autoDeliveryMessageTemplate: String(config.autoDeliveryMessageTemplate || '').trim(),
appSecretConfigured: Boolean(String(config.appSecret || '').trim()), appSecretConfigured: Boolean(String(config.appSecret || '').trim()),
apiVersion: String(config.apiVersion || '').trim(), apiVersion: String(config.apiVersion || '').trim(),
sendMessageEndpoint: String(config.sendMessageEndpoint || '').trim(), sendMessageEndpoint: String(config.sendMessageEndpoint || '').trim(),
@@ -64,7 +64,7 @@ function normalizeAgisoShopConfigMap(rawValue) {
next.enabled = enabled next.enabled = enabled
} }
for (const key of ['shopName', 'accessToken', 'messageTemplate', 'appSecret', 'apiVersion', 'sendMessageEndpoint', 'tradeDetailEndpoint']) { for (const key of ['shopName', 'accessToken', 'messageTemplate', 'autoDeliveryMessageTemplate', 'appSecret', 'apiVersion', 'sendMessageEndpoint', 'tradeDetailEndpoint']) {
const value = String(config[key] || '').trim() const value = String(config[key] || '').trim()
if (value) { if (value) {
next[key] = value next[key] = value
@@ -4,6 +4,8 @@ import { runtimeConfig } from '../../../../config/runtime.js'
import { listTasksByOrderId, updateTask } from '../../../../repositories/task-repo.js' import { listTasksByOrderId, updateTask } from '../../../../repositories/task-repo.js'
import { createTaskEvent } from '../../../../repositories/task-event-repo.js' import { createTaskEvent } from '../../../../repositories/task-event-repo.js'
import { getAgisoShopConfig } from '../shop-config-service.js' import { getAgisoShopConfig } from '../shop-config-service.js'
import { queryAgisoXianyuOrderDetail } from './order-detail-service.js'
import { ensureAgisoXianyuAutoDeliveryMessageDeliveredForTask } from './message-service.js'
import { parseJsonObject } from '../../../../utils/json.js' import { parseJsonObject } from '../../../../utils/json.js'
import { logWebhook } from '../../../../utils/logger.js' import { logWebhook } from '../../../../utils/logger.js'
import { nowIso } from '../../../../utils/time.js' import { nowIso } from '../../../../utils/time.js'
@@ -113,7 +115,46 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTask({
if (success) { if (success) {
const requestId = String(parsed?.RequestId || '').trim() const requestId = String(parsed?.RequestId || '').trim()
logWebhook('[agiso/xianyu/auto-delivery]', 'Agiso 咸鱼自动发货成功', {
// DummySend 接口返回成功后,再查一次 Order/Detail 确认订单真的进入已发货状态 meow~
const confirmResult = await confirmAgisoXianyuAutoDeliveryShipped({
shopId: order.shop_id,
platformOrderId: order.platform_order_id,
requestId,
})
if (!confirmResult.shipped) {
logWebhook('[agiso/xianyu/auto-delivery]', 'Agiso 咸鱼自动发货接口返回成功,但订单状态未确认进入已发货', {
trigger,
orderId: order.id,
taskId: task.id,
shopId: order.shop_id,
platformOrderId: order.platform_order_id,
responseStatus: response.status,
requestId,
confirmOrderStatus: confirmResult.orderStatus,
confirmShipTime: confirmResult.shipTime,
confirmReason: confirmResult.reason,
}, { level: 'warn' })
return persistAgisoAutoDeliveryResult(task, {
status: 'failed',
trigger,
reason: 'delivery_not_confirmed',
order,
responseStatus: response.status,
response: parsed,
errorMessage: `接口返回成功,但订单状态未确认进入已发货 (orderStatus=${confirmResult.orderStatus}, shipTime=${confirmResult.shipTime})`,
detail: {
requestId,
aldsType: config.aldsType,
confirmOrderStatus: confirmResult.orderStatus,
confirmShipTime: confirmResult.shipTime,
},
})
}
logWebhook('[agiso/xianyu/auto-delivery]', 'Agiso 咸鱼自动发货成功,订单已确认进入已发货状态', {
trigger, trigger,
orderId: order.id, orderId: order.id,
taskId: task.id, taskId: task.id,
@@ -121,9 +162,11 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTask({
platformOrderId: order.platform_order_id, platformOrderId: order.platform_order_id,
responseStatus: response.status, responseStatus: response.status,
requestId, requestId,
confirmOrderStatus: confirmResult.orderStatus,
confirmShipTime: confirmResult.shipTime,
}) })
return persistAgisoAutoDeliveryResult(task, { const result = await persistAgisoAutoDeliveryResult(task, {
status: 'success', status: 'success',
trigger, trigger,
reason: '', reason: '',
@@ -133,8 +176,15 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTask({
detail: { detail: {
requestId, requestId,
aldsType: config.aldsType, aldsType: config.aldsType,
confirmOrderStatus: confirmResult.orderStatus,
confirmShipTime: confirmResult.shipTime,
}, },
}) })
// 发货确认成功后,发送自定义消息通知 meow~
await sendAutoDeliveryMessage({ order, task })
return result
} }
const errorMessage = resolveAgisoAutoDeliveryErrorMessage(parsed, rawText, response.status) const errorMessage = resolveAgisoAutoDeliveryErrorMessage(parsed, rawText, response.status)
@@ -411,3 +461,72 @@ function normalizeBooleanLike(value, fallbackValue) {
function isPlainObject(value) { function isPlainObject(value) {
return Object.prototype.toString.call(value) === '[object Object]' return Object.prototype.toString.call(value) === '[object Object]'
} }
/**
* DummySend 返回成功后,再查一次 Order/Detail 确认订单是否真的进入已发货状态 meow~
* 只有 ship_time > 0 或 orderStatus >= 3 才算发货确认通过
*/
async function confirmAgisoXianyuAutoDeliveryShipped({ shopId = '', platformOrderId = '', requestId = '' } = {}) {
try {
const detailResult = await queryAgisoXianyuOrderDetail({ shopId, platformOrderId, requestId })
if (detailResult.success && detailResult.shipped) {
return {
shipped: true,
orderStatus: detailResult.orderStatus,
shipTime: detailResult.shipTime,
reason: '',
}
}
return {
shipped: false,
orderStatus: detailResult.orderStatus || 0,
shipTime: detailResult.shipTime || 0,
reason: detailResult.success ? 'order_not_shipped' : detailResult.reason,
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error || '订单发货状态确认失败')
logWebhook('[agiso/xianyu/auto-delivery]', '自动发货确认查询异常', {
requestId,
shopId,
platformOrderId,
errorMessage: message,
}, { level: 'warn' })
return {
shipped: false,
orderStatus: 0,
shipTime: 0,
reason: 'confirm_query_failed',
}
}
}
/**
* 自动发货成功后发送消息通知 meow~
*/
async function sendAutoDeliveryMessage({ order, task } = {}) {
try {
const result = await ensureAgisoXianyuAutoDeliveryMessageDeliveredForTask({ order, task })
logWebhook('[agiso/xianyu/auto-delivery]', '自动发货消息通知结果', {
orderId: order?.id,
taskId: task?.id,
platformOrderId: order?.platform_order_id,
sent: result.sent,
skipped: result.skipped,
reason: result.reason,
})
return result
} catch (error) {
const message = error instanceof Error ? error.message : String(error || '发送自动发货消息通知失败')
logWebhook('[agiso/xianyu/auto-delivery]', '自动发货消息通知异常', {
orderId: order?.id,
taskId: task?.id,
platformOrderId: order?.platform_order_id,
errorMessage: message,
}, { level: 'warn' })
return { sent: false, skipped: true, reason: 'message_send_error' }
}
}
@@ -10,9 +10,53 @@ import {
import { nowIso } from '../../../../utils/time.js' import { nowIso } from '../../../../utils/time.js'
const AGISO_XIANYU_MESSAGE_CHANNEL = 'agiso_im' const AGISO_XIANYU_MESSAGE_CHANNEL = 'agiso_im'
const AGISO_XIANYU_AUTO_DELIVERY_MESSAGE_CHANNEL = 'agiso_im_auto_delivery'
export async function ensureAgisoXianyuClaimMessageDeliveredForTask({ order, task, claimUrl, expiredAt }) { export async function ensureAgisoXianyuClaimMessageDeliveredForTask({ order, task, claimUrl, expiredAt }) {
if (!order || !task || !claimUrl) { const config = resolveAgisoXianyuMessagingConfig(order)
const messageContent = renderAgisoClaimMessage({
order,
task,
claimUrl,
expiredAt,
template: config.messageTemplate,
shopName: config.shopName,
})
return deliverAgisoXianyuMessageForTask({
order,
task,
channel: AGISO_XIANYU_MESSAGE_CHANNEL,
messageContent,
claimUrl,
})
}
export async function ensureAgisoXianyuAutoDeliveryMessageDeliveredForTask({ order, task } = {}) {
const config = resolveAgisoXianyuMessagingConfig(order)
const messageContent = renderAgisoAutoDeliveryMessage({
order,
task,
template: config.autoDeliveryMessageTemplate,
shopName: config.shopName,
})
return deliverAgisoXianyuMessageForTask({
order,
task,
channel: AGISO_XIANYU_AUTO_DELIVERY_MESSAGE_CHANNEL,
messageContent,
})
}
async function deliverAgisoXianyuMessageForTask({
order,
task,
channel,
messageContent,
claimUrl = '',
} = {}) {
if (!order || !task || !messageContent) {
return { sent: false, skipped: true, reason: 'missing_message_context' } return { sent: false, skipped: true, reason: 'missing_message_context' }
} }
@@ -28,19 +72,11 @@ export async function ensureAgisoXianyuClaimMessageDeliveredForTask({ order, tas
return { sent: false, skipped: true, reason: 'missing_endpoint_or_access_token_or_app_secret' } return { sent: false, skipped: true, reason: 'missing_endpoint_or_access_token_or_app_secret' }
} }
const successful = await findLatestSuccessfulMessageDeliveryByTask(task.id, AGISO_XIANYU_MESSAGE_CHANNEL) const successful = await findLatestSuccessfulMessageDeliveryByTask(task.id, channel)
if (successful) { if (successful) {
return { sent: false, skipped: true, reason: 'already_sent', deliveryId: successful.id } 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 url = buildRequestUrl(endpoint)
const requestBody = buildRequestBody({ const requestBody = buildRequestBody({
tid: String(order.platform_order_id || ''), tid: String(order.platform_order_id || ''),
@@ -57,7 +93,7 @@ export async function ensureAgisoXianyuClaimMessageDeliveredForTask({ order, tas
platform: String(order.platform || '').trim() || 'unknown', platform: String(order.platform || '').trim() || 'unknown',
shopId: String(order.shop_id || '').trim(), shopId: String(order.shop_id || '').trim(),
shopName: String(order.shop_name || '').trim(), shopName: String(order.shop_name || '').trim(),
channel: AGISO_XIANYU_MESSAGE_CHANNEL, channel,
orderId: order.id, orderId: order.id,
taskId: task.id, taskId: task.id,
platformOrderId: order.platform_order_id, platformOrderId: order.platform_order_id,
@@ -85,11 +121,12 @@ export async function ensureAgisoXianyuClaimMessageDeliveredForTask({ order, tas
const rawText = await response.text() const rawText = await response.text()
const parsed = safeParseJson(rawText) const parsed = safeParseJson(rawText)
const success = isAgisoSendSuccess(response.status, parsed) const success = isAgisoSendSuccess(response.status, parsed)
const errorMessage = success ? '' : resolveAgisoErrorMessage(parsed, rawText, response.status)
const updated = await updateMessageDelivery(delivery.id, { const updated = await updateMessageDelivery(delivery.id, {
status: success ? 'success' : 'failed', status: success ? 'success' : 'failed',
response_status: response.status, response_status: response.status,
response_json: JSON.stringify(parsed ?? { rawText }), response_json: JSON.stringify(parsed ?? { rawText }),
error_message: success ? '' : resolveAgisoErrorMessage(parsed, rawText, response.status), error_message: errorMessage,
sent_at: success ? nowIso() : null, sent_at: success ? nowIso() : null,
updated_at: nowIso(), updated_at: nowIso(),
}) })
@@ -100,7 +137,7 @@ export async function ensureAgisoXianyuClaimMessageDeliveredForTask({ order, tas
deliveryId: updated?.id || delivery.id, deliveryId: updated?.id || delivery.id,
responseStatus: response.status, responseStatus: response.status,
response: parsed ?? { rawText }, response: parsed ?? { rawText },
errorMessage: success ? '' : resolveAgisoErrorMessage(parsed, rawText, response.status), errorMessage,
} }
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : String(error || '发送消息失败') const message = error instanceof Error ? error.message : String(error || '发送消息失败')
@@ -174,15 +211,48 @@ function generateSign(params, appSecret) {
function renderAgisoClaimMessage({ order, task, claimUrl, expiredAt, template, shopName = '' }) { function renderAgisoClaimMessage({ order, task, claimUrl, expiredAt, template, shopName = '' }) {
const source = String(template || '').trim() || '您的订单 {platformOrderId} 已创建领取链接,请在 {expiredAt} 前完成领取:{claimUrl}' const source = String(template || '').trim() || '您的订单 {platformOrderId} 已创建领取链接,请在 {expiredAt} 前完成领取:{claimUrl}'
const resolvedShopName = String(shopName || order.shop_name || order.shop_id || '').trim()
return source return renderAgisoMessageTemplate({
.replaceAll('{platformOrderId}', String(order.platform_order_id || '')) order,
.replaceAll('{taskNo}', String(task.task_no || '')) task,
template: source,
shopName,
claimUrl,
expiredAt: String(expiredAt || '尽快'),
})
}
export function renderAgisoAutoDeliveryMessage({ order, task, template, shopName = '' }) {
const source = String(template || '').trim() || '您的订单 {platformOrderId} 已完成自动发货,请注意查收。'
return renderAgisoMessageTemplate({
order,
task,
template: source,
shopName,
resultMessage: '自动发货成功',
})
}
function renderAgisoMessageTemplate({
order,
task,
template,
shopName = '',
claimUrl = '',
expiredAt = '',
resultMessage = '',
} = {}) {
const resolvedShopName = String(shopName || order?.shop_name || order?.shop_id || '').trim()
return String(template || '')
.replaceAll('{platformOrderId}', String(order?.platform_order_id || ''))
.replaceAll('{taskNo}', String(task?.task_no || ''))
.replaceAll('{shopName}', resolvedShopName) .replaceAll('{shopName}', resolvedShopName)
.replaceAll('{shopId}', String(order.shop_id || '')) .replaceAll('{shopId}', String(order?.shop_id || ''))
.replaceAll('{claimUrl}', claimUrl) .replaceAll('{claimUrl}', String(claimUrl || ''))
.replaceAll('{expiredAt}', String(expiredAt || '尽快')) .replaceAll('{expiredAt}', String(expiredAt || ''))
.replaceAll('{resultMessage}', String(resultMessage || ''))
} }
function isAgisoSendSuccess(statusCode, payload) { function isAgisoSendSuccess(statusCode, payload) {
@@ -14,23 +14,68 @@ export async function enrichAgisoXianyuTradeOrder(parsed, { requestId = '' } = {
return { parsed, enriched: false, reason: 'not_needed' } return { parsed, enriched: false, reason: 'not_needed' }
} }
const config = resolveAgisoXianyuTradeDetailConfig(parsed.shopId) const detailResult = await queryAgisoXianyuOrderDetail({
shopId: parsed.shopId,
platformOrderId: parsed.platformOrderId,
requestId,
})
if (!detailResult.success) {
return {
parsed,
enriched: false,
reason: detailResult.reason,
errorMessage: detailResult.errorMessage,
}
}
const detailPayload = detailResult.detailPayload
if (detailResult.totalAmountFen <= 0) {
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,
}
}
export async function queryAgisoXianyuOrderDetail({ shopId = '', platformOrderId = '', requestId = '' } = {}) {
const normalizedShopId = String(shopId || '').trim()
const normalizedPlatformOrderId = String(platformOrderId || '').trim()
const config = resolveAgisoXianyuTradeDetailConfig(normalizedShopId)
if (!config.endpoint || !config.accessToken || !config.appSecret) { if (!config.endpoint || !config.accessToken || !config.appSecret) {
logWebhook('[agiso/xianyu/order-detail]', '跳过 Agiso 咸鱼订单详情补查:缺少必要配置', { logWebhook('[agiso/xianyu/order-detail]', '跳过 Agiso 咸鱼订单详情补查:缺少必要配置', {
requestId, requestId,
platform: parsed.platform, platform: 'xianyu',
shopId: parsed.shopId, shopId: normalizedShopId,
platformOrderId: parsed.platformOrderId, platformOrderId: normalizedPlatformOrderId,
hasEndpoint: Boolean(config.endpoint), hasEndpoint: Boolean(config.endpoint),
hasAccessToken: Boolean(config.accessToken), hasAccessToken: Boolean(config.accessToken),
hasAppSecret: Boolean(config.appSecret), hasAppSecret: Boolean(config.appSecret),
}, { level: 'warn' }) }, { level: 'warn' })
return { parsed, enriched: false, reason: 'missing_config' } return createFailedDetailResult('missing_config')
} }
const requestBody = buildRequestBody({ const requestBody = buildRequestBody({
platformOrderId: parsed.platformOrderId, platformOrderId: normalizedPlatformOrderId,
appSecret: config.appSecret, appSecret: config.appSecret,
}) })
const requestHeaders = buildRequestHeaders({ const requestHeaders = buildRequestHeaders({
@@ -40,9 +85,9 @@ export async function enrichAgisoXianyuTradeOrder(parsed, { requestId = '' } = {
logWebhook('[agiso/xianyu/order-detail]', '开始补查 Agiso 咸鱼订单详情', { logWebhook('[agiso/xianyu/order-detail]', '开始补查 Agiso 咸鱼订单详情', {
requestId, requestId,
platform: parsed.platform, platform: 'xianyu',
shopId: parsed.shopId, shopId: normalizedShopId,
platformOrderId: parsed.platformOrderId, platformOrderId: normalizedPlatformOrderId,
endpoint: config.endpoint, endpoint: config.endpoint,
}) })
@@ -55,92 +100,103 @@ export async function enrichAgisoXianyuTradeOrder(parsed, { requestId = '' } = {
const rawText = await response.text() const rawText = await response.text()
const payload = parseJsonObject(rawText, { preserveLargeIntegers: true }) const payload = parseJsonObject(rawText, { preserveLargeIntegers: true })
const detailPayload = extractAgisoDetailPayload(payload) const detailPayload = extractAgisoDetailPayload(payload)
const detailAmountFen = resolveTotalAmountFen(detailPayload) const delivery = resolveAgisoXianyuOrderDeliveryState(detailPayload)
const totalAmountFen = resolveTotalAmountFen(detailPayload)
if (response.status < 200 || response.status >= 300) { if (response.status < 200 || response.status >= 300) {
const message = resolveAgisoDetailErrorMessage(payload, rawText, response.status) const message = resolveAgisoDetailErrorMessage(payload, rawText, response.status)
logWebhook('[agiso/xianyu/order-detail]', 'Agiso 咸鱼订单详情补查失败', { logWebhook('[agiso/xianyu/order-detail]', 'Agiso 咸鱼订单详情补查失败', {
requestId, requestId,
platform: parsed.platform, platform: 'xianyu',
shopId: parsed.shopId, shopId: normalizedShopId,
platformOrderId: parsed.platformOrderId, platformOrderId: normalizedPlatformOrderId,
responseStatus: response.status, responseStatus: response.status,
errorMessage: message, errorMessage: message,
}, { level: 'warn' }) }, { level: 'warn' })
return { parsed, enriched: false, reason: 'http_error', errorMessage: message } return createFailedDetailResult('http_error', {
responseStatus: response.status,
payload,
detailPayload,
errorMessage: message,
totalAmountFen,
delivery,
})
} }
if (!isAgisoDetailSuccess(payload)) { if (!isAgisoDetailSuccess(payload)) {
const message = resolveAgisoDetailErrorMessage(payload, rawText, response.status) const message = resolveAgisoDetailErrorMessage(payload, rawText, response.status)
logWebhook('[agiso/xianyu/order-detail]', 'Agiso 咸鱼订单详情补查返回业务失败', { logWebhook('[agiso/xianyu/order-detail]', 'Agiso 咸鱼订单详情补查返回业务失败', {
requestId, requestId,
platform: parsed.platform, platform: 'xianyu',
shopId: parsed.shopId, shopId: normalizedShopId,
platformOrderId: parsed.platformOrderId, platformOrderId: normalizedPlatformOrderId,
responseStatus: response.status, responseStatus: response.status,
errorMessage: message, errorMessage: message,
response: payload, response: payload,
}, { level: 'warn' }) }, { level: 'warn' })
return { parsed, enriched: false, reason: 'business_error', errorMessage: message } return createFailedDetailResult('business_error', {
responseStatus: response.status,
payload,
detailPayload,
errorMessage: message,
totalAmountFen,
delivery,
})
} }
if (!detailPayload || typeof detailPayload !== 'object' || Object.keys(detailPayload).length === 0) { if (!isPlainObject(detailPayload) || Object.keys(detailPayload).length === 0) {
logWebhook('[agiso/xianyu/order-detail]', 'Agiso 咸鱼订单详情补查未返回可用订单体', { logWebhook('[agiso/xianyu/order-detail]', 'Agiso 咸鱼订单详情补查未返回可用订单体', {
requestId, requestId,
platform: parsed.platform, platform: 'xianyu',
shopId: parsed.shopId, shopId: normalizedShopId,
platformOrderId: parsed.platformOrderId, platformOrderId: normalizedPlatformOrderId,
responseStatus: response.status, responseStatus: response.status,
}, { level: 'warn' }) }, { level: 'warn' })
return { parsed, enriched: false, reason: 'empty_detail_payload' } return createFailedDetailResult('empty_detail_payload', {
responseStatus: response.status,
payload,
})
} }
if (detailAmountFen <= 0) { if (totalAmountFen <= 0) {
logWebhook('[agiso/xianyu/order-detail]', 'Agiso 咸鱼订单详情已返回,但仍未解析出金额', { logWebhook('[agiso/xianyu/order-detail]', 'Agiso 咸鱼订单详情已返回,但仍未解析出金额', {
requestId, requestId,
platform: parsed.platform, platform: 'xianyu',
shopId: parsed.shopId, shopId: normalizedShopId,
platformOrderId: parsed.platformOrderId, platformOrderId: normalizedPlatformOrderId,
responseStatus: response.status, responseStatus: response.status,
detailKeys: Object.keys(detailPayload), detailKeys: Object.keys(detailPayload),
}, { level: 'warn' }) }, { 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 { return {
parsed: merged, success: true,
enriched: true, reason: '',
totalAmountFen: merged.totalAmount, errorMessage: '',
responseStatus: response.status,
payload: isPlainObject(payload) ? payload : {},
detailPayload,
totalAmountFen,
shipped: delivery.shipped,
shipTime: delivery.shipTime,
orderStatus: delivery.orderStatus,
} }
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : String(error || 'Agiso 咸鱼订单详情补查失败') const message = error instanceof Error ? error.message : String(error || 'Agiso 咸鱼订单详情补查失败')
logWebhook('[agiso/xianyu/order-detail]', 'Agiso 咸鱼订单详情补查异常', { logWebhook('[agiso/xianyu/order-detail]', 'Agiso 咸鱼订单详情补查异常', {
requestId, requestId,
platform: parsed.platform, platform: 'xianyu',
shopId: parsed.shopId, shopId: normalizedShopId,
platformOrderId: parsed.platformOrderId, platformOrderId: normalizedPlatformOrderId,
errorMessage: message, errorMessage: message,
}, { level: 'warn' }) }, { level: 'warn' })
return { parsed, enriched: false, reason: 'request_failed', errorMessage: message } return createFailedDetailResult('request_failed', {
errorMessage: message,
})
} }
} }
@@ -210,6 +266,29 @@ function extractAgisoDetailPayload(payload) {
return payload return payload
} }
export function resolveAgisoXianyuOrderDeliveryState(payload) {
const shipTime = normalizeTimestampValue(pickFirstNonEmpty([
payload?.ship_time,
payload?.shipTime,
payload?.ShipTime,
payload?.delivery_time,
payload?.deliveryTime,
]))
const orderStatus = normalizeInteger(
pickFirstNonEmpty([
payload?.order_status,
payload?.orderStatus,
payload?.status,
]),
)
return {
shipped: shipTime > 0 || orderStatus >= 3,
shipTime,
orderStatus,
}
}
function resolveTotalAmountFen(payload) { function resolveTotalAmountFen(payload) {
const fenAmount = normalizeFenInteger(pickFirstNonEmpty([ const fenAmount = normalizeFenInteger(pickFirstNonEmpty([
payload.payment, payload.payment,
@@ -559,6 +638,33 @@ function normalizeFenInteger(value) {
return Number.isFinite(parsed) && parsed > 0 ? Math.round(parsed) : 0 return Number.isFinite(parsed) && parsed > 0 ? Math.round(parsed) : 0
} }
function normalizeTimestampValue(value) {
const parsed = Number(value)
return Number.isFinite(parsed) && parsed > 0 ? Math.round(parsed) : 0
}
function createFailedDetailResult(reason, {
responseStatus = 0,
payload = {},
detailPayload = {},
errorMessage = '',
totalAmountFen = 0,
delivery = { shipped: false, shipTime: 0, orderStatus: 0 },
} = {}) {
return {
success: false,
reason: String(reason || '').trim(),
errorMessage: String(errorMessage || '').trim(),
responseStatus: Number(responseStatus || 0),
payload: isPlainObject(payload) ? payload : {},
detailPayload: isPlainObject(detailPayload) ? detailPayload : {},
totalAmountFen: Number(totalAmountFen || 0),
shipped: Boolean(delivery?.shipped),
shipTime: Number(delivery?.shipTime || 0),
orderStatus: Number(delivery?.orderStatus || 0),
}
}
function pickFirstNonEmpty(values) { function pickFirstNonEmpty(values) {
for (const value of values) { for (const value of values) {
const normalized = String(value || '').trim() const normalized = String(value || '').trim()
@@ -37,6 +37,7 @@ export {}
* accessToken?: string * accessToken?: string
* enabled?: boolean | null * enabled?: boolean | null
* messageTemplate?: string * messageTemplate?: string
* autoDeliveryMessageTemplate?: string
* appSecret?: string * appSecret?: string
* apiVersion?: string * apiVersion?: string
* sendMessageEndpoint?: string * sendMessageEndpoint?: string
@@ -28,6 +28,7 @@ export {}
* accessTokenMasked: string * accessTokenMasked: string
* enabled: boolean | null * enabled: boolean | null
* messageTemplate: string * messageTemplate: string
* autoDeliveryMessageTemplate: string
* appSecretConfigured: boolean * appSecretConfigured: boolean
* apiVersion: string * apiVersion: string
* sendMessageEndpoint: string * sendMessageEndpoint: string
+2
View File
@@ -17,6 +17,7 @@ export {}
* shopName?: string * shopName?: string
* accessToken?: string * accessToken?: string
* messageTemplate?: string * messageTemplate?: string
* autoDeliveryMessageTemplate?: string
* appSecret?: string * appSecret?: string
* apiVersion?: string * apiVersion?: string
* sendMessageEndpoint?: string * sendMessageEndpoint?: string
@@ -91,6 +92,7 @@ export {}
* apiVersion: string * apiVersion: string
* sendMessageEndpoint: string * sendMessageEndpoint: string
* messageTemplate: string * messageTemplate: string
* autoDeliveryMessageTemplate: string
* shops: AgisoMessagingShopsConfig * shops: AgisoMessagingShopsConfig
* } * }
* } * }
+1
View File
@@ -55,6 +55,7 @@ export interface AdminAgisoShopConfigItem {
accessTokenMasked: string accessTokenMasked: string
enabled: boolean | null enabled: boolean | null
messageTemplate: string messageTemplate: string
autoDeliveryMessageTemplate: string
appSecretConfigured: boolean appSecretConfigured: boolean
apiVersion: string apiVersion: string
sendMessageEndpoint: string sendMessageEndpoint: string
@@ -14,6 +14,7 @@ type EditableShop = {
accessToken: string accessToken: string
enabled: boolean enabled: boolean
messageTemplate: string messageTemplate: string
autoDeliveryMessageTemplate: string
} }
const loading = ref(true) const loading = ref(true)
@@ -31,6 +32,7 @@ function createEmptyShop(): EditableShop {
accessToken: '', accessToken: '',
enabled: true, enabled: true,
messageTemplate: '', messageTemplate: '',
autoDeliveryMessageTemplate: '',
} }
} }
@@ -42,6 +44,7 @@ function mapEditableShop(item: AdminAgisoShopConfigItem): EditableShop {
accessToken: item.accessToken, accessToken: item.accessToken,
enabled: item.enabled !== false, enabled: item.enabled !== false,
messageTemplate: item.messageTemplate, messageTemplate: item.messageTemplate,
autoDeliveryMessageTemplate: item.autoDeliveryMessageTemplate,
} }
} }
@@ -82,6 +85,7 @@ function importObservedShop(item: AdminAgisoObservedShopItem) {
accessToken: '', accessToken: '',
enabled: true, enabled: true,
messageTemplate: '', messageTemplate: '',
autoDeliveryMessageTemplate: '',
}) })
} }
@@ -93,6 +97,7 @@ async function saveConfigs() {
accessToken: item.accessToken.trim(), accessToken: item.accessToken.trim(),
enabled: item.enabled, enabled: item.enabled,
messageTemplate: item.messageTemplate.trim(), messageTemplate: item.messageTemplate.trim(),
autoDeliveryMessageTemplate: item.autoDeliveryMessageTemplate.trim(),
})) }))
.filter((item) => item.shopId && item.accessToken) .filter((item) => item.shopId && item.accessToken)
@@ -174,13 +179,22 @@ onMounted(loadConfigs)
</label> </label>
<label class="field-block field-wide"> <label class="field-block field-wide">
<span>消息模板</span> <span>领取链接消息模板</span>
<textarea <textarea
v-model="shop.messageTemplate" v-model="shop.messageTemplate"
class="text-area" class="text-area"
placeholder="留空则沿用全局 AGISO_MESSAGE_TEMPLATE" placeholder="留空则沿用全局 AGISO_MESSAGE_TEMPLATE"
/> />
</label> </label>
<label class="field-block field-wide">
<span>自动发货消息模板</span>
<textarea
v-model="shop.autoDeliveryMessageTemplate"
class="text-area"
placeholder="留空则沿用全局 AGISO_AUTO_DELIVERY_MESSAGE_TEMPLATE"
/>
</label>
</div> </div>
<div class="shop-actions"> <div class="shop-actions">
@@ -224,6 +224,8 @@ function formatAutoDeliveryReason(value: string) {
return 'Agiso 接口返回失败' return 'Agiso 接口返回失败'
case 'request_error': case 'request_error':
return '请求 Agiso 接口时发生异常' return '请求 Agiso 接口时发生异常'
case 'delivery_not_confirmed':
return '接口返回成功,但订单状态未确认进入已发货'
case 'task_not_delivered': case 'task_not_delivered':
return '当前任务尚未进入已交付状态' return '当前任务尚未进入已交付状态'
case 'not_supported': case 'not_supported':