From 999fc7f79333253684439f4f4aae057c3bebbf07 Mon Sep 17 00:00:00 2001 From: richarjiang Date: Tue, 30 Sep 2025 16:43:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(challenges):=20=E6=94=AF=E6=8C=81=E5=85=AC?= =?UTF-8?q?=E5=BC=80=E8=AE=BF=E9=97=AE=E6=8C=91=E6=88=98=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E4=B8=8E=E8=AF=A6=E6=83=85=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 GET /challenges、GET /challenges/:id、GET /challenges/:id/rankings 添加 @Public() 装饰器,允许未登录用户访问 - 将 userId 改为可选参数,未登录时仍可返回基础数据 - 列表接口过滤掉 UPCOMING 状态挑战,仅展示进行中/已结束 - 返回 DTO 新增 unit 字段,用于前端展示进度单位 - 鉴权守卫优化:公开接口若携带 token 仍尝试解析并注入 user,方便后续业务逻辑 --- src/challenges/challenges.controller.ts | 15 +++++-- src/challenges/challenges.service.ts | 52 +++++++++++++--------- src/challenges/dto/challenge-detail.dto.ts | 1 + src/challenges/dto/challenge-list.dto.ts | 1 + src/common/guards/jwt-auth.guard.ts | 26 ++++++++--- src/users/services/apple-auth.service.ts | 2 +- 6 files changed, 64 insertions(+), 33 deletions(-) diff --git a/src/challenges/challenges.controller.ts b/src/challenges/challenges.controller.ts index a0a5295..8483403 100644 --- a/src/challenges/challenges.controller.ts +++ b/src/challenges/challenges.controller.ts @@ -9,17 +9,19 @@ import { ChallengeDetailDto } from './dto/challenge-detail.dto'; import { ChallengeListItemDto } from './dto/challenge-list.dto'; import { ChallengeProgressDto } from './dto/challenge-progress.dto'; import { ChallengeRankingListDto, GetChallengeRankingQueryDto } from './dto/challenge-ranking.dto'; +import { Public } from 'src/common/decorators/public.decorator'; @Controller('challenges') -@UseGuards(JwtAuthGuard) export class ChallengesController { constructor(private readonly challengesService: ChallengesService) { } @Get() + @Public() + @UseGuards(JwtAuthGuard) async getChallenges( @CurrentUser() user: AccessTokenPayload, ): Promise> { - const data = await this.challengesService.getChallengesForUser(user.sub); + const data = await this.challengesService.getChallengesForUser(user?.sub); return { code: ResponseCode.SUCCESS, message: '获取挑战列表成功', @@ -28,11 +30,12 @@ export class ChallengesController { } @Get(':id') + @Public() async getChallengeDetail( @Param('id') id: string, @CurrentUser() user: AccessTokenPayload, ): Promise> { - const data = await this.challengesService.getChallengeDetail(user.sub, id); + const data = await this.challengesService.getChallengeDetail(id, user?.sub); return { code: ResponseCode.SUCCESS, message: '获取挑战详情成功', @@ -41,6 +44,7 @@ export class ChallengesController { } @Get(':id/rankings') + @Public() async getChallengeRankings( @Param('id') id: string, @Query() query: GetChallengeRankingQueryDto, @@ -49,7 +53,7 @@ export class ChallengesController { const data = await this.challengesService.getChallengeRankings(id, { page: query.page, pageSize: query.pageSize, - userId: user.sub, + userId: user?.sub, }); return { code: ResponseCode.SUCCESS, @@ -59,6 +63,7 @@ export class ChallengesController { } @Post(':id/join') + @UseGuards(JwtAuthGuard) async joinChallenge( @Param('id') id: string, @CurrentUser() user: AccessTokenPayload, @@ -72,6 +77,7 @@ export class ChallengesController { } @Post(':id/leave') + @UseGuards(JwtAuthGuard) async leaveChallenge( @Param('id') id: string, @CurrentUser() user: AccessTokenPayload, @@ -85,6 +91,7 @@ export class ChallengesController { } @Post(':id/progress') + @UseGuards(JwtAuthGuard) async reportProgress( @Param('id') id: string, @Body() dto: UpdateChallengeProgressDto, diff --git a/src/challenges/challenges.service.ts b/src/challenges/challenges.service.ts index 70442eb..e4e88c2 100644 --- a/src/challenges/challenges.service.ts +++ b/src/challenges/challenges.service.ts @@ -28,7 +28,7 @@ export class ChallengesService { private readonly progressReportModel: typeof ChallengeProgressReport, ) { } - async getChallengesForUser(userId: string): Promise { + async getChallengesForUser(userId?: string): Promise { const challenges = await this.challengeModel.findAll({ order: [['startAt', 'ASC']], }); @@ -50,6 +50,7 @@ export class ChallengesService { challenge, status: this.computeStatus(challenge.startAt, challenge.endAt), })) + .filter(({ status }) => status !== ChallengeStatus.UPCOMING) .sort((a, b) => { const priorityDiff = statusPriority[a.status] - statusPriority[b.status]; if (priorityDiff !== 0) { @@ -81,19 +82,22 @@ export class ChallengesService { } } - const userParticipations = await this.participantModel.findAll({ - where: { - challengeId: challengeIds, - userId, - status: { - [Op.ne]: ChallengeParticipantStatus.LEFT, - }, - }, - }); - const participationMap = new Map(); - for (const participation of userParticipations) { - participationMap.set(participation.challengeId, participation); + + if (userId) { + const userParticipations = await this.participantModel.findAll({ + where: { + challengeId: challengeIds, + userId, + status: { + [Op.ne]: ChallengeParticipantStatus.LEFT, + }, + }, + }); + + for (const participation of userParticipations) { + participationMap.set(participation.challengeId, participation); + } } return challengesWithStatus.map(({ challenge, status }) => { @@ -111,6 +115,7 @@ export class ChallengesService { durationLabel: challenge.durationLabel, requirementLabel: challenge.requirementLabel, status, + unit: challenge.progressUnit, startAt: challenge.startAt, endAt: challenge.endAt, participantsCount: participantsCountMap.get(challenge.id) ?? 0, @@ -126,7 +131,7 @@ export class ChallengesService { }); } - async getChallengeDetail(userId: string, challengeId: string): Promise { + async getChallengeDetail(challengeId: string, userId?: string,): Promise { const challenge = await this.challengeModel.findByPk(challengeId); if (!challenge) { @@ -147,15 +152,17 @@ export class ChallengesService { status: ChallengeParticipantStatus.ACTIVE, }, }), - this.participantModel.findOne({ - where: { - challengeId, - userId, - status: { - [Op.ne]: ChallengeParticipantStatus.LEFT, + userId + ? this.participantModel.findOne({ + where: { + challengeId, + userId, + status: { + [Op.ne]: ChallengeParticipantStatus.LEFT, + }, }, - }, - }), + }) + : null, ]); this.winstonLogger.info('end get detail', { @@ -205,6 +212,7 @@ export class ChallengesService { progress, rankings, userRank, + unit: challenge.progressUnit, type: challenge.type, }; } diff --git a/src/challenges/dto/challenge-detail.dto.ts b/src/challenges/dto/challenge-detail.dto.ts index db8fb44..8e29a0a 100644 --- a/src/challenges/dto/challenge-detail.dto.ts +++ b/src/challenges/dto/challenge-detail.dto.ts @@ -19,4 +19,5 @@ export interface ChallengeDetailDto { rankings: RankingItemDto[]; userRank?: number; type: ChallengeType; + unit: string; } diff --git a/src/challenges/dto/challenge-list.dto.ts b/src/challenges/dto/challenge-list.dto.ts index 25821fc..43173eb 100644 --- a/src/challenges/dto/challenge-list.dto.ts +++ b/src/challenges/dto/challenge-list.dto.ts @@ -20,6 +20,7 @@ export interface ChallengeListItemDto { progress?: ChallengeProgressDto; isJoined: boolean; type: ChallengeType; + unit: string; } export interface ChallengeListResponseDto { diff --git a/src/common/guards/jwt-auth.guard.ts b/src/common/guards/jwt-auth.guard.ts index 2f92a6b..23f2344 100644 --- a/src/common/guards/jwt-auth.guard.ts +++ b/src/common/guards/jwt-auth.guard.ts @@ -17,21 +17,35 @@ export class JwtAuthGuard implements CanActivate { context.getClass(), ]); - if (isPublic) { - return true; - } - const request = context.switchToHttp().getRequest(); const authHeader = request.headers.authorization; - this.logger.log(`authHeader: ${authHeader}`); + this.logger.log(`authHeader: ${authHeader}, isPublic: ${isPublic}`); + + const token = this.appleAuthService.extractTokenFromHeader(authHeader); + + if (isPublic) { + // 公开接口如果有 token,也可以尝试获取用户信息 + if (token) { + try { + const payload = this.appleAuthService.verifyAccessToken(token); + this.logger.log(`鉴权成功: ${JSON.stringify(payload)}, token: ${token}`); + // 将用户信息添加到请求对象中 + request.user = payload; + } catch (error) { + this.logger.error(`鉴权失败: ${error.message}, token: ${token}`); + } + } + return true; + } + + if (!authHeader) { throw new UnauthorizedException('缺少授权头'); } try { - const token = this.appleAuthService.extractTokenFromHeader(authHeader); const payload = this.appleAuthService.verifyAccessToken(token); this.logger.log(`鉴权成功: ${JSON.stringify(payload)}, token: ${token}`); diff --git a/src/users/services/apple-auth.service.ts b/src/users/services/apple-auth.service.ts index e8e830a..a7b4d3c 100644 --- a/src/users/services/apple-auth.service.ts +++ b/src/users/services/apple-auth.service.ts @@ -219,7 +219,7 @@ export class AppleAuthService { */ extractTokenFromHeader(authHeader: string): string { if (!authHeader || !authHeader.startsWith('Bearer ')) { - throw new UnauthorizedException('无效的授权头格式'); + return '' } return authHeader.substring(7);