feat: 支持 push

This commit is contained in:
richarjiang
2025-10-11 17:38:04 +08:00
parent 999fc7f793
commit 305a969912
30 changed files with 4582 additions and 1 deletions

View File

@@ -0,0 +1,332 @@
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(userId: string, tokenData: RegisterDeviceTokenDto): Promise<UserPushToken> {
try {
this.logger.log(`Registering push token for user ${userId}`);
// 检查是否已存在相同的令牌
const existingToken = await this.pushTokenModel.findOne({
where: {
userId,
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 user ${userId}`);
return existingToken;
}
// 检查用户是否已有其他设备的令牌,可以选择是否停用旧令牌
const userTokens = await this.pushTokenModel.findAll({
where: {
userId,
isActive: true,
},
});
// 创建新令牌
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 user ${userId}`);
return newToken;
} catch (error) {
this.logger.error(`Failed to register push token for user ${userId}: ${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);
}
}
}