// @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} */ (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()) }