后端迁移通知服务
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
import { logWarn } from '../../utils/logger.js'
|
||||
import { sendInternalNotification } from './notification-service.js'
|
||||
|
||||
const DEFAULT_COOLDOWN_MS = 10 * 60 * 1000
|
||||
const notificationCooldownMap = new Map<string, number>()
|
||||
|
||||
type JsonObject = Record<string, any>
|
||||
type InternalNotificationPayload = {
|
||||
title: string
|
||||
body?: string
|
||||
category?: string
|
||||
url?: string
|
||||
cooldownKey?: string
|
||||
cooldownMs?: number
|
||||
}
|
||||
|
||||
export async function notifyInternalSafely(payload: InternalNotificationPayload) {
|
||||
const cooldownKey = String(payload.cooldownKey || '').trim()
|
||||
if (cooldownKey && isNotificationCoolingDown(cooldownKey, payload.cooldownMs)) {
|
||||
return {
|
||||
skipped: true,
|
||||
reason: 'cooldown',
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await sendInternalNotification({
|
||||
title: payload.title,
|
||||
body: payload.body,
|
||||
category: payload.category,
|
||||
url: payload.url,
|
||||
})
|
||||
markNotificationCooldown(cooldownKey)
|
||||
return result
|
||||
} catch (error) {
|
||||
logWarn('[notification/internal]', '内部通知发送失败,已跳过', {
|
||||
title: payload.title,
|
||||
category: payload.category,
|
||||
error,
|
||||
})
|
||||
markNotificationCooldown(cooldownKey)
|
||||
return {
|
||||
skipped: true,
|
||||
reason: 'send_failed',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function notifyKuaishouCloudAssetNotEnough({
|
||||
task = {},
|
||||
flow = {},
|
||||
assetBefore = 0,
|
||||
requiredAsset = 0,
|
||||
skuName = '',
|
||||
}: JsonObject = {}) {
|
||||
const taskRecord = toRecord(task)
|
||||
const flowRecord = toRecord(flow)
|
||||
const binding = toRecord(flowRecord.binding)
|
||||
|
||||
return notifyInternalSafely({
|
||||
title: '快手 Cloud 余额不足',
|
||||
body: [
|
||||
formatTaskLine(taskRecord),
|
||||
`当前余额:${Number(assetBefore || 0)}`,
|
||||
`最低需要:${Number(requiredAsset || 0)}`,
|
||||
`商品:${String(skuName || binding.skuName || flowRecord.internalSkuName || '').trim() || '-'}`,
|
||||
].join('\n'),
|
||||
category: 'kuaishou_cloud_asset_not_enough',
|
||||
cooldownKey: [
|
||||
'kuaishou_cloud_asset_not_enough',
|
||||
taskRecord.id || '',
|
||||
binding.skuId || '',
|
||||
].join(':'),
|
||||
})
|
||||
}
|
||||
|
||||
export function notifyCloudtentaclesAuthExpired({
|
||||
pathname = '',
|
||||
errorCode = '',
|
||||
message = '',
|
||||
cooldownSeconds = 600,
|
||||
}: JsonObject = {}) {
|
||||
return notifyInternalSafely({
|
||||
title: 'cloudtentacles 登录已过期',
|
||||
body: [
|
||||
`接口:${String(pathname || '').trim() || '-'}`,
|
||||
`错误码:${String(errorCode || '').trim() || '-'}`,
|
||||
`原因:${String(message || '').trim() || '登录态已失效,请到后台重新登录'}`,
|
||||
].join('\n'),
|
||||
category: 'cloudtentacles_auth_expired',
|
||||
cooldownKey: ['cloudtentacles_auth_expired', pathname, errorCode].join(':'),
|
||||
cooldownMs: Number(cooldownSeconds || 600) * 1000,
|
||||
})
|
||||
}
|
||||
|
||||
export function notifyCloudtentaclesAssetLow({
|
||||
asset = 0,
|
||||
threshold = 500,
|
||||
cooldownSeconds = 1800,
|
||||
}: JsonObject = {}) {
|
||||
return notifyInternalSafely({
|
||||
title: '快手 Cloud 余额低于阈值',
|
||||
body: [
|
||||
`当前余额:${Number(asset || 0)}`,
|
||||
`提醒阈值:${Number(threshold || 0)}`,
|
||||
'请及时补充 cloudtentacles 余额,避免自动履约失败。',
|
||||
].join('\n'),
|
||||
category: 'cloudtentacles_asset_low',
|
||||
cooldownKey: ['cloudtentacles_asset_low', threshold].join(':'),
|
||||
cooldownMs: Number(cooldownSeconds || 1800) * 1000,
|
||||
})
|
||||
}
|
||||
|
||||
export function notifyOpen91PendingConfig({
|
||||
order = {},
|
||||
orderNo = '',
|
||||
productNo = '',
|
||||
buyNum = 0,
|
||||
reason = '未命中履约配置',
|
||||
}: JsonObject = {}) {
|
||||
const orderRecord = toRecord(order)
|
||||
|
||||
return notifyInternalSafely({
|
||||
title: '91卡券订单待补配置',
|
||||
body: [
|
||||
`订单:${String(orderNo || orderRecord.platform_order_id || '').trim() || '-'}`,
|
||||
`商品:${String(productNo || '').trim() || '-'}`,
|
||||
`数量:${Number(buyNum || 0) || 0}`,
|
||||
`原因:${String(reason || '').trim() || '未命中履约配置'}`,
|
||||
].join('\n'),
|
||||
category: 'open91_pending_config',
|
||||
cooldownKey: ['open91_pending_config', orderNo || orderRecord.platform_order_id || '', productNo].join(':'),
|
||||
})
|
||||
}
|
||||
|
||||
export function notifyKuaishouCloudBindUrlRefreshFailed({
|
||||
task = {},
|
||||
errorMessage = '',
|
||||
}: JsonObject = {}) {
|
||||
const taskRecord = toRecord(task)
|
||||
|
||||
return notifyInternalSafely({
|
||||
title: '快手 Cloud 链接刷新失败',
|
||||
body: [
|
||||
formatTaskLine(taskRecord),
|
||||
`原因:${String(errorMessage || taskRecord.last_error || '').trim() || '-'}`,
|
||||
'旧虚拟号已退还,新绑定链接未准备成功。',
|
||||
].join('\n'),
|
||||
category: 'kuaishou_cloud_bind_url_refresh_failed',
|
||||
cooldownKey: ['kuaishou_cloud_bind_url_refresh_failed', taskRecord.id || ''].join(':'),
|
||||
})
|
||||
}
|
||||
|
||||
export function notifyKuaishouCloudConsumeFailed({
|
||||
task = {},
|
||||
order = {},
|
||||
ticketCodeMasked = '',
|
||||
shopId = '',
|
||||
shopName = '',
|
||||
errorMessage = '',
|
||||
}: JsonObject = {}) {
|
||||
const taskRecord = toRecord(task)
|
||||
const orderRecord = toRecord(order)
|
||||
|
||||
return notifyInternalSafely({
|
||||
title: '快手核销失败,需人工处理',
|
||||
body: [
|
||||
formatTaskLine(taskRecord),
|
||||
`订单:${String(orderRecord.platform_order_id || taskRecord.platform_order_id || '').trim() || '-'}`,
|
||||
`券码:${String(ticketCodeMasked || '').trim() || '-'}`,
|
||||
`店铺:${String(shopName || shopId || '').trim() || '-'}`,
|
||||
`原因:${String(errorMessage || taskRecord.last_error || '').trim() || '-'}`,
|
||||
].join('\n'),
|
||||
category: 'kuaishou_cloud_consume_failed',
|
||||
cooldownKey: ['kuaishou_cloud_consume_failed', taskRecord.id || ''].join(':'),
|
||||
})
|
||||
}
|
||||
|
||||
export function notifyTaskAutoManualReview({
|
||||
task = {},
|
||||
reason = '',
|
||||
source = '',
|
||||
}: JsonObject = {}) {
|
||||
const taskRecord = toRecord(task)
|
||||
|
||||
return notifyInternalSafely({
|
||||
title: '任务已自动转人工处理',
|
||||
body: [
|
||||
formatTaskLine(taskRecord),
|
||||
`来源:${String(source || '').trim() || '-'}`,
|
||||
`原因:${String(reason || taskRecord.last_error || '').trim() || '-'}`,
|
||||
].join('\n'),
|
||||
category: 'task_auto_manual_review',
|
||||
cooldownKey: ['task_auto_manual_review', taskRecord.id || '', source].join(':'),
|
||||
})
|
||||
}
|
||||
|
||||
export function notifyClaimRedeemNeedsAttention({
|
||||
task = {},
|
||||
order = {},
|
||||
status = '',
|
||||
errorMessage = '',
|
||||
}: JsonObject = {}) {
|
||||
const taskRecord = toRecord(task)
|
||||
const orderRecord = toRecord(order)
|
||||
|
||||
return notifyInternalSafely({
|
||||
title: status === 'waiting_inventory' ? '库存不足,任务等待补货' : '兑换失败,任务需关注',
|
||||
body: [
|
||||
formatTaskLine(taskRecord),
|
||||
`订单:${String(orderRecord.platform_order_id || taskRecord.platform_order_id || '').trim() || '-'}`,
|
||||
`状态:${String(status || taskRecord.task_status || '').trim() || '-'}`,
|
||||
`原因:${String(errorMessage || taskRecord.last_error || '').trim() || '-'}`,
|
||||
].join('\n'),
|
||||
category: status === 'waiting_inventory' ? 'task_waiting_inventory' : 'claim_redeem_failed',
|
||||
cooldownKey: ['claim_redeem_needs_attention', taskRecord.id || '', status].join(':'),
|
||||
})
|
||||
}
|
||||
|
||||
function formatTaskLine(task: unknown) {
|
||||
const taskRecord = toRecord(task)
|
||||
return [
|
||||
`任务:${String(taskRecord.task_no || taskRecord.id || '').trim() || '-'}`,
|
||||
`平台订单:${String(taskRecord.platform_order_id || '').trim() || '-'}`,
|
||||
].join(' / ')
|
||||
}
|
||||
|
||||
function toRecord(value: unknown): JsonObject {
|
||||
return value && typeof value === 'object' && !Array.isArray(value)
|
||||
? value as JsonObject
|
||||
: {}
|
||||
}
|
||||
|
||||
function isNotificationCoolingDown(cooldownKey: unknown, cooldownMs = DEFAULT_COOLDOWN_MS) {
|
||||
const key = String(cooldownKey || '').trim()
|
||||
if (!key) {
|
||||
return false
|
||||
}
|
||||
|
||||
const lastSentAt = Number(notificationCooldownMap.get(key) || 0)
|
||||
return Date.now() - lastSentAt < Number(cooldownMs || DEFAULT_COOLDOWN_MS)
|
||||
}
|
||||
|
||||
function markNotificationCooldown(cooldownKey: unknown) {
|
||||
const key = String(cooldownKey || '').trim()
|
||||
if (!key) {
|
||||
return
|
||||
}
|
||||
|
||||
notificationCooldownMap.set(key, Date.now())
|
||||
}
|
||||
Reference in New Issue
Block a user