feat(push): 新增设备推送和测试功能
- 新增基于设备令牌的推送通知接口 - 添加推送测试服务,支持应用启动时自动测试 - 新增推送测试文档说明 - 更新 APNS 配置和日志记录 - 迁移至 apns2 库的 PushType 枚举 - 替换订阅密钥文件 - 添加项目规则文档
This commit is contained in:
@@ -6,11 +6,13 @@ 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 { PushType } from './enums/push-type.enum';
|
||||
import { PushMessageStatus } from './enums/push-message-status.enum';
|
||||
import { PushType } from 'apns2';
|
||||
|
||||
@Injectable()
|
||||
export class PushNotificationsService {
|
||||
@@ -402,7 +404,7 @@ export class PushNotificationsService {
|
||||
title: '',
|
||||
body: '',
|
||||
payload,
|
||||
pushType: PushType.BACKGROUND,
|
||||
pushType: PushType.background,
|
||||
contentAvailable: true,
|
||||
};
|
||||
|
||||
@@ -428,6 +430,7 @@ export class PushNotificationsService {
|
||||
async registerToken(tokenData: any, userId?: string,): Promise<any> {
|
||||
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: '设备令牌注册成功',
|
||||
@@ -500,4 +503,288 @@ export class PushNotificationsService {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于设备令牌发送推送通知
|
||||
*/
|
||||
async sendNotificationToDevices(notificationData: SendPushToDevicesDto): Promise<DevicePushResponseDto> {
|
||||
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<BatchDevicePushResponseDto> {
|
||||
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: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user