优化启动流程

This commit is contained in:
yml2213
2026-04-13 19:01:48 +08:00
parent 7008c85e58
commit dc8a014601
18 changed files with 837 additions and 17 deletions
@@ -42,6 +42,7 @@ import { reserveInventoryForTask } from '../order/inventory-service.js'
import { replayAgisoTradeWebhookEvent } from '../order/webhook-service.js'
import { syncConfiguredFulfillmentBindings } from '../bootstrap/fulfillment-bootstrap-service.js'
import { extractAgisoTradePayload, resolveAgisoTradePlatformOrderId } from '../order/agiso-trade-parsing.js'
import { ensureAgisoXianyuAutoDeliveryForDeliveredTask } from '../platforms/agiso/xianyu/auto-delivery-service.js'
import { normalizeProductName } from '../order/product-match-service.js'
import { normalizeDateQuery, normalizePage, normalizePageSize, safeParseJson } from './admin-query-utils.js'
import { normalizeAdminRole } from './admin-auth-service.js'
@@ -1268,9 +1269,17 @@ export async function completeAdminTaskManualDispatch(taskId, payload = {}, sess
completedBy: manualDispatch.completedBy,
}, now)
const taskAfterAutoDelivery = outcome === 'delivered'
? (await ensureAgisoXianyuAutoDeliveryForDeliveredTask({
order: await getOrderById(task.order_id),
task: updatedTask,
trigger: 'manual_dispatch_completed',
})).task || updatedTask
: updatedTask
return {
outcome,
task: mapTaskActionPayload(updatedTask),
task: mapTaskActionPayload(taskAfterAutoDelivery),
}
}
@@ -15,6 +15,7 @@ import {
releaseReservedInventoryItem,
} from '../../repositories/inventory-repo.js'
import { buildClaimUrl } from './claim-service.js'
import { ensureAgisoXianyuAutoDeliveryForDeliveredTask } from '../platforms/agiso/xianyu/auto-delivery-service.js'
import { createHttpError } from '../../utils/http.js'
import { formatFenToAmount, normalizeFen } from '../../utils/money.js'
import { nowIso } from '../../utils/time.js'
@@ -215,10 +216,15 @@ async function finalizeClaimTaskRedeem(context) {
})
await markInventoryItemDelivered(inventoryItem.id, nowIso())
const autoDeliveryResult = await ensureAgisoXianyuAutoDeliveryForDeliveredTask({
order: context.order,
task: updatedTask,
trigger: 'claim_redeemed',
})
return buildClaimDetailPayload({
claimToken: context.claimToken,
task: updatedTask,
task: autoDeliveryResult.task || updatedTask,
order: context.order,
orderItem: context.orderItem,
session,
@@ -4,6 +4,7 @@ import { runtimeConfig } from '../../config/runtime.js'
import { createWebhookEvent, updateWebhookEvent } from '../../repositories/webhook-event-repo.js'
import { enrichAgisoXianyuTradeOrder } from '../platforms/agiso/xianyu/order-detail-service.js'
import {
extractAgisoTradeOrderItemSources,
extractAgisoTradePayload,
resolveAgisoTradePlatformOrderId,
} from './agiso-trade-parsing.js'
@@ -572,7 +573,7 @@ function parsePayloadJson(rawJson) {
}
function normalizeOrderItems(payload) {
const items = extractOrderItemSources(payload)
const items = extractAgisoTradeOrderItemSources(payload)
return items.map((item) => {
const source = isPlainObject(item) ? item : {}
@@ -0,0 +1,404 @@
import crypto from 'node:crypto'
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 { parseJsonObject } from '../../../../utils/json.js'
import { logWebhook } from '../../../../utils/logger.js'
import { nowIso } from '../../../../utils/time.js'
export async function ensureAgisoXianyuAutoDeliveryForDeliveredTask({
order,
task,
trigger = 'task_delivered',
} = {}) {
if (!isAgisoXianyuOrder(order) || !task?.id) {
return { sent: false, skipped: true, reason: 'not_supported', task }
}
if (String(task.delivery_status || '').trim() !== 'delivered') {
return { sent: false, skipped: true, reason: 'task_not_delivered', task }
}
const orderTasks = await listTasksByOrderId(order.id)
if (hasAgisoAutoDeliverySucceeded(orderTasks)) {
return { sent: false, skipped: true, reason: 'already_sent', task }
}
if (!isOrderReadyForAgisoAutoDelivery(orderTasks)) {
logWebhook('[agiso/xianyu/auto-delivery]', '跳过自动发货:订单下仍有任务未完成交付', {
trigger,
orderId: order.id,
taskId: task.id,
platformOrderId: order.platform_order_id,
taskStatuses: orderTasks.map((current) => ({
taskId: current.id,
taskStatus: current.task_status,
deliveryStatus: current.delivery_status,
})),
})
return { sent: false, skipped: true, reason: 'waiting_other_tasks', task }
}
const config = resolveAgisoXianyuAutoDeliveryConfig(order)
if (!config.enabled) {
return persistAgisoAutoDeliveryResult(task, {
status: 'skipped',
trigger,
reason: 'auto_delivery_disabled',
order,
})
}
if (!config.endpoint || !config.accessToken || !config.appSecret) {
return persistAgisoAutoDeliveryResult(task, {
status: 'skipped',
trigger,
reason: 'missing_config',
order,
detail: {
hasEndpoint: Boolean(config.endpoint),
hasAccessToken: Boolean(config.accessToken),
hasAppSecret: Boolean(config.appSecret),
},
})
}
const requestHeaders = buildRequestHeaders({
accessToken: config.accessToken,
apiVersion: config.apiVersion,
})
const requestBody = buildRequestBody({
tids: String(order.platform_order_id || '').trim(),
appSecret: config.appSecret,
aldsType: config.aldsType,
ignoreAldsLog: config.ignoreAldsLog,
ignoreBlackList: config.ignoreBlackList,
ignoreOnOff: config.ignoreOnOff,
ignoreRefundCheck: config.ignoreRefundCheck,
ignoreRestricted: config.ignoreRestricted,
ignoreTradeStatusCheck: config.ignoreTradeStatusCheck,
})
logWebhook('[agiso/xianyu/auto-delivery]', '开始执行 Agiso 咸鱼自动发货', {
trigger,
orderId: order.id,
taskId: task.id,
shopId: order.shop_id,
platformOrderId: order.platform_order_id,
endpoint: config.endpoint,
aldsType: config.aldsType,
})
try {
const response = await fetch(config.endpoint, {
method: 'POST',
headers: requestHeaders,
body: new URLSearchParams(requestBody).toString(),
})
const rawText = await response.text()
const parsed = parseJsonObject(rawText)
const success = isAgisoAutoDeliverySuccess(response.status, parsed)
if (success) {
const requestId = String(parsed?.RequestId || '').trim()
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,
})
return persistAgisoAutoDeliveryResult(task, {
status: 'success',
trigger,
reason: '',
order,
responseStatus: response.status,
response: parsed,
detail: {
requestId,
aldsType: config.aldsType,
},
})
}
const errorMessage = resolveAgisoAutoDeliveryErrorMessage(parsed, rawText, response.status)
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,
errorMessage,
response: parsed,
}, { level: 'warn' })
return persistAgisoAutoDeliveryResult(task, {
status: 'failed',
trigger,
reason: 'request_failed',
order,
responseStatus: response.status,
response: parsed,
errorMessage,
})
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error || 'Agiso 咸鱼自动发货失败')
logWebhook('[agiso/xianyu/auto-delivery]', 'Agiso 咸鱼自动发货异常', {
trigger,
orderId: order.id,
taskId: task.id,
shopId: order.shop_id,
platformOrderId: order.platform_order_id,
errorMessage,
}, { level: 'warn' })
return persistAgisoAutoDeliveryResult(task, {
status: 'failed',
trigger,
reason: 'request_error',
order,
responseStatus: 0,
response: {},
errorMessage,
})
}
}
export function hasAgisoAutoDeliverySucceeded(tasks = []) {
return (Array.isArray(tasks) ? tasks : []).some((task) => {
const context = parseTaskContext(task)
return String(context?.agisoAutoDelivery?.status || '').trim() === 'success'
})
}
export function isOrderReadyForAgisoAutoDelivery(tasks = []) {
const normalizedTasks = Array.isArray(tasks) ? tasks.filter(Boolean) : []
return normalizedTasks.length > 0
&& normalizedTasks.every((task) => String(task.delivery_status || '').trim() === 'delivered')
}
function resolveAgisoXianyuAutoDeliveryConfig(order) {
const baseConfig = runtimeConfig.platforms?.agiso?.autoDelivery || {}
const shopConfig = getAgisoShopConfig(String(order?.shop_id || '').trim()) || {}
return {
enabled: normalizeBooleanLike(baseConfig.enabled, true),
endpoint: String(baseConfig.endpoint || '').trim(),
apiVersion: String(baseConfig.apiVersion || shopConfig.apiVersion || '1').trim() || '1',
appSecret: String(shopConfig.appSecret || runtimeConfig.platforms?.agiso?.appSecret || '').trim(),
accessToken: String(shopConfig.accessToken || runtimeConfig.platforms?.agiso?.messaging?.accessToken || '').trim(),
aldsType: normalizePositiveInteger(baseConfig.aldsType, 1),
ignoreAldsLog: normalizeBooleanLike(baseConfig.ignoreAldsLog, false),
ignoreBlackList: normalizeBooleanLike(baseConfig.ignoreBlackList, false),
ignoreOnOff: normalizeBooleanLike(baseConfig.ignoreOnOff, false),
ignoreRefundCheck: normalizeBooleanLike(baseConfig.ignoreRefundCheck, false),
ignoreRestricted: normalizeBooleanLike(baseConfig.ignoreRestricted, false),
ignoreTradeStatusCheck: normalizeBooleanLike(baseConfig.ignoreTradeStatusCheck, false),
}
}
function buildRequestHeaders({ accessToken, apiVersion }) {
return {
Authorization: `Bearer ${accessToken}`,
ApiVersion: String(apiVersion || '1').trim() || '1',
'content-type': 'application/x-www-form-urlencoded; charset=utf-8',
}
}
function buildRequestBody({
tids,
appSecret,
aldsType,
ignoreAldsLog,
ignoreBlackList,
ignoreOnOff,
ignoreRefundCheck,
ignoreRestricted,
ignoreTradeStatusCheck,
}) {
const payload = {
tids: String(tids || '').trim(),
aldsType: String(normalizePositiveInteger(aldsType, 1)),
ignoreAldsLog: String(Boolean(ignoreAldsLog)),
ignoreBlackList: String(Boolean(ignoreBlackList)),
ignoreOnOff: String(Boolean(ignoreOnOff)),
ignoreRefundCheck: String(Boolean(ignoreRefundCheck)),
ignoreRestricted: String(Boolean(ignoreRestricted)),
ignoreTradeStatusCheck: String(Boolean(ignoreTradeStatusCheck)),
timestamp: String(Math.floor(Date.now() / 1000)),
}
payload.sign = generateSign(payload, appSecret)
return payload
}
function generateSign(params, appSecret) {
const sortedEntries = Object.entries(params).sort(([left], [right]) => left.localeCompare(right))
let raw = String(appSecret || '').trim()
for (const [key, value] of sortedEntries) {
raw += `${key}${value}`
}
raw += String(appSecret || '').trim()
return crypto.createHash('md5').update(raw, 'utf8').digest('hex').toLowerCase()
}
function isAgisoAutoDeliverySuccess(statusCode, payload) {
if (statusCode < 200 || statusCode >= 300) {
return false
}
if (!payload || typeof payload !== 'object' || Object.keys(payload).length === 0) {
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 resolveAgisoAutoDeliveryErrorMessage(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 persistAgisoAutoDeliveryResult(task, {
status,
trigger,
reason,
order,
responseStatus = 0,
response = {},
errorMessage = '',
detail = {},
} = {}) {
const now = nowIso()
const currentContext = parseTaskContext(task)
const nextContext = {
...currentContext,
agisoAutoDelivery: {
status,
trigger: String(trigger || '').trim(),
reason: String(reason || '').trim(),
platformOrderId: String(order?.platform_order_id || '').trim(),
responseStatus: Number(responseStatus || 0),
errorMessage: String(errorMessage || '').trim(),
response: isPlainObject(response) ? response : {},
updatedAt: now,
...detail,
},
}
const updatedTask = await updateTask(task.id, {
context_json: JSON.stringify(nextContext),
updated_at: now,
})
if (status === 'success' || status === 'failed' || status === 'skipped') {
await createTaskEvent(task.id, `agiso_auto_delivery_${status}`, {
trigger: String(trigger || '').trim(),
reason: String(reason || '').trim(),
platformOrderId: String(order?.platform_order_id || '').trim(),
responseStatus: Number(responseStatus || 0),
errorMessage: String(errorMessage || '').trim(),
...detail,
}, now)
}
return {
sent: status === 'success',
skipped: status === 'skipped',
reason: String(reason || '').trim(),
errorMessage: String(errorMessage || '').trim(),
responseStatus: Number(responseStatus || 0),
response,
task: updatedTask || task,
}
}
function parseTaskContext(task) {
const value = task?.context_json
if (!value) {
return {}
}
if (isPlainObject(value)) {
return value
}
try {
return JSON.parse(String(value || '{}'))
} catch {
return {}
}
}
function isAgisoXianyuOrder(order) {
return String(order?.provider || '').trim() === 'agiso'
&& String(order?.platform || '').trim() === 'xianyu'
&& Boolean(String(order?.platform_order_id || '').trim())
&& Number(order?.id || 0) > 0
}
function normalizePositiveInteger(value, fallbackValue) {
const parsed = Number(value)
if (Number.isFinite(parsed) && parsed > 0) {
return Math.floor(parsed)
}
return fallbackValue
}
function normalizeBooleanLike(value, fallbackValue) {
if (typeof value === 'boolean') {
return value
}
if (typeof value === 'undefined' || value === null || value === '') {
return fallbackValue
}
const normalized = String(value || '').trim().toLowerCase()
if (['1', 'true', 'yes', 'on'].includes(normalized)) {
return true
}
if (['0', 'false', 'no', 'off'].includes(normalized)) {
return false
}
return fallbackValue
}
function isPlainObject(value) {
return Object.prototype.toString.call(value) === '[object Object]'
}
@@ -0,0 +1,30 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import {
hasAgisoAutoDeliverySucceeded,
isOrderReadyForAgisoAutoDelivery,
} from './auto-delivery-service.js'
test('isOrderReadyForAgisoAutoDelivery requires every task to be delivered', () => {
assert.equal(isOrderReadyForAgisoAutoDelivery([]), false)
assert.equal(isOrderReadyForAgisoAutoDelivery([
{ delivery_status: 'delivered' },
{ delivery_status: 'delivered' },
]), true)
assert.equal(isOrderReadyForAgisoAutoDelivery([
{ delivery_status: 'delivered' },
{ delivery_status: 'processing' },
]), false)
})
test('hasAgisoAutoDeliverySucceeded detects successful context on any task', () => {
assert.equal(hasAgisoAutoDeliverySucceeded([
{ context_json: {} },
{ context_json: { agisoAutoDelivery: { status: 'success' } } },
]), true)
assert.equal(hasAgisoAutoDeliverySucceeded([
{ context_json: { agisoAutoDelivery: { status: 'failed' } } },
]), false)
})