feat(badges): 添加用户勋章系统,支持睡眠挑战勋章自动授予
实现完整的用户勋章功能模块: - 新增 BadgeConfig 和 UserBadge 数据模型,支持勋章配置和用户勋章管理 - 新增 BadgeService 服务,提供勋章授予、查询、展示状态管理等核心功能 - 在挑战服务中集成勋章授予逻辑,完成首次睡眠打卡授予 goodSleep 勋章,完成睡眠挑战授予 sleepChallengeMonth 勋章 - 新增用户勋章相关接口:获取用户勋章列表、获取可用勋章列表、标记勋章已展示 - 支持勋章分类(睡眠、运动、饮食等)、排序、启用状态管理 - 支持勋章来源追踪(挑战、系统、手动授予)和元数据记录
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user