diff --git a/src/challenges/challenges.controller.ts b/src/challenges/challenges.controller.ts index 4a8d831..107d6dc 100644 --- a/src/challenges/challenges.controller.ts +++ b/src/challenges/challenges.controller.ts @@ -38,6 +38,7 @@ export class ChallengesController { @Get(':id') @Public() + @UseGuards(JwtAuthGuard) async getChallengeDetail( @Param('id') id: string, @CurrentUser() user: AccessTokenPayload, diff --git a/src/challenges/challenges.service.ts b/src/challenges/challenges.service.ts index e5978b4..077c79f 100644 --- a/src/challenges/challenges.service.ts +++ b/src/challenges/challenges.service.ts @@ -48,7 +48,7 @@ export class ChallengesService { const joinedChallengeIds = await this.getJoinedCustomChallengeIds(userId); whereConditions.push( - { creatorId: userId, source: ChallengeSource.CUSTOM }, // 我创建的 + { creatorId: userId, source: ChallengeSource.CUSTOM, challengeState: { [Op.ne]: ChallengeState.ARCHIVED } }, // 我创建的 { id: { [Op.in]: joinedChallengeIds }, source: ChallengeSource.CUSTOM, @@ -60,6 +60,7 @@ export class ChallengesService { const challenges = await this.challengeModel.findAll({ where: { [Op.or]: whereConditions, + challengeState: { [Op.ne]: ChallengeState.ARCHIVED }, // 过滤掉已归档的挑战 }, order: [['startAt', 'ASC']], }); @@ -180,6 +181,7 @@ export class ChallengesService { badge: challenge.type === ChallengeType.SLEEP ? sleepBadge : undefined, source: challenge.source, shareCode: challenge.shareCode, + isCreator: userId ? challenge.creatorId === userId : false, }; }); } @@ -191,6 +193,10 @@ export class ChallengesService { throw new NotFoundException('挑战不存在'); } + if (challenge.challengeState === ChallengeState.ARCHIVED) { + throw new NotFoundException('挑战不存在'); + } + this.winstonLogger.info('start get detail', { context: 'getChallengeDetail', userId, @@ -289,6 +295,7 @@ export class ChallengesService { creatorId: challenge.creatorId, shareCode: challenge.shareCode, source: challenge.source, + isCreator: userId ? challenge.creatorId === userId : false, }; } @@ -302,6 +309,10 @@ export class ChallengesService { throw new NotFoundException('挑战不存在'); } + if (challenge.challengeState === ChallengeState.ARCHIVED) { + throw new NotFoundException('挑战不存在'); + } + const { userId } = params; const page = params.page && params.page > 0 ? params.page : 1; const requestedPageSize = params.pageSize && params.pageSize > 0 ? params.pageSize : 20; @@ -342,6 +353,10 @@ export class ChallengesService { throw new NotFoundException('挑战不存在'); } + if (challenge.challengeState === ChallengeState.ARCHIVED) { + throw new NotFoundException('挑战不存在'); + } + const status = this.computeStatus(challenge.startAt, challenge.endAt); if (status === ChallengeStatus.EXPIRED) { throw new BadRequestException('挑战已过期,无法加入'); @@ -392,6 +407,15 @@ export class ChallengesService { } async leaveChallenge(userId: string, challengeId: string): Promise { + // 先检查挑战是否存在且未归档 + const challenge = await this.challengeModel.findByPk(challengeId); + if (!challenge) { + throw new NotFoundException('挑战不存在'); + } + if (challenge.challengeState === ChallengeState.ARCHIVED) { + throw new NotFoundException('挑战不存在'); + } + const participant = await this.participantModel.findOne({ where: { challengeId, @@ -421,6 +445,10 @@ export class ChallengesService { throw new NotFoundException('挑战不存在'); } + if (challenge.challengeState === ChallengeState.ARCHIVED) { + throw new NotFoundException('挑战不存在'); + } + const status = this.computeStatus(challenge.startAt, challenge.endAt); if (status === ChallengeStatus.UPCOMING) { throw new BadRequestException('挑战尚未开始,无法上报进度'); @@ -778,14 +806,6 @@ export class ChallengesService { return participants.map(p => p.challengeId); } - /** - * 检查用户是否为挑战创建者 - */ - private async isCreator(userId: string, challengeId: string): Promise { - const challenge = await this.challengeModel.findByPk(challengeId); - return challenge?.source === ChallengeSource.CUSTOM && challenge.creatorId === userId; - } - /** * 检查挑战是否可以加入 */ @@ -908,6 +928,10 @@ export class ChallengesService { throw new NotFoundException('分享码无效或挑战不存在'); } + if (challenge.challengeState === ChallengeState.ARCHIVED) { + throw new NotFoundException('分享码无效或挑战不存在'); + } + // 检查是否可以加入 const { canJoin, reason } = await this.canJoinChallenge(challenge); if (!canJoin) { @@ -933,6 +957,10 @@ export class ChallengesService { throw new NotFoundException('分享码无效或挑战不存在'); } + if (challenge.challengeState === ChallengeState.ARCHIVED) { + throw new NotFoundException('分享码无效或挑战不存在'); + } + return this.getChallengeDetail(challenge.id, userId); } @@ -950,6 +978,10 @@ export class ChallengesService { throw new NotFoundException('挑战不存在'); } + if (challenge.challengeState === ChallengeState.ARCHIVED) { + throw new NotFoundException('挑战不存在'); + } + if (challenge.source !== ChallengeSource.CUSTOM) { throw new BadRequestException('只能编辑自定义挑战'); } @@ -968,6 +1000,12 @@ export class ChallengesService { 'highlightTitle', 'highlightSubtitle', 'ctaLabel', + 'title', + 'summary', + 'maxParticipants', + 'highlightSubtitle', + 'highlightTitle', + 'image' ]; const restrictedFields = Object.keys(dto).filter( @@ -975,7 +1013,8 @@ export class ChallengesService { ); if (restrictedFields.length > 0) { - throw new BadRequestException('挑战已开始,只能编辑概要、公开性和展示文案'); + const allowedFieldsDescription = '概要(summary)、公开性(isPublic)、展示文案(highlightTitle、highlightSubtitle、ctaLabel)、标题(title)、图片(image)和最大参与人数(maxParticipants)'; + throw new BadRequestException(`挑战已开始,只能编辑部分字段。可编辑的字段包括:${allowedFieldsDescription}。您尝试编辑的字段:${restrictedFields.join('、')} 不在允许范围内。`); } } @@ -1031,6 +1070,10 @@ export class ChallengesService { throw new NotFoundException('挑战不存在'); } + if (challenge.challengeState === ChallengeState.ARCHIVED) { + throw new NotFoundException('挑战不存在'); + } + if (challenge.source !== ChallengeSource.CUSTOM) { throw new BadRequestException('只能为自定义挑战重新生成分享码'); } diff --git a/src/challenges/dto/challenge-detail.dto.ts b/src/challenges/dto/challenge-detail.dto.ts index 70bf852..2b69cb5 100644 --- a/src/challenges/dto/challenge-detail.dto.ts +++ b/src/challenges/dto/challenge-detail.dto.ts @@ -32,4 +32,5 @@ export interface ChallengeDetailDto { creatorId: string | null; shareCode?: string | null; source: ChallengeSource; + isCreator: boolean; } diff --git a/src/challenges/dto/challenge-list.dto.ts b/src/challenges/dto/challenge-list.dto.ts index 8d8def1..58da866 100644 --- a/src/challenges/dto/challenge-list.dto.ts +++ b/src/challenges/dto/challenge-list.dto.ts @@ -31,6 +31,7 @@ export interface ChallengeListItemDto { unit: string; badge?: BadgeInfoDto; source: ChallengeSource; + isCreator: boolean; } export interface ChallengeListResponseDto { diff --git a/src/challenges/dto/create-custom-challenge.dto.ts b/src/challenges/dto/create-custom-challenge.dto.ts index 23edd74..bfeb76f 100644 --- a/src/challenges/dto/create-custom-challenge.dto.ts +++ b/src/challenges/dto/create-custom-challenge.dto.ts @@ -57,9 +57,8 @@ export class CreateCustomChallengeDto { @IsOptional() summary?: string; - @ApiProperty({ description: '进度单位', example: '天', required: false }) + @ApiProperty({ description: '进度单位', example: '天', required: true }) @IsString() - @IsOptional() @MaxLength(64) progressUnit?: string;