diff --git a/tenant-platform/src/api/app.ts b/tenant-platform/src/api/app.ts index d781fec..a2d579e 100644 --- a/tenant-platform/src/api/app.ts +++ b/tenant-platform/src/api/app.ts @@ -79,6 +79,26 @@ export interface PushServiceConfig { honor?: PushVendorConfig apns?: PushVendorConfig fcm?: PushVendorConfig + channels?: PushNotificationChannelConfig[] + routing?: Record +} + +export interface PushNotificationChannelConfig { + key: string + channelId: string + version: number + name: string + description?: string + importance: 'MIN' | 'LOW' | 'DEFAULT' | 'HIGH' | 'MAX' + sound: boolean + vibration: boolean + badge: boolean +} + +export interface PushNotificationRouteConfig { + channel: string + category: string + priority: 'LOW' | 'DEFAULT' | 'HIGH' } export const appApi = { diff --git a/tenant-platform/src/views/push/PushConfigView.vue b/tenant-platform/src/views/push/PushConfigView.vue index 5da6411..6f6229e 100644 --- a/tenant-platform/src/views/push/PushConfigView.vue +++ b/tenant-platform/src/views/push/PushConfigView.vue @@ -30,6 +30,81 @@ + 通知通道 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 新增通道 + + 业务分类路由 + + + + + + + + + + + + + + + + 厂商凭据
@@ -71,7 +146,14 @@ import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue' import { useRoute } from 'vue-router' import { ElMessage, ElMessageBox } from 'element-plus' import { CopyDocument } from '@element-plus/icons-vue' -import { appApi, type App, type FeatureService, type PushServiceConfig } from '@/api/app' +import { + appApi, + type App, + type FeatureService, + type PushNotificationChannelConfig, + type PushNotificationRouteConfig, + type PushServiceConfig, +} from '@/api/app' type VendorKey = keyof PushServiceConfig type FieldDef = { @@ -107,7 +189,29 @@ const pushConfig = reactive>({ honor: { appId: '', clientId: '', clientSecret: '' }, apns: { teamId: '', keyId: '', bundleId: '', keyPath: '', sandbox: false }, fcm: { serviceAccountJson: '' }, + channels: defaultChannels(), + routing: defaultRouting(), } as Required) +const originalChannels = ref([]) + +const importanceOptions = [ + { label: '最小', value: 'MIN' }, + { label: '低', value: 'LOW' }, + { label: '默认', value: 'DEFAULT' }, + { label: '高', value: 'HIGH' }, + { label: '最高', value: 'MAX' }, +] as const + +const routeTypes = [ + { type: 'IM_MESSAGE', channel: 'im_message', category: 'MESSAGE', priority: 'HIGH' }, + { type: 'FRIEND_REQUEST', channel: 'im_message', category: 'SOCIAL', priority: 'HIGH' }, + { type: 'SYSTEM_NOTICE', channel: 'system_notice', category: 'SYSTEM', priority: 'DEFAULT' }, +] as const + +const routeRows = computed(() => routeTypes.map(item => ({ + type: item.type, + route: pushConfig.routing[item.type] ?? ensureRoute(item.type, item.channel, item.category, item.priority), +}))) const vendorDefs: VendorDef[] = [ { @@ -196,6 +300,9 @@ function applyConfig(raw?: string | null) { pushConfig.honor = { ...pushConfig.honor, ...parsed.honor } pushConfig.apns = { ...pushConfig.apns, ...parsed.apns } pushConfig.fcm = { ...pushConfig.fcm, ...parsed.fcm } + pushConfig.channels = normalizeChannels(parsed.channels) + pushConfig.routing = normalizeRouting(parsed.routing) + originalChannels.value = pushConfig.channels.map(channel => ({ ...channel })) } function parseConfig(raw?: string | null): PushServiceConfig { @@ -211,6 +318,7 @@ async function saveConfig() { if (!app.value) return saving.value = true try { + bumpChangedChannelVersions() await appApi.updateServiceConfig(app.value.id, servicePlatform.value, 'PUSH', toPushConfigRequest()) ElMessage.success('推送配置已保存') await loadData() @@ -221,7 +329,7 @@ async function saveConfig() { } } -function toPushConfigRequest(): Record { +function toPushConfigRequest(): Record { return { huaweiAppId: pushConfig.huawei.appId ?? '', huaweiAppSecret: pushConfig.huawei.appSecret ?? '', @@ -243,9 +351,116 @@ function toPushConfigRequest(): Record { apnsKeyPath: pushConfig.apns.keyPath ?? '', apnsSandbox: pushConfig.apns.sandbox ?? false, fcmServiceAccountJson: pushConfig.fcm.serviceAccountJson ?? '', + channels: pushConfig.channels, + routing: pushConfig.routing, } } +function defaultChannels(): PushNotificationChannelConfig[] { + return [ + { + key: 'im_message', + channelId: 'xuqm_im_message', + version: 1, + name: '聊天消息', + description: '单聊、群聊和好友消息', + importance: 'HIGH', + sound: true, + vibration: true, + badge: true, + }, + { + key: 'system_notice', + channelId: 'xuqm_system_notice', + version: 1, + name: '系统通知', + description: '系统通知和业务提醒', + importance: 'DEFAULT', + sound: true, + vibration: true, + badge: true, + }, + ] +} + +function defaultRouting(): Record { + return { + IM_MESSAGE: { channel: 'im_message', category: 'MESSAGE', priority: 'HIGH' }, + FRIEND_REQUEST: { channel: 'im_message', category: 'SOCIAL', priority: 'HIGH' }, + SYSTEM_NOTICE: { channel: 'system_notice', category: 'SYSTEM', priority: 'DEFAULT' }, + } +} + +function normalizeChannels(channels?: PushNotificationChannelConfig[]): PushNotificationChannelConfig[] { + const source = Array.isArray(channels) && channels.length > 0 ? channels : defaultChannels() + return source.map((channel, index) => ({ + key: channel.key || `channel_${index + 1}`, + channelId: channel.channelId || `xuqm_channel_${index + 1}`, + version: Math.max(Number(channel.version || 1), 1), + name: channel.name || channel.key || `通知通道 ${index + 1}`, + description: channel.description ?? '', + importance: channel.importance || 'DEFAULT', + sound: channel.sound ?? true, + vibration: channel.vibration ?? true, + badge: channel.badge ?? true, + })) +} + +function normalizeRouting(routing?: Record): Record { + return { ...defaultRouting(), ...(routing ?? {}) } +} + +function ensureRoute( + type: string, + channel: string, + category: string, + priority: PushNotificationRouteConfig['priority'], +): PushNotificationRouteConfig { + const route = { channel, category, priority } + pushConfig.routing[type] = route + return route +} + +function addChannel() { + const next = pushConfig.channels.length + 1 + pushConfig.channels.push({ + key: `custom_${next}`, + channelId: `xuqm_custom_${next}`, + version: 1, + name: `自定义通道 ${next}`, + description: '', + importance: 'DEFAULT', + sound: true, + vibration: true, + badge: true, + }) +} + +function removeChannel(index: number) { + const [removed] = pushConfig.channels.splice(index, 1) + if (!removed) return + Object.values(pushConfig.routing).forEach(route => { + if (route.channel === removed.key) { + route.channel = pushConfig.channels[0]?.key ?? '' + } + }) +} + +function bumpChangedChannelVersions() { + const originalByKey = new Map(originalChannels.value.map(channel => [channel.key, channel])) + pushConfig.channels.forEach(channel => { + const original = originalByKey.get(channel.key) + if (!original) return + const immutableChanged = original.importance !== channel.importance + || original.sound !== channel.sound + || original.vibration !== channel.vibration + || original.badge !== channel.badge + if (immutableChanged && channel.version <= original.version) { + channel.version = original.version + 1 + } + }) +} + async function onTogglePushService(enable: boolean) { if (enable) { ElMessage.info('请通过服务开通流程启用推送服务')