feat: 支持 push
This commit is contained in:
332
src/push-notifications/push-token.service.ts
Normal file
332
src/push-notifications/push-token.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user