通知系统-1
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
// @ts-check
|
||||
|
||||
import { logWarn } from '../../utils/logger.js'
|
||||
import { sendInternalNotification } from './notification-service.js'
|
||||
|
||||
const DEFAULT_COOLDOWN_MS = 10 * 60 * 1000
|
||||
const notificationCooldownMap = new Map()
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* title: string
|
||||
* body?: string
|
||||
* category?: string
|
||||
* url?: string
|
||||
* cooldownKey?: string
|
||||
* cooldownMs?: number
|
||||
* }} InternalNotificationPayload
|
||||
*/
|
||||
|
||||
/** @param {InternalNotificationPayload} payload */
|
||||
export async function notifyInternalSafely(payload) {
|
||||
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 = '',
|
||||
} = {}) {
|
||||
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 = '',
|
||||
} = {}) {
|
||||
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(':'),
|
||||
})
|
||||
}
|
||||
|
||||
export function notifyOpen91PendingConfig({
|
||||
order = {},
|
||||
orderNo = '',
|
||||
productNo = '',
|
||||
buyNum = 0,
|
||||
reason = '未命中履约配置',
|
||||
} = {}) {
|
||||
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 = '',
|
||||
} = {}) {
|
||||
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 = '',
|
||||
} = {}) {
|
||||
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 = '',
|
||||
} = {}) {
|
||||
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 = '',
|
||||
} = {}) {
|
||||
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) {
|
||||
const taskRecord = toRecord(task)
|
||||
return [
|
||||
`任务:${String(taskRecord.task_no || taskRecord.id || '').trim() || '-'}`,
|
||||
`平台订单:${String(taskRecord.platform_order_id || '').trim() || '-'}`,
|
||||
].join(' / ')
|
||||
}
|
||||
|
||||
function toRecord(value) {
|
||||
return value && typeof value === 'object' && !Array.isArray(value)
|
||||
? /** @type {Record<string, unknown>} */ (value)
|
||||
: {}
|
||||
}
|
||||
|
||||
function isNotificationCoolingDown(cooldownKey, 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) {
|
||||
const key = String(cooldownKey || '').trim()
|
||||
if (!key) {
|
||||
return
|
||||
}
|
||||
|
||||
notificationCooldownMap.set(key, Date.now())
|
||||
}
|
||||
Reference in New Issue
Block a user