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