feat(challenges): 支持公开访问挑战列表与详情接口
- 在 GET /challenges、GET /challenges/:id、GET /challenges/:id/rankings 添加 @Public() 装饰器,允许未登录用户访问 - 将 userId 改为可选参数,未登录时仍可返回基础数据 - 列表接口过滤掉 UPCOMING 状态挑战,仅展示进行中/已结束 - 返回 DTO 新增 unit 字段,用于前端展示进度单位 - 鉴权守卫优化:公开接口若携带 token 仍尝试解析并注入 user,方便后续业务逻辑
This commit is contained in:
@@ -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<BaseResponseDto<ChallengeListItemDto[]>> {
|
||||
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<BaseResponseDto<ChallengeDetailDto>> {
|
||||
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,
|
||||
|
||||
@@ -28,7 +28,7 @@ export class ChallengesService {
|
||||
private readonly progressReportModel: typeof ChallengeProgressReport,
|
||||
) { }
|
||||
|
||||
async getChallengesForUser(userId: string): Promise<ChallengeListItemDto[]> {
|
||||
async getChallengesForUser(userId?: string): Promise<ChallengeListItemDto[]> {
|
||||
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,6 +82,9 @@ export class ChallengesService {
|
||||
}
|
||||
}
|
||||
|
||||
const participationMap = new Map<string, ChallengeParticipant>();
|
||||
|
||||
if (userId) {
|
||||
const userParticipations = await this.participantModel.findAll({
|
||||
where: {
|
||||
challengeId: challengeIds,
|
||||
@@ -91,10 +95,10 @@ export class ChallengesService {
|
||||
},
|
||||
});
|
||||
|
||||
const participationMap = new Map<string, ChallengeParticipant>();
|
||||
for (const participation of userParticipations) {
|
||||
participationMap.set(participation.challengeId, participation);
|
||||
}
|
||||
}
|
||||
|
||||
return challengesWithStatus.map(({ challenge, status }) => {
|
||||
const completionTarget = challenge.minimumCheckInDays
|
||||
@@ -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<ChallengeDetailDto> {
|
||||
async getChallengeDetail(challengeId: string, userId?: string,): Promise<ChallengeDetailDto> {
|
||||
const challenge = await this.challengeModel.findByPk(challengeId);
|
||||
|
||||
if (!challenge) {
|
||||
@@ -147,7 +152,8 @@ export class ChallengesService {
|
||||
status: ChallengeParticipantStatus.ACTIVE,
|
||||
},
|
||||
}),
|
||||
this.participantModel.findOne({
|
||||
userId
|
||||
? this.participantModel.findOne({
|
||||
where: {
|
||||
challengeId,
|
||||
userId,
|
||||
@@ -155,7 +161,8 @@ export class ChallengesService {
|
||||
[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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -19,4 +19,5 @@ export interface ChallengeDetailDto {
|
||||
rankings: RankingItemDto[];
|
||||
userRank?: number;
|
||||
type: ChallengeType;
|
||||
unit: string;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface ChallengeListItemDto {
|
||||
progress?: ChallengeProgressDto;
|
||||
isJoined: boolean;
|
||||
type: ChallengeType;
|
||||
unit: string;
|
||||
}
|
||||
|
||||
export interface ChallengeListResponseDto {
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -219,7 +219,7 @@ export class AppleAuthService {
|
||||
*/
|
||||
extractTokenFromHeader(authHeader: string): string {
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
throw new UnauthorizedException('无效的授权头格式');
|
||||
return ''
|
||||
}
|
||||
|
||||
return authHeader.substring(7);
|
||||
|
||||
Reference in New Issue
Block a user