后端迁移消息发送服务
This commit is contained in:
+199
-28
@@ -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 个用例通过
|
||||
|
||||
## 下一步建议
|
||||
|
||||
|
||||
Reference in New Issue
Block a user