diff --git a/apps/backend/src/services/notification/bark-service.js b/apps/backend/src/services/notification/bark-service.ts similarity index 75% rename from apps/backend/src/services/notification/bark-service.js rename to apps/backend/src/services/notification/bark-service.ts index 94cfbca9..a160ee3c 100644 --- a/apps/backend/src/services/notification/bark-service.js +++ b/apps/backend/src/services/notification/bark-service.ts @@ -1,26 +1,22 @@ -// @ts-check - import { createHttpError } from '../../utils/http.js' const DEFAULT_TIMEOUT_MS = 10000 -/** - * @typedef {{ - * serverUrl: string - * recipient: { - * id?: string - * name?: string - * deviceKey?: string - * } - * title: string - * body?: string - * group?: string - * url?: string - * }} BarkSendInput - */ +type JsonObject = Record +type BarkSendInput = { + serverUrl?: string + recipient?: { + id?: string + name?: string + deviceKey?: string + } + title?: string + body?: string + group?: string + url?: string +} -/** @param {BarkSendInput} input */ -export async function sendBarkNotification(input) { +export async function sendBarkNotification(input: BarkSendInput) { const serverUrl = String(input.serverUrl || 'https://api.day.app').trim().replace(/\/+$/, '') const deviceKey = String(input.recipient?.deviceKey || '').trim() const title = String(input.title || '').trim() @@ -78,7 +74,7 @@ export async function sendBarkNotification(input) { response: parsed || responseText, } } catch (error) { - if (error?.name === 'AbortError') { + if (isAbortError(error)) { throw createHttpError('Bark 通知发送超时', { statusCode: 503, errorCode: 'bark_send_timeout', @@ -91,7 +87,7 @@ export async function sendBarkNotification(input) { } } -function buildBarkEndpoint(serverUrl, deviceKey, title, body) { +function buildBarkEndpoint(serverUrl: string, deviceKey: string, title: string, body: string) { const segments = [ serverUrl, encodeURIComponent(deviceKey), @@ -105,7 +101,7 @@ function buildBarkEndpoint(serverUrl, deviceKey, title, body) { return segments.join('/') } -function parseJsonResponse(text) { +function parseJsonResponse(text: string) { if (!text) { return null } @@ -117,8 +113,8 @@ function parseJsonResponse(text) { } } -function isBarkFailure(parsed) { - if (!parsed || typeof parsed !== 'object') { +function isBarkFailure(parsed: unknown) { + if (!isPlainObject(parsed)) { return false } @@ -126,10 +122,18 @@ function isBarkFailure(parsed) { return Number.isFinite(code) && code !== 0 && code !== 200 } -function resolveBarkErrorMessage(parsed, responseText, status) { - if (parsed && typeof parsed === 'object') { +function resolveBarkErrorMessage(parsed: unknown, responseText: string, status: number) { + if (isPlainObject(parsed)) { return String(parsed.message || parsed.msg || parsed.error || '').trim() || `Bark 通知发送失败,HTTP ${status}` } return String(responseText || '').trim() || `Bark 通知发送失败,HTTP ${status}` } + +function isAbortError(error: unknown): error is Error { + return error instanceof Error && error.name === 'AbortError' +} + +function isPlainObject(value: unknown): value is JsonObject { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value) +} diff --git a/apps/backend/src/services/notification/config-service.js b/apps/backend/src/services/notification/config-service.ts similarity index 87% rename from apps/backend/src/services/notification/config-service.js rename to apps/backend/src/services/notification/config-service.ts index e6e308ea..7e987731 100644 --- a/apps/backend/src/services/notification/config-service.js +++ b/apps/backend/src/services/notification/config-service.ts @@ -1,5 +1,3 @@ -// @ts-check - import fs from 'node:fs' import path from 'node:path' @@ -8,6 +6,8 @@ import { PROJECT_ROOT } from '../../config/runtime.js' const NOTIFICATION_CONFIG_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'notification-config.json') const DEFAULT_BARK_SERVER_URL = 'https://api.day.app' +type JsonObject = Record + export function getNotificationConfigFilePath() { return NOTIFICATION_CONFIG_FILE_PATH } @@ -16,14 +16,14 @@ export function getNotificationConfig() { return loadNotificationConfigFromFile() } -export function saveNotificationConfig(rawValue) { +export function saveNotificationConfig(rawValue: unknown) { const normalized = normalizeNotificationConfig(rawValue) fs.mkdirSync(path.dirname(NOTIFICATION_CONFIG_FILE_PATH), { recursive: true }) fs.writeFileSync(NOTIFICATION_CONFIG_FILE_PATH, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8') return normalized } -export function listEnabledBarkRecipients(config = getNotificationConfig()) { +export function listEnabledBarkRecipients(config: JsonObject = getNotificationConfig()) { if (config.enabled === false || config.channels?.bark?.enabled === false) { return [] } @@ -45,7 +45,7 @@ function loadNotificationConfigFromFile() { } } -function normalizeNotificationConfig(rawValue) { +function normalizeNotificationConfig(rawValue: unknown) { const source = isPlainObject(rawValue) ? rawValue : {} const bark = isPlainObject(source.channels?.bark) ? source.channels.bark : {} @@ -63,7 +63,7 @@ function normalizeNotificationConfig(rawValue) { } } -function normalizeBarkRecipient(rawValue) { +function normalizeBarkRecipient(rawValue: unknown) { if (!isPlainObject(rawValue)) { return null } @@ -84,7 +84,7 @@ function normalizeBarkRecipient(rawValue) { } } -function normalizeBarkServerUrl(value) { +function normalizeBarkServerUrl(value: unknown) { const normalized = String(value || DEFAULT_BARK_SERVER_URL).trim() || DEFAULT_BARK_SERVER_URL return normalized.replace(/\/+$/, '') } @@ -102,6 +102,6 @@ function createDefaultNotificationConfig() { } } -function isPlainObject(value) { +function isPlainObject(value: unknown): value is JsonObject { return Object.prototype.toString.call(value) === '[object Object]' } diff --git a/apps/backend/src/services/notification/domain-notifications.js b/apps/backend/src/services/notification/domain-notifications.ts similarity index 90% rename from apps/backend/src/services/notification/domain-notifications.js rename to apps/backend/src/services/notification/domain-notifications.ts index 9ccae245..ed8d0041 100644 --- a/apps/backend/src/services/notification/domain-notifications.js +++ b/apps/backend/src/services/notification/domain-notifications.ts @@ -1,24 +1,20 @@ -// @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() +const notificationCooldownMap = new Map() -/** - * @typedef {{ - * title: string - * body?: string - * category?: string - * url?: string - * cooldownKey?: string - * cooldownMs?: number - * }} InternalNotificationPayload - */ +type JsonObject = Record +type InternalNotificationPayload = { + title: string + body?: string + category?: string + url?: string + cooldownKey?: string + cooldownMs?: number +} -/** @param {InternalNotificationPayload} payload */ -export async function notifyInternalSafely(payload) { +export async function notifyInternalSafely(payload: InternalNotificationPayload) { const cooldownKey = String(payload.cooldownKey || '').trim() if (cooldownKey && isNotificationCoolingDown(cooldownKey, payload.cooldownMs)) { return { @@ -56,7 +52,7 @@ export function notifyKuaishouCloudAssetNotEnough({ assetBefore = 0, requiredAsset = 0, skuName = '', -} = {}) { +}: JsonObject = {}) { const taskRecord = toRecord(task) const flowRecord = toRecord(flow) const binding = toRecord(flowRecord.binding) @@ -83,7 +79,7 @@ export function notifyCloudtentaclesAuthExpired({ errorCode = '', message = '', cooldownSeconds = 600, -} = {}) { +}: JsonObject = {}) { return notifyInternalSafely({ title: 'cloudtentacles 登录已过期', body: [ @@ -101,7 +97,7 @@ export function notifyCloudtentaclesAssetLow({ asset = 0, threshold = 500, cooldownSeconds = 1800, -} = {}) { +}: JsonObject = {}) { return notifyInternalSafely({ title: '快手 Cloud 余额低于阈值', body: [ @@ -121,7 +117,7 @@ export function notifyOpen91PendingConfig({ productNo = '', buyNum = 0, reason = '未命中履约配置', -} = {}) { +}: JsonObject = {}) { const orderRecord = toRecord(order) return notifyInternalSafely({ @@ -140,7 +136,7 @@ export function notifyOpen91PendingConfig({ export function notifyKuaishouCloudBindUrlRefreshFailed({ task = {}, errorMessage = '', -} = {}) { +}: JsonObject = {}) { const taskRecord = toRecord(task) return notifyInternalSafely({ @@ -162,7 +158,7 @@ export function notifyKuaishouCloudConsumeFailed({ shopId = '', shopName = '', errorMessage = '', -} = {}) { +}: JsonObject = {}) { const taskRecord = toRecord(task) const orderRecord = toRecord(order) @@ -184,7 +180,7 @@ export function notifyTaskAutoManualReview({ task = {}, reason = '', source = '', -} = {}) { +}: JsonObject = {}) { const taskRecord = toRecord(task) return notifyInternalSafely({ @@ -204,7 +200,7 @@ export function notifyClaimRedeemNeedsAttention({ order = {}, status = '', errorMessage = '', -} = {}) { +}: JsonObject = {}) { const taskRecord = toRecord(task) const orderRecord = toRecord(order) @@ -221,7 +217,7 @@ export function notifyClaimRedeemNeedsAttention({ }) } -function formatTaskLine(task) { +function formatTaskLine(task: unknown) { const taskRecord = toRecord(task) return [ `任务:${String(taskRecord.task_no || taskRecord.id || '').trim() || '-'}`, @@ -229,13 +225,13 @@ function formatTaskLine(task) { ].join(' / ') } -function toRecord(value) { +function toRecord(value: unknown): JsonObject { return value && typeof value === 'object' && !Array.isArray(value) - ? /** @type {Record} */ (value) + ? value as JsonObject : {} } -function isNotificationCoolingDown(cooldownKey, cooldownMs = DEFAULT_COOLDOWN_MS) { +function isNotificationCoolingDown(cooldownKey: unknown, cooldownMs = DEFAULT_COOLDOWN_MS) { const key = String(cooldownKey || '').trim() if (!key) { return false @@ -245,7 +241,7 @@ function isNotificationCoolingDown(cooldownKey, cooldownMs = DEFAULT_COOLDOWN_MS return Date.now() - lastSentAt < Number(cooldownMs || DEFAULT_COOLDOWN_MS) } -function markNotificationCooldown(cooldownKey) { +function markNotificationCooldown(cooldownKey: unknown) { const key = String(cooldownKey || '').trim() if (!key) { return diff --git a/apps/backend/src/services/notification/notification-service.js b/apps/backend/src/services/notification/notification-service.ts similarity index 74% rename from apps/backend/src/services/notification/notification-service.js rename to apps/backend/src/services/notification/notification-service.ts index c593dbce..4db8273f 100644 --- a/apps/backend/src/services/notification/notification-service.js +++ b/apps/backend/src/services/notification/notification-service.ts @@ -1,22 +1,18 @@ -// @ts-check - import { logWarn } from '../../utils/logger.js' import { getNotificationConfig, listEnabledBarkRecipients } from './config-service.js' import { sendBarkNotification } from './bark-service.js' -/** - * @typedef {{ - * title?: string - * body?: string - * category?: string - * url?: string - * }} NotificationInput - */ +type JsonObject = Record +type NotificationInput = { + title?: string + body?: string + category?: string + url?: string +} -/** @param {NotificationInput} input */ -export async function sendInternalNotification(input = {}) { +export async function sendInternalNotification(input: NotificationInput = {}) { const config = getNotificationConfig() - const bark = /** @type {{ serverUrl?: string }} */ (config.channels?.bark || {}) + const bark = (config.channels?.bark || {}) as { serverUrl?: string } const recipients = listEnabledBarkRecipients(config) const title = String(input.title || '订单系统通知').trim() || '订单系统通知' const body = String(input.body || '').trim() @@ -54,8 +50,8 @@ export async function sendInternalNotification(input = {}) { recipient, false, error instanceof Error ? error.message : 'Bark 内部通知发送失败', - Number(error?.statusCode || 0) || 0, - error?.context || null, + isPlainObject(error) ? Number(error.statusCode || 0) || 0 : 0, + isPlainObject(error) ? error.context || null : null, ) } })) @@ -70,7 +66,13 @@ export async function sendInternalNotification(input = {}) { } } -function mapNotificationResult(recipient, ok, errorMessage, status, response) { +function mapNotificationResult( + recipient: JsonObject, + ok: boolean, + errorMessage: string, + status: number, + response: unknown, +) { return { recipientId: String(recipient.id || '').trim(), recipientName: String(recipient.name || '').trim(), @@ -82,7 +84,7 @@ function mapNotificationResult(recipient, ok, errorMessage, status, response) { } } -function maskDeviceKey(value) { +function maskDeviceKey(value: unknown) { const normalized = String(value || '').trim() if (!normalized) { return '' @@ -94,3 +96,7 @@ function maskDeviceKey(value) { return `${normalized.slice(0, 6)}****${normalized.slice(-6)}` } + +function isPlainObject(value: unknown): value is JsonObject { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value) +} diff --git a/docs/backend-typescript-migration-plan.md b/docs/backend-typescript-migration-plan.md index d74c1bdf..ddc866f2 100644 --- a/docs/backend-typescript-migration-plan.md +++ b/docs/backend-typescript-migration-plan.md @@ -674,6 +674,17 @@ - `npm run typecheck` - `npm run build` - `npm test` 共 139 个用例通过 +165. 通知服务模块迁移到 `.ts`: + - `src/services/notification/config-service.ts` + - `src/services/notification/bark-service.ts` + - `src/services/notification/notification-service.ts` + - `src/services/notification/domain-notifications.ts` +166. Bark 通知发送、通知配置读写、内部通知分发、业务域告警与冷却控制已进入 TS 编译链路;Bark input、通知 payload、接收人配置、错误上下文与冷却 map 补齐类型 +167. Docker 内验证通过: + - `src/services/admin/platform-config/*.test.js` 共 37 个用例通过 + - `npm run typecheck` + - `npm run build` + - `npm test` 共 139 个用例通过 ## 下一步建议