feat(badges): 添加用户勋章系统,支持睡眠挑战勋章自动授予

实现完整的用户勋章功能模块:
- 新增 BadgeConfig 和 UserBadge 数据模型,支持勋章配置和用户勋章管理
- 新增 BadgeService 服务,提供勋章授予、查询、展示状态管理等核心功能
- 在挑战服务中集成勋章授予逻辑,完成首次睡眠打卡授予 goodSleep 勋章,完成睡眠挑战授予 sleepChallengeMonth 勋章
- 新增用户勋章相关接口:获取用户勋章列表、获取可用勋章列表、标记勋章已展示
- 支持勋章分类(睡眠、运动、饮食等)、排序、启用状态管理
- 支持勋章来源追踪(挑战、系统、手动授予)和元数据记录
This commit is contained in:
richarjiang
2025-11-14 17:08:02 +08:00
parent f04c2ccd5d
commit 7b4d7c4459
8 changed files with 792 additions and 3 deletions

View File

@@ -1,6 +1,6 @@
import { Injectable, NotFoundException, BadRequestException, ConflictException, Inject } from '@nestjs/common';
import { InjectModel } from '@nestjs/sequelize';
import { Challenge, ChallengeStatus } from './models/challenge.model';
import { Challenge, ChallengeStatus, ChallengeType } 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';
@@ -13,6 +13,8 @@ import * as dayjs from 'dayjs';
import { User } from '../users/models/user.model';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger as WinstonLogger } from 'winston';
import { BadgeService } from '../users/services/badge.service';
import { BadgeSource } from '../users/models/user-badge.model';
@Injectable()
export class ChallengesService {
@@ -26,6 +28,7 @@ export class ChallengesService {
private readonly participantModel: typeof ChallengeParticipant,
@InjectModel(ChallengeProgressReport)
private readonly progressReportModel: typeof ChallengeProgressReport,
private readonly badgeService: BadgeService,
) { }
async getChallengesForUser(userId?: string): Promise<ChallengeListItemDto[]> {
@@ -432,10 +435,70 @@ export class ChallengesService {
if (report.reportedValue >= reportCompletedValue && !dayjs(participant.lastProgressAt).isSame(dayjs(), 'd')) {
participant.progressValue++
participant.lastProgressAt = now;
// 🎖️ 检查是否为睡眠挑战且完成了第一次打卡,授予 goodSleep 勋章
if (challenge.type === ChallengeType.SLEEP) {
try {
await this.badgeService.awardBadge(userId, 'goodSleep', {
source: BadgeSource.CHALLENGE,
sourceId: challengeId,
metadata: {
challengeName: challenge.title,
challengeType: challenge.type,
},
});
this.winstonLogger.info('授予睡眠挑战勋章成功', {
context: 'reportProgress',
userId,
challengeId,
badgeCode: 'goodSleep',
});
} catch (error) {
// 勋章授予失败不应影响主流程,仅记录日志
this.winstonLogger.error('授予睡眠挑战勋章失败', {
context: 'reportProgress',
userId,
challengeId,
error: error instanceof Error ? error.message : '未知错误',
});
}
}
}
if (participant.progressValue >= (participant.challenge?.minimumCheckInDays || 0) && participant.status !== ChallengeParticipantStatus.COMPLETED) {
participant.status = ChallengeParticipantStatus.COMPLETED;
// 🎖️ 完成睡眠挑战时,授予 sleepChallengeMonth 勋章
if (challenge.type === ChallengeType.SLEEP) {
try {
await this.badgeService.awardBadge(userId, 'sleepChallengeMonth', {
source: BadgeSource.CHALLENGE,
sourceId: challengeId,
metadata: {
challengeName: challenge.title,
challengeType: challenge.type,
completedDays: participant.progressValue,
completedAt: new Date(),
},
});
this.winstonLogger.info('授予睡眠挑战完成勋章成功', {
context: 'reportProgress',
userId,
challengeId,
badgeCode: 'sleepChallengeMonth',
completedDays: participant.progressValue,
});
} catch (error) {
// 勋章授予失败不应影响主流程,仅记录日志
this.winstonLogger.error('授予睡眠挑战完成勋章失败', {
context: 'reportProgress',
userId,
challengeId,
badgeCode: 'sleepChallengeMonth',
error: error instanceof Error ? error.message : '未知错误',
});
}
}
}
await participant.save();