feat(challenges): 添加用户自定义挑战功能及分享机制

实现完整的自定义挑战系统,支持用户创建、分享和管理个人挑战:

- 数据库扩展:添加 source、creator_id、share_code、is_public、max_participants、challenge_state 字段
- 分享机制:自动生成6位唯一分享码,支持公开和私密模式
- API接口:创建挑战、通过分享码加入、获取创建列表、更新归档挑战、重新生成分享码
- 权限控制:创建者专属编辑权限,频率限制防滥用(每日5个)
- 业务逻辑:人数限制检查、挑战状态流转、参与者统计
- 文档完善:使用文档和部署指南,包含API示例和回滚方案

兼容现有系统挑战,使用相同的打卡、排行榜和勋章系统
This commit is contained in:
richarjiang
2025-11-25 19:07:09 +08:00
parent 2d1d43922d
commit 93b4fcf553
8 changed files with 895 additions and 3 deletions

View File

@@ -0,0 +1,109 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsNotEmpty, IsNumber, Min, Max, IsEnum, IsOptional, IsBoolean, MaxLength, MinLength } from 'class-validator';
import { ChallengeType } from '../models/challenge.model';
export class CreateCustomChallengeDto {
@ApiProperty({ description: '挑战标题', example: '21天喝水挑战' })
@IsString()
@IsNotEmpty()
@MaxLength(100)
title: string;
@ApiProperty({ description: '挑战类型', enum: ChallengeType, example: ChallengeType.WATER })
@IsEnum(ChallengeType)
@IsNotEmpty()
type: ChallengeType;
@ApiProperty({ description: '挑战封面图 URL', required: false })
@IsString()
@IsOptional()
@MaxLength(512)
image?: string;
@ApiProperty({ description: '开始时间戳(毫秒)', example: 1704067200000 })
@IsNumber()
@Min(Date.now())
startAt: number;
@ApiProperty({ description: '结束时间戳(毫秒)', example: 1705881600000 })
@IsNumber()
@Min(Date.now() + 86400000) // 至少未来 1 天
endAt: number;
@ApiProperty({ description: '每日目标值如喝水8杯', example: 8, minimum: 1, maximum: 1000 })
@IsNumber()
@Min(1)
@Max(1000)
targetValue: number;
@ApiProperty({ description: '最少打卡天数', example: 21, minimum: 1, maximum: 365 })
@IsNumber()
@Min(1)
@Max(365)
minimumCheckInDays: number;
@ApiProperty({ description: '持续时间标签', example: '持续21天' })
@IsString()
@IsNotEmpty()
@MaxLength(128)
durationLabel: string;
@ApiProperty({ description: '挑战要求标签', example: '每日喝水8杯' })
@IsString()
@IsNotEmpty()
@MaxLength(255)
requirementLabel: string;
@ApiProperty({ description: '挑战概要说明', required: false })
@IsString()
@IsOptional()
summary?: string;
@ApiProperty({ description: '进度单位', example: '天', required: false })
@IsString()
@IsOptional()
@MaxLength(64)
progressUnit?: string;
@ApiProperty({ description: '周期标签', example: '21天挑战', required: false })
@IsString()
@IsOptional()
@MaxLength(128)
periodLabel?: string;
@ApiProperty({ description: '排行榜描述', example: '连续打卡榜', required: false })
@IsString()
@IsOptional()
@MaxLength(255)
rankingDescription?: string;
@ApiProperty({ description: '高亮标题', example: '坚持21天', required: false })
@IsString()
@IsOptional()
@MaxLength(255)
highlightTitle?: string;
@ApiProperty({ description: '高亮副标题', example: '养成好习惯', required: false })
@IsString()
@IsOptional()
@MaxLength(255)
highlightSubtitle?: string;
@ApiProperty({ description: 'CTA 按钮文字', example: '立即加入', required: false })
@IsString()
@IsOptional()
@MaxLength(128)
ctaLabel?: string;
@ApiProperty({ description: '是否公开(可通过分享码加入)', default: true })
@IsBoolean()
@IsOptional()
isPublic?: boolean;
@ApiProperty({ description: '最大参与人数限制null表示无限制', required: false, minimum: 2, maximum: 10000 })
@IsNumber()
@IsOptional()
@Min(2)
@Max(10000)
maxParticipants?: number;
}