From d87fc84575245396fd7372da8b2b5c5857c8002f Mon Sep 17 00:00:00 2001 From: richarjiang Date: Mon, 29 Sep 2025 10:25:20 +0800 Subject: [PATCH] =?UTF-8?q?feat(challenges):=20=E4=BD=BF=E7=94=A8=20minimu?= =?UTF-8?q?mCheckInDays=20=E7=BB=9F=E4=B8=80=E8=BF=9B=E5=BA=A6=E7=9B=AE?= =?UTF-8?q?=E6=A0=87=E8=AE=A1=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将挑战完成目标从 targetValue/progressUnit 改为 minimumCheckInDays 字段驱动,确保列表、详情、加入、打卡各场景使用一致的完成天数标准,并移除前端展示字段 badge/subtitle。 --- src/challenges/challenges.service.ts | 83 ++++++++++++++------ src/challenges/dto/challenge-detail.dto.ts | 1 + src/challenges/dto/challenge-list.dto.ts | 1 + src/challenges/dto/challenge-progress.dto.ts | 2 - src/challenges/models/challenge.model.ts | 8 ++ 5 files changed, 71 insertions(+), 24 deletions(-) diff --git a/src/challenges/challenges.service.ts b/src/challenges/challenges.service.ts index 5b7ef3f..3671e70 100644 --- a/src/challenges/challenges.service.ts +++ b/src/challenges/challenges.service.ts @@ -72,9 +72,10 @@ export class ChallengesService { } return challenges.map((challenge) => { + const completionTarget = challenge.minimumCheckInDays const participation = participationMap.get(challenge.id); const progress = participation - ? this.buildChallengeProgress(participation.progressValue, participation.targetValue, challenge.progressUnit) + ? this.buildChallengeProgress(participation.progressValue, completionTarget) : undefined; return { @@ -92,6 +93,7 @@ export class ChallengesService { highlightTitle: challenge.highlightTitle, highlightSubtitle: challenge.highlightSubtitle, ctaLabel: challenge.ctaLabel, + minimumCheckInDays: completionTarget, progress, isJoined: Boolean(participation), }; @@ -161,17 +163,22 @@ export class ChallengesService { rankingsRawCount: rankingsRaw.length, }); + const completionTarget = challenge.minimumCheckInDays + const progress = participation - ? this.buildChallengeProgress(participation.progressValue, participation.targetValue, challenge.progressUnit) + ? this.buildChallengeProgress(participation.progressValue, completionTarget) : undefined; - const rankings: RankingItemDto[] = rankingsRaw.map((item, index) => ({ - id: item.user?.id ?? item.userId, - name: item.user?.name ?? '未知用户', - avatar: item.user?.avatar ?? null, - metric: `${item.progressValue}/${item.targetValue}${challenge.progressUnit}`, - badge: this.resolveRankingBadge(index), - })); + const rankings: RankingItemDto[] = rankingsRaw.map((item, index) => { + const itemTarget = item.targetValue && item.targetValue > 0 ? item.targetValue : completionTarget; + return { + id: item.user?.id ?? item.userId, + name: item.user?.name ?? '未知用户', + avatar: item.user?.avatar ?? null, + metric: `${item.progressValue}/${itemTarget}天`, + badge: this.resolveRankingBadge(index), + }; + }); const userRank = participation ? await this.calculateUserRank(challengeId, participation) : undefined; @@ -187,6 +194,7 @@ export class ChallengesService { highlightTitle: challenge.highlightTitle, highlightSubtitle: challenge.highlightSubtitle, ctaLabel: challenge.ctaLabel, + minimumCheckInDays: completionTarget, participantsCount, progress, rankings, @@ -206,6 +214,12 @@ export class ChallengesService { throw new BadRequestException('挑战已过期,无法加入'); } + const completionTarget = challenge.minimumCheckInDays + + if (completionTarget <= 0) { + throw new BadRequestException('挑战配置存在问题,请联系管理员'); + } + const existing = await this.participantModel.findOne({ where: { challengeId, @@ -223,25 +237,25 @@ export class ChallengesService { if (existing && existing.status === ChallengeParticipantStatus.LEFT) { existing.progressValue = 0; - existing.targetValue = challenge.targetValue; + existing.targetValue = completionTarget; existing.status = ChallengeParticipantStatus.ACTIVE; existing.joinedAt = new Date(); existing.leftAt = null; existing.lastProgressAt = null; await existing.save(); - return this.buildChallengeProgress(existing.progressValue, existing.targetValue, challenge.progressUnit); + return this.buildChallengeProgress(existing.progressValue, completionTarget); } const participant = await this.participantModel.create({ challengeId, userId, progressValue: 0, - targetValue: challenge.targetValue, + targetValue: completionTarget, status: ChallengeParticipantStatus.ACTIVE, joinedAt: new Date(), }); - return this.buildChallengeProgress(participant.progressValue, participant.targetValue, challenge.progressUnit); + return this.buildChallengeProgress(participant.progressValue, completionTarget); } async leaveChallenge(userId: string, challengeId: string): Promise { @@ -294,14 +308,31 @@ export class ChallengesService { throw new NotFoundException('请先加入挑战'); } + const completionTarget = challenge.minimumCheckInDays + + if (completionTarget <= 0) { + throw new BadRequestException('挑战配置存在问题,请联系管理员'); + } + const increment = dto.increment ?? 1; if (increment < 1) { throw new BadRequestException('进度增量必须大于 0'); } + let participantNeedsSave = false; + if (participant.targetValue !== completionTarget) { + participant.targetValue = completionTarget; + participantNeedsSave = true; + } + + if (participant.progressValue >= completionTarget && participant.status !== ChallengeParticipantStatus.COMPLETED) { + participant.status = ChallengeParticipantStatus.COMPLETED; + participantNeedsSave = true; + } + const reportDate = dayjs().format('YYYY-MM-DD'); const now = new Date(); - const remainingCapacity = Math.max(participant.targetValue - participant.progressValue, 0); + const remainingCapacity = Math.max(completionTarget - participant.progressValue, 0); const effectiveIncrement = Math.min(increment, remainingCapacity); let created = false; @@ -328,41 +359,49 @@ export class ChallengesService { } } catch (error) { if (error instanceof UniqueConstraintError) { - return this.buildChallengeProgress(participant.progressValue, participant.targetValue, challenge.progressUnit); + if (participantNeedsSave) { + await participant.save(); + } + return this.buildChallengeProgress(participant.progressValue, completionTarget); } throw error; } if (!created) { - return this.buildChallengeProgress(participant.progressValue, participant.targetValue, challenge.progressUnit); + if (participantNeedsSave) { + await participant.save(); + } + return this.buildChallengeProgress(participant.progressValue, completionTarget); } if (effectiveIncrement > 0) { - const newProgress = Math.min(participant.progressValue + effectiveIncrement, participant.targetValue); + const newProgress = Math.min(participant.progressValue + effectiveIncrement, completionTarget); participant.progressValue = newProgress; participant.lastProgressAt = now; - if (participant.progressValue >= participant.targetValue) { + if (participant.progressValue >= completionTarget) { participant.status = ChallengeParticipantStatus.COMPLETED; } + participantNeedsSave = true; + } + if (participantNeedsSave) { await participant.save(); } - return this.buildChallengeProgress(participant.progressValue, participant.targetValue, challenge.progressUnit); + return this.buildChallengeProgress(participant.progressValue, completionTarget); } - private buildChallengeProgress(completed: number, target: number, unit: string): ChallengeProgressDto { + private buildChallengeProgress(completed: number, target: number, unit = '天'): ChallengeProgressDto { const remaining = Math.max(target - completed, 0); return { completed, target, remaining, - badge: completed >= target ? '已完成' : `已坚持 ${completed}${unit}`, - subtitle: remaining > 0 ? `还差 ${remaining}${unit}` : undefined, }; } + private computeStatus(startAt: number, endAt: number): ChallengeStatus { const now = dayjs(); const start = dayjs(startAt); diff --git a/src/challenges/dto/challenge-detail.dto.ts b/src/challenges/dto/challenge-detail.dto.ts index 3777bae..a660a22 100644 --- a/src/challenges/dto/challenge-detail.dto.ts +++ b/src/challenges/dto/challenge-detail.dto.ts @@ -12,6 +12,7 @@ export interface ChallengeDetailDto { highlightTitle: string; highlightSubtitle: string; ctaLabel: string; + minimumCheckInDays: number; participantsCount: number; progress?: ChallengeProgressDto; rankings: RankingItemDto[]; diff --git a/src/challenges/dto/challenge-list.dto.ts b/src/challenges/dto/challenge-list.dto.ts index 54ebdc1..2f2bf0e 100644 --- a/src/challenges/dto/challenge-list.dto.ts +++ b/src/challenges/dto/challenge-list.dto.ts @@ -16,6 +16,7 @@ export interface ChallengeListItemDto { highlightTitle: string; highlightSubtitle: string; ctaLabel: string; + minimumCheckInDays: number; progress?: ChallengeProgressDto; isJoined: boolean; } diff --git a/src/challenges/dto/challenge-progress.dto.ts b/src/challenges/dto/challenge-progress.dto.ts index 33eefc0..deb5fc3 100644 --- a/src/challenges/dto/challenge-progress.dto.ts +++ b/src/challenges/dto/challenge-progress.dto.ts @@ -2,8 +2,6 @@ export interface ChallengeProgressDto { completed: number; target: number; remaining: number; - badge: string; - subtitle?: string; } export interface RankingItemDto { diff --git a/src/challenges/models/challenge.model.ts b/src/challenges/models/challenge.model.ts index 304aec9..76fe08f 100644 --- a/src/challenges/models/challenge.model.ts +++ b/src/challenges/models/challenge.model.ts @@ -90,6 +90,14 @@ export class Challenge extends Model { }) declare progressUnit: string; + @Column({ + type: DataType.INTEGER, + allowNull: false, + defaultValue: 0, + comment: '最低打卡天数,用于判断挑战成功', + }) + declare minimumCheckInDays: number; + @Column({ type: DataType.STRING(255), allowNull: true,