feat(challenges): 新增挑战类型字段并重构进度上报逻辑

- 数据库新增 type 列区分 water/exercise/diet/mood/sleep/weight 六类挑战
- 进度上报由增量模式改为绝对值模式,字段 increment_value → reportedValue
- 服务层按 challenge.targetValue 判断当日是否完成,再按 minimumCheckInDays 统计总进度
- 相关 DTO 与模型同步更新,支持新类型返回

BREAKING CHANGE: 上报接口字段由 increment 改为 value,且为当日绝对值
This commit is contained in:
richarjiang
2025-09-29 15:14:48 +08:00
parent d87fc84575
commit 64460a9d68
7 changed files with 103 additions and 59 deletions

View File

@@ -96,6 +96,7 @@ export class ChallengesService {
minimumCheckInDays: completionTarget,
progress,
isJoined: Boolean(participation),
type: challenge.type,
};
});
}
@@ -199,6 +200,7 @@ export class ChallengesService {
progress,
rankings,
userRank,
type: challenge.type,
};
}
@@ -267,6 +269,7 @@ export class ChallengesService {
[Op.ne]: ChallengeParticipantStatus.LEFT,
},
},
});
if (!participant) {
@@ -302,40 +305,45 @@ export class ChallengesService {
userId,
status: ChallengeParticipantStatus.ACTIVE,
},
include: [{
model: Challenge,
as: 'challenge',
attributes: ['minimumCheckInDays', 'targetValue'],
}],
});
this.winstonLogger.info('start report progress', {
context: 'reportProgress',
userId,
challengeId,
participant,
});
if (!participant) {
throw new NotFoundException('请先加入挑战');
}
const completionTarget = challenge.minimumCheckInDays
// 如果要完成当日挑战,最低的上报数据
const reportCompletedValue = challenge.targetValue
if (completionTarget <= 0) {
if (reportCompletedValue <= 0) {
throw new BadRequestException('挑战配置存在问题,请联系管理员');
}
const increment = dto.increment ?? 1;
if (increment < 1) {
throw new BadRequestException('进度增量必须大于 0');
if (dto.value === undefined || dto.value === null) {
throw new BadRequestException('缺少上报的进度数据');
}
let participantNeedsSave = false;
if (participant.targetValue !== completionTarget) {
participant.targetValue = completionTarget;
participantNeedsSave = true;
if (dto.value < 0) {
throw new BadRequestException('进度数据必须大于等于 0');
}
if (participant.progressValue >= completionTarget && participant.status !== ChallengeParticipantStatus.COMPLETED) {
participant.status = ChallengeParticipantStatus.COMPLETED;
participantNeedsSave = true;
}
const reportedValue = dto.value;
const reportDate = dayjs().format('YYYY-MM-DD');
const now = new Date();
const remainingCapacity = Math.max(completionTarget - participant.progressValue, 0);
const effectiveIncrement = Math.min(increment, remainingCapacity);
let created = false;
try {
const [report, wasCreated] = await this.progressReportModel.findOrCreate({
where: {
@@ -344,52 +352,53 @@ export class ChallengesService {
reportDate,
},
defaults: {
incrementValue: effectiveIncrement,
reportedValue,
reportedAt: now,
},
});
created = wasCreated;
if (wasCreated) {
await report.update({
incrementValue: effectiveIncrement,
reportedAt: now,
});
if (report.reportedValue !== reportedValue) {
await report.update({
reportedValue,
reportedAt: now,
});
}
} else {
if (report.reportedValue !== reportedValue) {
report.reportedAt = now;
report.reportedValue = reportedValue;
await report.save();
}
}
if (report.reportedValue >= reportCompletedValue && !dayjs(participant.lastProgressAt).isSame(dayjs(), 'd')) {
participant.progressValue++
participant.lastProgressAt = now;
}
if (participant.progressValue >= (participant.challenge?.minimumCheckInDays || 0) && participant.status !== ChallengeParticipantStatus.COMPLETED) {
participant.status = ChallengeParticipantStatus.COMPLETED;
}
await participant.save();
this.winstonLogger.info('end report progress', {
context: 'reportProgress',
userId,
challengeId,
participant,
});
return this.buildChallengeProgress(participant.progressValue, participant.targetValue);
} catch (error) {
if (error instanceof UniqueConstraintError) {
if (participantNeedsSave) {
await participant.save();
}
return this.buildChallengeProgress(participant.progressValue, completionTarget);
return this.buildChallengeProgress(participant.progressValue, participant.targetValue);
}
throw error;
}
if (!created) {
if (participantNeedsSave) {
await participant.save();
}
return this.buildChallengeProgress(participant.progressValue, completionTarget);
}
if (effectiveIncrement > 0) {
const newProgress = Math.min(participant.progressValue + effectiveIncrement, completionTarget);
participant.progressValue = newProgress;
participant.lastProgressAt = now;
if (participant.progressValue >= completionTarget) {
participant.status = ChallengeParticipantStatus.COMPLETED;
}
participantNeedsSave = true;
}
if (participantNeedsSave) {
await participant.save();
}
return this.buildChallengeProgress(participant.progressValue, completionTarget);
}
private buildChallengeProgress(completed: number, target: number, unit = '天'): ChallengeProgressDto {