From ed45eab26c9d2666da4a8d0a3b58682988146bff Mon Sep 17 00:00:00 2001 From: yml Date: Tue, 14 Apr 2026 15:54:13 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BA=86=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=8F=91=E8=B4=A7=E5=92=8C=E5=8F=91=E8=B4=A7=E5=90=8E=E7=9A=84?= =?UTF-8?q?=E6=B6=88=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/backend/config/default.cjs | 3 +- apps/backend/src/config/runtime.js | 7 +- .../admin/admin-platform-config-service.js | 16 +- .../platforms/agiso/shop-config-service.js | 2 +- .../agiso/xianyu/auto-delivery-service.js | 123 ++++++++++- .../platforms/agiso/xianyu/message-service.js | 110 ++++++++-- .../agiso/xianyu/order-detail-service.js | 206 +++++++++++++----- apps/backend/src/types/admin-write-inputs.js | 1 + apps/backend/src/types/admin-write-models.js | 1 + apps/backend/src/types/runtime-config.js | 2 + apps/frontend/src/types/admin.ts | 1 + .../views/admin/AdminPlatformShopsView.vue | 16 +- .../src/views/admin/AdminTaskDetailView.vue | 2 + 13 files changed, 413 insertions(+), 77 deletions(-) diff --git a/apps/backend/config/default.cjs b/apps/backend/config/default.cjs index 819e6807..4fde9a9d 100644 --- a/apps/backend/config/default.cjs +++ b/apps/backend/config/default.cjs @@ -53,7 +53,7 @@ module.exports = { }, autoDelivery: { enabled: true, - endpoint: 'https://gw-api.agiso.com/aldsIdle/Alds/AldsSend', + endpoint: 'https://gw-api.agiso.com/aldsIdle/Order/DummySend', apiVersion: '1', aldsType: 1, ignoreAldsLog: false, @@ -72,6 +72,7 @@ module.exports = { accessToken: '', appSecret: '', messageTemplate: '您的订单 {platformOrderId} 已创建领取链接,请在 {expiredAt} 前完成领取:{claimUrl}', + autoDeliveryMessageTemplate: '您的订单 {platformOrderId} 已完成自动发货,请注意查收。', shops: {}, }, }, diff --git a/apps/backend/src/config/runtime.js b/apps/backend/src/config/runtime.js index 4f2888ac..3598ab3d 100644 --- a/apps/backend/src/config/runtime.js +++ b/apps/backend/src/config/runtime.js @@ -270,6 +270,11 @@ function applyEnvOverrides(baseConfig) { 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) if (agisoShops) { nextConfig.platforms.agiso.messaging.shops = normalizeAgisoMessagingShops(agisoShops) @@ -402,7 +407,7 @@ function normalizeAgisoMessagingShops(rawValue) { 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() if (value) { next[key] = value diff --git a/apps/backend/src/services/admin/admin-platform-config-service.js b/apps/backend/src/services/admin/admin-platform-config-service.js index 29352d08..0597fd02 100644 --- a/apps/backend/src/services/admin/admin-platform-config-service.js +++ b/apps/backend/src/services/admin/admin-platform-config-service.js @@ -52,6 +52,7 @@ export async function getAdminAgisoShopConfigs() { accessTokenMasked: maskSecret(config.accessToken), enabled: typeof config.enabled === 'boolean' ? config.enabled : null, messageTemplate: String(config.messageTemplate || '').trim(), + autoDeliveryMessageTemplate: String(config.autoDeliveryMessageTemplate || '').trim(), appSecretConfigured: Boolean(String(config.appSecret || '').trim()), apiVersion: String(config.apiVersion || '').trim(), sendMessageEndpoint: String(config.sendMessageEndpoint || '').trim(), @@ -70,6 +71,7 @@ export async function getAdminAgisoShopConfigs() { /** @param {AdminAgisoShopConfigSaveInput} [payload] */ export function updateAdminAgisoShopConfigs(payload = /** @type {AdminAgisoShopConfigSaveInput} */ ({})) { const rawItems = Array.isArray(payload.shops) ? payload.shops : [] + const currentMap = getAgisoShopConfigMap() const nextMap = {} for (const item of rawItems) { @@ -78,22 +80,33 @@ export function updateAdminAgisoShopConfigs(payload = /** @type {AdminAgisoShopC continue } - const next = {} + const current = currentMap[shopId] || {} + const next = { ...current } const shopName = String(item?.shopName || '').trim() const accessToken = String(item?.accessToken || '').trim() const messageTemplate = String(item?.messageTemplate || '').trim() + const autoDeliveryMessageTemplate = String(item?.autoDeliveryMessageTemplate || '').trim() const appSecret = String(item?.appSecret || '').trim() const apiVersion = String(item?.apiVersion || '').trim() const sendMessageEndpoint = String(item?.sendMessageEndpoint || '').trim() if (shopName) { next.shopName = shopName + } else { + delete next.shopName } if (accessToken) { next.accessToken = accessToken } if (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) { next.appSecret = appSecret @@ -128,6 +141,7 @@ export function updateAdminAgisoShopConfigs(payload = /** @type {AdminAgisoShopC accessTokenMasked: maskSecret(config.accessToken), enabled: typeof config.enabled === 'boolean' ? config.enabled : null, messageTemplate: String(config.messageTemplate || '').trim(), + autoDeliveryMessageTemplate: String(config.autoDeliveryMessageTemplate || '').trim(), appSecretConfigured: Boolean(String(config.appSecret || '').trim()), apiVersion: String(config.apiVersion || '').trim(), sendMessageEndpoint: String(config.sendMessageEndpoint || '').trim(), diff --git a/apps/backend/src/services/platforms/agiso/shop-config-service.js b/apps/backend/src/services/platforms/agiso/shop-config-service.js index ecf97d19..b52a83d8 100644 --- a/apps/backend/src/services/platforms/agiso/shop-config-service.js +++ b/apps/backend/src/services/platforms/agiso/shop-config-service.js @@ -64,7 +64,7 @@ function normalizeAgisoShopConfigMap(rawValue) { 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() if (value) { next[key] = value diff --git a/apps/backend/src/services/platforms/agiso/xianyu/auto-delivery-service.js b/apps/backend/src/services/platforms/agiso/xianyu/auto-delivery-service.js index 958ff3a3..1c1fda80 100644 --- a/apps/backend/src/services/platforms/agiso/xianyu/auto-delivery-service.js +++ b/apps/backend/src/services/platforms/agiso/xianyu/auto-delivery-service.js @@ -4,6 +4,8 @@ import { runtimeConfig } from '../../../../config/runtime.js' import { listTasksByOrderId, updateTask } from '../../../../repositories/task-repo.js' import { createTaskEvent } from '../../../../repositories/task-event-repo.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 { logWebhook } from '../../../../utils/logger.js' import { nowIso } from '../../../../utils/time.js' @@ -113,7 +115,46 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTask({ if (success) { 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, orderId: order.id, taskId: task.id, @@ -121,9 +162,11 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTask({ platformOrderId: order.platform_order_id, responseStatus: response.status, requestId, + confirmOrderStatus: confirmResult.orderStatus, + confirmShipTime: confirmResult.shipTime, }) - return persistAgisoAutoDeliveryResult(task, { + const result = await persistAgisoAutoDeliveryResult(task, { status: 'success', trigger, reason: '', @@ -133,8 +176,15 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTask({ detail: { requestId, aldsType: config.aldsType, + confirmOrderStatus: confirmResult.orderStatus, + confirmShipTime: confirmResult.shipTime, }, }) + + // 发货确认成功后,发送自定义消息通知 meow~ + await sendAutoDeliveryMessage({ order, task }) + + return result } const errorMessage = resolveAgisoAutoDeliveryErrorMessage(parsed, rawText, response.status) @@ -411,3 +461,72 @@ function normalizeBooleanLike(value, fallbackValue) { function isPlainObject(value) { 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' } + } +} diff --git a/apps/backend/src/services/platforms/agiso/xianyu/message-service.js b/apps/backend/src/services/platforms/agiso/xianyu/message-service.js index 69b89c1e..7f4e39c6 100644 --- a/apps/backend/src/services/platforms/agiso/xianyu/message-service.js +++ b/apps/backend/src/services/platforms/agiso/xianyu/message-service.js @@ -10,9 +10,53 @@ import { import { nowIso } from '../../../../utils/time.js' 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 }) { - 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' } } @@ -28,19 +72,11 @@ export async function ensureAgisoXianyuClaimMessageDeliveredForTask({ order, tas 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) { 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 || ''), @@ -57,7 +93,7 @@ export async function ensureAgisoXianyuClaimMessageDeliveredForTask({ order, tas platform: String(order.platform || '').trim() || 'unknown', shopId: String(order.shop_id || '').trim(), shopName: String(order.shop_name || '').trim(), - channel: AGISO_XIANYU_MESSAGE_CHANNEL, + channel, orderId: order.id, taskId: task.id, platformOrderId: order.platform_order_id, @@ -85,11 +121,12 @@ export async function ensureAgisoXianyuClaimMessageDeliveredForTask({ order, tas const rawText = await response.text() const parsed = safeParseJson(rawText) const success = isAgisoSendSuccess(response.status, parsed) + const errorMessage = success ? '' : resolveAgisoErrorMessage(parsed, rawText, response.status) const updated = await 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), + error_message: errorMessage, sent_at: success ? nowIso() : null, updated_at: nowIso(), }) @@ -100,7 +137,7 @@ export async function ensureAgisoXianyuClaimMessageDeliveredForTask({ order, tas deliveryId: updated?.id || delivery.id, responseStatus: response.status, response: parsed ?? { rawText }, - errorMessage: success ? '' : resolveAgisoErrorMessage(parsed, rawText, response.status), + errorMessage, } } catch (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 = '' }) { 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 || '')) + return renderAgisoMessageTemplate({ + order, + 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('{shopId}', String(order.shop_id || '')) - .replaceAll('{claimUrl}', claimUrl) - .replaceAll('{expiredAt}', String(expiredAt || '尽快')) + .replaceAll('{shopId}', String(order?.shop_id || '')) + .replaceAll('{claimUrl}', String(claimUrl || '')) + .replaceAll('{expiredAt}', String(expiredAt || '')) + .replaceAll('{resultMessage}', String(resultMessage || '')) } function isAgisoSendSuccess(statusCode, payload) { diff --git a/apps/backend/src/services/platforms/agiso/xianyu/order-detail-service.js b/apps/backend/src/services/platforms/agiso/xianyu/order-detail-service.js index 459bf230..d2736750 100644 --- a/apps/backend/src/services/platforms/agiso/xianyu/order-detail-service.js +++ b/apps/backend/src/services/platforms/agiso/xianyu/order-detail-service.js @@ -14,23 +14,68 @@ export async function enrichAgisoXianyuTradeOrder(parsed, { requestId = '' } = { 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) { logWebhook('[agiso/xianyu/order-detail]', '跳过 Agiso 咸鱼订单详情补查:缺少必要配置', { requestId, - platform: parsed.platform, - shopId: parsed.shopId, - platformOrderId: parsed.platformOrderId, + platform: 'xianyu', + shopId: normalizedShopId, + platformOrderId: normalizedPlatformOrderId, hasEndpoint: Boolean(config.endpoint), hasAccessToken: Boolean(config.accessToken), hasAppSecret: Boolean(config.appSecret), }, { level: 'warn' }) - return { parsed, enriched: false, reason: 'missing_config' } + return createFailedDetailResult('missing_config') } const requestBody = buildRequestBody({ - platformOrderId: parsed.platformOrderId, + platformOrderId: normalizedPlatformOrderId, appSecret: config.appSecret, }) const requestHeaders = buildRequestHeaders({ @@ -40,9 +85,9 @@ export async function enrichAgisoXianyuTradeOrder(parsed, { requestId = '' } = { logWebhook('[agiso/xianyu/order-detail]', '开始补查 Agiso 咸鱼订单详情', { requestId, - platform: parsed.platform, - shopId: parsed.shopId, - platformOrderId: parsed.platformOrderId, + platform: 'xianyu', + shopId: normalizedShopId, + platformOrderId: normalizedPlatformOrderId, endpoint: config.endpoint, }) @@ -55,92 +100,103 @@ export async function enrichAgisoXianyuTradeOrder(parsed, { requestId = '' } = { const rawText = await response.text() const payload = parseJsonObject(rawText, { preserveLargeIntegers: true }) const detailPayload = extractAgisoDetailPayload(payload) - const detailAmountFen = resolveTotalAmountFen(detailPayload) + const delivery = resolveAgisoXianyuOrderDeliveryState(detailPayload) + const totalAmountFen = 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, + platform: 'xianyu', + shopId: normalizedShopId, + platformOrderId: normalizedPlatformOrderId, responseStatus: response.status, errorMessage: message, }, { 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)) { const message = resolveAgisoDetailErrorMessage(payload, rawText, response.status) logWebhook('[agiso/xianyu/order-detail]', 'Agiso 咸鱼订单详情补查返回业务失败', { requestId, - platform: parsed.platform, - shopId: parsed.shopId, - platformOrderId: parsed.platformOrderId, + platform: 'xianyu', + shopId: normalizedShopId, + platformOrderId: normalizedPlatformOrderId, responseStatus: response.status, errorMessage: message, response: payload, }, { 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 咸鱼订单详情补查未返回可用订单体', { requestId, - platform: parsed.platform, - shopId: parsed.shopId, - platformOrderId: parsed.platformOrderId, + platform: 'xianyu', + shopId: normalizedShopId, + platformOrderId: normalizedPlatformOrderId, responseStatus: response.status, }, { 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 咸鱼订单详情已返回,但仍未解析出金额', { requestId, - platform: parsed.platform, - shopId: parsed.shopId, - platformOrderId: parsed.platformOrderId, + platform: 'xianyu', + shopId: normalizedShopId, + platformOrderId: normalizedPlatformOrderId, 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, + success: true, + reason: '', + errorMessage: '', + responseStatus: response.status, + payload: isPlainObject(payload) ? payload : {}, + detailPayload, + totalAmountFen, + shipped: delivery.shipped, + shipTime: delivery.shipTime, + orderStatus: delivery.orderStatus, } } 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, + platform: 'xianyu', + shopId: normalizedShopId, + platformOrderId: normalizedPlatformOrderId, errorMessage: message, }, { 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 } +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) { const fenAmount = normalizeFenInteger(pickFirstNonEmpty([ payload.payment, @@ -559,6 +638,33 @@ function normalizeFenInteger(value) { 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) { for (const value of values) { const normalized = String(value || '').trim() diff --git a/apps/backend/src/types/admin-write-inputs.js b/apps/backend/src/types/admin-write-inputs.js index 1384f7b3..5dcf101d 100644 --- a/apps/backend/src/types/admin-write-inputs.js +++ b/apps/backend/src/types/admin-write-inputs.js @@ -37,6 +37,7 @@ export {} * accessToken?: string * enabled?: boolean | null * messageTemplate?: string + * autoDeliveryMessageTemplate?: string * appSecret?: string * apiVersion?: string * sendMessageEndpoint?: string diff --git a/apps/backend/src/types/admin-write-models.js b/apps/backend/src/types/admin-write-models.js index c9157e0f..7373f7bb 100644 --- a/apps/backend/src/types/admin-write-models.js +++ b/apps/backend/src/types/admin-write-models.js @@ -28,6 +28,7 @@ export {} * accessTokenMasked: string * enabled: boolean | null * messageTemplate: string + * autoDeliveryMessageTemplate: string * appSecretConfigured: boolean * apiVersion: string * sendMessageEndpoint: string diff --git a/apps/backend/src/types/runtime-config.js b/apps/backend/src/types/runtime-config.js index 8a1f3c6a..7b373515 100644 --- a/apps/backend/src/types/runtime-config.js +++ b/apps/backend/src/types/runtime-config.js @@ -17,6 +17,7 @@ export {} * shopName?: string * accessToken?: string * messageTemplate?: string + * autoDeliveryMessageTemplate?: string * appSecret?: string * apiVersion?: string * sendMessageEndpoint?: string @@ -91,6 +92,7 @@ export {} * apiVersion: string * sendMessageEndpoint: string * messageTemplate: string + * autoDeliveryMessageTemplate: string * shops: AgisoMessagingShopsConfig * } * } diff --git a/apps/frontend/src/types/admin.ts b/apps/frontend/src/types/admin.ts index cfb5277a..d2d45c03 100644 --- a/apps/frontend/src/types/admin.ts +++ b/apps/frontend/src/types/admin.ts @@ -55,6 +55,7 @@ export interface AdminAgisoShopConfigItem { accessTokenMasked: string enabled: boolean | null messageTemplate: string + autoDeliveryMessageTemplate: string appSecretConfigured: boolean apiVersion: string sendMessageEndpoint: string diff --git a/apps/frontend/src/views/admin/AdminPlatformShopsView.vue b/apps/frontend/src/views/admin/AdminPlatformShopsView.vue index 451dcfb7..e7948cf3 100644 --- a/apps/frontend/src/views/admin/AdminPlatformShopsView.vue +++ b/apps/frontend/src/views/admin/AdminPlatformShopsView.vue @@ -14,6 +14,7 @@ type EditableShop = { accessToken: string enabled: boolean messageTemplate: string + autoDeliveryMessageTemplate: string } const loading = ref(true) @@ -31,6 +32,7 @@ function createEmptyShop(): EditableShop { accessToken: '', enabled: true, messageTemplate: '', + autoDeliveryMessageTemplate: '', } } @@ -42,6 +44,7 @@ function mapEditableShop(item: AdminAgisoShopConfigItem): EditableShop { accessToken: item.accessToken, enabled: item.enabled !== false, messageTemplate: item.messageTemplate, + autoDeliveryMessageTemplate: item.autoDeliveryMessageTemplate, } } @@ -82,6 +85,7 @@ function importObservedShop(item: AdminAgisoObservedShopItem) { accessToken: '', enabled: true, messageTemplate: '', + autoDeliveryMessageTemplate: '', }) } @@ -93,6 +97,7 @@ async function saveConfigs() { accessToken: item.accessToken.trim(), enabled: item.enabled, messageTemplate: item.messageTemplate.trim(), + autoDeliveryMessageTemplate: item.autoDeliveryMessageTemplate.trim(), })) .filter((item) => item.shopId && item.accessToken) @@ -174,13 +179,22 @@ onMounted(loadConfigs)