- 新增基于设备令牌的推送通知接口 - 添加推送测试服务,支持应用启动时自动测试 - 新增推送测试文档说明 - 更新 APNS 配置和日志记录 - 迁移至 apns2 库的 PushType 枚举 - 替换订阅密钥文件 - 添加项目规则文档
354 lines
9.6 KiB
TypeScript
354 lines
9.6 KiB
TypeScript
import { Injectable, Logger, NotFoundException, ConflictException } from '@nestjs/common';
|
||
import { InjectModel } from '@nestjs/sequelize';
|
||
import { Op } from 'sequelize';
|
||
import { UserPushToken } from './models/user-push-token.model';
|
||
import { DeviceType } from './enums/device-type.enum';
|
||
import { RegisterDeviceTokenDto } from './dto/register-device-token.dto';
|
||
import { UpdateDeviceTokenDto } from './dto/update-device-token.dto';
|
||
|
||
@Injectable()
|
||
export class PushTokenService {
|
||
private readonly logger = new Logger(PushTokenService.name);
|
||
|
||
constructor(
|
||
@InjectModel(UserPushToken)
|
||
private readonly pushTokenModel: typeof UserPushToken,
|
||
) { }
|
||
|
||
/**
|
||
* 注册设备令牌
|
||
*/
|
||
async registerToken(tokenData: RegisterDeviceTokenDto, userId?: string): Promise<UserPushToken> {
|
||
try {
|
||
this.logger.log(`Registering push token for device ${tokenData.deviceToken}`);
|
||
|
||
// 检查是否已存在相同的令牌
|
||
const existingToken = await this.pushTokenModel.findOne({
|
||
where: {
|
||
deviceToken: tokenData.deviceToken,
|
||
},
|
||
});
|
||
|
||
if (existingToken) {
|
||
// 更新现有令牌信息
|
||
await existingToken.update({
|
||
deviceType: tokenData.deviceType,
|
||
appVersion: tokenData.appVersion,
|
||
osVersion: tokenData.osVersion,
|
||
deviceName: tokenData.deviceName,
|
||
isActive: true,
|
||
lastUsedAt: new Date(),
|
||
});
|
||
|
||
this.logger.log(`Updated existing push token for device ${tokenData.deviceToken}`);
|
||
return existingToken;
|
||
}
|
||
|
||
|
||
// 创建新令牌
|
||
const newToken = await this.pushTokenModel.create({
|
||
userId,
|
||
deviceToken: tokenData.deviceToken,
|
||
deviceType: tokenData.deviceType,
|
||
appVersion: tokenData.appVersion,
|
||
osVersion: tokenData.osVersion,
|
||
deviceName: tokenData.deviceName,
|
||
isActive: true,
|
||
lastUsedAt: new Date(),
|
||
});
|
||
|
||
this.logger.log(`Successfully registered new push token for device ${tokenData.deviceToken}`);
|
||
return newToken;
|
||
} catch (error) {
|
||
this.logger.error(`Failed to register push token for device ${tokenData.deviceToken}: ${error.message}`, error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更新设备令牌
|
||
*/
|
||
async updateToken(userId: string, tokenData: UpdateDeviceTokenDto): Promise<UserPushToken> {
|
||
try {
|
||
this.logger.log(`Updating push token for user ${userId}`);
|
||
|
||
// 查找当前令牌
|
||
const currentToken = await this.pushTokenModel.findOne({
|
||
where: {
|
||
userId,
|
||
deviceToken: tokenData.currentDeviceToken,
|
||
isActive: true,
|
||
},
|
||
});
|
||
|
||
if (!currentToken) {
|
||
throw new NotFoundException('Current device token not found or inactive');
|
||
}
|
||
|
||
// 检查新令牌是否已存在
|
||
const existingNewToken = await this.pushTokenModel.findOne({
|
||
where: {
|
||
userId,
|
||
deviceToken: tokenData.newDeviceToken,
|
||
},
|
||
});
|
||
|
||
if (existingNewToken) {
|
||
// 如果新令牌已存在,激活它并停用当前令牌
|
||
await existingNewToken.update({
|
||
isActive: true,
|
||
lastUsedAt: new Date(),
|
||
appVersion: tokenData.appVersion || existingNewToken.appVersion,
|
||
osVersion: tokenData.osVersion || existingNewToken.osVersion,
|
||
deviceName: tokenData.deviceName || existingNewToken.deviceName,
|
||
});
|
||
|
||
await currentToken.update({
|
||
isActive: false,
|
||
});
|
||
|
||
this.logger.log(`Activated existing new token and deactivated old token for user ${userId}`);
|
||
return existingNewToken;
|
||
}
|
||
|
||
// 更新当前令牌为新令牌
|
||
await currentToken.update({
|
||
deviceToken: tokenData.newDeviceToken,
|
||
appVersion: tokenData.appVersion,
|
||
osVersion: tokenData.osVersion,
|
||
deviceName: tokenData.deviceName,
|
||
lastUsedAt: new Date(),
|
||
});
|
||
|
||
this.logger.log(`Successfully updated push token for user ${userId}`);
|
||
return currentToken;
|
||
} catch (error) {
|
||
this.logger.error(`Failed to update push token for user ${userId}: ${error.message}`, error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 注销设备令牌
|
||
*/
|
||
async unregisterToken(userId: string, deviceToken: string): Promise<void> {
|
||
try {
|
||
this.logger.log(`Unregistering push token for user ${userId}`);
|
||
|
||
const token = await this.pushTokenModel.findOne({
|
||
where: {
|
||
userId,
|
||
deviceToken,
|
||
isActive: true,
|
||
},
|
||
});
|
||
|
||
if (!token) {
|
||
throw new NotFoundException('Device token not found or inactive');
|
||
}
|
||
|
||
await token.update({
|
||
isActive: false,
|
||
});
|
||
|
||
this.logger.log(`Successfully unregistered push token for user ${userId}`);
|
||
} catch (error) {
|
||
this.logger.error(`Failed to unregister push token for user ${userId}: ${error.message}`, error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取用户的所有有效令牌
|
||
*/
|
||
async getActiveTokens(userId: string): Promise<UserPushToken[]> {
|
||
try {
|
||
const tokens = await this.pushTokenModel.findAll({
|
||
where: {
|
||
userId,
|
||
isActive: true,
|
||
},
|
||
order: [['lastUsedAt', 'DESC']],
|
||
});
|
||
|
||
this.logger.log(`Found ${tokens.length} active tokens for user ${userId}`);
|
||
return tokens;
|
||
} catch (error) {
|
||
this.logger.error(`Failed to get active tokens for user ${userId}: ${error.message}`, error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取用户的所有令牌(包括非活跃的)
|
||
*/
|
||
async getAllTokens(userId: string): Promise<UserPushToken[]> {
|
||
try {
|
||
const tokens = await this.pushTokenModel.findAll({
|
||
where: {
|
||
userId,
|
||
},
|
||
order: [['createdAt', 'DESC']],
|
||
});
|
||
|
||
this.logger.log(`Found ${tokens.length} total tokens for user ${userId}`);
|
||
return tokens;
|
||
} catch (error) {
|
||
this.logger.error(`Failed to get all tokens for user ${userId}: ${error.message}`, error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 验证令牌有效性
|
||
*/
|
||
async validateToken(deviceToken: string): Promise<boolean> {
|
||
try {
|
||
const token = await this.pushTokenModel.findOne({
|
||
where: {
|
||
deviceToken,
|
||
isActive: true,
|
||
},
|
||
});
|
||
|
||
return !!token;
|
||
} catch (error) {
|
||
this.logger.error(`Failed to validate token: ${error.message}`, error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 清理无效令牌
|
||
*/
|
||
async cleanupInvalidTokens(): Promise<number> {
|
||
try {
|
||
this.logger.log('Starting cleanup of invalid tokens');
|
||
|
||
// 清理超过30天未使用的令牌
|
||
const thirtyDaysAgo = new Date();
|
||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||
|
||
const result = await this.pushTokenModel.update(
|
||
{
|
||
isActive: false,
|
||
},
|
||
{
|
||
where: {
|
||
isActive: true,
|
||
lastUsedAt: {
|
||
[Op.lt]: thirtyDaysAgo,
|
||
},
|
||
},
|
||
},
|
||
);
|
||
|
||
const cleanedCount = result[0];
|
||
this.logger.log(`Cleaned up ${cleanedCount} inactive tokens`);
|
||
return cleanedCount;
|
||
} catch (error) {
|
||
this.logger.error(`Failed to cleanup invalid tokens: ${error.message}`, error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 根据设备令牌获取用户ID
|
||
*/
|
||
async getUserIdByDeviceToken(deviceToken: string): Promise<string | null> {
|
||
try {
|
||
const token = await this.pushTokenModel.findOne({
|
||
where: {
|
||
deviceToken,
|
||
isActive: true,
|
||
},
|
||
});
|
||
|
||
return token ? token.userId : null;
|
||
} catch (error) {
|
||
this.logger.error(`Failed to get user ID by device token: ${error.message}`, error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 批量获取用户的设备令牌
|
||
*/
|
||
async getDeviceTokensByUserIds(userIds: string[]): Promise<Map<string, string[]>> {
|
||
try {
|
||
const tokens = await this.pushTokenModel.findAll({
|
||
where: {
|
||
userId: {
|
||
[Op.in]: userIds,
|
||
},
|
||
isActive: true,
|
||
},
|
||
});
|
||
|
||
const userTokensMap = new Map<string, string[]>();
|
||
|
||
tokens.forEach((token) => {
|
||
if (!userTokensMap.has(token.userId)) {
|
||
userTokensMap.set(token.userId, []);
|
||
}
|
||
userTokensMap.get(token.userId)!.push(token.deviceToken);
|
||
});
|
||
|
||
return userTokensMap;
|
||
} catch (error) {
|
||
this.logger.error(`Failed to get device tokens by user IDs: ${error.message}`, error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 更新令牌最后使用时间
|
||
*/
|
||
async updateLastUsedTime(deviceToken: string): Promise<void> {
|
||
try {
|
||
await this.pushTokenModel.update(
|
||
{
|
||
lastUsedAt: new Date(),
|
||
},
|
||
{
|
||
where: {
|
||
deviceToken,
|
||
isActive: true,
|
||
},
|
||
},
|
||
);
|
||
} catch (error) {
|
||
this.logger.error(`Failed to update last used time: ${error.message}`, error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 直接停用设备令牌(不需要用户ID)
|
||
*/
|
||
async deactivateToken(deviceToken: string): Promise<void> {
|
||
try {
|
||
this.logger.log(`Deactivating push token: ${deviceToken}`);
|
||
|
||
const token = await this.pushTokenModel.findOne({
|
||
where: {
|
||
deviceToken,
|
||
isActive: true,
|
||
},
|
||
});
|
||
|
||
if (!token) {
|
||
this.logger.warn(`Device token not found or already inactive: ${deviceToken}`);
|
||
return;
|
||
}
|
||
|
||
await token.update({
|
||
isActive: false,
|
||
});
|
||
|
||
this.logger.log(`Successfully deactivated push token: ${deviceToken}`);
|
||
} catch (error) {
|
||
this.logger.error(`Failed to deactivate push token: ${deviceToken}: ${error.message}`, error);
|
||
throw error;
|
||
}
|
||
}
|
||
} |