后端迁移消息发送服务

This commit is contained in:
yml
2026-05-21 14:49:20 +08:00
parent aedc5d928e
commit 4740f870ba
2 changed files with 207 additions and 28 deletions
@@ -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<string, unknown>
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<string>
}
type FetchLike = (
input: string | URL,
init?: RequestInit,
) => Promise<FetchResponseLike>
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<MessageDeliveryLike | null>
createMessageDelivery?: (
input: MessageDeliveryCreateInput,
) => Promise<MessageDeliveryLike | null>
updateMessageDelivery?: (
deliveryId: number | string,
patch: MessageDeliveryPatch,
) => Promise<MessageDeliveryLike | null>
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<string, string> {
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<string, string> {
const timestamp = String(Math.floor(Date.now() / 1000))
const payload = {
const payload: Record<string, string> = {
tid,
msg,
timestamp,
@@ -236,7 +388,7 @@ function buildRequestBody({ tid, msg, appSecret }) {
return payload
}
function generateSign(params, appSecret) {
function generateSign(params: Record<string, string>, 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<string, string>): Record<string, string> {
const output = { ...headers }
if (output.Authorization) {
@@ -359,7 +530,7 @@ function maskHeadersForStorage(headers) {
return output
}
function maskBodyForStorage(body) {
function maskBodyForStorage(body: Record<string, string>): Record<string, string> {
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]'
}
@@ -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 个用例通过
## 下一步建议