Compare commits
2 Commits
029b8f46b9
...
5d64a99ce5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d64a99ce5 | ||
|
|
26e88ae610 |
@@ -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,
|
||||||
|
|||||||
@@ -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']],
|
||||||
});
|
});
|
||||||
@@ -178,6 +179,9 @@ export class ChallengesService {
|
|||||||
isJoined: Boolean(participation),
|
isJoined: Boolean(participation),
|
||||||
type: challenge.type,
|
type: challenge.type,
|
||||||
badge: challenge.type === ChallengeType.SLEEP ? sleepBadge : undefined,
|
badge: challenge.type === ChallengeType.SLEEP ? sleepBadge : undefined,
|
||||||
|
source: challenge.source,
|
||||||
|
shareCode: challenge.shareCode,
|
||||||
|
isCreator: userId ? challenge.creatorId === userId : false,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -189,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,
|
||||||
@@ -284,6 +292,10 @@ export class ChallengesService {
|
|||||||
unit: challenge.progressUnit,
|
unit: challenge.progressUnit,
|
||||||
type: challenge.type,
|
type: challenge.type,
|
||||||
badge,
|
badge,
|
||||||
|
creatorId: challenge.creatorId,
|
||||||
|
shareCode: challenge.shareCode,
|
||||||
|
source: challenge.source,
|
||||||
|
isCreator: userId ? challenge.creatorId === userId : false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,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;
|
||||||
@@ -337,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('挑战已过期,无法加入');
|
||||||
@@ -387,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,
|
||||||
@@ -416,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('挑战尚未开始,无法上报进度');
|
||||||
@@ -773,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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查挑战是否可以加入
|
* 检查挑战是否可以加入
|
||||||
*/
|
*/
|
||||||
@@ -903,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) {
|
||||||
@@ -928,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -945,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('只能编辑自定义挑战');
|
||||||
}
|
}
|
||||||
@@ -963,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(
|
||||||
@@ -970,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('、')} 不在允许范围内。`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1026,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('只能为自定义挑战重新生成分享码');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ChallengeProgressDto, RankingItemDto } from './challenge-progress.dto';
|
import { ChallengeProgressDto, RankingItemDto } from './challenge-progress.dto';
|
||||||
import { ChallengeType } from '../models/challenge.model';
|
import { ChallengeSource, ChallengeType } from '../models/challenge.model';
|
||||||
|
|
||||||
export interface BadgeInfoDto {
|
export interface BadgeInfoDto {
|
||||||
code: string;
|
code: string;
|
||||||
@@ -29,4 +29,8 @@ export interface ChallengeDetailDto {
|
|||||||
type: ChallengeType;
|
type: ChallengeType;
|
||||||
unit: string;
|
unit: string;
|
||||||
badge?: BadgeInfoDto;
|
badge?: BadgeInfoDto;
|
||||||
|
creatorId: string | null;
|
||||||
|
shareCode?: string | null;
|
||||||
|
source: ChallengeSource;
|
||||||
|
isCreator: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ChallengeStatus, ChallengeType } from '../models/challenge.model';
|
import { ChallengeSource, ChallengeStatus, ChallengeType } from '../models/challenge.model';
|
||||||
import { ChallengeProgressDto } from './challenge-progress.dto';
|
import { ChallengeProgressDto } from './challenge-progress.dto';
|
||||||
|
|
||||||
export interface BadgeInfoDto {
|
export interface BadgeInfoDto {
|
||||||
@@ -30,6 +30,8 @@ export interface ChallengeListItemDto {
|
|||||||
type: ChallengeType;
|
type: ChallengeType;
|
||||||
unit: string;
|
unit: string;
|
||||||
badge?: BadgeInfoDto;
|
badge?: BadgeInfoDto;
|
||||||
|
source: ChallengeSource;
|
||||||
|
isCreator: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChallengeListResponseDto {
|
export interface ChallengeListResponseDto {
|
||||||
|
|||||||
@@ -22,12 +22,10 @@ export class CreateCustomChallengeDto {
|
|||||||
|
|
||||||
@ApiProperty({ description: '开始时间戳(毫秒)', example: 1704067200000 })
|
@ApiProperty({ description: '开始时间戳(毫秒)', example: 1704067200000 })
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@Min(Date.now())
|
|
||||||
startAt: number;
|
startAt: number;
|
||||||
|
|
||||||
@ApiProperty({ description: '结束时间戳(毫秒)', example: 1705881600000 })
|
@ApiProperty({ description: '结束时间戳(毫秒)', example: 1705881600000 })
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@Min(Date.now() + 86400000) // 至少未来 1 天
|
|
||||||
endAt: number;
|
endAt: number;
|
||||||
|
|
||||||
@ApiProperty({ description: '每日目标值(如喝水8杯)', example: 8, minimum: 1, maximum: 1000 })
|
@ApiProperty({ description: '每日目标值(如喝水8杯)', example: 8, minimum: 1, maximum: 1000 })
|
||||||
@@ -59,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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user