feat(challenges): 新增每日进度上报防重复机制
- 创建 t_challenge_progress_reports 表记录用户每日上报 - 通过唯一索引 (challenge_id, user_id, report_date) 确保每日仅一次有效上报 - 更新 progress 时先写入报告表,冲突则直接返回当前进度 - 模块中新增 ChallengeProgressReport 模型及相关依赖
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user