- 新增基于设备令牌的推送通知接口 - 添加推送测试服务,支持应用启动时自动测试 - 新增推送测试文档说明 - 更新 APNS 配置和日志记录 - 迁移至 apns2 库的 PushType 枚举 - 替换订阅密钥文件 - 添加项目规则文档
790 lines
27 KiB
TypeScript
790 lines
27 KiB
TypeScript
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<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,
|
||
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<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,
|
||
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<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(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: '设备令牌注册成功',
|
||
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,
|
||
},
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 基于设备令牌发送推送通知
|
||
*/
|
||
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: [],
|
||
},
|
||
};
|
||
}
|
||
}
|
||
} |