feat(challenges): 新增每日进度上报防重复机制

- 创建 t_challenge_progress_reports 表记录用户每日上报
- 通过唯一索引 (challenge_id, user_id, report_date) 确保每日仅一次有效上报
- 更新 progress 时先写入报告表,冲突则直接返回当前进度
- 模块中新增 ChallengeProgressReport 模型及相关依赖
This commit is contained in:
richarjiang
2025-09-28 12:13:31 +08:00
parent 1b7132a325
commit ae8039c9ed
4 changed files with 127 additions and 8 deletions

View File

@@ -2,11 +2,12 @@ import { Injectable, NotFoundException, BadRequestException, ConflictException }
import { InjectModel } from '@nestjs/sequelize';
import { Challenge, ChallengeStatus } from './models/challenge.model';
import { ChallengeParticipant, ChallengeParticipantStatus } from './models/challenge-participant.model';
import { ChallengeProgressReport } from './models/challenge-progress-report.model';
import { UpdateChallengeProgressDto } from './dto/update-challenge-progress.dto';
import { ChallengeDetailDto } from './dto/challenge-detail.dto';
import { ChallengeListItemDto } from './dto/challenge-list.dto';
import { ChallengeProgressDto, RankingItemDto } from './dto/challenge-progress.dto';
import { fn, col, Op } from 'sequelize';
import { fn, col, Op, UniqueConstraintError } from 'sequelize';
import * as dayjs from 'dayjs';
import { User } from '../users/models/user.model';
@@ -17,6 +18,8 @@ export class ChallengesService {
private readonly challengeModel: typeof Challenge,
@InjectModel(ChallengeParticipant)
private readonly participantModel: typeof ChallengeParticipant,
@InjectModel(ChallengeProgressReport)
private readonly progressReportModel: typeof ChallengeProgressReport,
) { }
async getChallengesForUser(userId: string): Promise<ChallengeListItemDto[]> {
@@ -266,15 +269,55 @@ export class ChallengesService {
throw new BadRequestException('进度增量必须大于 0');
}
const newProgress = participant.progressValue + increment;
participant.progressValue = Math.min(newProgress, participant.targetValue);
participant.lastProgressAt = new Date();
const reportDate = dayjs().format('YYYY-MM-DD');
const now = new Date();
const remainingCapacity = Math.max(participant.targetValue - participant.progressValue, 0);
const effectiveIncrement = Math.min(increment, remainingCapacity);
if (participant.progressValue >= participant.targetValue) {
participant.status = ChallengeParticipantStatus.COMPLETED;
let created = false;
try {
const [report, wasCreated] = await this.progressReportModel.findOrCreate({
where: {
challengeId,
userId,
reportDate,
},
defaults: {
incrementValue: effectiveIncrement,
reportedAt: now,
},
});
created = wasCreated;
if (wasCreated) {
await report.update({
incrementValue: effectiveIncrement,
reportedAt: now,
});
}
} catch (error) {
if (error instanceof UniqueConstraintError) {
return this.buildChallengeProgress(participant.progressValue, participant.targetValue, challenge.progressUnit);
}
throw error;
}
await participant.save();
if (!created) {
return this.buildChallengeProgress(participant.progressValue, participant.targetValue, challenge.progressUnit);
}
if (effectiveIncrement > 0) {
const newProgress = Math.min(participant.progressValue + effectiveIncrement, participant.targetValue);
participant.progressValue = newProgress;
participant.lastProgressAt = now;
if (participant.progressValue >= participant.targetValue) {
participant.status = ChallengeParticipantStatus.COMPLETED;
}
await participant.save();
}
return this.buildChallengeProgress(participant.progressValue, participant.targetValue, challenge.progressUnit);
}