feat(challenges): 添加挑战创建者标识和归档状态过滤

- 为挑战详情和列表接口添加isCreator字段标识创建者
- 过滤掉已归档的挑战,避免在列表和操作中显示
- 为挑战详情接口添加JWT认证守卫
- 将自定义挑战的progressUnit字段设为必填
- 优化挑战编辑时的错误提示信息
- 移除冗余的isCreator私有方法,直接在响应中设置标识
This commit is contained in:
richarjiang
2025-11-26 18:57:13 +08:00
parent 26e88ae610
commit 5d64a99ce5
5 changed files with 57 additions and 12 deletions

View File

@@ -38,6 +38,7 @@ export class ChallengesController {
@Get(':id') @Get(':id')
@Public() @Public()
@UseGuards(JwtAuthGuard)
async getChallengeDetail( async getChallengeDetail(
@Param('id') id: string, @Param('id') id: string,
@CurrentUser() user: AccessTokenPayload, @CurrentUser() user: AccessTokenPayload,

View File

@@ -48,7 +48,7 @@ export class ChallengesService {
const joinedChallengeIds = await this.getJoinedCustomChallengeIds(userId); const joinedChallengeIds = await this.getJoinedCustomChallengeIds(userId);
whereConditions.push( whereConditions.push(
{ creatorId: userId, source: ChallengeSource.CUSTOM }, // 我创建的 { creatorId: userId, source: ChallengeSource.CUSTOM, challengeState: { [Op.ne]: ChallengeState.ARCHIVED } }, // 我创建的
{ {
id: { [Op.in]: joinedChallengeIds }, id: { [Op.in]: joinedChallengeIds },
source: ChallengeSource.CUSTOM, source: ChallengeSource.CUSTOM,
@@ -60,6 +60,7 @@ export class ChallengesService {
const challenges = await this.challengeModel.findAll({ const challenges = await this.challengeModel.findAll({
where: { where: {
[Op.or]: whereConditions, [Op.or]: whereConditions,
challengeState: { [Op.ne]: ChallengeState.ARCHIVED }, // 过滤掉已归档的挑战
}, },
order: [['startAt', 'ASC']], order: [['startAt', 'ASC']],
}); });
@@ -180,6 +181,7 @@ export class ChallengesService {
badge: challenge.type === ChallengeType.SLEEP ? sleepBadge : undefined, badge: challenge.type === ChallengeType.SLEEP ? sleepBadge : undefined,
source: challenge.source, source: challenge.source,
shareCode: challenge.shareCode, shareCode: challenge.shareCode,
isCreator: userId ? challenge.creatorId === userId : false,
}; };
}); });
} }
@@ -191,6 +193,10 @@ export class ChallengesService {
throw new NotFoundException('挑战不存在'); throw new NotFoundException('挑战不存在');
} }
if (challenge.challengeState === ChallengeState.ARCHIVED) {
throw new NotFoundException('挑战不存在');
}
this.winstonLogger.info('start get detail', { this.winstonLogger.info('start get detail', {
context: 'getChallengeDetail', context: 'getChallengeDetail',
userId, userId,
@@ -289,6 +295,7 @@ export class ChallengesService {
creatorId: challenge.creatorId, creatorId: challenge.creatorId,
shareCode: challenge.shareCode, shareCode: challenge.shareCode,
source: challenge.source, source: challenge.source,
isCreator: userId ? challenge.creatorId === userId : false,
}; };
} }
@@ -302,6 +309,10 @@ export class ChallengesService {
throw new NotFoundException('挑战不存在'); throw new NotFoundException('挑战不存在');
} }
if (challenge.challengeState === ChallengeState.ARCHIVED) {
throw new NotFoundException('挑战不存在');
}
const { userId } = params; const { userId } = params;
const page = params.page && params.page > 0 ? params.page : 1; const page = params.page && params.page > 0 ? params.page : 1;
const requestedPageSize = params.pageSize && params.pageSize > 0 ? params.pageSize : 20; const requestedPageSize = params.pageSize && params.pageSize > 0 ? params.pageSize : 20;
@@ -342,6 +353,10 @@ export class ChallengesService {
throw new NotFoundException('挑战不存在'); throw new NotFoundException('挑战不存在');
} }
if (challenge.challengeState === ChallengeState.ARCHIVED) {
throw new NotFoundException('挑战不存在');
}
const status = this.computeStatus(challenge.startAt, challenge.endAt); const status = this.computeStatus(challenge.startAt, challenge.endAt);
if (status === ChallengeStatus.EXPIRED) { if (status === ChallengeStatus.EXPIRED) {
throw new BadRequestException('挑战已过期,无法加入'); throw new BadRequestException('挑战已过期,无法加入');
@@ -392,6 +407,15 @@ export class ChallengesService {
} }
async leaveChallenge(userId: string, challengeId: string): Promise<boolean> { async leaveChallenge(userId: string, challengeId: string): Promise<boolean> {
// 先检查挑战是否存在且未归档
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({ const participant = await this.participantModel.findOne({
where: { where: {
challengeId, challengeId,
@@ -421,6 +445,10 @@ export class ChallengesService {
throw new NotFoundException('挑战不存在'); throw new NotFoundException('挑战不存在');
} }
if (challenge.challengeState === ChallengeState.ARCHIVED) {
throw new NotFoundException('挑战不存在');
}
const status = this.computeStatus(challenge.startAt, challenge.endAt); const status = this.computeStatus(challenge.startAt, challenge.endAt);
if (status === ChallengeStatus.UPCOMING) { if (status === ChallengeStatus.UPCOMING) {
throw new BadRequestException('挑战尚未开始,无法上报进度'); throw new BadRequestException('挑战尚未开始,无法上报进度');
@@ -778,14 +806,6 @@ export class ChallengesService {
return participants.map(p => p.challengeId); return participants.map(p => p.challengeId);
} }
/**
* 检查用户是否为挑战创建者
*/
private async isCreator(userId: string, challengeId: string): Promise<boolean> {
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('分享码无效或挑战不存在'); throw new NotFoundException('分享码无效或挑战不存在');
} }
if (challenge.challengeState === ChallengeState.ARCHIVED) {
throw new NotFoundException('分享码无效或挑战不存在');
}
// 检查是否可以加入 // 检查是否可以加入
const { canJoin, reason } = await this.canJoinChallenge(challenge); const { canJoin, reason } = await this.canJoinChallenge(challenge);
if (!canJoin) { if (!canJoin) {
@@ -933,6 +957,10 @@ export class ChallengesService {
throw new NotFoundException('分享码无效或挑战不存在'); throw new NotFoundException('分享码无效或挑战不存在');
} }
if (challenge.challengeState === ChallengeState.ARCHIVED) {
throw new NotFoundException('分享码无效或挑战不存在');
}
return this.getChallengeDetail(challenge.id, userId); return this.getChallengeDetail(challenge.id, userId);
} }
@@ -950,6 +978,10 @@ export class ChallengesService {
throw new NotFoundException('挑战不存在'); throw new NotFoundException('挑战不存在');
} }
if (challenge.challengeState === ChallengeState.ARCHIVED) {
throw new NotFoundException('挑战不存在');
}
if (challenge.source !== ChallengeSource.CUSTOM) { if (challenge.source !== ChallengeSource.CUSTOM) {
throw new BadRequestException('只能编辑自定义挑战'); throw new BadRequestException('只能编辑自定义挑战');
} }
@@ -968,6 +1000,12 @@ export class ChallengesService {
'highlightTitle', 'highlightTitle',
'highlightSubtitle', 'highlightSubtitle',
'ctaLabel', 'ctaLabel',
'title',
'summary',
'maxParticipants',
'highlightSubtitle',
'highlightTitle',
'image'
]; ];
const restrictedFields = Object.keys(dto).filter( const restrictedFields = Object.keys(dto).filter(
@@ -975,7 +1013,8 @@ export class ChallengesService {
); );
if (restrictedFields.length > 0) { 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('挑战不存在'); throw new NotFoundException('挑战不存在');
} }
if (challenge.challengeState === ChallengeState.ARCHIVED) {
throw new NotFoundException('挑战不存在');
}
if (challenge.source !== ChallengeSource.CUSTOM) { if (challenge.source !== ChallengeSource.CUSTOM) {
throw new BadRequestException('只能为自定义挑战重新生成分享码'); throw new BadRequestException('只能为自定义挑战重新生成分享码');
} }

View File

@@ -32,4 +32,5 @@ export interface ChallengeDetailDto {
creatorId: string | null; creatorId: string | null;
shareCode?: string | null; shareCode?: string | null;
source: ChallengeSource; source: ChallengeSource;
isCreator: boolean;
} }

View File

@@ -31,6 +31,7 @@ export interface ChallengeListItemDto {
unit: string; unit: string;
badge?: BadgeInfoDto; badge?: BadgeInfoDto;
source: ChallengeSource; source: ChallengeSource;
isCreator: boolean;
} }
export interface ChallengeListResponseDto { export interface ChallengeListResponseDto {

View File

@@ -57,9 +57,8 @@ export class CreateCustomChallengeDto {
@IsOptional() @IsOptional()
summary?: string; summary?: string;
@ApiProperty({ description: '进度单位', example: '天', required: false }) @ApiProperty({ description: '进度单位', example: '天', required: true })
@IsString() @IsString()
@IsOptional()
@MaxLength(64) @MaxLength(64)
progressUnit?: string; progressUnit?: string;