构建了从照片到药品档案的自动化处理流程,通过GLM多模态大模型实现药品信息的智能采集: 核心能力: - 创建任务追踪表 t_medication_recognition_tasks 存储识别任务状态 - 四阶段渐进式分析:基础识别→人群适配→成分解析→风险评估 - 提供三个REST端点支持任务创建、进度查询和结果确认 - 前端可通过轮询方式获取0-100%的实时进度反馈 - VIP用户免费使用,普通用户按次扣费 技术实现: - 利用GLM-4V-Plus模型处理多角度药品图像(正面+侧面+说明书) - 采用GLM-4-Flash模型进行文本深度分析 - 异步任务执行机制避免接口阻塞 - 完整的异常处理和任务失败恢复策略 - 新增AI_RECOGNITION.md文档详细说明集成方式 同步修复: - 修正会员用户AI配额扣减逻辑,避免不必要的次数消耗 - 优化APNs推送中无效设备令牌的检测和清理流程 - 将服药提醒的提前通知时间从15分钟缩短为5分钟
862 lines
30 KiB
TypeScript
862 lines
30 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 error = failure.error as any;
|
||
const errorMessage = error?.message || `APNs Error: ${failure.status || 'Unknown'}`;
|
||
|
||
await this.pushMessageService.updateMessageStatus(
|
||
message.id,
|
||
PushMessageStatus.FAILED,
|
||
failure.response,
|
||
errorMessage
|
||
);
|
||
|
||
// 检查是否是无效令牌错误 - 需要停用 token
|
||
const shouldDeactivateToken = this.shouldDeactivateToken(error, failure.response);
|
||
|
||
if (shouldDeactivateToken) {
|
||
this.logger.warn(`Deactivating invalid token for user ${userId}: ${errorMessage}`);
|
||
await this.pushTokenService.deactivateToken(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: [],
|
||
},
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 判断是否应该停用 token
|
||
* 根据 APNs 错误类型判断 token 是否已失效
|
||
*/
|
||
private shouldDeactivateToken(error: any, response: any): boolean {
|
||
if (!error && !response) {
|
||
return false;
|
||
}
|
||
|
||
// 检查错误对象的 reason 字段(apns2 库的错误格式)
|
||
const reason = error?.reason || response?.reason || '';
|
||
|
||
// APNs 返回的需要停用 token 的错误原因
|
||
const invalidTokenReasons = [
|
||
'BadDeviceToken', // 无效的设备令牌格式
|
||
'Unregistered', // 设备已注销(用户卸载了应用)
|
||
'DeviceTokenNotForTopic', // token 与 bundle ID 不匹配
|
||
'ExpiredToken', // token 已过期
|
||
];
|
||
|
||
// 检查是否包含这些错误原因
|
||
if (invalidTokenReasons.some(r => reason.includes(r))) {
|
||
return true;
|
||
}
|
||
|
||
// 检查 HTTP 状态码 410 (Gone) - 表示设备令牌永久失效
|
||
const statusCode = error?.statusCode || response?.statusCode;
|
||
if (statusCode === 410 || statusCode === '410') {
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 使用模板发送推送通知
|
||
*/
|
||
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 error = failure.error as any;
|
||
const errorMessage = error?.message || `APNs Error: ${failure.status || 'Unknown'}`;
|
||
|
||
await this.pushMessageService.updateMessageStatus(
|
||
message.id,
|
||
PushMessageStatus.FAILED,
|
||
failure.response,
|
||
errorMessage
|
||
);
|
||
|
||
// 检查是否是无效令牌错误 - 需要停用 token
|
||
const shouldDeactivateToken = this.shouldDeactivateToken(error, failure.response);
|
||
|
||
if (shouldDeactivateToken) {
|
||
this.logger.warn(`Deactivating invalid token for user ${userId}: ${errorMessage}`);
|
||
await this.pushTokenService.deactivateToken(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,
|
||
},
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更新令牌绑定的用户ID
|
||
*/
|
||
async updateTokenUserId(userId: string, deviceToken: string): Promise<any> {
|
||
try {
|
||
const token = await this.pushTokenService.updateTokenUserId(deviceToken, userId);
|
||
this.logger.log(`Updated user ID for device token: ${deviceToken}`);
|
||
return {
|
||
code: ResponseCode.SUCCESS,
|
||
message: '令牌用户ID更新成功',
|
||
data: {
|
||
success: true,
|
||
tokenId: token.id,
|
||
},
|
||
};
|
||
} catch (error) {
|
||
this.logger.error(`Failed to update user ID for device token: ${error.message}`, error);
|
||
return {
|
||
code: ResponseCode.ERROR,
|
||
message: `令牌用户ID更新失败: ${error.message}`,
|
||
data: {
|
||
success: false,
|
||
tokenId: '',
|
||
},
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 基于设备令牌发送推送通知
|
||
*/
|
||
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 error = failure.error as any;
|
||
const errorMessage = error?.message || `APNs Error: ${failure.status || 'Unknown'}`;
|
||
|
||
await this.pushMessageService.updateMessageStatus(
|
||
message.id,
|
||
PushMessageStatus.FAILED,
|
||
failure.response,
|
||
errorMessage
|
||
);
|
||
|
||
// 检查是否是无效令牌错误 - 需要停用 token
|
||
const shouldDeactivateToken = this.shouldDeactivateToken(error, failure.response);
|
||
|
||
if (shouldDeactivateToken) {
|
||
this.logger.warn(`Deactivating invalid token for device ${deviceToken}: ${errorMessage}`);
|
||
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({
|
||
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,
|
||
});
|
||
|
||
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 error = failure.error as any;
|
||
const errorMessage = error?.message || `APNs Error: ${failure.status || 'Unknown'}`;
|
||
|
||
await this.pushMessageService.updateMessageStatus(
|
||
message.id,
|
||
PushMessageStatus.FAILED,
|
||
failure.response,
|
||
errorMessage
|
||
);
|
||
|
||
// 检查是否是无效令牌错误 - 需要停用 token
|
||
const shouldDeactivateToken = this.shouldDeactivateToken(error, failure.response);
|
||
|
||
if (shouldDeactivateToken) {
|
||
this.logger.warn(`Deactivating invalid token for device ${deviceToken}: ${errorMessage}`);
|
||
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: [],
|
||
},
|
||
};
|
||
}
|
||
}
|
||
} |