feat(challenges): 支持自定义挑战类型并优化必填字段验证

- 新增 CUSTOM 挑战类型枚举值
- requirementLabel 字段改为可选,允许为空并添加默认值处理
- minimumCheckInDays 最大值从 365 提升至 1000,支持更长周期挑战
- 推送通知模板支持自定义挑战的动态文案生成
- 新增 getCustomEncouragementTemplate 和 getCustomInvitationTemplate 函数
This commit is contained in:
richarjiang
2025-11-27 11:11:26 +08:00
parent 7a05097226
commit ac231a7742
6 changed files with 58 additions and 15 deletions

View File

@@ -164,7 +164,7 @@ export class ChallengesService {
image: challenge.image, image: challenge.image,
periodLabel: challenge.periodLabel, periodLabel: challenge.periodLabel,
durationLabel: challenge.durationLabel, durationLabel: challenge.durationLabel,
requirementLabel: challenge.requirementLabel, requirementLabel: challenge.requirementLabel || '',
status, status,
unit: challenge.progressUnit, unit: challenge.progressUnit,
startAt: new Date(challenge.startAt).getTime(), startAt: new Date(challenge.startAt).getTime(),
@@ -278,7 +278,7 @@ export class ChallengesService {
image: challenge.image, image: challenge.image,
periodLabel: challenge.periodLabel, periodLabel: challenge.periodLabel,
durationLabel: challenge.durationLabel, durationLabel: challenge.durationLabel,
requirementLabel: challenge.requirementLabel, requirementLabel: challenge.requirementLabel || '',
summary: challenge.summary, summary: challenge.summary,
rankingDescription: challenge.rankingDescription, rankingDescription: challenge.rankingDescription,
highlightTitle: challenge.highlightTitle, highlightTitle: challenge.highlightTitle,
@@ -1205,7 +1205,7 @@ export class ChallengesService {
endAt: new Date(challenge.endAt).getTime(), endAt: new Date(challenge.endAt).getTime(),
periodLabel: challenge.periodLabel, periodLabel: challenge.periodLabel,
durationLabel: challenge.durationLabel, durationLabel: challenge.durationLabel,
requirementLabel: challenge.requirementLabel, requirementLabel: challenge.requirementLabel || '',
summary: challenge.summary, summary: challenge.summary,
targetValue: challenge.targetValue, targetValue: challenge.targetValue,
progressUnit: challenge.progressUnit, progressUnit: challenge.progressUnit,

View File

@@ -34,10 +34,10 @@ export class CreateCustomChallengeDto {
@Max(1000) @Max(1000)
targetValue: number; targetValue: number;
@ApiProperty({ description: '最少打卡天数', example: 21, minimum: 1, maximum: 365 }) @ApiProperty({ description: '最少打卡天数', example: 21, minimum: 1, maximum: 1000 })
@IsNumber() @IsNumber()
@Min(1) @Min(1)
@Max(365) @Max(1000)
minimumCheckInDays: number; minimumCheckInDays: number;
@ApiProperty({ description: '持续时间标签', example: '持续21天' }) @ApiProperty({ description: '持续时间标签', example: '持续21天' })
@@ -48,7 +48,7 @@ export class CreateCustomChallengeDto {
@ApiProperty({ description: '挑战要求标签', example: '每日喝水8杯' }) @ApiProperty({ description: '挑战要求标签', example: '每日喝水8杯' })
@IsString() @IsString()
@IsNotEmpty() @IsOptional()
@MaxLength(255) @MaxLength(255)
requirementLabel: string; requirementLabel: string;

View File

@@ -1,5 +1,5 @@
import { ApiProperty } from '@nestjs/swagger'; 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 { export class UpdateCustomChallengeDto {
@ApiProperty({ description: '挑战标题', required: false }) @ApiProperty({ description: '挑战标题', required: false })

View File

@@ -15,6 +15,7 @@ export enum ChallengeType {
MOOD = 'mood', MOOD = 'mood',
SLEEP = 'sleep', SLEEP = 'sleep',
WEIGHT = 'weight', WEIGHT = 'weight',
CUSTOM = 'custom',
} }
export enum ChallengeSource { export enum ChallengeSource {
@@ -84,10 +85,10 @@ export class Challenge extends Model {
@Column({ @Column({
type: DataType.STRING(255), type: DataType.STRING(255),
allowNull: false, allowNull: true,
comment: '挑战要求标签,例如「每日练习 1 次」', comment: '挑战要求标签,例如「每日练习 1 次」',
}) })
declare requirementLabel: string; declare requirementLabel?: string;
@Column({ @Column({
type: DataType.TEXT, type: DataType.TEXT,
@@ -112,7 +113,7 @@ export class Challenge extends Model {
declare progressUnit: string; declare progressUnit: string;
@Column({ @Column({
type: DataType.INTEGER, type: DataType.INTEGER.UNSIGNED,
allowNull: false, allowNull: false,
defaultValue: 0, defaultValue: 0,
comment: '最低打卡天数,用于判断挑战成功', comment: '最低打卡天数,用于判断挑战成功',
@@ -148,7 +149,7 @@ export class Challenge extends Model {
declare ctaLabel: string; declare ctaLabel: string;
@Column({ @Column({
type: DataType.ENUM('water', 'exercise', 'diet', 'mood', 'sleep', 'weight'), type: DataType.ENUM('water', 'exercise', 'diet', 'mood', 'sleep', 'weight', 'custom'),
allowNull: false, allowNull: false,
defaultValue: ChallengeType.WATER, defaultValue: ChallengeType.WATER,
comment: '挑战类型', comment: '挑战类型',

View File

@@ -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({ const result = await this.pushNotificationsService.sendBatchNotificationToDevices({
@@ -281,7 +281,7 @@ export class ChallengeReminderService {
// 获取邀请文案 // 获取邀请文案
const template = reminderType === ReminderType.GENERAL_INVITATION const template = reminderType === ReminderType.GENERAL_INVITATION
? getGeneralInvitationTemplate() ? getGeneralInvitationTemplate()
: getInvitationTemplate(randomChallenge.type); : getInvitationTemplate(randomChallenge.type, randomChallenge.title);
// 发送推送 // 发送推送
const result = await this.pushNotificationsService.sendBatchNotificationToDevices({ const result = await this.pushNotificationsService.sendBatchNotificationToDevices({

View File

@@ -46,6 +46,9 @@ export const ENCOURAGEMENT_TEMPLATES = {
{ title: '团队体重挑战', body: '今天体重挑战群里又有很多人完成了目标!一起健康生活!' }, { title: '团队体重挑战', body: '今天体重挑战群里又有很多人完成了目标!一起健康生活!' },
{ title: '健康生活记录', body: '挑战者们都在坚持健康饮食+适量运动,你也是其中一员!' }, { title: '健康生活记录', body: '挑战者们都在坚持健康饮食+适量运动,你也是其中一员!' },
], ],
[ChallengeType.CUSTOM]: [
// 自定义挑战使用动态模板,不在此处定义
],
}; };
/** /**
@@ -82,6 +85,9 @@ export const INVITATION_TEMPLATES = {
{ title: '健康体重挑战', body: '21天体重管理见证自己的变化' }, { title: '健康体重挑战', body: '21天体重管理见证自己的变化' },
{ title: '体重目标挑战', body: '科学管理体重,享受健康生活!' }, { title: '体重目标挑战', body: '科学管理体重,享受健康生活!' },
], ],
[ChallengeType.CUSTOM]: [
// 自定义挑战使用动态模板,不在此处定义
],
}; };
/** /**
@@ -106,7 +112,12 @@ export function getRandomTemplate<T>(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]; const templates = ENCOURAGEMENT_TEMPLATES[challengeType] || ENCOURAGEMENT_TEMPLATES[ChallengeType.EXERCISE];
return getRandomTemplate(templates); 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]; const templates = INVITATION_TEMPLATES[challengeType] || INVITATION_TEMPLATES[ChallengeType.EXERCISE];
return getRandomTemplate(templates); return getRandomTemplate(templates);
} }
@@ -124,4 +140,30 @@ export function getInvitationTemplate(challengeType: ChallengeType) {
*/ */
export function getGeneralInvitationTemplate() { export function getGeneralInvitationTemplate() {
return getRandomTemplate(GENERAL_INVITATION_TEMPLATES); 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);
} }