import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { ApnsProvider } from './apns.provider'; import { PushTokenService } from './push-token.service'; import { PushTemplateService } from './push-template.service'; import { PushMessageService, CreatePushMessageDto } from './push-message.service'; import { SendPushNotificationDto } from './dto/send-push-notification.dto'; import { SendPushByTemplateDto } from './dto/send-push-by-template.dto'; import { SendPushToDevicesDto } from './dto/send-push-to-devices.dto'; import { PushResult, BatchPushResult } from './interfaces/push-notification.interface'; import { PushResponseDto, BatchPushResponseDto } from './dto/push-response.dto'; import { DevicePushResponseDto, BatchDevicePushResponseDto, DevicePushResult } from './dto/device-push-response.dto'; import { ResponseCode } from '../base.dto'; import { PushMessageStatus } from './enums/push-message-status.enum'; import { PushType } from 'apns2'; @Injectable() export class PushNotificationsService { private readonly logger = new Logger(PushNotificationsService.name); private readonly bundleId: string; constructor( private readonly apnsProvider: ApnsProvider, private readonly pushTokenService: PushTokenService, private readonly pushTemplateService: PushTemplateService, private readonly pushMessageService: PushMessageService, private readonly configService: ConfigService, ) { this.bundleId = this.configService.get('APNS_BUNDLE_ID') || ''; } /** * 发送单个推送通知 */ async sendNotification(notificationData: SendPushNotificationDto): Promise { try { this.logger.log(`Sending push notification to ${notificationData.userIds.length} users`); const results: PushResult[] = []; let sentCount = 0; let failedCount = 0; // 获取所有用户的设备令牌 const userTokensMap = await this.pushTokenService.getDeviceTokensByUserIds(notificationData.userIds); // 为每个用户创建消息记录并发送推送 for (const userId of notificationData.userIds) { const deviceTokens = userTokensMap.get(userId) || []; if (deviceTokens.length === 0) { this.logger.warn(`No active device tokens found for user ${userId}`); results.push({ userId, deviceToken: '', success: false, error: 'No active device tokens found', }); failedCount++; continue; } // 为每个设备令牌创建消息记录 for (const deviceToken of deviceTokens) { try { // 创建消息记录 const messageData: CreatePushMessageDto = { userId, deviceToken, messageType: 'manual', title: notificationData.title, body: notificationData.body, payload: notificationData.payload, pushType: notificationData.pushType, priority: notificationData.priority, expiry: notificationData.expiry ? new Date(Date.now() + notificationData.expiry * 1000) : undefined, collapseId: notificationData.collapseId, }; const message = await this.pushMessageService.createMessage(messageData); // 创建APNs通知 const apnsNotification = this.apnsProvider.createNotification({ title: notificationData.title, body: notificationData.body, data: notificationData.payload, pushType: notificationData.pushType, priority: notificationData.priority, expiry: notificationData.expiry, collapseId: notificationData.collapseId, topic: this.bundleId, sound: notificationData.sound, badge: notificationData.badge, mutableContent: notificationData.mutableContent, contentAvailable: notificationData.contentAvailable, }); // 发送推送 const apnsResults = await this.apnsProvider.send(apnsNotification, [deviceToken]); // 处理结果 if (apnsResults.sent.length > 0) { await this.pushMessageService.updateMessageStatus(message.id, PushMessageStatus.SENT, apnsResults); await this.pushTokenService.updateLastUsedTime(deviceToken); results.push({ userId, deviceToken, success: true, apnsResponse: apnsResults, }); sentCount++; } else { const failure = apnsResults.failed[0]; const errorMessage = failure.error ? failure.error.message : `APNs Error: ${failure.status}`; await this.pushMessageService.updateMessageStatus( message.id, PushMessageStatus.FAILED, failure.response, errorMessage ); // 如果是无效令牌,停用该令牌 if (failure.status === '410' || failure.response?.reason === 'Unregistered') { await this.pushTokenService.unregisterToken(userId, deviceToken); } results.push({ userId, deviceToken, success: false, error: errorMessage, apnsResponse: failure.response, }); failedCount++; } } catch (error) { this.logger.error(`Failed to send push to user ${userId}, device ${deviceToken}: ${error.message}`, error); results.push({ userId, deviceToken, success: false, error: error.message, }); failedCount++; } } } const success = failedCount === 0; return { code: success ? ResponseCode.SUCCESS : ResponseCode.ERROR, message: success ? '推送发送成功' : '部分推送发送失败', data: { success, sentCount, failedCount, results, }, }; } catch (error) { this.logger.error(`Failed to send push notification: ${error.message}`, error); return { code: ResponseCode.ERROR, message: `推送发送失败: ${error.message}`, data: { success: false, sentCount: 0, failedCount: notificationData.userIds.length, results: [], }, }; } } /** * 使用模板发送推送通知 */ async sendNotificationByTemplate(templateData: SendPushByTemplateDto): Promise { try { this.logger.log(`Sending push notification using template: ${templateData.templateKey}`); // 渲染模板 const renderedTemplate = await this.pushTemplateService.renderTemplate( templateData.templateKey, templateData.data ); // 构建推送数据 const notificationData: SendPushNotificationDto = { userIds: templateData.userIds, title: renderedTemplate.title, body: renderedTemplate.body, payload: { ...renderedTemplate.payload, ...templateData.payload }, pushType: renderedTemplate.pushType, priority: renderedTemplate.priority, collapseId: templateData.collapseId, sound: templateData.sound, badge: templateData.badge, }; // 发送推送 return this.sendNotification(notificationData); } catch (error) { this.logger.error(`Failed to send push notification by template: ${error.message}`, error); return { code: ResponseCode.ERROR, message: `模板推送发送失败: ${error.message}`, data: { success: false, sentCount: 0, failedCount: templateData.userIds.length, results: [], }, }; } } /** * 批量发送推送通知 */ async sendBatchNotifications(notificationData: SendPushNotificationDto): Promise { try { this.logger.log(`Sending batch push notification to ${notificationData.userIds.length} users`); const results: PushResult[] = []; let totalUsers = notificationData.userIds.length; let totalTokens = 0; let successCount = 0; let failedCount = 0; // 获取所有用户的设备令牌 const userTokensMap = await this.pushTokenService.getDeviceTokensByUserIds(notificationData.userIds); // 统计总令牌数 for (const tokens of userTokensMap.values()) { totalTokens += tokens.length; } // 创建APNs通知 const apnsNotification = this.apnsProvider.createNotification({ title: notificationData.title, body: notificationData.body, data: notificationData.payload, pushType: notificationData.pushType, priority: notificationData.priority, expiry: notificationData.expiry, collapseId: notificationData.collapseId, topic: this.bundleId, sound: notificationData.sound, badge: notificationData.badge, mutableContent: notificationData.mutableContent, contentAvailable: notificationData.contentAvailable, }); // 批量发送推送 const allDeviceTokens = Array.from(userTokensMap.values()).flat(); if (allDeviceTokens.length === 0) { return { code: ResponseCode.ERROR, message: '没有找到有效的设备令牌', data: { totalUsers, totalTokens: 0, successCount: 0, failedCount: totalUsers, results: [], }, }; } const apnsResults = await this.apnsProvider.send(apnsNotification, allDeviceTokens); // 处理结果并创建消息记录 for (const [userId, deviceTokens] of userTokensMap.entries()) { for (const deviceToken of deviceTokens) { try { // 创建消息记录 const messageData: CreatePushMessageDto = { userId, deviceToken, messageType: 'batch', title: notificationData.title, body: notificationData.body, payload: notificationData.payload, pushType: notificationData.pushType, priority: notificationData.priority, expiry: notificationData.expiry ? new Date(Date.now() + notificationData.expiry * 1000) : undefined, collapseId: notificationData.collapseId, }; const message = await this.pushMessageService.createMessage(messageData); // 查找对应的APNs结果 const apnsResult = apnsResults.sent.includes(deviceToken) ? { device: deviceToken, success: true } : apnsResults.failed.find(f => f.device === deviceToken); if (apnsResult) { if (apnsResult.device === deviceToken && 'success' in apnsResult && apnsResult.success) { // 成功发送 await this.pushMessageService.updateMessageStatus(message.id, PushMessageStatus.SENT, apnsResult); await this.pushTokenService.updateLastUsedTime(deviceToken); results.push({ userId, deviceToken, success: true, apnsResponse: apnsResult, }); successCount++; } else { // 发送失败 const failure = apnsResult as any; const errorMessage = failure.error ? failure.error.message : `APNs Error: ${failure.status}`; await this.pushMessageService.updateMessageStatus( message.id, PushMessageStatus.FAILED, failure.response, errorMessage ); // 如果是无效令牌,停用该令牌 if (failure.status === '410' || failure.response?.reason === 'Unregistered') { await this.pushTokenService.unregisterToken(userId, deviceToken); } results.push({ userId, deviceToken, success: false, error: errorMessage, apnsResponse: failure.response, }); failedCount++; } } else { // 未找到结果,标记为失败 await this.pushMessageService.updateMessageStatus( message.id, PushMessageStatus.FAILED, null, 'No APNs result found' ); results.push({ userId, deviceToken, success: false, error: 'No APNs result found', }); failedCount++; } } catch (error) { this.logger.error(`Failed to process batch push result for user ${userId}, device ${deviceToken}: ${error.message}`, error); results.push({ userId, deviceToken, success: false, error: error.message, }); failedCount++; } } } const success = failedCount === 0; return { code: success ? ResponseCode.SUCCESS : ResponseCode.ERROR, message: success ? '批量推送发送成功' : '部分批量推送发送失败', data: { totalUsers, totalTokens, successCount, failedCount, results, }, }; } catch (error) { this.logger.error(`Failed to send batch push notification: ${error.message}`, error); return { code: ResponseCode.ERROR, message: `批量推送发送失败: ${error.message}`, data: { totalUsers: notificationData.userIds.length, totalTokens: 0, successCount: 0, failedCount: notificationData.userIds.length, results: [], }, }; } } /** * 发送静默推送 */ async sendSilentNotification(userId: string, payload: any): Promise { try { this.logger.log(`Sending silent push notification to user ${userId}`); const notificationData: SendPushNotificationDto = { userIds: [userId], title: '', body: '', payload, pushType: PushType.background, contentAvailable: true, }; return this.sendNotification(notificationData); } catch (error) { this.logger.error(`Failed to send silent push notification: ${error.message}`, error); return { code: ResponseCode.ERROR, message: `静默推送发送失败: ${error.message}`, data: { success: false, sentCount: 0, failedCount: 1, results: [], }, }; } } /** * 注册设备令牌 */ async registerToken(tokenData: any, userId?: string,): Promise { try { const token = await this.pushTokenService.registerToken(tokenData, userId); this.logger.log(`Registered device token for user ${userId}: ${token.id}`); return { code: ResponseCode.SUCCESS, message: '设备令牌注册成功', data: { success: true, tokenId: token.id, }, }; } catch (error) { this.logger.error(`Failed to register device token: ${error.message}`, error); return { code: ResponseCode.ERROR, message: `设备令牌注册失败: ${error.message}`, data: { success: false, tokenId: '', }, }; } } /** * 更新设备令牌 */ async updateToken(userId: string, tokenData: any): Promise { try { const token = await this.pushTokenService.updateToken(userId, tokenData); return { code: ResponseCode.SUCCESS, message: '设备令牌更新成功', data: { success: true, tokenId: token.id, }, }; } catch (error) { this.logger.error(`Failed to update device token: ${error.message}`, error); return { code: ResponseCode.ERROR, message: `设备令牌更新失败: ${error.message}`, data: { success: false, tokenId: '', }, }; } } /** * 注销设备令牌 */ async unregisterToken(userId: string, deviceToken: string): Promise { try { await this.pushTokenService.unregisterToken(userId, deviceToken); return { code: ResponseCode.SUCCESS, message: '设备令牌注销成功', data: { success: true, }, }; } catch (error) { this.logger.error(`Failed to unregister device token: ${error.message}`, error); return { code: ResponseCode.ERROR, message: `设备令牌注销失败: ${error.message}`, data: { success: false, }, }; } } /** * 基于设备令牌发送推送通知 */ async sendNotificationToDevices(notificationData: SendPushToDevicesDto): Promise { try { this.logger.log(`Sending push notification to ${notificationData.deviceTokens.length} devices`); const results: DevicePushResult[] = []; let sentCount = 0; let failedCount = 0; // 为每个设备令牌创建消息记录并发送推送 for (const deviceToken of notificationData.deviceTokens) { try { // 尝试获取设备令牌对应的用户ID const userId = await this.pushTokenService.getUserIdByDeviceToken(deviceToken); // 创建消息记录 const messageData: CreatePushMessageDto = { userId: userId || '', deviceToken, messageType: 'manual', title: notificationData.title, body: notificationData.body, payload: notificationData.payload, pushType: notificationData.pushType, priority: notificationData.priority, expiry: notificationData.expiry ? new Date(Date.now() + notificationData.expiry * 1000) : undefined, collapseId: notificationData.collapseId, }; const message = await this.pushMessageService.createMessage(messageData); // 创建APNs通知 const apnsNotification = this.apnsProvider.createNotification({ title: notificationData.title, body: notificationData.body, data: notificationData.payload, pushType: notificationData.pushType, priority: notificationData.priority, expiry: notificationData.expiry, collapseId: notificationData.collapseId, topic: this.bundleId, sound: notificationData.sound, badge: notificationData.badge, mutableContent: notificationData.mutableContent, contentAvailable: notificationData.contentAvailable, }); // 发送推送 const apnsResults = await this.apnsProvider.send(apnsNotification, [deviceToken]); // 处理结果 if (apnsResults.sent.length > 0) { await this.pushMessageService.updateMessageStatus(message.id, PushMessageStatus.SENT, apnsResults); await this.pushTokenService.updateLastUsedTime(deviceToken); results.push({ deviceToken, userId: userId || undefined, success: true, apnsResponse: apnsResults, }); sentCount++; } else { const failure = apnsResults.failed[0]; const errorMessage = failure.error ? failure.error.message : `APNs Error: ${failure.status}`; await this.pushMessageService.updateMessageStatus( message.id, PushMessageStatus.FAILED, failure.response, errorMessage ); // 如果是无效令牌,停用该令牌 if (failure.status === '410' || failure.response?.reason === 'Unregistered') { if (userId) { await this.pushTokenService.unregisterToken(userId, deviceToken); } else { // 如果没有用户ID,直接停用令牌 await this.pushTokenService.deactivateToken(deviceToken); } } results.push({ deviceToken, userId: userId || undefined, success: false, error: errorMessage, apnsResponse: failure.response, }); failedCount++; } } catch (error) { this.logger.error(`Failed to send push to device ${deviceToken}: ${error.message}`, error); results.push({ deviceToken, success: false, error: error.message, }); failedCount++; } } const success = failedCount === 0; return { code: success ? ResponseCode.SUCCESS : ResponseCode.ERROR, message: success ? '推送发送成功' : '部分推送发送失败', data: { success, sentCount, failedCount, results, }, }; } catch (error) { this.logger.error(`Failed to send push notification to devices: ${error.message}`, error); return { code: ResponseCode.ERROR, message: `推送发送失败: ${error.message}`, data: { success: false, sentCount: 0, failedCount: notificationData.deviceTokens.length, results: [], }, }; } } /** * 批量基于设备令牌发送推送通知 */ async sendBatchNotificationToDevices(notificationData: SendPushToDevicesDto): Promise { try { this.logger.log(`Sending batch push notification to ${notificationData.deviceTokens.length} devices`); const results: DevicePushResult[] = []; let totalTokens = notificationData.deviceTokens.length; let successCount = 0; let failedCount = 0; // 创建APNs通知 const apnsNotification = this.apnsProvider.createNotification({ alert: notificationData.title, title: notificationData.title, body: notificationData.body, data: notificationData.payload, pushType: notificationData.pushType, topic: this.bundleId, sound: notificationData.sound, badge: notificationData.badge, }); this.logger.log(`apnsNotification: ${JSON.stringify(apnsNotification, null, 2)}`); // 批量发送推送 const apnsResults = await this.apnsProvider.send(apnsNotification, notificationData.deviceTokens); // 处理结果并创建消息记录 for (const deviceToken of notificationData.deviceTokens) { try { // 尝试获取设备令牌对应的用户ID const userId = await this.pushTokenService.getUserIdByDeviceToken(deviceToken); // 创建消息记录 const messageData: CreatePushMessageDto = { userId: userId || '', deviceToken, messageType: 'batch', title: notificationData.title, body: notificationData.body, payload: notificationData.payload, pushType: notificationData.pushType, priority: notificationData.priority, expiry: notificationData.expiry ? new Date(Date.now() + notificationData.expiry * 1000) : undefined, collapseId: notificationData.collapseId, }; const message = await this.pushMessageService.createMessage(messageData); // 查找对应的APNs结果 const apnsResult = apnsResults.sent.includes(deviceToken) ? { device: deviceToken, success: true } : apnsResults.failed.find(f => f.device === deviceToken); if (apnsResult) { if (apnsResult.device === deviceToken && 'success' in apnsResult && apnsResult.success) { // 成功发送 await this.pushMessageService.updateMessageStatus(message.id, PushMessageStatus.SENT, apnsResult); await this.pushTokenService.updateLastUsedTime(deviceToken); results.push({ deviceToken, userId: userId || undefined, success: true, apnsResponse: apnsResult, }); successCount++; } else { // 发送失败 const failure = apnsResult as any; const errorMessage = failure.error ? failure.error.message : `APNs Error: ${failure.status}`; await this.pushMessageService.updateMessageStatus( message.id, PushMessageStatus.FAILED, failure.response, errorMessage ); // 如果是无效令牌,停用该令牌 if (failure.status === '410' || failure.response?.reason === 'Unregistered') { if (userId) { await this.pushTokenService.unregisterToken(userId, deviceToken); } else { // 如果没有用户ID,直接停用令牌 await this.pushTokenService.deactivateToken(deviceToken); } } results.push({ deviceToken, userId: userId || undefined, success: false, error: errorMessage, apnsResponse: failure.response, }); failedCount++; } } else { // 未找到结果,标记为失败 await this.pushMessageService.updateMessageStatus( message.id, PushMessageStatus.FAILED, null, 'No APNs result found' ); results.push({ deviceToken, userId: userId || undefined, success: false, error: 'No APNs result found', }); failedCount++; } } catch (error) { this.logger.error(`Failed to process batch push result for device ${deviceToken}: ${error.message}`, error); results.push({ deviceToken, success: false, error: error.message, }); failedCount++; } } const success = failedCount === 0; return { code: success ? ResponseCode.SUCCESS : ResponseCode.ERROR, message: success ? '批量推送发送成功' : '部分批量推送发送失败', data: { totalTokens, successCount, failedCount, results, }, }; } catch (error) { this.logger.error(`Failed to send batch push notification to devices: ${error.message}`, error); return { code: ResponseCode.ERROR, message: `批量推送发送失败: ${error.message}`, data: { totalTokens: notificationData.deviceTokens.length, successCount: 0, failedCount: notificationData.deviceTokens.length, results: [], }, }; } } }