feat(challenges): 支持自定义挑战类型并优化必填字段验证
- 新增 CUSTOM 挑战类型枚举值 - requirementLabel 字段改为可选,允许为空并添加默认值处理 - minimumCheckInDays 最大值从 365 提升至 1000,支持更长周期挑战 - 推送通知模板支持自定义挑战的动态文案生成 - 新增 getCustomEncouragementTemplate 和 getCustomInvitationTemplate 函数
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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 })
|
||||||
|
|||||||
@@ -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: '挑战类型',
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -125,3 +141,29 @@ 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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user