diff --git a/apps/backend/data/cloudtentacles-session.json b/apps/backend/data/cloudtentacles-session.json index 968918a4..61e954de 100644 --- a/apps/backend/data/cloudtentacles-session.json +++ b/apps/backend/data/cloudtentacles-session.json @@ -9,24 +9,6 @@ "deviceId": "-", "deviceType": 0 }, - "测试12": { - "token": "", - "baseUrl": "", - "username": "", - "phone": "", - "loggedInAt": "", - "deviceId": "-", - "deviceType": 0 - }, - "account3": { - "token": "", - "baseUrl": "", - "username": "", - "phone": "", - "loggedInAt": "", - "deviceId": "-", - "deviceType": 0 - }, "account2": { "token": "KU2draP6Rm7AQDIeN0SSIVeNBkdMdGODlN1ToNhAo0wYk6clo9EeHh8fE5cN2kIhxThC30gCZXZl6LQfs19K03S9", "baseUrl": "https://123.207.217.176", diff --git a/apps/backend/data/cloudtentacles-sources.json b/apps/backend/data/cloudtentacles-sources.json index 82640585..9382f626 100644 --- a/apps/backend/data/cloudtentacles-sources.json +++ b/apps/backend/data/cloudtentacles-sources.json @@ -4,6 +4,7 @@ { "key": "default", "label": "默认账号", + "enabled": true, "baseUrl": "https://123.207.217.176", "username": "17665234375", "password": "yaochao11", @@ -14,6 +15,7 @@ { "key": "account2", "label": "备用账号 1", + "enabled": true, "baseUrl": "https://123.207.217.176", "username": "18216161068", "password": "yaochao11", diff --git a/apps/backend/data/notification-config.json b/apps/backend/data/notification-config.json index 56bd5f6c..573fe861 100644 --- a/apps/backend/data/notification-config.json +++ b/apps/backend/data/notification-config.json @@ -18,6 +18,23 @@ "enabled": true } ] + }, + "wpush": { + "enabled": true, + "recipients": [ + { + "id": "1c1089bb-4a54-4ec9-a10f-15b8099fdfb0", + "name": "y", + "apiKey": "WPUSHcPSmgodKUwMCB13IFSHJsyBqUI4", + "enabled": true + }, + { + "id": "5affc7d0-3944-43ee-90cb-8242ab9b34e7", + "name": "yml", + "apiKey": "WPUSHsa44qjF35klEZRqVHxAouoDE2A6", + "enabled": true + } + ] } } } diff --git a/apps/backend/src/routes/admin/platform-config/notifications.ts b/apps/backend/src/routes/admin/platform-config/notifications.ts index c44dc157..3a94680a 100644 --- a/apps/backend/src/routes/admin/platform-config/notifications.ts +++ b/apps/backend/src/routes/admin/platform-config/notifications.ts @@ -53,6 +53,11 @@ router.post( ) ? result.source.channels.bark.recipients.length : 0, + wpushRecipientCount: Array.isArray( + result.source?.channels?.wpush?.recipients + ) + ? result.source.channels.wpush.recipients.length + : 0, }, }; }, diff --git a/apps/backend/src/services/admin/platform-config/notification-service.ts b/apps/backend/src/services/admin/platform-config/notification-service.ts index 4f698476..e83cfea8 100644 --- a/apps/backend/src/services/admin/platform-config/notification-service.ts +++ b/apps/backend/src/services/admin/platform-config/notification-service.ts @@ -45,7 +45,7 @@ export function updateAdminNotificationConfig(payload: AdminNotificationConfigIn export async function testAdminNotification(payload: AdminNotificationTestInput = {}) { return sendInternalNotification({ title: String(payload.title || '订单系统测试通知').trim() || '订单系统测试通知', - body: String(payload.body || '这是一条 Bark 内部通知测试。').trim() || '这是一条 Bark 内部通知测试。', + body: String(payload.body || '这是一条内部通知测试。').trim() || '这是一条内部通知测试。', category: 'test', url: String(payload.url || '').trim(), }) @@ -83,6 +83,7 @@ export async function runAdminScheduledJobNow(jobId: unknown) { function mapAdminNotificationConfig(config: JsonObject = {}) { const bark = config.channels?.bark || {} + const wpush = config.channels?.wpush || {} return { enabled: config.enabled !== false, @@ -98,6 +99,16 @@ function mapAdminNotificationConfig(config: JsonObject = {}) { enabled: item.enabled !== false, })), }, + wpush: { + enabled: wpush.enabled !== false, + recipients: (Array.isArray(wpush.recipients) ? wpush.recipients : []).map((item) => ({ + id: String(item.id || '').trim(), + name: String(item.name || '').trim(), + apiKey: String(item.apiKey || item.apikey || '').trim(), + apiKeyMasked: maskSecret(item.apiKey || item.apikey), + enabled: item.enabled !== false, + })), + }, }, } } diff --git a/apps/backend/src/services/notification/config-service.test.ts b/apps/backend/src/services/notification/config-service.test.ts new file mode 100644 index 00000000..d7b33062 --- /dev/null +++ b/apps/backend/src/services/notification/config-service.test.ts @@ -0,0 +1,90 @@ +import test from 'node:test' +import assert from 'node:assert/strict' + +import { + createDefaultNotificationConfig, + normalizeNotificationConfig, +} from './config-service.js' + +test('normalizeNotificationConfig normalizes bark and wpush recipients', () => { + assert.deepEqual( + normalizeNotificationConfig({ + enabled: false, + channels: { + bark: { + enabled: true, + serverUrl: 'https://api.day.app///', + recipients: [ + { + name: ' 值班A ', + deviceKey: ' bark-key ', + enabled: false, + }, + { + name: ' ', + deviceKey: ' ', + }, + ], + }, + wpush: { + enabled: true, + recipients: [ + { + name: ' 值班B ', + apikey: ' wpush-key ', + }, + { + name: '', + apiKey: '', + }, + ], + }, + }, + }), + { + enabled: false, + channels: { + bark: { + enabled: true, + serverUrl: 'https://api.day.app', + recipients: [ + { + id: 'bark-key', + name: '值班A', + deviceKey: 'bark-key', + enabled: false, + }, + ], + }, + wpush: { + enabled: true, + recipients: [ + { + id: 'wpush-key', + name: '值班B', + apiKey: 'wpush-key', + enabled: true, + }, + ], + }, + }, + }, + ) +}) + +test('createDefaultNotificationConfig includes wpush channel', () => { + assert.deepEqual(createDefaultNotificationConfig(), { + enabled: true, + channels: { + bark: { + enabled: true, + serverUrl: 'https://api.day.app', + recipients: [], + }, + wpush: { + enabled: true, + recipients: [], + }, + }, + }) +}) diff --git a/apps/backend/src/services/notification/config-service.ts b/apps/backend/src/services/notification/config-service.ts index 8a56aeb2..cdd03559 100644 --- a/apps/backend/src/services/notification/config-service.ts +++ b/apps/backend/src/services/notification/config-service.ts @@ -29,6 +29,15 @@ export function listEnabledBarkRecipients(config: JsonObject = getNotificationCo .filter((item) => item.enabled !== false && String(item.deviceKey || '').trim()) } +export function listEnabledWpushRecipients(config: JsonObject = getNotificationConfig()) { + if (config.enabled === false || config.channels?.wpush?.enabled === false) { + return [] + } + + return (Array.isArray(config.channels?.wpush?.recipients) ? config.channels.wpush.recipients : []) + .filter((item) => item.enabled !== false && String(item.apiKey || item.apikey || '').trim()) +} + function loadNotificationConfigFromFile() { return readJsonFile( NOTIFICATION_CONFIG_FILE_PATH, @@ -37,9 +46,10 @@ function loadNotificationConfigFromFile() { ) } -function normalizeNotificationConfig(rawValue: unknown) { +export function normalizeNotificationConfig(rawValue: unknown) { const source = isPlainObject(rawValue) ? rawValue : {} const bark = isPlainObject(source.channels?.bark) ? source.channels.bark : {} + const wpush = isPlainObject(source.channels?.wpush) ? source.channels.wpush : {} return { enabled: typeof source.enabled === 'boolean' ? source.enabled : true, @@ -51,6 +61,12 @@ function normalizeNotificationConfig(rawValue: unknown) { .map((item) => normalizeBarkRecipient(item)) .filter(Boolean), }, + wpush: { + enabled: typeof wpush.enabled === 'boolean' ? wpush.enabled : true, + recipients: (Array.isArray(wpush.recipients) ? wpush.recipients : []) + .map((item) => normalizeWpushRecipient(item)) + .filter(Boolean), + }, }, } } @@ -81,7 +97,28 @@ function normalizeBarkServerUrl(value: unknown) { return normalized.replace(/\/+$/, '') } -function createDefaultNotificationConfig() { +function normalizeWpushRecipient(rawValue: unknown) { + if (!isPlainObject(rawValue)) { + return null + } + + const name = String(rawValue.name || '').trim() + const apiKey = String(rawValue.apiKey || rawValue.apikey || '').trim() + const id = String(rawValue.id || apiKey || name || '').trim() + + if (!name && !apiKey) { + return null + } + + return { + id, + name, + apiKey, + enabled: typeof rawValue.enabled === 'boolean' ? rawValue.enabled : true, + } +} + +export function createDefaultNotificationConfig() { return { enabled: true, channels: { @@ -90,6 +127,10 @@ function createDefaultNotificationConfig() { serverUrl: DEFAULT_BARK_SERVER_URL, recipients: [], }, + wpush: { + enabled: true, + recipients: [], + }, }, } } diff --git a/apps/backend/src/services/notification/notification-service.ts b/apps/backend/src/services/notification/notification-service.ts index 4db8273f..07646fba 100644 --- a/apps/backend/src/services/notification/notification-service.ts +++ b/apps/backend/src/services/notification/notification-service.ts @@ -1,6 +1,11 @@ import { logWarn } from '../../utils/logger.js' -import { getNotificationConfig, listEnabledBarkRecipients } from './config-service.js' +import { + getNotificationConfig, + listEnabledBarkRecipients, + listEnabledWpushRecipients, +} from './config-service.js' import { sendBarkNotification } from './bark-service.js' +import { sendWpushNotification } from './wpush-service.js' type JsonObject = Record type NotificationInput = { @@ -13,7 +18,8 @@ type NotificationInput = { export async function sendInternalNotification(input: NotificationInput = {}) { const config = getNotificationConfig() const bark = (config.channels?.bark || {}) as { serverUrl?: string } - const recipients = listEnabledBarkRecipients(config) + const barkRecipients = listEnabledBarkRecipients(config) + const wpushRecipients = listEnabledWpushRecipients(config) const title = String(input.title || '订单系统通知').trim() || '订单系统通知' const body = String(input.body || '').trim() const category = String(input.category || 'system').trim() || 'system' @@ -21,44 +27,73 @@ export async function sendInternalNotification(input: NotificationInput = {}) { if (config.enabled === false) { return { enabled: false, - channel: 'bark', + channel: 'internal', successCount: 0, failedCount: 0, - skippedCount: recipients.length, + skippedCount: barkRecipients.length + wpushRecipients.length, results: [], } } - const results = await Promise.all(recipients.map(async (recipient) => { - try { - const result = await sendBarkNotification({ - serverUrl: bark.serverUrl, - recipient, - title, - body, - group: `订单系统/${category}`, - url: input.url, - }) - return mapNotificationResult(recipient, true, '', result.status, result.response) - } catch (error) { - logWarn('[notification/bark]', 'Bark 内部通知发送失败', { - recipientId: recipient.id, - recipientName: recipient.name, - error, - }) - return mapNotificationResult( - recipient, - false, - error instanceof Error ? error.message : 'Bark 内部通知发送失败', - isPlainObject(error) ? Number(error.statusCode || 0) || 0 : 0, - isPlainObject(error) ? error.context || null : null, - ) - } - })) + const results = [ + ...(await Promise.all(barkRecipients.map(async (recipient) => { + try { + const result = await sendBarkNotification({ + serverUrl: bark.serverUrl, + recipient, + title, + body, + group: `订单系统/${category}`, + url: input.url, + }) + return mapNotificationResult('bark', recipient, recipient.deviceKey, true, '', result.status, result.response) + } catch (error) { + logWarn('[notification/bark]', 'Bark 内部通知发送失败', { + recipientId: recipient.id, + recipientName: recipient.name, + error, + }) + return mapNotificationResult( + 'bark', + recipient, + recipient.deviceKey, + false, + error instanceof Error ? error.message : 'Bark 内部通知发送失败', + isPlainObject(error) ? Number(error.statusCode || 0) || 0 : 0, + isPlainObject(error) ? error.context || null : null, + ) + } + }))), + ...(await Promise.all(wpushRecipients.map(async (recipient) => { + try { + const result = await sendWpushNotification({ + recipient, + title, + body, + }) + return mapNotificationResult('wpush', recipient, recipient.apiKey, true, '', result.status, result.response) + } catch (error) { + logWarn('[notification/wpush]', 'WPush 内部通知发送失败', { + recipientId: recipient.id, + recipientName: recipient.name, + error, + }) + return mapNotificationResult( + 'wpush', + recipient, + recipient.apiKey, + false, + error instanceof Error ? error.message : 'WPush 内部通知发送失败', + isPlainObject(error) ? Number(error.statusCode || 0) || 0 : 0, + isPlainObject(error) ? error.context || null : null, + ) + } + }))), + ] return { enabled: true, - channel: 'bark', + channel: 'internal', successCount: results.filter((item) => item.ok).length, failedCount: results.filter((item) => !item.ok).length, skippedCount: 0, @@ -67,16 +102,19 @@ export async function sendInternalNotification(input: NotificationInput = {}) { } function mapNotificationResult( + channel: string, recipient: JsonObject, + recipientKey: unknown, ok: boolean, errorMessage: string, status: number, response: unknown, ) { return { + channel, recipientId: String(recipient.id || '').trim(), recipientName: String(recipient.name || '').trim(), - recipientKeyMasked: maskDeviceKey(recipient.deviceKey), + recipientKeyMasked: maskSecretKey(recipientKey), ok, status, errorMessage, @@ -84,7 +122,7 @@ function mapNotificationResult( } } -function maskDeviceKey(value: unknown) { +function maskSecretKey(value: unknown) { const normalized = String(value || '').trim() if (!normalized) { return '' diff --git a/apps/backend/src/services/notification/wpush-service.test.ts b/apps/backend/src/services/notification/wpush-service.test.ts new file mode 100644 index 00000000..63d71717 --- /dev/null +++ b/apps/backend/src/services/notification/wpush-service.test.ts @@ -0,0 +1,79 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import http from 'node:http' + +import { sendWpushNotification } from './wpush-service.js' + +test('sendWpushNotification sends POST json payload', async () => { + const captured = { + method: '', + contentType: '', + body: '', + } + + const server = http.createServer((req, res) => { + captured.method = String(req.method || '') + captured.contentType = String(req.headers['content-type'] || '') + + const chunks: Buffer[] = [] + req.on('data', (chunk) => { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)) + }) + req.on('end', () => { + captured.body = Buffer.concat(chunks).toString('utf8') + res.writeHead(200, { 'content-type': 'application/json' }) + res.end(JSON.stringify({ code: 0, message: 'ok' })) + }) + }) + + await new Promise((resolve) => { + server.listen(0, '127.0.0.1', () => resolve()) + }) + + try { + const address = server.address() + assert.ok(address && typeof address === 'object') + + const result = await sendWpushNotification({ + endpoint: `http://127.0.0.1:${address.port}/send`, + recipient: { + apiKey: 'test-key-1', + }, + title: '测试标题', + body: '测试内容', + }) + + assert.equal(result.ok, true) + assert.equal(captured.method, 'POST') + assert.equal(captured.contentType, 'application/json') + assert.deepEqual(JSON.parse(captured.body), { + apikey: 'test-key-1', + title: '测试标题', + content: '测试内容', + }) + } finally { + await new Promise((resolve, reject) => { + server.close((error) => { + if (error) { + reject(error) + return + } + resolve() + }) + }) + } +}) + +test('sendWpushNotification rejects missing api key', async () => { + await assert.rejects( + () => sendWpushNotification({ + recipient: {}, + title: '测试标题', + body: '测试内容', + }), + (error: any) => { + assert.equal(error?.errorCode, 'wpush_api_key_required') + return true + }, + ) +}) diff --git a/apps/backend/src/services/notification/wpush-service.ts b/apps/backend/src/services/notification/wpush-service.ts new file mode 100644 index 00000000..592ed59d --- /dev/null +++ b/apps/backend/src/services/notification/wpush-service.ts @@ -0,0 +1,194 @@ +import http from 'node:http' +import https from 'node:https' + +import { createHttpError } from '../../utils/http.js' + +const DEFAULT_WPUSH_ENDPOINT = 'https://api.wpush.cn/api/v1/send' +const DEFAULT_TIMEOUT_MS = 10000 + +type JsonObject = Record +type NodeHttpResponse = { + ok: boolean + status: number + bodyText: string +} +type WpushSendInput = { + recipient?: { + id?: string + name?: string + apiKey?: string + } + title?: string + body?: string + endpoint?: string +} + +export async function sendWpushNotification(input: WpushSendInput) { + const apiKey = String(input.recipient?.apiKey || '').trim() + const title = String(input.title || '').trim() + const body = String(input.body || '').trim() + const endpoint = String(input.endpoint || DEFAULT_WPUSH_ENDPOINT).trim() || DEFAULT_WPUSH_ENDPOINT + + if (!apiKey) { + throw createHttpError('WPush 接收人缺少 apikey', { + statusCode: 400, + errorCode: 'wpush_api_key_required', + }) + } + + if (!title && !body) { + throw createHttpError('WPush 通知标题或内容不能为空', { + statusCode: 400, + errorCode: 'wpush_message_required', + }) + } + + const payload = JSON.stringify({ + apikey: apiKey, + title: title || body, + content: body || title, + }) + const controller = new AbortController() + const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS) + + try { + const response = await requestViaNodeHttp(new URL(endpoint), { + method: 'POST', + headers: { + 'content-type': 'application/json', + accept: 'application/json, text/plain, */*', + 'content-length': String(Buffer.byteLength(payload)), + }, + body: payload, + signal: controller.signal, + }) + const parsed = parseJsonResponse(response.bodyText) + + if (!response.ok || isWpushFailure(parsed)) { + throw createHttpError(resolveWpushErrorMessage(parsed, response.bodyText, response.status), { + statusCode: response.ok ? 502 : response.status, + errorCode: 'wpush_send_failed', + context: { + status: response.status, + response: parsed || response.bodyText, + }, + }) + } + + return { + ok: true, + status: response.status, + response: parsed || response.bodyText, + } + } catch (error) { + if (isAbortError(error)) { + throw createHttpError('WPush 通知发送超时', { + statusCode: 503, + errorCode: 'wpush_send_timeout', + cause: error, + }) + } + + throw error + } finally { + clearTimeout(timer) + } +} + +async function requestViaNodeHttp( + url: URL, + { method, headers, body, signal }: { + method: string + headers: Record + body?: string + signal?: AbortSignal + }, +): Promise { + const transport = url.protocol === 'https:' ? https : http + + return new Promise((resolve, reject) => { + const request = transport.request(url, { + method, + headers, + rejectUnauthorized: false, + }, (response) => { + const chunks: Buffer[] = [] + + response.on('data', (chunk) => { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)) + }) + + response.on('end', () => { + resolve({ + ok: Number(response.statusCode || 0) >= 200 && Number(response.statusCode || 0) < 300, + status: Number(response.statusCode || 0), + bodyText: Buffer.concat(chunks).toString('utf8'), + }) + }) + }) + + request.on('error', reject) + + if (signal) { + if (signal.aborted) { + const error = new Error('Request aborted') + error.name = 'AbortError' + request.destroy(error) + } else { + signal.addEventListener('abort', () => { + const error = new Error('Request aborted') + error.name = 'AbortError' + request.destroy(error) + }, { once: true }) + } + } + + if (typeof body !== 'undefined') { + request.write(body) + } + + request.end() + }) +} + +function parseJsonResponse(text: string) { + if (!text) { + return null + } + + try { + return JSON.parse(text) + } catch { + return null + } +} + +function isWpushFailure(parsed: unknown) { + if (!isPlainObject(parsed)) { + return false + } + + const success = parsed.success + if (typeof success === 'boolean') { + return success === false + } + + const code = Number(parsed.code ?? parsed.status ?? 0) + return Number.isFinite(code) && code !== 0 && code !== 200 +} + +function resolveWpushErrorMessage(parsed: unknown, responseText: string, status: number) { + if (isPlainObject(parsed)) { + return String(parsed.message || parsed.msg || parsed.error || '').trim() || `WPush 通知发送失败,HTTP ${status}` + } + + return String(responseText || '').trim() || `WPush 通知发送失败,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/types/admin-write-inputs.ts b/apps/backend/src/types/admin-write-inputs.ts index 86b04b7d..4476f956 100644 --- a/apps/backend/src/types/admin-write-inputs.ts +++ b/apps/backend/src/types/admin-write-inputs.ts @@ -102,6 +102,14 @@ export type AdminNotificationBarkRecipientInput = { enabled?: boolean } +export type AdminNotificationWpushRecipientInput = { + id?: string + name?: string + apiKey?: string + apikey?: string + enabled?: boolean +} + export type AdminNotificationConfigInput = { enabled?: boolean channels?: { @@ -110,6 +118,10 @@ export type AdminNotificationConfigInput = { serverUrl?: string recipients?: AdminNotificationBarkRecipientInput[] } + wpush?: { + enabled?: boolean + recipients?: AdminNotificationWpushRecipientInput[] + } } } diff --git a/apps/frontend/src/types/admin/index.ts b/apps/frontend/src/types/admin/index.ts index 9656adcf..872b20ce 100644 --- a/apps/frontend/src/types/admin/index.ts +++ b/apps/frontend/src/types/admin/index.ts @@ -60,6 +60,7 @@ export type { AdminAgisoMessagingDefaults, AdminAgisoObservedShopItem, AdminNotificationBarkRecipient, + AdminNotificationWpushRecipient, AdminNotificationConfig, AdminNotificationSendResult, AdminNotificationTestResult, diff --git a/apps/frontend/src/types/admin/platform-config/index.ts b/apps/frontend/src/types/admin/platform-config/index.ts index 634641e7..0ab4c2ca 100644 --- a/apps/frontend/src/types/admin/platform-config/index.ts +++ b/apps/frontend/src/types/admin/platform-config/index.ts @@ -6,6 +6,7 @@ export type { export type { AdminNotificationBarkRecipient, + AdminNotificationWpushRecipient, AdminNotificationConfig, AdminNotificationSendResult, AdminNotificationTestResult, diff --git a/apps/frontend/src/types/admin/platform-config/notifications.ts b/apps/frontend/src/types/admin/platform-config/notifications.ts index 64bd35db..1a847acb 100644 --- a/apps/frontend/src/types/admin/platform-config/notifications.ts +++ b/apps/frontend/src/types/admin/platform-config/notifications.ts @@ -6,6 +6,14 @@ export interface AdminNotificationBarkRecipient { enabled: boolean } +export interface AdminNotificationWpushRecipient { + id: string + name: string + apiKey: string + apiKeyMasked: string + enabled: boolean +} + export interface AdminNotificationConfig { enabled: boolean channels: { @@ -14,10 +22,15 @@ export interface AdminNotificationConfig { serverUrl: string recipients: AdminNotificationBarkRecipient[] } + wpush: { + enabled: boolean + recipients: AdminNotificationWpushRecipient[] + } } } export interface AdminNotificationSendResult { + channel: string recipientId: string recipientName: string recipientKeyMasked: string diff --git a/apps/frontend/src/views/admin/platform-shops/AdminPlatformShopsView.vue b/apps/frontend/src/views/admin/platform-shops/AdminPlatformShopsView.vue index 809569d4..409f4a19 100644 --- a/apps/frontend/src/views/admin/platform-shops/AdminPlatformShopsView.vue +++ b/apps/frontend/src/views/admin/platform-shops/AdminPlatformShopsView.vue @@ -64,6 +64,7 @@ const { notificationFilePath, notificationForm, notificationRecipients, + wpushRecipients, notificationSaving, notificationTesting, notificationResultError, @@ -263,10 +264,12 @@ onMounted(loadConfigs) { key: 'notifications', label: '内部通知', - tag: 'Bark / 值班', - value: notificationStats.enabledRecipientCount, + tag: 'Bark / WPush', + value: + notificationStats.barkEnabledRecipientCount + + notificationStats.wpushEnabledRecipientCount, unit: '启用接收人', - meta: `配置 ${notificationStats.configuredRecipientCount} 人 · 最近成功 ${notificationStats.lastSuccessCount} 个`, + meta: `Bark ${notificationStats.barkConfiguredRecipientCount} 人 · WPush ${notificationStats.wpushConfiguredRecipientCount} 人 · 最近成功 ${notificationStats.lastSuccessCount} 个`, }, { key: 'ninetyone', @@ -356,6 +359,7 @@ onMounted(loadConfigs) :notification-file-path="notificationFilePath" :notification-form="notificationForm" :notification-recipients="notificationRecipients" + :wpush-recipients="wpushRecipients" :notification-saving="notificationSaving" :notification-testing="notificationTesting" :notification-result-error="notificationResultError" diff --git a/apps/frontend/src/views/admin/platform-shops/components/notification/AdminNotificationBarkCard.vue b/apps/frontend/src/views/admin/platform-shops/components/notification/AdminNotificationBarkCard.vue index d76bfeaa..b19c75ea 100644 --- a/apps/frontend/src/views/admin/platform-shops/components/notification/AdminNotificationBarkCard.vue +++ b/apps/frontend/src/views/admin/platform-shops/components/notification/AdminNotificationBarkCard.vue @@ -5,15 +5,20 @@ type NotificationForm = { enabled: boolean barkEnabled: boolean barkServerUrl: string + wpushEnabled: boolean testTitle: string testBody: string testUrl: string } type NotificationStats = { - configuredRecipientCount: number - enabledRecipientCount: number + barkConfiguredRecipientCount: number + barkEnabledRecipientCount: number + wpushConfiguredRecipientCount: number + wpushEnabledRecipientCount: number barkReady: boolean + wpushReady: boolean + ready: boolean lastSuccessCount: number lastFailedCount: number scheduledJobEnabledCount: number @@ -40,8 +45,8 @@ defineProps()
Bark 通知

- 已配置 {{ notificationStats.configuredRecipientCount }} 人,启用 - {{ notificationStats.enabledRecipientCount }} 人。 + 已配置 {{ notificationStats.barkConfiguredRecipientCount }} 人,启用 + {{ notificationStats.barkEnabledRecipientCount }} 人。

diff --git a/apps/frontend/src/views/admin/platform-shops/components/notification/AdminNotificationInfoCard.vue b/apps/frontend/src/views/admin/platform-shops/components/notification/AdminNotificationInfoCard.vue index 9b63d3ad..a6e18474 100644 --- a/apps/frontend/src/views/admin/platform-shops/components/notification/AdminNotificationInfoCard.vue +++ b/apps/frontend/src/views/admin/platform-shops/components/notification/AdminNotificationInfoCard.vue @@ -16,6 +16,9 @@ defineProps() 内部运营通知,仅发送给后台管理员、客服或值班人员。 + 当前支持 Bark 与 WPush,两种通道可同时启用。 {{ notificationFilePath || '-' }} diff --git a/apps/frontend/src/views/admin/platform-shops/components/notification/AdminNotificationTestCard.vue b/apps/frontend/src/views/admin/platform-shops/components/notification/AdminNotificationTestCard.vue index 1b806199..be099f27 100644 --- a/apps/frontend/src/views/admin/platform-shops/components/notification/AdminNotificationTestCard.vue +++ b/apps/frontend/src/views/admin/platform-shops/components/notification/AdminNotificationTestCard.vue @@ -5,15 +5,20 @@ type NotificationForm = { enabled: boolean barkEnabled: boolean barkServerUrl: string + wpushEnabled: boolean testTitle: string testBody: string testUrl: string } type NotificationStats = { - configuredRecipientCount: number - enabledRecipientCount: number + barkConfiguredRecipientCount: number + barkEnabledRecipientCount: number + wpushConfiguredRecipientCount: number + wpushEnabledRecipientCount: number barkReady: boolean + wpushReady: boolean + ready: boolean lastSuccessCount: number lastFailedCount: number scheduledJobEnabledCount: number @@ -36,11 +41,11 @@ defineProps()
测试发送 -

测试会发送给所有已启用且填写了 Device Key 的接收人。

+

测试会发送给所有已启用且已填写 Bark Device Key 或 WPush API Key 的接收人。

() size="small" class="mt-4" > +