Files
order_site/apps/backend/src/services/notification/domain-notifications.ts
T
yml2213 042574dea3 继续收紧 TypeScript:ClaimIdentity 契约与 flow 出口类型
- 新增 types/claim-identity,统一 ClaimIdentity / Payload / AdminSummary
- 导出 KuaishouCloudFlow、KuaishouFeifeiFlow 作为 normalize 出口
- 生产路径 JsonObject 统一从 types/json 导入,去掉 rebind as any
- 前后端 claimIdentity 类型字段对齐
2026-07-10 15:56:25 +08:00

284 lines
8.7 KiB
TypeScript

import type { JsonObject } from '../../types/json.js'
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 InternalNotificationPayload = {
title: string
body?: string
category?: string
url?: string
cooldownKey?: string
cooldownMs?: number
}
type KuaishouCloudAssetNotEnoughPayload = {
task?: unknown
flow?: unknown
assetBefore?: unknown
requiredAsset?: unknown
skuName?: unknown
}
type CloudtentaclesAuthExpiredPayload = {
pathname?: unknown
errorCode?: unknown
message?: unknown
cooldownSeconds?: unknown
sourceKey?: unknown
accountLabel?: unknown
}
type CloudtentaclesAssetLowPayload = {
asset?: unknown
threshold?: unknown
cooldownSeconds?: unknown
sourceKey?: unknown
accountLabel?: unknown
}
type Open91PendingConfigPayload = {
order?: unknown
orderNo?: unknown
productNo?: unknown
buyNum?: unknown
reason?: unknown
}
type TaskNotificationPayload = {
task?: unknown
reason?: unknown
source?: unknown
errorMessage?: unknown
}
type ClaimRedeemNeedsAttentionPayload = {
task?: unknown
order?: unknown
status?: unknown
errorMessage?: unknown
}
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,
...(payload.body !== undefined ? { body: payload.body } : {}),
...(payload.category !== undefined ? { category: payload.category } : {}),
...(payload.url !== undefined ? { 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 = '',
}: KuaishouCloudAssetNotEnoughPayload = {}) {
const taskRecord = toRecord(task)
const flowRecord = toRecord(flow)
const binding = toRecord(flowRecord.binding)
return notifyInternalSafely({
title: 'kuaishou-lewan 余额不足',
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,
sourceKey = '',
accountLabel = '',
}: CloudtentaclesAuthExpiredPayload = {}) {
const normalizedSourceKey = String(sourceKey || '').trim()
const normalizedAccountLabel = String(accountLabel || '').trim()
return notifyInternalSafely({
title: normalizedAccountLabel
? `cloudtentacles 登录已过期:${normalizedAccountLabel}`
: 'cloudtentacles 登录已过期',
body: [
normalizedSourceKey ? `账号:${normalizedAccountLabel || normalizedSourceKey}` : '',
`接口:${String(pathname || '').trim() || '-'}`,
`错误码:${String(errorCode || '').trim() || '-'}`,
`原因:${String(message || '').trim() || '登录态已失效,请到后台重新登录'}`,
].filter(Boolean).join('\n'),
category: 'cloudtentacles_auth_expired',
cooldownKey: ['cloudtentacles_auth_expired', normalizedSourceKey || 'default', pathname, errorCode].join(':'),
cooldownMs: Number(cooldownSeconds || 600) * 1000,
})
}
export function notifyCloudtentaclesAssetLow({
asset = 0,
threshold = 500,
cooldownSeconds = 1800,
sourceKey = '',
accountLabel = '',
}: CloudtentaclesAssetLowPayload = {}) {
const normalizedSourceKey = String(sourceKey || '').trim()
const normalizedAccountLabel = String(accountLabel || '').trim()
return notifyInternalSafely({
title: normalizedAccountLabel
? `kuaishou-lewan 余额低于阈值:${normalizedAccountLabel}`
: 'kuaishou-lewan 余额低于阈值',
body: [
normalizedSourceKey ? `账号:${normalizedAccountLabel || normalizedSourceKey}` : '',
`当前余额:${Number(asset || 0)}`,
`提醒阈值:${Number(threshold || 0)}`,
'请及时补充 cloudtentacles 余额,避免自动履约失败。',
].filter(Boolean).join('\n'),
category: 'cloudtentacles_asset_low',
cooldownKey: ['cloudtentacles_asset_low', normalizedSourceKey || 'default', threshold].join(':'),
cooldownMs: Number(cooldownSeconds || 1800) * 1000,
})
}
export function notifyOpen91PendingConfig({
order = {},
orderNo = '',
productNo = '',
buyNum = 0,
reason = '未命中履约配置',
}: Open91PendingConfigPayload = {}) {
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 = '',
}: TaskNotificationPayload = {}) {
const taskRecord = toRecord(task)
return notifyInternalSafely({
title: 'kuaishou-lewan 链接刷新失败',
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 notifyTaskAutoManualReview({
task = {},
reason = '',
source = '',
}: TaskNotificationPayload = {}) {
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 = '',
}: ClaimRedeemNeedsAttentionPayload = {}) {
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(status || taskRecord.task_status || '').trim() || '-'}`,
`原因:${String(errorMessage || taskRecord.last_error || '').trim() || '-'}`,
].join('\n'),
category: '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())
}