后端迁移消息发送服务
This commit is contained in:
+199
-28
@@ -1,5 +1,3 @@
|
|||||||
// @ts-nocheck
|
|
||||||
|
|
||||||
import crypto from 'node:crypto'
|
import crypto from 'node:crypto'
|
||||||
|
|
||||||
import { runtimeConfig } from '../../../../config/runtime.js'
|
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_MESSAGE_CHANNEL = 'agiso_im'
|
||||||
const AGISO_XIANYU_AUTO_DELIVERY_MESSAGE_CHANNEL = 'agiso_im_auto_delivery'
|
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 config = resolveAgisoXianyuMessagingConfig(order)
|
||||||
const messageContent = renderAgisoClaimMessage({
|
const messageContent = renderAgisoClaimMessage({
|
||||||
order,
|
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 config = resolveAgisoXianyuMessagingConfig(order)
|
||||||
const messageContent = renderAgisoAutoDeliveryMessage({
|
const messageContent = renderAgisoAutoDeliveryMessage({
|
||||||
order,
|
order,
|
||||||
@@ -51,7 +193,7 @@ export async function ensureAgisoXianyuAutoDeliveryMessageDeliveredForTask({ ord
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeAgisoMessageTemplate(value) {
|
export function normalizeAgisoMessageTemplate(value: unknown): string {
|
||||||
return String(value || '')
|
return String(value || '')
|
||||||
.replaceAll('\\r\\n', '\n')
|
.replaceAll('\\r\\n', '\n')
|
||||||
.replaceAll('\\n', '\n')
|
.replaceAll('\\n', '\n')
|
||||||
@@ -64,7 +206,7 @@ async function deliverAgisoXianyuMessageForTask({
|
|||||||
channel,
|
channel,
|
||||||
messageContent,
|
messageContent,
|
||||||
claimUrl = '',
|
claimUrl = '',
|
||||||
} = {}) {
|
}: DeliverMessageInput = {}) {
|
||||||
return deliverAgisoXianyuMessageForTaskWithDeps({
|
return deliverAgisoXianyuMessageForTaskWithDeps({
|
||||||
order,
|
order,
|
||||||
task,
|
task,
|
||||||
@@ -80,12 +222,12 @@ export async function deliverAgisoXianyuMessageForTaskWithDeps({
|
|||||||
channel,
|
channel,
|
||||||
messageContent,
|
messageContent,
|
||||||
claimUrl = '',
|
claimUrl = '',
|
||||||
} = {}, deps = {}) {
|
}: DeliverMessageInput = {}, deps: DeliverMessageDeps = {}) {
|
||||||
const now = deps.nowIso || nowIso
|
const now = deps.nowIso || nowIso
|
||||||
const findSuccessfulDelivery = deps.findLatestSuccessfulMessageDelivery || findLatestSuccessfulMessageDelivery
|
const findSuccessfulDelivery = deps.findLatestSuccessfulMessageDelivery || findLatestSuccessfulMessageDelivery
|
||||||
const insertMessageDelivery = deps.createMessageDelivery || createMessageDelivery
|
const insertMessageDelivery = deps.createMessageDelivery || createMessageDelivery
|
||||||
const patchMessageDelivery = deps.updateMessageDelivery || updateMessageDelivery
|
const patchMessageDelivery = deps.updateMessageDelivery || updateMessageDelivery
|
||||||
const sendRequest = deps.fetch || fetch
|
const sendRequest = deps.fetch || (fetch as FetchLike)
|
||||||
|
|
||||||
if (!order || !task || !messageContent) {
|
if (!order || !task || !messageContent) {
|
||||||
return { sent: false, skipped: true, reason: 'missing_message_context' }
|
return { sent: false, skipped: true, reason: 'missing_message_context' }
|
||||||
@@ -149,6 +291,9 @@ export async function deliverAgisoXianyuMessageForTaskWithDeps({
|
|||||||
createdAt,
|
createdAt,
|
||||||
updatedAt: createdAt,
|
updatedAt: createdAt,
|
||||||
})
|
})
|
||||||
|
if (!delivery) {
|
||||||
|
return { sent: false, skipped: false, reason: 'delivery_create_failed' }
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await sendRequest(url, {
|
const response = await sendRequest(url, {
|
||||||
@@ -197,8 +342,8 @@ export async function deliverAgisoXianyuMessageForTaskWithDeps({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveAgisoXianyuMessagingConfig(order) {
|
function resolveAgisoXianyuMessagingConfig(order: MessageOrder | null | undefined): AgisoMessagingConfig {
|
||||||
const baseConfig = runtimeConfig.platforms?.agiso?.messaging || {}
|
const baseConfig = runtimeConfig.platforms.agiso.messaging
|
||||||
const fileDefaults = getAgisoMessagingDefaults()
|
const fileDefaults = getAgisoMessagingDefaults()
|
||||||
const shopId = String(order?.shop_id || '').trim()
|
const shopId = String(order?.shop_id || '').trim()
|
||||||
const shopConfigs = getAgisoShopConfigMap()
|
const shopConfigs = getAgisoShopConfigMap()
|
||||||
@@ -211,11 +356,14 @@ function resolveAgisoXianyuMessagingConfig(order) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildRequestUrl(endpoint) {
|
function buildRequestUrl(endpoint: string): string {
|
||||||
return new URL(endpoint).toString()
|
return new URL(endpoint).toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildRequestHeaders({ accessToken, apiVersion }) {
|
function buildRequestHeaders({
|
||||||
|
accessToken,
|
||||||
|
apiVersion,
|
||||||
|
}: { accessToken: string; apiVersion: string }): Record<string, string> {
|
||||||
return {
|
return {
|
||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
ApiVersion: apiVersion,
|
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 timestamp = String(Math.floor(Date.now() / 1000))
|
||||||
const payload = {
|
const payload: Record<string, string> = {
|
||||||
tid,
|
tid,
|
||||||
msg,
|
msg,
|
||||||
timestamp,
|
timestamp,
|
||||||
@@ -236,7 +388,7 @@ function buildRequestBody({ tid, msg, appSecret }) {
|
|||||||
return payload
|
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))
|
const sortedEntries = Object.entries(params).sort(([left], [right]) => left.localeCompare(right))
|
||||||
let raw = appSecret
|
let raw = appSecret
|
||||||
|
|
||||||
@@ -249,7 +401,14 @@ function generateSign(params, appSecret) {
|
|||||||
return crypto.createHash('md5').update(raw, 'utf8').digest('hex').toLowerCase()
|
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())
|
const source = normalizeAgisoMessageTemplate(String(template || '').trim())
|
||||||
|| '您的订单 {platformOrderId} 已创建领取链接,请在 {expiredAt} 前完成领取:{claimUrl}'
|
|| '您的订单 {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())
|
const source = normalizeAgisoMessageTemplate(String(template || '').trim())
|
||||||
|| '您的订单 {platformOrderId} 已完成自动发货,请注意查收。'
|
|| '您的订单 {platformOrderId} 已完成自动发货,请注意查收。'
|
||||||
|
|
||||||
@@ -284,7 +448,7 @@ function renderAgisoMessageTemplate({
|
|||||||
claimUrl = '',
|
claimUrl = '',
|
||||||
expiredAt = '',
|
expiredAt = '',
|
||||||
resultMessage = '',
|
resultMessage = '',
|
||||||
} = {}) {
|
}: RenderMessageTemplateInput = {}): string {
|
||||||
const resolvedShopName = String(shopName || order?.shop_name || order?.shop_id || '').trim()
|
const resolvedShopName = String(shopName || order?.shop_name || order?.shop_id || '').trim()
|
||||||
|
|
||||||
return normalizeAgisoMessageTemplate(template)
|
return normalizeAgisoMessageTemplate(template)
|
||||||
@@ -297,7 +461,7 @@ function renderAgisoMessageTemplate({
|
|||||||
.replaceAll('{resultMessage}', String(resultMessage || ''))
|
.replaceAll('{resultMessage}', String(resultMessage || ''))
|
||||||
}
|
}
|
||||||
|
|
||||||
function isAgisoSendSuccess(statusCode, payload) {
|
function isAgisoSendSuccess(statusCode: number, payload: unknown): boolean {
|
||||||
if (statusCode < 200 || statusCode >= 300) {
|
if (statusCode < 200 || statusCode >= 300) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -306,24 +470,31 @@ function isAgisoSendSuccess(statusCode, payload) {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload.IsSuccess === true) {
|
const normalizedPayload = payload as JsonObject
|
||||||
|
if (normalizedPayload.IsSuccess === true) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Number(payload.Error_Code) === 0) {
|
if (Number(normalizedPayload.Error_Code) === 0) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Number(payload.code) === 0) {
|
if (Number(normalizedPayload.code) === 0) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveAgisoErrorMessage(payload, rawText, statusCode) {
|
function resolveAgisoErrorMessage(payload: unknown, rawText: string, statusCode: number): string {
|
||||||
if (payload && typeof payload === 'object') {
|
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()
|
const normalized = String(value || '').trim()
|
||||||
if (normalized) {
|
if (normalized) {
|
||||||
return normalized
|
return normalized
|
||||||
@@ -335,7 +506,7 @@ function resolveAgisoErrorMessage(payload, rawText, statusCode) {
|
|||||||
return text || `Agiso 咸鱼发消息失败,HTTP ${statusCode}`
|
return text || `Agiso 咸鱼发消息失败,HTTP ${statusCode}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function safeParseJson(rawText) {
|
function safeParseJson(rawText: string): JsonObject | null {
|
||||||
const normalized = String(rawText || '').trim()
|
const normalized = String(rawText || '').trim()
|
||||||
|
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
@@ -349,7 +520,7 @@ function safeParseJson(rawText) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function maskHeadersForStorage(headers) {
|
function maskHeadersForStorage(headers: Record<string, string>): Record<string, string> {
|
||||||
const output = { ...headers }
|
const output = { ...headers }
|
||||||
|
|
||||||
if (output.Authorization) {
|
if (output.Authorization) {
|
||||||
@@ -359,7 +530,7 @@ function maskHeadersForStorage(headers) {
|
|||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
function maskBodyForStorage(body) {
|
function maskBodyForStorage(body: Record<string, string>): Record<string, string> {
|
||||||
const output = { ...body }
|
const output = { ...body }
|
||||||
|
|
||||||
if (output.sign) {
|
if (output.sign) {
|
||||||
@@ -369,6 +540,6 @@ function maskBodyForStorage(body) {
|
|||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPlainObject(value) {
|
function isPlainObject(value: unknown): value is JsonObject {
|
||||||
return Object.prototype.toString.call(value) === '[object Object]'
|
return Object.prototype.toString.call(value) === '[object Object]'
|
||||||
}
|
}
|
||||||
@@ -329,6 +329,14 @@
|
|||||||
- `npm run typecheck`
|
- `npm run typecheck`
|
||||||
- `npm run build`
|
- `npm run build`
|
||||||
- `npm test` 共 131 个用例通过
|
- `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