232 lines
6.3 KiB
JavaScript
232 lines
6.3 KiB
JavaScript
import { runtimeConfig } from '../config/runtime.js'
|
|
import crypto from 'node:crypto'
|
|
import {
|
|
createMessageDelivery,
|
|
findLatestSuccessfulMessageDeliveryByTask,
|
|
updateMessageDelivery,
|
|
} from '../repositories/message-delivery-repo.js'
|
|
import { nowIso } from '../utils/time.js'
|
|
|
|
const AGISO_MESSAGE_CHANNEL = 'agiso_im'
|
|
|
|
export async function ensureAgisoClaimMessageDeliveredForTask({ order, task, claimUrl, expiredAt }) {
|
|
if (!order || !task || !claimUrl) {
|
|
return { sent: false, skipped: true, reason: 'missing_message_context' }
|
|
}
|
|
|
|
const config = runtimeConfig.platforms?.agiso?.messaging || {}
|
|
if (!config.enabled) {
|
|
return { sent: false, skipped: true, reason: 'messaging_disabled' }
|
|
}
|
|
|
|
const endpoint = String(config.sendMessageEndpoint || '').trim()
|
|
const accessToken = String(config.accessToken || '').trim()
|
|
const appSecret = String(config.appSecret || runtimeConfig.platforms?.agiso?.appSecret || '').trim()
|
|
if (!endpoint || !accessToken || !appSecret) {
|
|
return { sent: false, skipped: true, reason: 'missing_endpoint_or_access_token_or_app_secret' }
|
|
}
|
|
|
|
const successful = findLatestSuccessfulMessageDeliveryByTask(task.id, AGISO_MESSAGE_CHANNEL)
|
|
if (successful) {
|
|
return { sent: false, skipped: true, reason: 'already_sent', deliveryId: successful.id }
|
|
}
|
|
|
|
const messageContent = renderAgisoClaimMessage({ order, task, claimUrl, expiredAt, template: config.messageTemplate })
|
|
const url = buildRequestUrl(endpoint)
|
|
const requestBody = buildRequestBody({
|
|
tid: String(order.platform_order_id || ''),
|
|
msg: messageContent,
|
|
appSecret,
|
|
})
|
|
const requestHeaders = buildRequestHeaders({
|
|
accessToken,
|
|
apiVersion: String(config.apiVersion || '1').trim() || '1',
|
|
})
|
|
const createdAt = nowIso()
|
|
const delivery = createMessageDelivery({
|
|
platform: 'agiso',
|
|
channel: AGISO_MESSAGE_CHANNEL,
|
|
orderId: order.id,
|
|
taskId: task.id,
|
|
platformOrderId: order.platform_order_id,
|
|
recipientKey: order.platform_order_id,
|
|
messageContent,
|
|
claimUrl,
|
|
status: 'pending',
|
|
requestUrl: url,
|
|
requestHeadersJson: JSON.stringify(maskHeadersForStorage(requestHeaders)),
|
|
requestBodyJson: JSON.stringify(maskBodyForStorage(requestBody)),
|
|
responseStatus: 0,
|
|
responseJson: '{}',
|
|
errorMessage: '',
|
|
sentAt: null,
|
|
createdAt,
|
|
updatedAt: createdAt,
|
|
})
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
headers: requestHeaders,
|
|
body: new URLSearchParams(requestBody).toString(),
|
|
})
|
|
const rawText = await response.text()
|
|
const parsed = safeParseJson(rawText)
|
|
const success = isAgisoSendSuccess(response.status, parsed)
|
|
const updated = updateMessageDelivery(delivery.id, {
|
|
status: success ? 'success' : 'failed',
|
|
response_status: response.status,
|
|
response_json: JSON.stringify(parsed ?? { rawText }),
|
|
error_message: success ? '' : resolveAgisoErrorMessage(parsed, rawText, response.status),
|
|
sent_at: success ? nowIso() : null,
|
|
updated_at: nowIso(),
|
|
})
|
|
|
|
return {
|
|
sent: success,
|
|
skipped: false,
|
|
deliveryId: updated?.id || delivery.id,
|
|
responseStatus: response.status,
|
|
response: parsed ?? { rawText },
|
|
errorMessage: success ? '' : resolveAgisoErrorMessage(parsed, rawText, response.status),
|
|
}
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error || '发送消息失败')
|
|
updateMessageDelivery(delivery.id, {
|
|
status: 'failed',
|
|
response_status: 0,
|
|
response_json: '{}',
|
|
error_message: message,
|
|
sent_at: null,
|
|
updated_at: nowIso(),
|
|
})
|
|
|
|
return {
|
|
sent: false,
|
|
skipped: false,
|
|
deliveryId: delivery.id,
|
|
errorMessage: message,
|
|
}
|
|
}
|
|
}
|
|
|
|
function buildRequestUrl(endpoint) {
|
|
return new URL(endpoint).toString()
|
|
}
|
|
|
|
function buildRequestHeaders({ accessToken, apiVersion }) {
|
|
return {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
ApiVersion: apiVersion,
|
|
'content-type': 'application/x-www-form-urlencoded; charset=utf-8',
|
|
}
|
|
}
|
|
|
|
function buildRequestBody({ tid, msg, appSecret }) {
|
|
const timestamp = String(Math.floor(Date.now() / 1000))
|
|
const payload = {
|
|
tid,
|
|
msg,
|
|
timestamp,
|
|
}
|
|
|
|
payload.sign = generateSign(payload, appSecret)
|
|
|
|
return payload
|
|
}
|
|
|
|
function generateSign(params, appSecret) {
|
|
const sortedEntries = Object.entries(params).sort(([left], [right]) => left.localeCompare(right))
|
|
let raw = appSecret
|
|
|
|
for (const [key, value] of sortedEntries) {
|
|
raw += `${key}${value}`
|
|
}
|
|
|
|
raw += appSecret
|
|
|
|
return crypto.createHash('md5').update(raw, 'utf8').digest('hex').toLowerCase()
|
|
}
|
|
|
|
function renderAgisoClaimMessage({ order, task, claimUrl, expiredAt, template }) {
|
|
const source = String(template || '').trim() || '您的订单 {platformOrderId} 已创建领取链接,请在 {expiredAt} 前完成领取:{claimUrl}'
|
|
|
|
return source
|
|
.replaceAll('{platformOrderId}', String(order.platform_order_id || ''))
|
|
.replaceAll('{taskNo}', String(task.task_no || ''))
|
|
.replaceAll('{claimUrl}', claimUrl)
|
|
.replaceAll('{expiredAt}', String(expiredAt || '尽快'))
|
|
}
|
|
|
|
function isAgisoSendSuccess(statusCode, payload) {
|
|
if (statusCode < 200 || statusCode >= 300) {
|
|
return false
|
|
}
|
|
|
|
if (!payload || typeof payload !== 'object') {
|
|
return true
|
|
}
|
|
|
|
if (payload.IsSuccess === true) {
|
|
return true
|
|
}
|
|
|
|
if (Number(payload.Error_Code) === 0) {
|
|
return true
|
|
}
|
|
|
|
if (Number(payload.code) === 0) {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
function resolveAgisoErrorMessage(payload, rawText, statusCode) {
|
|
if (payload && typeof payload === 'object') {
|
|
for (const value of [payload.Error_Msg, payload.msg, payload.message, payload.error]) {
|
|
const normalized = String(value || '').trim()
|
|
if (normalized) {
|
|
return normalized
|
|
}
|
|
}
|
|
}
|
|
|
|
const text = String(rawText || '').trim()
|
|
return text || `Agiso 发消息失败,HTTP ${statusCode}`
|
|
}
|
|
|
|
function safeParseJson(rawText) {
|
|
const normalized = String(rawText || '').trim()
|
|
|
|
if (!normalized) {
|
|
return null
|
|
}
|
|
|
|
try {
|
|
return JSON.parse(normalized)
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
function maskHeadersForStorage(headers) {
|
|
const output = { ...headers }
|
|
|
|
if (output.Authorization) {
|
|
output.Authorization = '[masked]'
|
|
}
|
|
|
|
return output
|
|
}
|
|
|
|
function maskBodyForStorage(body) {
|
|
const output = { ...body }
|
|
|
|
if (output.sign) {
|
|
output.sign = '[masked]'
|
|
}
|
|
|
|
return output
|
|
}
|