From ac231a7742fc021d3772d1e30bb9fe6130bd9014 Mon Sep 17 00:00:00 2001 From: richarjiang Date: Thu, 27 Nov 2025 11:11:26 +0800 Subject: [PATCH] =?UTF-8?q?feat(challenges):=20=E6=94=AF=E6=8C=81=E8=87=AA?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=E6=8C=91=E6=88=98=E7=B1=BB=E5=9E=8B=E5=B9=B6?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=BF=85=E5=A1=AB=E5=AD=97=E6=AE=B5=E9=AA=8C?= =?UTF-8?q?=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 CUSTOM 挑战类型枚举值 - requirementLabel 字段改为可选,允许为空并添加默认值处理 - minimumCheckInDays 最大值从 365 提升至 1000,支持更长周期挑战 - 推送通知模板支持自定义挑战的动态文案生成 - 新增 getCustomEncouragementTemplate 和 getCustomInvitationTemplate 函数 --- src/challenges/challenges.service.ts | 6 +-- .../dto/create-custom-challenge.dto.ts | 6 +-- .../dto/update-custom-challenge.dto.ts | 2 +- src/challenges/models/challenge.model.ts | 9 ++-- .../challenge-reminder.service.ts | 4 +- .../templates/challenge-templates.ts | 46 ++++++++++++++++++- 6 files changed, 58 insertions(+), 15 deletions(-) diff --git a/src/challenges/challenges.service.ts b/src/challenges/challenges.service.ts index 52ccbde..3b84b6d 100644 --- a/src/challenges/challenges.service.ts +++ b/src/challenges/challenges.service.ts @@ -164,7 +164,7 @@ export class ChallengesService { image: challenge.image, periodLabel: challenge.periodLabel, durationLabel: challenge.durationLabel, - requirementLabel: challenge.requirementLabel, + requirementLabel: challenge.requirementLabel || '', status, unit: challenge.progressUnit, startAt: new Date(challenge.startAt).getTime(), @@ -278,7 +278,7 @@ export class ChallengesService { image: challenge.image, periodLabel: challenge.periodLabel, durationLabel: challenge.durationLabel, - requirementLabel: challenge.requirementLabel, + requirementLabel: challenge.requirementLabel || '', summary: challenge.summary, rankingDescription: challenge.rankingDescription, highlightTitle: challenge.highlightTitle, @@ -1205,7 +1205,7 @@ export class ChallengesService { endAt: new Date(challenge.endAt).getTime(), periodLabel: challenge.periodLabel, durationLabel: challenge.durationLabel, - requirementLabel: challenge.requirementLabel, + requirementLabel: challenge.requirementLabel || '', summary: challenge.summary, targetValue: challenge.targetValue, progressUnit: challenge.progressUnit, diff --git a/src/challenges/dto/create-custom-challenge.dto.ts b/src/challenges/dto/create-custom-challenge.dto.ts index bfeb76f..902d717 100644 --- a/src/challenges/dto/create-custom-challenge.dto.ts +++ b/src/challenges/dto/create-custom-challenge.dto.ts @@ -34,10 +34,10 @@ export class CreateCustomChallengeDto { @Max(1000) targetValue: number; - @ApiProperty({ description: '最少打卡天数', example: 21, minimum: 1, maximum: 365 }) + @ApiProperty({ description: '最少打卡天数', example: 21, minimum: 1, maximum: 1000 }) @IsNumber() @Min(1) - @Max(365) + @Max(1000) minimumCheckInDays: number; @ApiProperty({ description: '持续时间标签', example: '持续21天' }) @@ -48,7 +48,7 @@ export class CreateCustomChallengeDto { @ApiProperty({ description: '挑战要求标签', example: '每日喝水8杯' }) @IsString() - @IsNotEmpty() + @IsOptional() @MaxLength(255) requirementLabel: string; diff --git a/src/challenges/dto/update-custom-challenge.dto.ts b/src/challenges/dto/update-custom-challenge.dto.ts index 764f119..ee339d9 100644 --- a/src/challenges/dto/update-custom-challenge.dto.ts +++ b/src/challenges/dto/update-custom-challenge.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsString, IsOptional, IsBoolean, MaxLength, IsNumber, Min, Max } from 'class-validator'; +import { IsString, IsOptional, IsBoolean, MaxLength, IsNumber, Min, Max, IsEnum } from 'class-validator'; export class UpdateCustomChallengeDto { @ApiProperty({ description: '挑战标题', required: false }) diff --git a/src/challenges/models/challenge.model.ts b/src/challenges/models/challenge.model.ts index 2f6f052..44faa24 100644 --- a/src/challenges/models/challenge.model.ts +++ b/src/challenges/models/challenge.model.ts @@ -15,6 +15,7 @@ export enum ChallengeType { MOOD = 'mood', SLEEP = 'sleep', WEIGHT = 'weight', + CUSTOM = 'custom', } export enum ChallengeSource { @@ -84,10 +85,10 @@ export class Challenge extends Model { @Column({ type: DataType.STRING(255), - allowNull: false, + allowNull: true, comment: '挑战要求标签,例如「每日练习 1 次」', }) - declare requirementLabel: string; + declare requirementLabel?: string; @Column({ type: DataType.TEXT, @@ -112,7 +113,7 @@ export class Challenge extends Model { declare progressUnit: string; @Column({ - type: DataType.INTEGER, + type: DataType.INTEGER.UNSIGNED, allowNull: false, defaultValue: 0, comment: '最低打卡天数,用于判断挑战成功', @@ -148,7 +149,7 @@ export class Challenge extends Model { declare ctaLabel: string; @Column({ - type: DataType.ENUM('water', 'exercise', 'diet', 'mood', 'sleep', 'weight'), + type: DataType.ENUM('water', 'exercise', 'diet', 'mood', 'sleep', 'weight', 'custom'), allowNull: false, defaultValue: ChallengeType.WATER, comment: '挑战类型', diff --git a/src/push-notifications/challenge-reminder.service.ts b/src/push-notifications/challenge-reminder.service.ts index 09ef42b..5306af8 100644 --- a/src/push-notifications/challenge-reminder.service.ts +++ b/src/push-notifications/challenge-reminder.service.ts @@ -216,7 +216,7 @@ export class ChallengeReminderService { } // 获取鼓励文案 - const template = getEncouragementTemplate(participant.challenge.type); + const template = getEncouragementTemplate(participant.challenge.type, participant.challenge.title); // 发送推送 const result = await this.pushNotificationsService.sendBatchNotificationToDevices({ @@ -281,7 +281,7 @@ export class ChallengeReminderService { // 获取邀请文案 const template = reminderType === ReminderType.GENERAL_INVITATION ? getGeneralInvitationTemplate() - : getInvitationTemplate(randomChallenge.type); + : getInvitationTemplate(randomChallenge.type, randomChallenge.title); // 发送推送 const result = await this.pushNotificationsService.sendBatchNotificationToDevices({ diff --git a/src/push-notifications/templates/challenge-templates.ts b/src/push-notifications/templates/challenge-templates.ts index 949039e..7ca4c66 100644 --- a/src/push-notifications/templates/challenge-templates.ts +++ b/src/push-notifications/templates/challenge-templates.ts @@ -46,6 +46,9 @@ export const ENCOURAGEMENT_TEMPLATES = { { title: '团队体重挑战', body: '今天体重挑战群里又有很多人完成了目标!一起健康生活!' }, { title: '健康生活记录', body: '挑战者们都在坚持健康饮食+适量运动,你也是其中一员!' }, ], + [ChallengeType.CUSTOM]: [ + // 自定义挑战使用动态模板,不在此处定义 + ], }; /** @@ -82,6 +85,9 @@ export const INVITATION_TEMPLATES = { { title: '健康体重挑战', body: '21天体重管理,见证自己的变化!' }, { title: '体重目标挑战', body: '科学管理体重,享受健康生活!' }, ], + [ChallengeType.CUSTOM]: [ + // 自定义挑战使用动态模板,不在此处定义 + ], }; /** @@ -106,7 +112,12 @@ export function getRandomTemplate(templates: T[]): T { /** * 根据挑战类型获取鼓励文案 */ -export function getEncouragementTemplate(challengeType: ChallengeType) { +export function getEncouragementTemplate(challengeType: ChallengeType, challengeTitle?: string) { + // 自定义挑战使用动态模板 + if (challengeType === ChallengeType.CUSTOM && challengeTitle) { + return getCustomEncouragementTemplate(challengeTitle); + } + const templates = ENCOURAGEMENT_TEMPLATES[challengeType] || ENCOURAGEMENT_TEMPLATES[ChallengeType.EXERCISE]; return getRandomTemplate(templates); } @@ -114,7 +125,12 @@ export function getEncouragementTemplate(challengeType: ChallengeType) { /** * 根据挑战类型获取邀请文案 */ -export function getInvitationTemplate(challengeType: ChallengeType) { +export function getInvitationTemplate(challengeType: ChallengeType, challengeTitle?: string) { + // 自定义挑战使用动态模板 + if (challengeType === ChallengeType.CUSTOM && challengeTitle) { + return getCustomInvitationTemplate(challengeTitle); + } + const templates = INVITATION_TEMPLATES[challengeType] || INVITATION_TEMPLATES[ChallengeType.EXERCISE]; return getRandomTemplate(templates); } @@ -124,4 +140,30 @@ export function getInvitationTemplate(challengeType: ChallengeType) { */ export function getGeneralInvitationTemplate() { return getRandomTemplate(GENERAL_INVITATION_TEMPLATES); +} + +/** + * 生成自定义挑战的鼓励文案模板 + */ +export function getCustomEncouragementTemplate(challengeTitle: string) { + const templates = [ + { title: `${challengeTitle}进行中`, body: `今天已有多人参与「${challengeTitle}」!你也要加油哦!` }, + { title: `${challengeTitle}提醒`, body: `挑战伙伴们都在坚持「${challengeTitle}」,每一步努力都值得!` }, + { title: `${challengeTitle}打卡`, body: `看到很多挑战者都在参与「${challengeTitle}」,你也是其中一员!` }, + { title: `团队挑战`, body: `「${challengeTitle}」挑战群里又有很多人完成了目标!别掉队,一起加油!` }, + { title: `挑战分享`, body: `挑战者们都在分享「${challengeTitle}」的心得,你今天打卡了吗?` }, + ]; + return getRandomTemplate(templates); +} + +/** + * 生成自定义挑战的邀请文案模板 + */ +export function getCustomInvitationTemplate(challengeTitle: string) { + const templates = [ + { title: `${challengeTitle}邀请`, body: `加入「${challengeTitle}」,一起挑战自我,收获成长!` }, + { title: `挑战邀请`, body: `「${challengeTitle}」正在进行中,快来加入我们吧!` }, + { title: `一起挑战`, body: `开启「${challengeTitle}」之旅,遇见更好的自己!` }, + ]; + return getRandomTemplate(templates); } \ No newline at end of file