From 4740f870ba7c46d5ef8e6ff97e34deade9f9f8e4 Mon Sep 17 00:00:00 2001 From: yml Date: Thu, 21 May 2026 14:49:20 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8E=E7=AB=AF=E8=BF=81=E7=A7=BB=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E5=8F=91=E9=80=81=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...{message-service.js => message-service.ts} | 227 +++++++++++++++--- docs/backend-typescript-migration-plan.md | 8 + 2 files changed, 207 insertions(+), 28 deletions(-) rename apps/backend/src/services/platforms/agiso/xianyu/{message-service.js => message-service.ts} (63%) diff --git a/apps/backend/src/services/platforms/agiso/xianyu/message-service.js b/apps/backend/src/services/platforms/agiso/xianyu/message-service.ts similarity index 63% rename from apps/backend/src/services/platforms/agiso/xianyu/message-service.js rename to apps/backend/src/services/platforms/agiso/xianyu/message-service.ts index dee6b7bd..5a9ccd14 100644 --- a/apps/backend/src/services/platforms/agiso/xianyu/message-service.js +++ b/apps/backend/src/services/platforms/agiso/xianyu/message-service.ts @@ -1,5 +1,3 @@ -// @ts-nocheck - import crypto from 'node:crypto' import { runtimeConfig } from '../../../../config/runtime.js' @@ -14,7 +12,148 @@ 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 }) { +type JsonObject = Record + +type MessageOrder = { + id?: number | string + platform?: string + shop_id?: string + shop_name?: string + platform_order_id?: string + [key: string]: unknown +} + +type MessageTask = { + id?: number | string + task_no?: string + [key: string]: unknown +} + +type AgisoMessagingConfig = { + enabled?: boolean + sendMessageEndpoint?: string + accessToken?: string + appSecret?: string + apiVersion?: string + messageTemplate?: string + autoDeliveryMessageTemplate?: string + shopName?: string + [key: string]: unknown +} + +type EnsureClaimMessageInput = { + order?: MessageOrder | null + task?: MessageTask | null + claimUrl?: string + expiredAt?: string +} + +type EnsureAutoDeliveryMessageInput = { + order?: MessageOrder | null + task?: MessageTask | null +} + +type DeliverMessageInput = EnsureAutoDeliveryMessageInput & { + channel?: string + messageContent?: string + claimUrl?: string +} + +type FetchResponseLike = { + status: number + text: () => Promise +} + +type FetchLike = ( + input: string | URL, + init?: RequestInit, +) => Promise + +type MessageDeliveryLike = { + id: number | string + [key: string]: unknown +} + +type MessageDeliveryCreateInput = { + provider: string + platform: string + shopId: string + shopName: string + channel: string + orderId?: number | string | null + taskId?: number | string | null + platformOrderId?: string + recipientKey?: string + messageContent: string + claimUrl: string + status: string + requestUrl: string + requestHeadersJson: string + requestBodyJson: string + responseStatus: number + responseJson: string + errorMessage: string + sentAt: string | null + createdAt: string + updatedAt: string +} + +type MessageDeliveryPatch = { + status?: string + response_status?: number + response_json?: string + error_message?: string + sent_at?: string | null + updated_at?: string +} + +type FindSuccessfulDeliveryInput = { + provider: string + platform: string + shopId: string + platformOrderId: string + channel: string + claimUrl: string +} + +type DeliverMessageDeps = { + nowIso?: () => string + findLatestSuccessfulMessageDelivery?: ( + input: FindSuccessfulDeliveryInput, + ) => Promise + createMessageDelivery?: ( + input: MessageDeliveryCreateInput, + ) => Promise + updateMessageDelivery?: ( + deliveryId: number | string, + patch: MessageDeliveryPatch, + ) => Promise + fetch?: FetchLike +} + +type RenderClaimMessageInput = EnsureClaimMessageInput & { + template?: string + shopName?: string +} + +type RenderAutoDeliveryMessageInput = EnsureAutoDeliveryMessageInput & { + template?: string + shopName?: string +} + +type RenderMessageTemplateInput = RenderAutoDeliveryMessageInput & { + template?: string + claimUrl?: string + expiredAt?: string + resultMessage?: string +} + +export async function ensureAgisoXianyuClaimMessageDeliveredForTask({ + order, + task, + claimUrl, + expiredAt, +}: EnsureClaimMessageInput = {}) { const config = resolveAgisoXianyuMessagingConfig(order) const messageContent = renderAgisoClaimMessage({ order, @@ -34,7 +173,10 @@ export async function ensureAgisoXianyuClaimMessageDeliveredForTask({ order, tas }) } -export async function ensureAgisoXianyuAutoDeliveryMessageDeliveredForTask({ order, task } = {}) { +export async function ensureAgisoXianyuAutoDeliveryMessageDeliveredForTask({ + order, + task, +}: EnsureAutoDeliveryMessageInput = {}) { const config = resolveAgisoXianyuMessagingConfig(order) const messageContent = renderAgisoAutoDeliveryMessage({ order, @@ -51,7 +193,7 @@ export async function ensureAgisoXianyuAutoDeliveryMessageDeliveredForTask({ ord }) } -export function normalizeAgisoMessageTemplate(value) { +export function normalizeAgisoMessageTemplate(value: unknown): string { return String(value || '') .replaceAll('\\r\\n', '\n') .replaceAll('\\n', '\n') @@ -64,7 +206,7 @@ async function deliverAgisoXianyuMessageForTask({ channel, messageContent, claimUrl = '', -} = {}) { +}: DeliverMessageInput = {}) { return deliverAgisoXianyuMessageForTaskWithDeps({ order, task, @@ -80,12 +222,12 @@ export async function deliverAgisoXianyuMessageForTaskWithDeps({ channel, messageContent, claimUrl = '', -} = {}, deps = {}) { +}: DeliverMessageInput = {}, deps: DeliverMessageDeps = {}) { const now = deps.nowIso || nowIso const findSuccessfulDelivery = deps.findLatestSuccessfulMessageDelivery || findLatestSuccessfulMessageDelivery const insertMessageDelivery = deps.createMessageDelivery || createMessageDelivery const patchMessageDelivery = deps.updateMessageDelivery || updateMessageDelivery - const sendRequest = deps.fetch || fetch + const sendRequest = deps.fetch || (fetch as FetchLike) if (!order || !task || !messageContent) { return { sent: false, skipped: true, reason: 'missing_message_context' } @@ -149,6 +291,9 @@ export async function deliverAgisoXianyuMessageForTaskWithDeps({ createdAt, updatedAt: createdAt, }) + if (!delivery) { + return { sent: false, skipped: false, reason: 'delivery_create_failed' } + } try { const response = await sendRequest(url, { @@ -197,8 +342,8 @@ export async function deliverAgisoXianyuMessageForTaskWithDeps({ } } -function resolveAgisoXianyuMessagingConfig(order) { - const baseConfig = runtimeConfig.platforms?.agiso?.messaging || {} +function resolveAgisoXianyuMessagingConfig(order: MessageOrder | null | undefined): AgisoMessagingConfig { + const baseConfig = runtimeConfig.platforms.agiso.messaging const fileDefaults = getAgisoMessagingDefaults() const shopId = String(order?.shop_id || '').trim() const shopConfigs = getAgisoShopConfigMap() @@ -211,11 +356,14 @@ function resolveAgisoXianyuMessagingConfig(order) { } } -function buildRequestUrl(endpoint) { +function buildRequestUrl(endpoint: string): string { return new URL(endpoint).toString() } -function buildRequestHeaders({ accessToken, apiVersion }) { +function buildRequestHeaders({ + accessToken, + apiVersion, +}: { accessToken: string; apiVersion: string }): Record { return { Authorization: `Bearer ${accessToken}`, ApiVersion: apiVersion, @@ -223,9 +371,13 @@ function buildRequestHeaders({ accessToken, apiVersion }) { } } -function buildRequestBody({ tid, msg, appSecret }) { +function buildRequestBody({ + tid, + msg, + appSecret, +}: { tid: string; msg: string; appSecret: string }): Record { const timestamp = String(Math.floor(Date.now() / 1000)) - const payload = { + const payload: Record = { tid, msg, timestamp, @@ -236,7 +388,7 @@ function buildRequestBody({ tid, msg, 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 = appSecret @@ -249,7 +401,14 @@ function generateSign(params, appSecret) { return crypto.createHash('md5').update(raw, 'utf8').digest('hex').toLowerCase() } -function renderAgisoClaimMessage({ order, task, claimUrl, expiredAt, template, shopName = '' }) { +function renderAgisoClaimMessage({ + order, + task, + claimUrl, + expiredAt, + template, + shopName = '', +}: RenderClaimMessageInput): string { const source = normalizeAgisoMessageTemplate(String(template || '').trim()) || '您的订单 {platformOrderId} 已创建领取链接,请在 {expiredAt} 前完成领取:{claimUrl}' @@ -263,7 +422,12 @@ function renderAgisoClaimMessage({ order, task, claimUrl, expiredAt, template, s }) } -export function renderAgisoAutoDeliveryMessage({ order, task, template, shopName = '' }) { +export function renderAgisoAutoDeliveryMessage({ + order, + task, + template, + shopName = '', +}: RenderAutoDeliveryMessageInput): string { const source = normalizeAgisoMessageTemplate(String(template || '').trim()) || '您的订单 {platformOrderId} 已完成自动发货,请注意查收。' @@ -284,7 +448,7 @@ function renderAgisoMessageTemplate({ claimUrl = '', expiredAt = '', resultMessage = '', -} = {}) { +}: RenderMessageTemplateInput = {}): string { const resolvedShopName = String(shopName || order?.shop_name || order?.shop_id || '').trim() return normalizeAgisoMessageTemplate(template) @@ -297,7 +461,7 @@ function renderAgisoMessageTemplate({ .replaceAll('{resultMessage}', String(resultMessage || '')) } -function isAgisoSendSuccess(statusCode, payload) { +function isAgisoSendSuccess(statusCode: number, payload: unknown): boolean { if (statusCode < 200 || statusCode >= 300) { return false } @@ -306,24 +470,31 @@ function isAgisoSendSuccess(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 } -function resolveAgisoErrorMessage(payload, rawText, statusCode) { +function resolveAgisoErrorMessage(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 @@ -335,7 +506,7 @@ function resolveAgisoErrorMessage(payload, rawText, statusCode) { return text || `Agiso 咸鱼发消息失败,HTTP ${statusCode}` } -function safeParseJson(rawText) { +function safeParseJson(rawText: string): JsonObject | null { const normalized = String(rawText || '').trim() if (!normalized) { @@ -349,7 +520,7 @@ function safeParseJson(rawText) { } } -function maskHeadersForStorage(headers) { +function maskHeadersForStorage(headers: Record): Record { const output = { ...headers } if (output.Authorization) { @@ -359,7 +530,7 @@ function maskHeadersForStorage(headers) { return output } -function maskBodyForStorage(body) { +function maskBodyForStorage(body: Record): Record { const output = { ...body } if (output.sign) { @@ -369,6 +540,6 @@ function maskBodyForStorage(body) { return output } -function isPlainObject(value) { +function isPlainObject(value: unknown): value is JsonObject { return Object.prototype.toString.call(value) === '[object Object]' } diff --git a/docs/backend-typescript-migration-plan.md b/docs/backend-typescript-migration-plan.md index 373a34fd..6403bfb1 100644 --- a/docs/backend-typescript-migration-plan.md +++ b/docs/backend-typescript-migration-plan.md @@ -329,6 +329,14 @@ - `npm run typecheck` - `npm run build` - `npm test` 共 131 个用例通过 +51. Agiso 消息发送服务迁移到 `.ts`: + - `src/services/platforms/agiso/xianyu/message-service.ts` +52. 消息订单 / 任务、消息配置、模板渲染、去重查询、发送请求、delivery 创建与更新 payload 已显式类型化 +53. Docker 内验证通过: + - `src/services/platforms/agiso/xianyu/message-service.test.js` 共 3 个用例通过 + - `npm run typecheck` + - `npm run build` + - `npm test` 共 131 个用例通过 ## 下一步建议