Files
order_site/apps/backend/src/services/notification/domain-notifications.js
T
2026-05-14 17:29:12 +08:00

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())
}