feat(challenges): 新增每日进度上报防重复机制
- 创建 t_challenge_progress_reports 表记录用户每日上报 - 通过唯一索引 (challenge_id, user_id, report_date) 确保每日仅一次有效上报 - 更新 progress 时先写入报告表,冲突则直接返回当前进度 - 模块中新增 ChallengeProgressReport 模型及相关依赖
This commit is contained in:
@@ -4,12 +4,13 @@ import { ChallengesController } from './challenges.controller';
|
||||
import { ChallengesService } from './challenges.service';
|
||||
import { Challenge } from './models/challenge.model';
|
||||
import { ChallengeParticipant } from './models/challenge-participant.model';
|
||||
import { ChallengeProgressReport } from './models/challenge-progress-report.model';
|
||||
import { UsersModule } from '../users/users.module';
|
||||
import { User } from '../users/models/user.model';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
SequelizeModule.forFeature([Challenge, ChallengeParticipant, User]),
|
||||
SequelizeModule.forFeature([Challenge, ChallengeParticipant, ChallengeProgressReport, User]),
|
||||
UsersModule,
|
||||
],
|
||||
controllers: [ChallengesController],
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
61
src/challenges/models/challenge-progress-report.model.ts
Normal file
61
src/challenges/models/challenge-progress-report.model.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Table, Column, DataType, Model, ForeignKey, BelongsTo, Index } from 'sequelize-typescript';
|
||||
import { Challenge } from './challenge.model';
|
||||
import { User } from '../../users/models/user.model';
|
||||
|
||||
@Table({
|
||||
tableName: 't_challenge_progress_reports',
|
||||
underscored: true,
|
||||
})
|
||||
export class ChallengeProgressReport extends Model {
|
||||
@Column({
|
||||
type: DataType.CHAR(36),
|
||||
defaultValue: DataType.UUIDV4,
|
||||
primaryKey: true,
|
||||
})
|
||||
declare id: string;
|
||||
|
||||
@ForeignKey(() => Challenge)
|
||||
@Column({
|
||||
type: DataType.CHAR(36),
|
||||
allowNull: false,
|
||||
comment: '挑战 ID',
|
||||
})
|
||||
declare challengeId: string;
|
||||
|
||||
@ForeignKey(() => User)
|
||||
@Column({
|
||||
type: DataType.STRING(64),
|
||||
allowNull: false,
|
||||
comment: '用户 ID',
|
||||
})
|
||||
declare userId: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.DATEONLY,
|
||||
allowNull: false,
|
||||
comment: '自然日,确保每日仅上报一次',
|
||||
})
|
||||
declare reportDate: string;
|
||||
|
||||
@Column({
|
||||
type: DataType.INTEGER,
|
||||
allowNull: false,
|
||||
defaultValue: 1,
|
||||
comment: '本次上报的进度增量',
|
||||
})
|
||||
declare incrementValue: number;
|
||||
|
||||
@Column({
|
||||
type: DataType.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataType.NOW,
|
||||
comment: '上报时间戳',
|
||||
})
|
||||
declare reportedAt: Date;
|
||||
|
||||
@BelongsTo(() => Challenge)
|
||||
declare challenge?: Challenge;
|
||||
|
||||
@BelongsTo(() => User)
|
||||
declare user?: User;
|
||||
}
|
||||
Reference in New Issue
Block a user