|
|
|
|
@@ -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<boolean> {
|
|
|
|
|
@@ -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);
|
|
|
|
|
|