feat(push-notifications): 新增挑战提醒定时推送功能
新增每日定时推送系统,根据用户参与状态发送不同类型的挑战提醒: - 已参与用户:每日发送鼓励推送 - 未参与用户:隔天发送挑战邀请 - 匿名用户:隔天发送通用邀请 包含推送历史记录表、定时任务调度、多类型文案模板和防重复发送机制
This commit is contained in:
@@ -4,6 +4,7 @@ import { AppService } from "./app.service";
|
||||
import { DatabaseModule } from "./database/database.module";
|
||||
import { UsersModule } from "./users/users.module";
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
import { LoggerModule } from './common/logger/logger.module';
|
||||
import { CheckinsModule } from './checkins/checkins.module';
|
||||
import { AiCoachModule } from './ai-coach/ai-coach.module';
|
||||
@@ -27,6 +28,7 @@ import { PushNotificationsModule } from './push-notifications/push-notifications
|
||||
isGlobal: true,
|
||||
envFilePath: '.env',
|
||||
}),
|
||||
ScheduleModule.forRoot(),
|
||||
LoggerModule,
|
||||
DatabaseModule,
|
||||
UsersModule,
|
||||
|
||||
384
src/push-notifications/challenge-reminder.service.ts
Normal file
384
src/push-notifications/challenge-reminder.service.ts
Normal file
@@ -0,0 +1,384 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Cron } from '@nestjs/schedule';
|
||||
import { InjectModel } from '@nestjs/sequelize';
|
||||
import { Op } from 'sequelize';
|
||||
import { PushType } from 'apns2';
|
||||
import { PushNotificationsService } from './push-notifications.service';
|
||||
import { PushTokenService } from './push-token.service';
|
||||
import { UserPushToken } from './models/user-push-token.model';
|
||||
import { PushReminderHistory, ReminderType } from './models/push-reminder-history.model';
|
||||
import { ChallengesService } from '../challenges/challenges.service';
|
||||
import { Challenge } from '../challenges/models/challenge.model';
|
||||
import { ChallengeParticipant, ChallengeParticipantStatus } from '../challenges/models/challenge-participant.model';
|
||||
import {
|
||||
getEncouragementTemplate,
|
||||
getInvitationTemplate,
|
||||
getGeneralInvitationTemplate
|
||||
} from './templates/challenge-templates';
|
||||
import * as dayjs from 'dayjs';
|
||||
|
||||
@Injectable()
|
||||
export class ChallengeReminderService {
|
||||
private readonly logger = new Logger(ChallengeReminderService.name);
|
||||
|
||||
constructor(
|
||||
@InjectModel(UserPushToken)
|
||||
private readonly pushTokenModel: typeof UserPushToken,
|
||||
@InjectModel(PushReminderHistory)
|
||||
private readonly reminderHistoryModel: typeof PushReminderHistory,
|
||||
@InjectModel(ChallengeParticipant)
|
||||
private readonly participantModel: typeof ChallengeParticipant,
|
||||
private readonly pushNotificationsService: PushNotificationsService,
|
||||
private readonly pushTokenService: PushTokenService,
|
||||
private readonly challengesService: ChallengesService,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 每晚8点执行的挑战提醒定时任务
|
||||
*/
|
||||
@Cron('0 20 * * *', {
|
||||
name: 'challengeReminder',
|
||||
timeZone: 'Asia/Shanghai',
|
||||
})
|
||||
async handleChallengeReminder(): Promise<void> {
|
||||
this.logger.log('Starting daily challenge reminder task...');
|
||||
|
||||
try {
|
||||
// 检查是否为主进程(NODE_APP_INSTANCE 为 0)
|
||||
const nodeAppInstance = this.configService.get<number>('NODE_APP_INSTANCE', 0);
|
||||
if (Number(nodeAppInstance) !== 0) {
|
||||
this.logger.log(`Not the primary process (instance: ${nodeAppInstance}). Skipping challenge reminder...`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log('Primary process detected. Running challenge reminder...');
|
||||
|
||||
// 1. 获取所有活跃的推送令牌
|
||||
const activeTokens = await this.getActivePushTokens();
|
||||
if (activeTokens.length === 0) {
|
||||
this.logger.log('No active push tokens found');
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(`Found ${activeTokens.length} active push tokens`);
|
||||
|
||||
// 2. 获取正在进行的挑战
|
||||
const ongoingChallenges = await this.getOngoingChallenges();
|
||||
if (ongoingChallenges.length === 0) {
|
||||
this.logger.log('No ongoing challenges found');
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(`Found ${ongoingChallenges.length} ongoing challenges`);
|
||||
|
||||
// 3. 获取参与挑战的活跃用户
|
||||
const activeParticipants = await this.getActiveParticipants(ongoingChallenges);
|
||||
const participantUserIds = new Set(activeParticipants.map(p => p.userId));
|
||||
|
||||
this.logger.log(`Found ${activeParticipants.length} active participants`);
|
||||
|
||||
// 4. 按用户类型分组处理
|
||||
await this.processTokensByUserType(activeTokens, participantUserIds, ongoingChallenges);
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error(`Error during challenge reminder task: ${error.message}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有活跃的推送令牌
|
||||
*/
|
||||
private async getActivePushTokens(): Promise<UserPushToken[]> {
|
||||
return await this.pushTokenModel.findAll({
|
||||
where: {
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取正在进行中的挑战
|
||||
*/
|
||||
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);
|
||||
|
||||
return await this.participantModel.findAll({
|
||||
where: {
|
||||
challengeId: {
|
||||
[Op.in]: challengeIds,
|
||||
},
|
||||
status: ChallengeParticipantStatus.ACTIVE,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 按用户类型分组处理推送令牌
|
||||
*/
|
||||
private async processTokensByUserType(
|
||||
tokens: UserPushToken[],
|
||||
participantUserIds: Set<string>,
|
||||
challenges: Challenge[]
|
||||
): Promise<void> {
|
||||
// 分组:已参与挑战的用户、未参与挑战但有userId的用户、没有userId的用户
|
||||
const participatingTokens: UserPushToken[] = [];
|
||||
const nonParticipatingTokens: UserPushToken[] = [];
|
||||
const anonymousTokens: UserPushToken[] = [];
|
||||
|
||||
for (const token of tokens) {
|
||||
if (!token.userId) {
|
||||
anonymousTokens.push(token);
|
||||
} else if (participantUserIds.has(token.userId)) {
|
||||
participatingTokens.push(token);
|
||||
} else {
|
||||
nonParticipatingTokens.push(token);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(`Token groups - Participating: ${participatingTokens.length}, Non-participating: ${nonParticipatingTokens.length}, Anonymous: ${anonymousTokens.length}`);
|
||||
|
||||
// 处理已参与挑战的用户 - 发送鼓励文案(每天)
|
||||
await this.sendEncouragementReminders(participatingTokens);
|
||||
|
||||
// 处理未参与挑战但有userId的用户 - 发送邀请文案(隔天)
|
||||
await this.sendInvitationReminders(nonParticipatingTokens, challenges, ReminderType.CHALLENGE_INVITATION);
|
||||
|
||||
// 处理没有userId的用户 - 发送通用邀请(隔天)
|
||||
await this.sendInvitationReminders(anonymousTokens, challenges, ReminderType.GENERAL_INVITATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送鼓励提醒给已参与挑战的用户
|
||||
*/
|
||||
private async sendEncouragementReminders(tokens: UserPushToken[]): Promise<void> {
|
||||
if (tokens.length === 0) return;
|
||||
|
||||
this.logger.log(`Sending encouragement reminders to ${tokens.length} participating users`);
|
||||
|
||||
let totalSent = 0;
|
||||
let totalFailed = 0;
|
||||
|
||||
for (const token of tokens) {
|
||||
try {
|
||||
// 检查今天是否已经发送过鼓励推送
|
||||
const today = dayjs().startOf('day').toDate();
|
||||
const recentReminder = await this.reminderHistoryModel.findOne({
|
||||
where: {
|
||||
userId: token.userId,
|
||||
deviceToken: token.deviceToken,
|
||||
reminderType: ReminderType.CHALLENGE_ENCOURAGEMENT,
|
||||
lastSentAt: {
|
||||
[Op.gte]: today,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (recentReminder) {
|
||||
this.logger.log(`User ${token.userId} already received encouragement reminder today`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取用户参与的挑战信息
|
||||
const participant = await this.participantModel.findOne({
|
||||
where: {
|
||||
userId: token.userId,
|
||||
status: ChallengeParticipantStatus.ACTIVE,
|
||||
},
|
||||
include: [{
|
||||
model: Challenge,
|
||||
as: 'challenge',
|
||||
}],
|
||||
});
|
||||
|
||||
if (!participant || !participant.challenge) {
|
||||
this.logger.warn(`No active challenge found for user ${token.userId}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取鼓励文案
|
||||
const template = getEncouragementTemplate(participant.challenge.type);
|
||||
|
||||
// 发送推送
|
||||
const result = await this.pushNotificationsService.sendBatchNotificationToDevices({
|
||||
deviceTokens: [token.deviceToken],
|
||||
title: template.title,
|
||||
body: template.body,
|
||||
pushType: PushType.alert,
|
||||
});
|
||||
|
||||
if (result.code === 0) {
|
||||
totalSent += result.data.successCount;
|
||||
totalFailed += result.data.failedCount;
|
||||
|
||||
// 记录推送历史
|
||||
await this.updateReminderHistory(
|
||||
token.userId,
|
||||
token.deviceToken,
|
||||
ReminderType.CHALLENGE_ENCOURAGEMENT
|
||||
);
|
||||
|
||||
this.logger.log(`Encouragement reminder sent to user ${token.userId}`);
|
||||
} else {
|
||||
totalFailed++;
|
||||
this.logger.warn(`Failed to send encouragement reminder to user ${token.userId}: ${result.message}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error(`Error sending encouragement reminder to user ${token.userId}: ${error.message}`, error);
|
||||
totalFailed++;
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(`Encouragement reminders completed. Sent: ${totalSent}, Failed: ${totalFailed}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送邀请提醒给未参与挑战的用户
|
||||
*/
|
||||
private async sendInvitationReminders(
|
||||
tokens: UserPushToken[],
|
||||
challenges: Challenge[],
|
||||
reminderType: ReminderType
|
||||
): Promise<void> {
|
||||
if (tokens.length === 0 || challenges.length === 0) return;
|
||||
|
||||
this.logger.log(`Sending invitation reminders to ${tokens.length} users`);
|
||||
|
||||
let totalSent = 0;
|
||||
let totalFailed = 0;
|
||||
|
||||
// 随机选择一个挑战类型用于邀请
|
||||
const randomChallenge = challenges[Math.floor(Math.random() * challenges.length)];
|
||||
|
||||
for (const token of tokens) {
|
||||
try {
|
||||
// 检查是否可以发送(隔天推送)
|
||||
const canSend = await this.canSendInvitation(token, reminderType);
|
||||
if (!canSend) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取邀请文案
|
||||
const template = reminderType === ReminderType.GENERAL_INVITATION
|
||||
? getGeneralInvitationTemplate()
|
||||
: getInvitationTemplate(randomChallenge.type);
|
||||
|
||||
// 发送推送
|
||||
const result = await this.pushNotificationsService.sendBatchNotificationToDevices({
|
||||
deviceTokens: [token.deviceToken],
|
||||
title: template.title,
|
||||
body: template.body,
|
||||
pushType: PushType.alert,
|
||||
});
|
||||
|
||||
if (result.code === 0) {
|
||||
totalSent += result.data.successCount;
|
||||
totalFailed += result.data.failedCount;
|
||||
|
||||
// 记录推送历史,设置下次可发送时间为2天后
|
||||
await this.updateReminderHistory(
|
||||
token.userId,
|
||||
token.deviceToken,
|
||||
reminderType,
|
||||
2 // 2天后可再次发送
|
||||
);
|
||||
|
||||
this.logger.log(`Invitation reminder sent to ${token.userId || 'anonymous user'}`);
|
||||
} else {
|
||||
totalFailed++;
|
||||
this.logger.warn(`Failed to send invitation reminder: ${result.message}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error(`Error sending invitation reminder: ${error.message}`, error);
|
||||
totalFailed++;
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(`Invitation reminders completed. Sent: ${totalSent}, Failed: ${totalFailed}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否可以发送邀请(隔天推送逻辑)
|
||||
*/
|
||||
private async canSendInvitation(token: UserPushToken, reminderType: ReminderType): Promise<boolean> {
|
||||
const reminder = await this.reminderHistoryModel.findOne({
|
||||
where: {
|
||||
userId: token.userId,
|
||||
deviceToken: token.deviceToken,
|
||||
reminderType,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!reminder) {
|
||||
return true; // 没有记录,可以发送
|
||||
}
|
||||
|
||||
// 检查是否到了下次可发送时间
|
||||
if (reminder.nextAvailableAt && dayjs().isBefore(reminder.nextAvailableAt)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新推送历史记录
|
||||
*/
|
||||
private async updateReminderHistory(
|
||||
userId: string | null,
|
||||
deviceToken: string,
|
||||
reminderType: ReminderType,
|
||||
daysUntilNext = 1
|
||||
): Promise<void> {
|
||||
const now = new Date();
|
||||
const nextAvailableAt = dayjs().add(daysUntilNext, 'day').toDate();
|
||||
|
||||
const [reminder, created] = await this.reminderHistoryModel.findOrCreate({
|
||||
where: {
|
||||
userId,
|
||||
deviceToken,
|
||||
reminderType,
|
||||
},
|
||||
defaults: {
|
||||
userId,
|
||||
deviceToken,
|
||||
reminderType,
|
||||
lastSentAt: now,
|
||||
sentCount: 1,
|
||||
nextAvailableAt,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!created) {
|
||||
await reminder.update({
|
||||
lastSentAt: now,
|
||||
sentCount: reminder.sentCount + 1,
|
||||
nextAvailableAt,
|
||||
isActive: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
83
src/push-notifications/models/push-reminder-history.model.ts
Normal file
83
src/push-notifications/models/push-reminder-history.model.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { Table, Column, Model, DataType, Index } from 'sequelize-typescript';
|
||||
|
||||
export enum ReminderType {
|
||||
CHALLENGE_ENCOURAGEMENT = 'challenge_encouragement', // 已参与挑战用户的鼓励推送
|
||||
CHALLENGE_INVITATION = 'challenge_invitation', // 未参与挑战用户的邀请推送
|
||||
GENERAL_INVITATION = 'general_invitation', // 无userId用户的通用邀请
|
||||
}
|
||||
|
||||
@Table({
|
||||
tableName: 't_push_reminder_history',
|
||||
underscored: true,
|
||||
})
|
||||
export class PushReminderHistory extends Model {
|
||||
@Column({
|
||||
type: DataType.UUID,
|
||||
defaultValue: DataType.UUIDV4,
|
||||
primaryKey: true,
|
||||
})
|
||||
declare id: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
allowNull: true,
|
||||
comment: '用户ID,可能为空',
|
||||
})
|
||||
declare userId: string | null;
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
allowNull: false,
|
||||
comment: '设备推送令牌',
|
||||
})
|
||||
declare deviceToken: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.ENUM(...Object.values(ReminderType)),
|
||||
allowNull: false,
|
||||
comment: '提醒类型',
|
||||
})
|
||||
declare reminderType: ReminderType;
|
||||
|
||||
@Column({
|
||||
type: DataType.DATE,
|
||||
allowNull: false,
|
||||
comment: '最后发送时间',
|
||||
})
|
||||
declare lastSentAt: Date;
|
||||
|
||||
@Column({
|
||||
type: DataType.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 0,
|
||||
comment: '发送次数',
|
||||
})
|
||||
declare sentCount: number;
|
||||
|
||||
@Column({
|
||||
type: DataType.DATE,
|
||||
allowNull: true,
|
||||
comment: '下次可发送时间',
|
||||
})
|
||||
declare nextAvailableAt: Date | null;
|
||||
|
||||
@Column({
|
||||
type: DataType.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: true,
|
||||
comment: '是否激活',
|
||||
})
|
||||
declare isActive: boolean;
|
||||
|
||||
@Column({
|
||||
type: DataType.DATE,
|
||||
defaultValue: DataType.NOW,
|
||||
})
|
||||
declare createdAt: Date;
|
||||
|
||||
@Column({
|
||||
type: DataType.DATE,
|
||||
defaultValue: DataType.NOW,
|
||||
})
|
||||
declare updatedAt: Date;
|
||||
}
|
||||
@@ -8,9 +8,11 @@ import { PushTokenService } from './push-token.service';
|
||||
import { PushTemplateService } from './push-template.service';
|
||||
import { PushMessageService } from './push-message.service';
|
||||
import { PushTestService } from './push-test.service';
|
||||
import { ChallengeReminderService } from './challenge-reminder.service';
|
||||
import { UserPushToken } from './models/user-push-token.model';
|
||||
import { PushMessage } from './models/push-message.model';
|
||||
import { PushTemplate } from './models/push-template.model';
|
||||
import { PushReminderHistory } from './models/push-reminder-history.model';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { DatabaseModule } from '../database/database.module';
|
||||
import { UsersModule } from '../users/users.module';
|
||||
@@ -27,6 +29,7 @@ import { ChallengeParticipant } from '../challenges/models/challenge-participant
|
||||
UserPushToken,
|
||||
PushMessage,
|
||||
PushTemplate,
|
||||
PushReminderHistory,
|
||||
ChallengeParticipant,
|
||||
]),
|
||||
],
|
||||
@@ -41,6 +44,7 @@ import { ChallengeParticipant } from '../challenges/models/challenge-participant
|
||||
PushTemplateService,
|
||||
PushMessageService,
|
||||
PushTestService,
|
||||
ChallengeReminderService,
|
||||
],
|
||||
exports: [
|
||||
ApnsProvider,
|
||||
@@ -49,6 +53,7 @@ import { ChallengeParticipant } from '../challenges/models/challenge-participant
|
||||
PushTemplateService,
|
||||
PushMessageService,
|
||||
PushTestService,
|
||||
ChallengeReminderService,
|
||||
],
|
||||
})
|
||||
export class PushNotificationsModule { }
|
||||
127
src/push-notifications/templates/challenge-templates.ts
Normal file
127
src/push-notifications/templates/challenge-templates.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { ChallengeType } from '../../challenges/models/challenge.model';
|
||||
|
||||
/**
|
||||
* 挑战鼓励文案模板 - 针对已参与挑战的用户
|
||||
*/
|
||||
export const ENCOURAGEMENT_TEMPLATES = {
|
||||
[ChallengeType.WATER]: [
|
||||
{ title: '饮水挑战进行中', body: '今天已有多人完成饮水目标!你也要记得多喝水哦!' },
|
||||
{ title: '健康饮水提醒', body: '加入饮水挑战的小伙伴们今天都很棒!你今天喝水达标了吗?' },
|
||||
{ title: '水分补充时间', body: '挑战者们都在坚持每日饮水目标,你也要跟上大家的步伐!' },
|
||||
{ title: '饮水习惯养成', body: '看到很多挑战伙伴都养成了良好饮水习惯,你也是其中一员!' },
|
||||
{ title: '团队饮水挑战', body: '今天挑战群里又有很多人完成了目标!别掉队,一起加油!' },
|
||||
],
|
||||
[ChallengeType.EXERCISE]: [
|
||||
{ title: '运动挑战进行时', body: '今天已有多人完成运动目标!你也来动一动吧!' },
|
||||
{ title: '活力运动提醒', body: '挑战伙伴们都在坚持运动,每一滴汗水都是进步的见证!' },
|
||||
{ title: '运动习惯养成', body: '看到很多挑战者都养成了运动习惯,你也是其中一员!' },
|
||||
{ title: '团队运动挑战', body: '今天运动挑战群里又有很多人完成了目标!别掉队,一起加油!' },
|
||||
{ title: '运动生活分享', body: '挑战者们都在分享运动心得,你的今天运动了吗?' },
|
||||
],
|
||||
[ChallengeType.DIET]: [
|
||||
{ title: '饮食挑战进行中', body: '今天已有多人记录了健康饮食!你也要记得合理搭配哦!' },
|
||||
{ title: '营养均衡提醒', body: '挑战伙伴们都在坚持健康饮食,为身体提供充足营养!' },
|
||||
{ title: '健康饮食分享', body: '看到很多挑战者都在分享健康餐,你今天吃了什么?' },
|
||||
{ title: '团队饮食挑战', body: '今天饮食挑战群里又有很多人完成了目标!一起健康饮食!' },
|
||||
{ title: '营养生活记录', body: '挑战者们都在记录饮食,关注健康,让每一餐都有意义!' },
|
||||
],
|
||||
[ChallengeType.MOOD]: [
|
||||
{ title: '心情记录挑战', body: '今天已有多人记录了心情!你也来分享今天的感受吧!' },
|
||||
{ title: '情绪管理提醒', body: '挑战伙伴们都在关注情绪变化,让心情更加愉悦平和!' },
|
||||
{ title: '心情日记分享', body: '看到很多挑战者都在记录心情故事,你今天心情如何?' },
|
||||
{ title: '团队心情挑战', body: '今天心情挑战群里又有很多人分享了感受!一起关注心理健康!' },
|
||||
{ title: '情绪健康关怀', body: '挑战者们都在关爱自己的情绪,从记录心情开始!' },
|
||||
],
|
||||
[ChallengeType.SLEEP]: [
|
||||
{ title: '睡眠挑战进行中', body: '今天已有多人保持了规律作息!你也要早睡早起哦!' },
|
||||
{ title: '优质睡眠提醒', body: '挑战伙伴们都在保持良好睡眠质量,享受每一个宁静的夜晚!' },
|
||||
{ title: '规律作息养成', body: '看到很多挑战者都养成了规律作息,你也是其中一员!' },
|
||||
{ title: '团队睡眠挑战', body: '今天睡眠挑战群里又有很多人完成了目标!一起健康作息!' },
|
||||
{ title: '休息时光分享', body: '挑战者们都在分享睡眠心得,你今天休息得好吗?' },
|
||||
],
|
||||
[ChallengeType.WEIGHT]: [
|
||||
{ title: '体重管理挑战', body: '今天已有多人记录了体重变化!你也要坚持健康生活方式!' },
|
||||
{ title: '健康体重提醒', body: '挑战伙伴们都在坚持体重管理,享受轻松自在的生活!' },
|
||||
{ title: '体重记录分享', body: '看到很多挑战者都在记录体重变化,见证自己的进步!' },
|
||||
{ title: '团队体重挑战', body: '今天体重挑战群里又有很多人完成了目标!一起健康生活!' },
|
||||
{ title: '健康生活记录', body: '挑战者们都在坚持健康饮食+适量运动,你也是其中一员!' },
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* 挑战邀请文案模板 - 针对未参与挑战但有userId的用户
|
||||
*/
|
||||
export const INVITATION_TEMPLATES = {
|
||||
[ChallengeType.WATER]: [
|
||||
{ title: '饮水挑战邀请', body: '加入21天饮水挑战,养成健康饮水习惯!' },
|
||||
{ title: '健康饮水挑战', body: '每天适量饮水,让身体更健康。立即加入挑战!' },
|
||||
{ title: '饮水习惯养成', body: '挑战自己,养成良好饮水习惯,收获健康!' },
|
||||
],
|
||||
[ChallengeType.EXERCISE]: [
|
||||
{ title: '运动挑战邀请', body: '加入运动挑战,让身体充满活力!' },
|
||||
{ title: '健身挑战', body: '21天运动挑战,塑造更好的自己!' },
|
||||
{ title: '运动习惯养成', body: '坚持运动,收获健康体魄,立即加入!' },
|
||||
],
|
||||
[ChallengeType.DIET]: [
|
||||
{ title: '饮食挑战邀请', body: '加入健康饮食挑战,培养良好饮食习惯!' },
|
||||
{ title: '营养挑战', body: '21天饮食挑战,让营养均衡成为习惯!' },
|
||||
{ title: '健康饮食计划', body: '科学饮食,健康生活,从挑战开始!' },
|
||||
],
|
||||
[ChallengeType.MOOD]: [
|
||||
{ title: '心情记录挑战', body: '加入心情记录挑战,关注情绪健康!' },
|
||||
{ title: '情绪管理挑战', body: '21天心情记录,让心灵更加宁静。' },
|
||||
{ title: '心理健康挑战', body: '记录心情,关爱自己,从现在开始!' },
|
||||
],
|
||||
[ChallengeType.SLEEP]: [
|
||||
{ title: '睡眠挑战邀请', body: '加入优质睡眠挑战,养成规律作息!' },
|
||||
{ title: '作息规律挑战', body: '21天睡眠挑战,让身体得到充分休息。' },
|
||||
{ title: '健康睡眠计划', body: '改善睡眠质量,享受每一个清晨!' },
|
||||
],
|
||||
[ChallengeType.WEIGHT]: [
|
||||
{ title: '体重管理挑战', body: '加入体重管理挑战,收获健康理想体重!' },
|
||||
{ title: '健康体重挑战', body: '21天体重管理,见证自己的变化!' },
|
||||
{ title: '体重目标挑战', body: '科学管理体重,享受健康生活!' },
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* 通用邀请文案模板 - 针对没有userId的用户
|
||||
*/
|
||||
export const GENERAL_INVITATION_TEMPLATES = [
|
||||
{ title: '健康挑战邀请', body: '加入我们的健康挑战,开启健康生活新篇章!' },
|
||||
{ title: '21天挑战', body: '21天养成健康习惯,你准备好了吗?' },
|
||||
{ title: '健康生活', body: '追求健康生活,从参加挑战开始!' },
|
||||
{ title: '挑战自我', body: '挑战自己,收获健康,让生活更美好!' },
|
||||
{ title: '健康之旅', body: '开启健康之旅,遇见更好的自己!' },
|
||||
];
|
||||
|
||||
/**
|
||||
* 随机选择一个模板
|
||||
*/
|
||||
export function getRandomTemplate<T>(templates: T[]): T {
|
||||
const randomIndex = Math.floor(Math.random() * templates.length);
|
||||
return templates[randomIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据挑战类型获取鼓励文案
|
||||
*/
|
||||
export function getEncouragementTemplate(challengeType: ChallengeType) {
|
||||
const templates = ENCOURAGEMENT_TEMPLATES[challengeType] || ENCOURAGEMENT_TEMPLATES[ChallengeType.EXERCISE];
|
||||
return getRandomTemplate(templates);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据挑战类型获取邀请文案
|
||||
*/
|
||||
export function getInvitationTemplate(challengeType: ChallengeType) {
|
||||
const templates = INVITATION_TEMPLATES[challengeType] || INVITATION_TEMPLATES[ChallengeType.EXERCISE];
|
||||
return getRandomTemplate(templates);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取通用邀请文案
|
||||
*/
|
||||
export function getGeneralInvitationTemplate() {
|
||||
return getRandomTemplate(GENERAL_INVITATION_TEMPLATES);
|
||||
}
|
||||
Reference in New Issue
Block a user