feat: 支持 push
This commit is contained in:
502
src/push-notifications/push-notifications.service.ts
Normal file
502
src/push-notifications/push-notifications.service.ts
Normal file
@@ -0,0 +1,502 @@
|
||||
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 { PushResult, BatchPushResult } from './interfaces/push-notification.interface';
|
||||
import { PushResponseDto, BatchPushResponseDto } from './dto/push-response.dto';
|
||||
import { ResponseCode } from '../base.dto';
|
||||
import { PushType } from './enums/push-type.enum';
|
||||
import { PushMessageStatus } from './enums/push-message-status.enum';
|
||||
|
||||
@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<string>('APNS_BUNDLE_ID') || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送单个推送通知
|
||||
*/
|
||||
async sendNotification(notificationData: SendPushNotificationDto): Promise<PushResponseDto> {
|
||||
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,
|
||||
payload: 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<PushResponseDto> {
|
||||
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<BatchPushResponseDto> {
|
||||
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,
|
||||
payload: 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.find(s => s.device === deviceToken) ||
|
||||
apnsResults.failed.find(f => f.device === deviceToken);
|
||||
|
||||
if (apnsResult) {
|
||||
if ('device' in apnsResult && apnsResult.device === deviceToken) {
|
||||
// 成功发送
|
||||
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<PushResponseDto> {
|
||||
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(userId: string, tokenData: any): Promise<any> {
|
||||
try {
|
||||
const token = await this.pushTokenService.registerToken(userId, tokenData);
|
||||
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<any> {
|
||||
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<any> {
|
||||
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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user