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 { ChallengeListItemDto } from './dto/challenge-list.dto';
|
||||||
import { ChallengeProgressDto } from './dto/challenge-progress.dto';
|
import { ChallengeProgressDto } from './dto/challenge-progress.dto';
|
||||||
import { ChallengeRankingListDto, GetChallengeRankingQueryDto } from './dto/challenge-ranking.dto';
|
import { ChallengeRankingListDto, GetChallengeRankingQueryDto } from './dto/challenge-ranking.dto';
|
||||||
|
import { Public } from 'src/common/decorators/public.decorator';
|
||||||
|
|
||||||
@Controller('challenges')
|
@Controller('challenges')
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
export class ChallengesController {
|
export class ChallengesController {
|
||||||
constructor(private readonly challengesService: ChallengesService) { }
|
constructor(private readonly challengesService: ChallengesService) { }
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
|
@Public()
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
async getChallenges(
|
async getChallenges(
|
||||||
@CurrentUser() user: AccessTokenPayload,
|
@CurrentUser() user: AccessTokenPayload,
|
||||||
): Promise<BaseResponseDto<ChallengeListItemDto[]>> {
|
): Promise<BaseResponseDto<ChallengeListItemDto[]>> {
|
||||||
const data = await this.challengesService.getChallengesForUser(user.sub);
|
const data = await this.challengesService.getChallengesForUser(user?.sub);
|
||||||
return {
|
return {
|
||||||
code: ResponseCode.SUCCESS,
|
code: ResponseCode.SUCCESS,
|
||||||
message: '获取挑战列表成功',
|
message: '获取挑战列表成功',
|
||||||
@@ -28,11 +30,12 @@ export class ChallengesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
|
@Public()
|
||||||
async getChallengeDetail(
|
async getChallengeDetail(
|
||||||
@Param('id') id: string,
|
@Param('id') id: string,
|
||||||
@CurrentUser() user: AccessTokenPayload,
|
@CurrentUser() user: AccessTokenPayload,
|
||||||
): Promise<BaseResponseDto<ChallengeDetailDto>> {
|
): Promise<BaseResponseDto<ChallengeDetailDto>> {
|
||||||
const data = await this.challengesService.getChallengeDetail(user.sub, id);
|
const data = await this.challengesService.getChallengeDetail(id, user?.sub);
|
||||||
return {
|
return {
|
||||||
code: ResponseCode.SUCCESS,
|
code: ResponseCode.SUCCESS,
|
||||||
message: '获取挑战详情成功',
|
message: '获取挑战详情成功',
|
||||||
@@ -41,6 +44,7 @@ export class ChallengesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id/rankings')
|
@Get(':id/rankings')
|
||||||
|
@Public()
|
||||||
async getChallengeRankings(
|
async getChallengeRankings(
|
||||||
@Param('id') id: string,
|
@Param('id') id: string,
|
||||||
@Query() query: GetChallengeRankingQueryDto,
|
@Query() query: GetChallengeRankingQueryDto,
|
||||||
@@ -49,7 +53,7 @@ export class ChallengesController {
|
|||||||
const data = await this.challengesService.getChallengeRankings(id, {
|
const data = await this.challengesService.getChallengeRankings(id, {
|
||||||
page: query.page,
|
page: query.page,
|
||||||
pageSize: query.pageSize,
|
pageSize: query.pageSize,
|
||||||
userId: user.sub,
|
userId: user?.sub,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
code: ResponseCode.SUCCESS,
|
code: ResponseCode.SUCCESS,
|
||||||
@@ -59,6 +63,7 @@ export class ChallengesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/join')
|
@Post(':id/join')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
async joinChallenge(
|
async joinChallenge(
|
||||||
@Param('id') id: string,
|
@Param('id') id: string,
|
||||||
@CurrentUser() user: AccessTokenPayload,
|
@CurrentUser() user: AccessTokenPayload,
|
||||||
@@ -72,6 +77,7 @@ export class ChallengesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/leave')
|
@Post(':id/leave')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
async leaveChallenge(
|
async leaveChallenge(
|
||||||
@Param('id') id: string,
|
@Param('id') id: string,
|
||||||
@CurrentUser() user: AccessTokenPayload,
|
@CurrentUser() user: AccessTokenPayload,
|
||||||
@@ -85,6 +91,7 @@ export class ChallengesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/progress')
|
@Post(':id/progress')
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
async reportProgress(
|
async reportProgress(
|
||||||
@Param('id') id: string,
|
@Param('id') id: string,
|
||||||
@Body() dto: UpdateChallengeProgressDto,
|
@Body() dto: UpdateChallengeProgressDto,
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export class ChallengesService {
|
|||||||
private readonly progressReportModel: typeof ChallengeProgressReport,
|
private readonly progressReportModel: typeof ChallengeProgressReport,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
async getChallengesForUser(userId: string): Promise<ChallengeListItemDto[]> {
|
async getChallengesForUser(userId?: string): Promise<ChallengeListItemDto[]> {
|
||||||
const challenges = await this.challengeModel.findAll({
|
const challenges = await this.challengeModel.findAll({
|
||||||
order: [['startAt', 'ASC']],
|
order: [['startAt', 'ASC']],
|
||||||
});
|
});
|
||||||
@@ -50,6 +50,7 @@ export class ChallengesService {
|
|||||||
challenge,
|
challenge,
|
||||||
status: this.computeStatus(challenge.startAt, challenge.endAt),
|
status: this.computeStatus(challenge.startAt, challenge.endAt),
|
||||||
}))
|
}))
|
||||||
|
.filter(({ status }) => status !== ChallengeStatus.UPCOMING)
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const priorityDiff = statusPriority[a.status] - statusPriority[b.status];
|
const priorityDiff = statusPriority[a.status] - statusPriority[b.status];
|
||||||
if (priorityDiff !== 0) {
|
if (priorityDiff !== 0) {
|
||||||
@@ -81,6 +82,9 @@ export class ChallengesService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const participationMap = new Map<string, ChallengeParticipant>();
|
||||||
|
|
||||||
|
if (userId) {
|
||||||
const userParticipations = await this.participantModel.findAll({
|
const userParticipations = await this.participantModel.findAll({
|
||||||
where: {
|
where: {
|
||||||
challengeId: challengeIds,
|
challengeId: challengeIds,
|
||||||
@@ -91,10 +95,10 @@ export class ChallengesService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const participationMap = new Map<string, ChallengeParticipant>();
|
|
||||||
for (const participation of userParticipations) {
|
for (const participation of userParticipations) {
|
||||||
participationMap.set(participation.challengeId, participation);
|
participationMap.set(participation.challengeId, participation);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return challengesWithStatus.map(({ challenge, status }) => {
|
return challengesWithStatus.map(({ challenge, status }) => {
|
||||||
const completionTarget = challenge.minimumCheckInDays
|
const completionTarget = challenge.minimumCheckInDays
|
||||||
@@ -111,6 +115,7 @@ export class ChallengesService {
|
|||||||
durationLabel: challenge.durationLabel,
|
durationLabel: challenge.durationLabel,
|
||||||
requirementLabel: challenge.requirementLabel,
|
requirementLabel: challenge.requirementLabel,
|
||||||
status,
|
status,
|
||||||
|
unit: challenge.progressUnit,
|
||||||
startAt: challenge.startAt,
|
startAt: challenge.startAt,
|
||||||
endAt: challenge.endAt,
|
endAt: challenge.endAt,
|
||||||
participantsCount: participantsCountMap.get(challenge.id) ?? 0,
|
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);
|
const challenge = await this.challengeModel.findByPk(challengeId);
|
||||||
|
|
||||||
if (!challenge) {
|
if (!challenge) {
|
||||||
@@ -147,7 +152,8 @@ export class ChallengesService {
|
|||||||
status: ChallengeParticipantStatus.ACTIVE,
|
status: ChallengeParticipantStatus.ACTIVE,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
this.participantModel.findOne({
|
userId
|
||||||
|
? this.participantModel.findOne({
|
||||||
where: {
|
where: {
|
||||||
challengeId,
|
challengeId,
|
||||||
userId,
|
userId,
|
||||||
@@ -155,7 +161,8 @@ export class ChallengesService {
|
|||||||
[Op.ne]: ChallengeParticipantStatus.LEFT,
|
[Op.ne]: ChallengeParticipantStatus.LEFT,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
})
|
||||||
|
: null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
this.winstonLogger.info('end get detail', {
|
this.winstonLogger.info('end get detail', {
|
||||||
@@ -205,6 +212,7 @@ export class ChallengesService {
|
|||||||
progress,
|
progress,
|
||||||
rankings,
|
rankings,
|
||||||
userRank,
|
userRank,
|
||||||
|
unit: challenge.progressUnit,
|
||||||
type: challenge.type,
|
type: challenge.type,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,4 +19,5 @@ export interface ChallengeDetailDto {
|
|||||||
rankings: RankingItemDto[];
|
rankings: RankingItemDto[];
|
||||||
userRank?: number;
|
userRank?: number;
|
||||||
type: ChallengeType;
|
type: ChallengeType;
|
||||||
|
unit: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export interface ChallengeListItemDto {
|
|||||||
progress?: ChallengeProgressDto;
|
progress?: ChallengeProgressDto;
|
||||||
isJoined: boolean;
|
isJoined: boolean;
|
||||||
type: ChallengeType;
|
type: ChallengeType;
|
||||||
|
unit: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChallengeListResponseDto {
|
export interface ChallengeListResponseDto {
|
||||||
|
|||||||
@@ -17,21 +17,35 @@ export class JwtAuthGuard implements CanActivate {
|
|||||||
context.getClass(),
|
context.getClass(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (isPublic) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const request = context.switchToHttp().getRequest();
|
const request = context.switchToHttp().getRequest();
|
||||||
const authHeader = request.headers.authorization;
|
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) {
|
if (!authHeader) {
|
||||||
throw new UnauthorizedException('缺少授权头');
|
throw new UnauthorizedException('缺少授权头');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = this.appleAuthService.extractTokenFromHeader(authHeader);
|
|
||||||
const payload = this.appleAuthService.verifyAccessToken(token);
|
const payload = this.appleAuthService.verifyAccessToken(token);
|
||||||
|
|
||||||
this.logger.log(`鉴权成功: ${JSON.stringify(payload)}, token: ${token}`);
|
this.logger.log(`鉴权成功: ${JSON.stringify(payload)}, token: ${token}`);
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ export class AppleAuthService {
|
|||||||
*/
|
*/
|
||||||
extractTokenFromHeader(authHeader: string): string {
|
extractTokenFromHeader(authHeader: string): string {
|
||||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||||
throw new UnauthorizedException('无效的授权头格式');
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
return authHeader.substring(7);
|
return authHeader.substring(7);
|
||||||
|
|||||||
Reference in New Issue
Block a user