feat(push-notifications): 将推送测试改为基于挑战的个性化提醒
重构推送测试服务,从简单的测试推送改为针对正在进行中挑战的个性化提醒推送。 新增功能包括: - 获取正在进行中的挑战和活跃参与者 - 根据挑战类型生成个性化推送内容 - 为挑战参与者发送针对性的提醒推送 - 支持多种挑战类型的推送模板(饮水、运动、饮食、心情、睡眠、体重管理)
This commit is contained in:
@@ -14,16 +14,20 @@ import { PushTemplate } from './models/push-template.model';
|
|||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { DatabaseModule } from '../database/database.module';
|
import { DatabaseModule } from '../database/database.module';
|
||||||
import { UsersModule } from '../users/users.module';
|
import { UsersModule } from '../users/users.module';
|
||||||
|
import { ChallengesModule } from '../challenges/challenges.module';
|
||||||
|
import { ChallengeParticipant } from '../challenges/models/challenge-participant.model';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule,
|
ConfigModule,
|
||||||
DatabaseModule,
|
DatabaseModule,
|
||||||
UsersModule,
|
UsersModule,
|
||||||
|
ChallengesModule,
|
||||||
SequelizeModule.forFeature([
|
SequelizeModule.forFeature([
|
||||||
UserPushToken,
|
UserPushToken,
|
||||||
PushMessage,
|
PushMessage,
|
||||||
PushTemplate,
|
PushTemplate,
|
||||||
|
ChallengeParticipant,
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
controllers: [
|
controllers: [
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import { UserPushToken } from './models/user-push-token.model';
|
|||||||
import { InjectModel } from '@nestjs/sequelize';
|
import { InjectModel } from '@nestjs/sequelize';
|
||||||
import { Op } from 'sequelize';
|
import { Op } from 'sequelize';
|
||||||
import { PushType } from 'apns2';
|
import { PushType } from 'apns2';
|
||||||
|
import { ChallengesService } from '../challenges/challenges.service';
|
||||||
|
import { Challenge } from '../challenges/models/challenge.model';
|
||||||
|
import { ChallengeParticipant, ChallengeParticipantStatus } from '../challenges/models/challenge-participant.model';
|
||||||
|
import * as dayjs from 'dayjs';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PushTestService implements OnModuleInit {
|
export class PushTestService implements OnModuleInit {
|
||||||
@@ -14,8 +18,11 @@ export class PushTestService implements OnModuleInit {
|
|||||||
constructor(
|
constructor(
|
||||||
@InjectModel(UserPushToken)
|
@InjectModel(UserPushToken)
|
||||||
private readonly pushTokenModel: typeof UserPushToken,
|
private readonly pushTokenModel: typeof UserPushToken,
|
||||||
|
@InjectModel(ChallengeParticipant)
|
||||||
|
private readonly participantModel: typeof ChallengeParticipant,
|
||||||
private readonly pushNotificationsService: PushNotificationsService,
|
private readonly pushNotificationsService: PushNotificationsService,
|
||||||
private readonly pushTokenService: PushTokenService,
|
private readonly pushTokenService: PushTokenService,
|
||||||
|
private readonly challengesService: ChallengesService,
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
@@ -45,55 +52,226 @@ export class PushTestService implements OnModuleInit {
|
|||||||
* 执行推送测试
|
* 执行推送测试
|
||||||
*/
|
*/
|
||||||
private async performPushTest(): Promise<void> {
|
private async performPushTest(): Promise<void> {
|
||||||
this.logger.log('Starting push test...');
|
this.logger.log('Starting challenge-based push test...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 获取所有活跃的推送令牌
|
// 1. 获取正在进行中的挑战
|
||||||
const activeTokens = await this.pushTokenModel.findAll({
|
const ongoingChallenges = await this.getOngoingChallenges();
|
||||||
where: {
|
|
||||||
isActive: true,
|
if (ongoingChallenges.length === 0) {
|
||||||
},
|
this.logger.log('No ongoing challenges found for testing');
|
||||||
limit: 10, // 限制测试数量,避免发送过多推送
|
|
||||||
});
|
|
||||||
|
|
||||||
if (activeTokens.length === 0) {
|
|
||||||
this.logger.log('No active push tokens found for testing');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(`Found ${activeTokens.length} active tokens for testing`);
|
this.logger.log(`Found ${ongoingChallenges.length} ongoing challenges`);
|
||||||
|
|
||||||
// 准备测试推送内容
|
// 2. 获取参与这些挑战的活跃用户
|
||||||
const testTitle = this.configService.get<string>('PUSH_TEST_TITLE', '测试推送');
|
const activeParticipants = await this.getActiveParticipants(ongoingChallenges);
|
||||||
const testBody = this.configService.get<string>('PUSH_TEST_BODY', '这是一条测试推送消息,用于验证推送功能是否正常工作。');
|
|
||||||
|
if (activeParticipants.length === 0) {
|
||||||
|
this.logger.log('No active participants found in ongoing challenges');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 发送测试推送
|
this.logger.log(`Found ${activeParticipants.length} active participants`);
|
||||||
|
|
||||||
|
// 3. 获取这些用户的活跃推送令牌
|
||||||
|
const userTokensMap = await this.getUserTokensMap(activeParticipants);
|
||||||
|
|
||||||
|
if (userTokensMap.size === 0) {
|
||||||
|
this.logger.log('No active push tokens found for challenge participants');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(`Found push tokens for ${userTokensMap.size} users`);
|
||||||
|
|
||||||
|
// 4. 发送挑战相关推送
|
||||||
|
await this.sendChallengeReminders(ongoingChallenges, userTokensMap, activeParticipants);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Error during challenge-based push test: ${error.message}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取正在进行中的挑战
|
||||||
|
*/
|
||||||
|
private async getOngoingChallenges(): Promise<Challenge[]> {
|
||||||
|
// 获取所有挑战,然后在内存中过滤
|
||||||
|
// 这样可以避免数据库查询时的类型转换问题
|
||||||
|
const challenges = await this.challengesService['challengeModel'].findAll({
|
||||||
|
order: [['startAt', 'ASC']],
|
||||||
|
});
|
||||||
|
|
||||||
|
// 直接实现状态计算逻辑,过滤出真正进行中的挑战
|
||||||
|
return challenges.filter(challenge => {
|
||||||
|
const start = dayjs(challenge.startAt);
|
||||||
|
const end = dayjs(challenge.endAt);
|
||||||
|
const current = dayjs();
|
||||||
|
|
||||||
|
// 检查当前时间是否在开始和结束时间之间
|
||||||
|
return current.isAfter(start, 'minute') && current.isBefore(end, 'minute');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取参与挑战的活跃用户
|
||||||
|
*/
|
||||||
|
private async getActiveParticipants(challenges: Challenge[]): Promise<ChallengeParticipant[]> {
|
||||||
|
const challengeIds = challenges.map(challenge => challenge.id);
|
||||||
|
|
||||||
|
const participants = await this.participantModel.findAll({
|
||||||
|
where: {
|
||||||
|
challengeId: {
|
||||||
|
[Op.in]: challengeIds,
|
||||||
|
},
|
||||||
|
status: ChallengeParticipantStatus.ACTIVE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return participants;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户的推送令牌映射
|
||||||
|
*/
|
||||||
|
private async getUserTokensMap(participants: ChallengeParticipant[]): Promise<Map<string, UserPushToken[]>> {
|
||||||
|
const userIds = [...new Set(participants.map(p => p.userId))];
|
||||||
|
|
||||||
|
const tokens = await this.pushTokenModel.findAll({
|
||||||
|
where: {
|
||||||
|
userId: {
|
||||||
|
[Op.in]: userIds,
|
||||||
|
},
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const userTokensMap = new Map<string, UserPushToken[]>();
|
||||||
|
|
||||||
|
tokens.forEach(token => {
|
||||||
|
if (token.userId) {
|
||||||
|
if (!userTokensMap.has(token.userId)) {
|
||||||
|
userTokensMap.set(token.userId, []);
|
||||||
|
}
|
||||||
|
userTokensMap.get(token.userId)!.push(token);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return userTokensMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送挑战提醒推送
|
||||||
|
*/
|
||||||
|
private async sendChallengeReminders(
|
||||||
|
challenges: Challenge[],
|
||||||
|
userTokensMap: Map<string, UserPushToken[]>,
|
||||||
|
participants: ChallengeParticipant[]
|
||||||
|
): Promise<void> {
|
||||||
|
// 创建挑战ID到挑战对象的映射
|
||||||
|
const challengeMap = new Map<string, Challenge>();
|
||||||
|
challenges.forEach(challenge => {
|
||||||
|
challengeMap.set(challenge.id, challenge);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建用户ID到参与者的映射
|
||||||
|
const userParticipantMap = new Map<string, ChallengeParticipant>();
|
||||||
|
participants.forEach(participant => {
|
||||||
|
userParticipantMap.set(participant.userId, participant);
|
||||||
|
});
|
||||||
|
|
||||||
|
let totalSent = 0;
|
||||||
|
let totalFailed = 0;
|
||||||
|
const maxUsers = this.configService.get<number>('PUSH_TEST_MAX_USERS', 50); // 限制推送用户数量
|
||||||
|
let userCount = 0;
|
||||||
|
|
||||||
|
// 为每个用户发送推送
|
||||||
|
for (const [userId, tokens] of userTokensMap) {
|
||||||
|
if (userCount >= maxUsers) {
|
||||||
|
this.logger.log(`Reached maximum user limit (${maxUsers}), stopping push test`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const participant = userParticipantMap.get(userId);
|
||||||
|
const challenge = participant ? challengeMap.get(participant.challengeId) : null;
|
||||||
|
|
||||||
|
if (!challenge || !participant) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 个性化推送内容
|
||||||
|
const { title, body } = this.generateChallengePushContent(challenge, participant);
|
||||||
|
|
||||||
|
// 发送推送
|
||||||
const result = await this.pushNotificationsService.sendBatchNotificationToDevices({
|
const result = await this.pushNotificationsService.sendBatchNotificationToDevices({
|
||||||
deviceTokens: activeTokens.map(token => token.deviceToken),
|
deviceTokens: tokens.map(token => token.deviceToken),
|
||||||
title: testTitle,
|
title,
|
||||||
body: testBody,
|
body,
|
||||||
pushType: PushType.alert,
|
pushType: PushType.alert,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.code === 0) {
|
if (result.code === 0) {
|
||||||
this.logger.log(`Push test completed successfully. Sent: ${result.data.successCount}, Failed: ${result.data.failedCount}`);
|
totalSent += result.data.successCount;
|
||||||
|
totalFailed += result.data.failedCount;
|
||||||
|
|
||||||
|
this.logger.log(`Challenge reminder sent to user ${userId} for challenge "${challenge.title}". Success: ${result.data.successCount}, Failed: ${result.data.failedCount}`);
|
||||||
} else {
|
} else {
|
||||||
this.logger.warn(`Push test completed with issues. Sent: ${result.data.successCount}, Failed: ${result.data.failedCount}`);
|
totalFailed += tokens.length;
|
||||||
|
this.logger.warn(`Failed to send challenge reminder to user ${userId}: ${result.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 记录详细结果
|
userCount++;
|
||||||
if (result.data.results && result.data.results.length > 0) {
|
|
||||||
result.data.results.forEach((resultItem, index) => {
|
|
||||||
if (resultItem.success) {
|
|
||||||
this.logger.log(`Push test success for user ${resultItem.userId}, device ${resultItem.deviceToken.substring(0, 10)}...`);
|
|
||||||
} else {
|
|
||||||
this.logger.warn(`Push test failed for user ${resultItem.userId}, device ${resultItem.deviceToken.substring(0, 10)}...: ${resultItem.error}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Error during push test: ${error.message}`, error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.log(`Challenge-based push test completed. Total sent: ${totalSent}, Total failed: ${totalFailed}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成挑战推送内容
|
||||||
|
*/
|
||||||
|
private generateChallengePushContent(
|
||||||
|
challenge: Challenge,
|
||||||
|
participant: ChallengeParticipant
|
||||||
|
): { title: string; body: string } {
|
||||||
|
const progress = participant.progressValue;
|
||||||
|
const target = participant.targetValue;
|
||||||
|
const remaining = Math.max(target - progress, 0);
|
||||||
|
|
||||||
|
// 根据挑战类型生成不同的推送内容
|
||||||
|
let title = '挑战提醒';
|
||||||
|
let body = `您正在参与的"${challenge.title}"挑战进行中!`;
|
||||||
|
|
||||||
|
switch (challenge.type) {
|
||||||
|
case 'water':
|
||||||
|
title = '饮水挑战提醒';
|
||||||
|
body = `今日饮水挑战进行中!已完成 ${progress}/${target} 天,继续加油!`;
|
||||||
|
break;
|
||||||
|
case 'exercise':
|
||||||
|
title = '运动挑战提醒';
|
||||||
|
body = `今日运动挑战进行中!已完成 ${progress}/${target} 天,坚持就是胜利!`;
|
||||||
|
break;
|
||||||
|
case 'diet':
|
||||||
|
title = '饮食挑战提醒';
|
||||||
|
body = `健康饮食挑战进行中!已完成 ${progress}/${target} 天,保持良好饮食习惯!`;
|
||||||
|
break;
|
||||||
|
case 'mood':
|
||||||
|
title = '心情记录提醒';
|
||||||
|
body = `心情记录挑战进行中!已完成 ${progress}/${target} 天,记录今日心情吧!`;
|
||||||
|
break;
|
||||||
|
case 'sleep':
|
||||||
|
title = '睡眠挑战提醒';
|
||||||
|
body = `优质睡眠挑战进行中!已完成 ${progress}/${target} 天,保持规律作息!`;
|
||||||
|
break;
|
||||||
|
case 'weight':
|
||||||
|
title = '体重管理提醒';
|
||||||
|
body = `体重管理挑战进行中!已完成 ${progress}/${target} 天,坚持健康生活方式!`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
body = `您正在参与的"${challenge.title}"挑战进行中!已完成 ${progress}/${target} 天,还需 ${remaining} 天完成挑战,加油!`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { title, body };
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user