From aedc5d928e872559b6c951f5b0c53a49d9a170b6 Mon Sep 17 00:00:00 2001 From: yml Date: Thu, 21 May 2026 14:46:56 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8E=E7=AB=AF=E8=BF=81=E7=A7=BB=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=8F=91=E8=B4=A7=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ry-service.js => auto-delivery-service.ts} | 244 +++++++++++++----- apps/backend/tsconfig.json | 2 +- docs/backend-typescript-migration-plan.md | 9 + 3 files changed, 195 insertions(+), 60 deletions(-) rename apps/backend/src/services/platforms/agiso/xianyu/{auto-delivery-service.js => auto-delivery-service.ts} (68%) 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.ts similarity index 68% rename from apps/backend/src/services/platforms/agiso/xianyu/auto-delivery-service.js rename to apps/backend/src/services/platforms/agiso/xianyu/auto-delivery-service.ts index ba058c90..a8aa9537 100644 --- a/apps/backend/src/services/platforms/agiso/xianyu/auto-delivery-service.js +++ b/apps/backend/src/services/platforms/agiso/xianyu/auto-delivery-service.ts @@ -1,5 +1,3 @@ -// @ts-nocheck - import crypto from 'node:crypto' import { runtimeConfig } from '../../../../config/runtime.js' @@ -11,23 +9,125 @@ import { ensureAgisoXianyuAutoDeliveryMessageDeliveredForTask } from './message- import { parseJsonObject } from '../../../../utils/json.js' import { logWebhook } from '../../../../utils/logger.js' import { nowIso } from '../../../../utils/time.js' +import type { TaskRow } from '../../../../types/repository-rows.js' const AGISO_DUMMY_SEND_ENDPOINT = 'https://gw-api.agiso.com/aldsIdle/Order/DummySend' -/** - * @typedef {{ - * order?: Record | null - * task?: Record | null - * trigger?: string - * }} EnsureAgisoXianyuAutoDeliveryInput - */ +type JsonObject = Record + +type AutoDeliveryOrder = { + id?: number | string + provider?: string + platform?: string + shop_id?: string + shop_name?: string + platform_order_id?: string + [key: string]: unknown +} + +type SupportedAgisoOrder = AutoDeliveryOrder & { + id: number | string + provider: 'agiso' + platform: 'xianyu' + platform_order_id: string +} + +type AutoDeliveryTask = Partial & { + id?: number | string + delivery_status?: string + task_status?: string + context_json?: string | JsonObject | null + [key: string]: unknown +} + +type AutoDeliveryTaskWithId = AutoDeliveryTask & { + id: number | string +} + +type EnsureAgisoXianyuAutoDeliveryInput = { + order?: AutoDeliveryOrder | null + task?: AutoDeliveryTask | null + trigger?: string +} + +type AutoDeliveryStatus = 'success' | 'failed' | 'skipped' + +type FetchResponseLike = { + status: number + text: () => Promise +} + +type FetchLike = ( + input: string | URL, + init?: RequestInit, +) => Promise + +type AgisoAutoDeliveryConfirmInput = { + shopId?: string + platformOrderId?: string + requestId?: string +} + +type AgisoAutoDeliveryConfirmResult = { + shipped: boolean + orderStatus: number + shipTime: number + reason: string +} + +type AutoDeliveryMessageResult = { + sent: boolean + skipped?: boolean + reason?: string + [key: string]: unknown +} + +type EnsureAgisoXianyuAutoDeliveryDeps = { + listTasksByOrderId?: (orderId: number | string) => Promise + fetch?: FetchLike + confirmAgisoXianyuAutoDeliveryShipped?: ( + input: AgisoAutoDeliveryConfirmInput, + ) => Promise + updateTask?: ( + taskId: number | string, + patch: { context_json: string; updated_at: string }, + ) => Promise + createTaskEvent?: ( + taskId: number | string, + eventType: string, + payload?: unknown, + createdAt?: string, + ) => Promise + nowIso?: () => string + ensureAgisoXianyuAutoDeliveryMessageDeliveredForTask?: ( + input: { order?: AutoDeliveryOrder | null; task?: AutoDeliveryTask | null }, + ) => Promise +} + +type PersistAgisoAutoDeliveryResultInput = { + status: AutoDeliveryStatus + trigger?: string + reason?: string + order?: AutoDeliveryOrder | null + responseStatus?: number + response?: unknown + errorMessage?: string + detail?: JsonObject +} + +type AgisoAutoDeliveryConfig = { + enabled: boolean + endpoint: string + apiVersion: string + appSecret: string + accessToken: string +} -/** @param {EnsureAgisoXianyuAutoDeliveryInput} input */ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTask({ order, task, trigger = 'task_delivered', -} = {}) { +}: EnsureAgisoXianyuAutoDeliveryInput = {}) { return ensureAgisoXianyuAutoDeliveryForDeliveredTaskWithDeps({ order, task, trigger }) } @@ -35,29 +135,30 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTaskWithDeps({ order, task, trigger = 'task_delivered', -} = {}, deps = {}) { +}: EnsureAgisoXianyuAutoDeliveryInput = {}, deps: EnsureAgisoXianyuAutoDeliveryDeps = {}) { const listOrderTasks = deps.listTasksByOrderId || listTasksByOrderId - const sendRequest = deps.fetch || fetch + const sendRequest = deps.fetch || (fetch as FetchLike) const confirmShipped = deps.confirmAgisoXianyuAutoDeliveryShipped || confirmAgisoXianyuAutoDeliveryShipped 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 currentTask = task as AutoDeliveryTaskWithId + if (String(currentTask.delivery_status || '').trim() !== 'delivered') { + return { sent: false, skipped: true, reason: 'task_not_delivered', task: currentTask } } const orderTasks = await listOrderTasks(order.id) if (hasAgisoAutoDeliverySucceeded(orderTasks)) { - return { sent: false, skipped: true, reason: 'already_sent', task } + return { sent: false, skipped: true, reason: 'already_sent', task: currentTask } } if (!isOrderReadyForAgisoAutoDelivery(orderTasks)) { logWebhook('[agiso/xianyu/auto-delivery]', '跳过自动发货:订单下仍有任务未完成交付', { trigger, orderId: order.id, - taskId: task.id, + taskId: currentTask.id, platformOrderId: order.platform_order_id, taskStatuses: orderTasks.map((current) => ({ taskId: current.id, @@ -66,12 +167,12 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTaskWithDeps({ })), }) - return { sent: false, skipped: true, reason: 'waiting_other_tasks', task } + return { sent: false, skipped: true, reason: 'waiting_other_tasks', task: currentTask } } const config = resolveAgisoXianyuAutoDeliveryConfig(order) if (!config.enabled) { - return persistAgisoAutoDeliveryResult(task, { + return persistAgisoAutoDeliveryResult(currentTask, { status: 'skipped', trigger, reason: 'auto_delivery_disabled', @@ -80,7 +181,7 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTaskWithDeps({ } if (!config.endpoint || !config.accessToken || !config.appSecret) { - return persistAgisoAutoDeliveryResult(task, { + return persistAgisoAutoDeliveryResult(currentTask, { status: 'skipped', trigger, reason: 'missing_config', @@ -105,7 +206,7 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTaskWithDeps({ logWebhook('[agiso/xianyu/auto-delivery]', '开始执行 Agiso 咸鱼自动发货', { trigger, orderId: order.id, - taskId: task.id, + taskId: currentTask.id, shopId: order.shop_id, platformOrderId: order.platform_order_id, endpoint: config.endpoint, @@ -118,13 +219,13 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTaskWithDeps({ body: new URLSearchParams(requestBody).toString(), }) const rawText = await response.text() - const parsed = parseJsonObject(rawText) + const parsed = parseJsonObject(rawText) as JsonObject const success = isAgisoAutoDeliverySuccess(response.status, parsed) if (success) { const requestId = String(parsed?.RequestId || '').trim() - // 发货接口返回成功后,再查一次 Order/Detail 确认订单真的进入已发货状态 meow~ + // 发货接口返回成功后,再查一次 Order/Detail 确认订单真的进入已发货状态。 const confirmResult = await confirmShipped({ shopId: order.shop_id, platformOrderId: order.platform_order_id, @@ -135,7 +236,7 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTaskWithDeps({ logWebhook('[agiso/xianyu/auto-delivery]', 'Agiso 咸鱼自动发货接口返回成功,但订单状态未确认进入已发货', { trigger, orderId: order.id, - taskId: task.id, + taskId: currentTask.id, shopId: order.shop_id, platformOrderId: order.platform_order_id, responseStatus: response.status, @@ -145,7 +246,7 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTaskWithDeps({ confirmReason: confirmResult.reason, }, { level: 'warn' }) - return persistAgisoAutoDeliveryResult(task, { + return persistAgisoAutoDeliveryResult(currentTask, { status: 'failed', trigger, reason: 'delivery_not_confirmed', @@ -164,7 +265,7 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTaskWithDeps({ logWebhook('[agiso/xianyu/auto-delivery]', 'Agiso 咸鱼自动发货成功,订单已确认进入已发货状态', { trigger, orderId: order.id, - taskId: task.id, + taskId: currentTask.id, shopId: order.shop_id, platformOrderId: order.platform_order_id, responseStatus: response.status, @@ -173,7 +274,7 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTaskWithDeps({ confirmShipTime: confirmResult.shipTime, }) - const result = await persistAgisoAutoDeliveryResult(task, { + const result = await persistAgisoAutoDeliveryResult(currentTask, { status: 'success', trigger, reason: '', @@ -187,8 +288,8 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTaskWithDeps({ }, }, deps) - // 发货确认成功后,发送自定义消息通知 meow~ - await sendAutoDeliveryMessage({ order, task }, deps) + // 发货确认成功后,发送自定义消息通知。 + await sendAutoDeliveryMessage({ order, task: currentTask }, deps) return result } @@ -197,7 +298,7 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTaskWithDeps({ logWebhook('[agiso/xianyu/auto-delivery]', 'Agiso 咸鱼自动发货失败', { trigger, orderId: order.id, - taskId: task.id, + taskId: currentTask.id, shopId: order.shop_id, platformOrderId: order.platform_order_id, responseStatus: response.status, @@ -205,7 +306,7 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTaskWithDeps({ response: parsed, }, { level: 'warn' }) - return persistAgisoAutoDeliveryResult(task, { + return persistAgisoAutoDeliveryResult(currentTask, { status: 'failed', trigger, reason: 'request_failed', @@ -219,13 +320,13 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTaskWithDeps({ logWebhook('[agiso/xianyu/auto-delivery]', 'Agiso 咸鱼自动发货异常', { trigger, orderId: order.id, - taskId: task.id, + taskId: currentTask.id, shopId: order.shop_id, platformOrderId: order.platform_order_id, errorMessage, }, { level: 'warn' }) - return persistAgisoAutoDeliveryResult(task, { + return persistAgisoAutoDeliveryResult(currentTask, { status: 'failed', trigger, reason: 'request_error', @@ -237,21 +338,22 @@ export async function ensureAgisoXianyuAutoDeliveryForDeliveredTaskWithDeps({ } } -export function hasAgisoAutoDeliverySucceeded(tasks = []) { +export function hasAgisoAutoDeliverySucceeded(tasks: AutoDeliveryTask[] = []): boolean { return (Array.isArray(tasks) ? tasks : []).some((task) => { const context = parseTaskContext(task) - return String(context?.agisoAutoDelivery?.status || '').trim() === 'success' + const autoDelivery = isPlainObject(context.agisoAutoDelivery) ? context.agisoAutoDelivery : {} + return String(autoDelivery.status || '').trim() === 'success' }) } -export function isOrderReadyForAgisoAutoDelivery(tasks = []) { +export function isOrderReadyForAgisoAutoDelivery(tasks: AutoDeliveryTask[] = []): boolean { 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 || {} +function resolveAgisoXianyuAutoDeliveryConfig(order: AutoDeliveryOrder): AgisoAutoDeliveryConfig { + const baseConfig = runtimeConfig.platforms.agiso.autoDelivery const shopConfig = getAgisoShopConfig(String(order?.shop_id || '').trim()) || {} return { @@ -263,7 +365,10 @@ function resolveAgisoXianyuAutoDeliveryConfig(order) { } } -function buildRequestHeaders({ accessToken, apiVersion }) { +function buildRequestHeaders({ + accessToken, + apiVersion, +}: { accessToken: string; apiVersion: string }): Record { return { Authorization: `Bearer ${accessToken}`, ApiVersion: String(apiVersion || '1').trim() || '1', @@ -271,8 +376,11 @@ function buildRequestHeaders({ accessToken, apiVersion }) { } } -function buildRequestBody({ platformOrderId, appSecret }) { - const payload = { +function buildRequestBody({ + platformOrderId, + appSecret, +}: { platformOrderId: string; appSecret: string }): Record { + const payload: Record = { tid: String(platformOrderId || '').trim(), timestamp: String(Math.floor(Date.now() / 1000)), } @@ -281,7 +389,7 @@ function buildRequestBody({ platformOrderId, appSecret }) { return payload } -function generateSign(params, appSecret) { +function generateSign(params: Record, appSecret: string): string { const sortedEntries = Object.entries(params).sort(([left], [right]) => left.localeCompare(right)) let raw = String(appSecret || '').trim() @@ -294,7 +402,7 @@ function generateSign(params, appSecret) { return crypto.createHash('md5').update(raw, 'utf8').digest('hex').toLowerCase() } -export function isAgisoAutoDeliverySuccess(statusCode, payload) { +export function isAgisoAutoDeliverySuccess(statusCode: number, payload: unknown): boolean { if (statusCode < 200 || statusCode >= 300) { return false } @@ -303,24 +411,35 @@ export function isAgisoAutoDeliverySuccess(statusCode, payload) { return true } - if (payload.IsSuccess === true) { + const normalizedPayload = payload as JsonObject + if (normalizedPayload.IsSuccess === true) { return true } - if (Number(payload.Error_Code) === 0) { + if (Number(normalizedPayload.Error_Code) === 0) { return true } - if (Number(payload.code) === 0) { + if (Number(normalizedPayload.code) === 0) { return true } return false } -export function resolveAgisoAutoDeliveryErrorMessage(payload, rawText, statusCode) { +export function resolveAgisoAutoDeliveryErrorMessage( + payload: unknown, + rawText: string, + statusCode: number, +): string { if (payload && typeof payload === 'object') { - for (const value of [payload.Error_Msg, payload.msg, payload.message, payload.error]) { + const normalizedPayload = payload as JsonObject + for (const value of [ + normalizedPayload.Error_Msg, + normalizedPayload.msg, + normalizedPayload.message, + normalizedPayload.error, + ]) { const normalized = String(value || '').trim() if (normalized) { return normalized @@ -332,11 +451,11 @@ export function resolveAgisoAutoDeliveryErrorMessage(payload, rawText, statusCod return text || `Agiso 咸鱼自动发货失败,HTTP ${statusCode}` } -export function resolveAgisoAutoDeliveryEndpoint(value) { +export function resolveAgisoAutoDeliveryEndpoint(value: unknown): string { return AGISO_DUMMY_SEND_ENDPOINT } -async function persistAgisoAutoDeliveryResult(task, { +async function persistAgisoAutoDeliveryResult(task: AutoDeliveryTaskWithId, { status, trigger, reason, @@ -345,7 +464,7 @@ async function persistAgisoAutoDeliveryResult(task, { response = {}, errorMessage = '', detail = {}, -} = {}, deps = {}) { +}: PersistAgisoAutoDeliveryResultInput, deps: EnsureAgisoXianyuAutoDeliveryDeps = {}) { const getNowIso = deps.nowIso || nowIso const patchTask = deps.updateTask || updateTask const insertTaskEvent = deps.createTaskEvent || createTaskEvent @@ -392,7 +511,7 @@ async function persistAgisoAutoDeliveryResult(task, { } } -function parseTaskContext(task) { +function parseTaskContext(task: AutoDeliveryTask | null | undefined): JsonObject { const value = task?.context_json if (!value) { @@ -410,14 +529,14 @@ function parseTaskContext(task) { } } -function isAgisoXianyuOrder(order) { +function isAgisoXianyuOrder(order: AutoDeliveryOrder | null | undefined): order is SupportedAgisoOrder { return String(order?.provider || '').trim() === 'agiso' && String(order?.platform || '').trim() === 'xianyu' && Boolean(String(order?.platform_order_id || '').trim()) && Number(order?.id || 0) > 0 } -function normalizeBooleanLike(value, fallbackValue) { +function normalizeBooleanLike(value: unknown, fallbackValue: boolean): boolean { if (typeof value === 'boolean') { return value } @@ -438,15 +557,19 @@ function normalizeBooleanLike(value, fallbackValue) { return fallbackValue } -function isPlainObject(value) { +function isPlainObject(value: unknown): value is JsonObject { return Object.prototype.toString.call(value) === '[object Object]' } /** - * 发货接口返回成功后,再查一次 Order/Detail 确认订单是否真的进入已发货状态 meow~ + * 发货接口返回成功后,再查一次 Order/Detail 确认订单是否真的进入已发货状态。 * 只有 ship_time > 0 或 orderStatus >= 3 才算发货确认通过 */ -async function confirmAgisoXianyuAutoDeliveryShipped({ shopId = '', platformOrderId = '', requestId = '' } = {}) { +async function confirmAgisoXianyuAutoDeliveryShipped({ + shopId = '', + platformOrderId = '', + requestId = '', +}: AgisoAutoDeliveryConfirmInput = {}): Promise { try { const detailResult = await queryAgisoXianyuOrderDetail({ shopId, platformOrderId, requestId }) @@ -484,9 +607,12 @@ async function confirmAgisoXianyuAutoDeliveryShipped({ shopId = '', platformOrde } /** - * 自动发货成功后发送消息通知 meow~ + * 自动发货成功后发送消息通知。 */ -async function sendAutoDeliveryMessage({ order, task } = {}, deps = {}) { +async function sendAutoDeliveryMessage( + { order, task }: { order?: AutoDeliveryOrder | null; task?: AutoDeliveryTask | null } = {}, + deps: EnsureAgisoXianyuAutoDeliveryDeps = {}, +): Promise { try { const deliverMessage = deps.ensureAgisoXianyuAutoDeliveryMessageDeliveredForTask || ensureAgisoXianyuAutoDeliveryMessageDeliveredForTask diff --git a/apps/backend/tsconfig.json b/apps/backend/tsconfig.json index 171105ce..4111c803 100644 --- a/apps/backend/tsconfig.json +++ b/apps/backend/tsconfig.json @@ -20,6 +20,6 @@ "data", "src/**/*.test.js", "src/services/session/**", - "src/services/platforms/agiso/**" + "src/services/platforms/agiso/**/*.js" ] } diff --git a/docs/backend-typescript-migration-plan.md b/docs/backend-typescript-migration-plan.md index 3f38a307..373a34fd 100644 --- a/docs/backend-typescript-migration-plan.md +++ b/docs/backend-typescript-migration-plan.md @@ -320,6 +320,15 @@ - `npm run typecheck` - `npm run build` - `npm test` 共 131 个用例通过 +47. Agiso 自动发货服务迁移到 `.ts`: + - `src/services/platforms/agiso/xianyu/auto-delivery-service.ts` +48. 自动发货订单 / 任务形态、依赖注入、HTTP 响应、发货确认结果、上下文落库 payload 已显式类型化 +49. `tsconfig.json` 已将 Agiso 新增 `.ts` 文件纳入 `typecheck`,旧 Agiso `.js` 文件继续暂时排除 +50. Docker 内验证通过: + - `src/services/platforms/agiso/xianyu/auto-delivery-service.test.js` 共 9 个用例通过 + - `npm run typecheck` + - `npm run build` + - `npm test` 共 131 个用例通过 ## 下一步建议