diff --git a/src/push-notifications/push-notifications.module.ts b/src/push-notifications/push-notifications.module.ts index 56e6945..7f60f32 100644 --- a/src/push-notifications/push-notifications.module.ts +++ b/src/push-notifications/push-notifications.module.ts @@ -14,16 +14,20 @@ import { PushTemplate } from './models/push-template.model'; import { ConfigModule } from '@nestjs/config'; import { DatabaseModule } from '../database/database.module'; import { UsersModule } from '../users/users.module'; +import { ChallengesModule } from '../challenges/challenges.module'; +import { ChallengeParticipant } from '../challenges/models/challenge-participant.model'; @Module({ imports: [ ConfigModule, DatabaseModule, UsersModule, + ChallengesModule, SequelizeModule.forFeature([ UserPushToken, PushMessage, PushTemplate, + ChallengeParticipant, ]), ], controllers: [ diff --git a/src/push-notifications/push-test.service.ts b/src/push-notifications/push-test.service.ts index 1ef491a..fac32b2 100644 --- a/src/push-notifications/push-test.service.ts +++ b/src/push-notifications/push-test.service.ts @@ -6,6 +6,10 @@ import { UserPushToken } from './models/user-push-token.model'; import { InjectModel } from '@nestjs/sequelize'; import { Op } from 'sequelize'; 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() export class PushTestService implements OnModuleInit { @@ -14,8 +18,11 @@ export class PushTestService implements OnModuleInit { constructor( @InjectModel(UserPushToken) private readonly pushTokenModel: typeof UserPushToken, + @InjectModel(ChallengeParticipant) + private readonly participantModel: typeof ChallengeParticipant, private readonly pushNotificationsService: PushNotificationsService, private readonly pushTokenService: PushTokenService, + private readonly challengesService: ChallengesService, private readonly configService: ConfigService, ) { } @@ -45,55 +52,226 @@ export class PushTestService implements OnModuleInit { * 执行推送测试 */ private async performPushTest(): Promise { - this.logger.log('Starting push test...'); + this.logger.log('Starting challenge-based push test...'); try { - // 获取所有活跃的推送令牌 - const activeTokens = await this.pushTokenModel.findAll({ - where: { - isActive: true, - }, - limit: 10, // 限制测试数量,避免发送过多推送 - }); - - if (activeTokens.length === 0) { - this.logger.log('No active push tokens found for testing'); + // 1. 获取正在进行中的挑战 + const ongoingChallenges = await this.getOngoingChallenges(); + + if (ongoingChallenges.length === 0) { + this.logger.log('No ongoing challenges found for testing'); return; } - this.logger.log(`Found ${activeTokens.length} active tokens for testing`); + this.logger.log(`Found ${ongoingChallenges.length} ongoing challenges`); - // 准备测试推送内容 - const testTitle = this.configService.get('PUSH_TEST_TITLE', '测试推送'); - const testBody = this.configService.get('PUSH_TEST_BODY', '这是一条测试推送消息,用于验证推送功能是否正常工作。'); + // 2. 获取参与这些挑战的活跃用户 + const activeParticipants = await this.getActiveParticipants(ongoingChallenges); + + 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 { + // 获取所有挑战,然后在内存中过滤 + // 这样可以避免数据库查询时的类型转换问题 + 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 { + 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> { + 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(); + + 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, + participants: ChallengeParticipant[] + ): Promise { + // 创建挑战ID到挑战对象的映射 + const challengeMap = new Map(); + challenges.forEach(challenge => { + challengeMap.set(challenge.id, challenge); + }); + + // 创建用户ID到参与者的映射 + const userParticipantMap = new Map(); + participants.forEach(participant => { + userParticipantMap.set(participant.userId, participant); + }); + + let totalSent = 0; + let totalFailed = 0; + const maxUsers = this.configService.get('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({ - deviceTokens: activeTokens.map(token => token.deviceToken), - title: testTitle, - body: testBody, + deviceTokens: tokens.map(token => token.deviceToken), + title, + body, pushType: PushType.alert, }); 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 { - 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}`); } - // 记录详细结果 - 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); + userCount++; } + + 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 }; } } \ No newline at end of file