236 lines
7.0 KiB
JavaScript
236 lines
7.0 KiB
JavaScript
// @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())
|
|
}
|