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 { Injectable, NotFoundException, BadRequestException, ConflictException, Inject } from '@nestjs/common';
import { InjectModel } from '@nestjs/sequelize'; 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 { ChallengeParticipant, ChallengeParticipantStatus } from './models/challenge-participant.model';
import { ChallengeProgressReport } from './models/challenge-progress-report.model'; import { ChallengeProgressReport } from './models/challenge-progress-report.model';
import { UpdateChallengeProgressDto } from './dto/update-challenge-progress.dto'; 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 { User } from '../users/models/user.model';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger as WinstonLogger } from 'winston'; import { Logger as WinstonLogger } from 'winston';
import { BadgeService } from '../users/services/badge.service';
import { BadgeSource } from '../users/models/user-badge.model';
@Injectable() @Injectable()
export class ChallengesService { export class ChallengesService {
@@ -26,6 +28,7 @@ export class ChallengesService {
private readonly participantModel: typeof ChallengeParticipant, private readonly participantModel: typeof ChallengeParticipant,
@InjectModel(ChallengeProgressReport) @InjectModel(ChallengeProgressReport)
private readonly progressReportModel: typeof ChallengeProgressReport, private readonly progressReportModel: typeof ChallengeProgressReport,
private readonly badgeService: BadgeService,
) { } ) { }
async getChallengesForUser(userId?: string): Promise<ChallengeListItemDto[]> { async getChallengesForUser(userId?: string): Promise<ChallengeListItemDto[]> {
@@ -432,10 +435,70 @@ export class ChallengesService {
if (report.reportedValue >= reportCompletedValue && !dayjs(participant.lastProgressAt).isSame(dayjs(), 'd')) { if (report.reportedValue >= reportCompletedValue && !dayjs(participant.lastProgressAt).isSame(dayjs(), 'd')) {
participant.progressValue++ participant.progressValue++
participant.lastProgressAt = now; 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) { if (participant.progressValue >= (participant.challenge?.minimumCheckInDays || 0) && participant.status !== ChallengeParticipantStatus.COMPLETED) {
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(); await participant.save();

157
src/users/dto/badge.dto.ts Normal file
View File

@@ -0,0 +1,157 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsOptional, IsString, IsBoolean, IsDateString, IsNotEmpty } from 'class-validator';
import { BaseResponseDto } from '../../base.dto';
// 勋章基本信息
export class BadgeInfoDto {
@ApiProperty({ description: '勋章代码', example: 'goodSleep' })
code: string;
@ApiProperty({ description: '勋章名称', example: '好眠达人' })
name: string;
@ApiProperty({ description: '勋章描述', example: '完成首次睡眠挑战' })
description: string;
@ApiProperty({ description: '勋章图片URL' })
imageUrl: string;
@ApiProperty({ description: '勋章分类', example: 'sleep' })
category: string;
@ApiProperty({ description: '排序顺序', example: 1 })
sortOrder: number;
}
// 用户勋章信息
export class UserBadgeDto {
@ApiProperty({ description: '记录ID', example: 1 })
id: number;
@ApiProperty({ description: '勋章代码', example: 'goodSleep' })
code: string;
@ApiProperty({ description: '勋章名称', example: '好眠达人' })
name: string;
@ApiProperty({ description: '勋章描述', example: '完成首次睡眠挑战' })
description: string;
@ApiProperty({ description: '勋章图片URL' })
imageUrl: string;
@ApiProperty({ description: '勋章分类', example: 'sleep' })
category: string;
@ApiProperty({ description: '获得时间' })
awardedAt: Date;
@ApiProperty({ description: '授予来源', example: 'challenge' })
source: string;
@ApiProperty({ description: '来源ID如挑战ID', required: false })
sourceId?: string;
@ApiProperty({ description: '元数据', required: false })
metadata?: Record<string, any>;
@ApiProperty({ description: '是否已展示过', example: false })
isShow: boolean;
}
// 可用勋章信息(包含是否已获得)
export class AvailableBadgeDto extends BadgeInfoDto {
@ApiProperty({ description: '是否已获得', example: false })
isAwarded: boolean;
@ApiProperty({ description: '获得时间(如果已获得)', required: false })
awardedAt?: Date;
@ApiProperty({ description: '是否已展示过(如果已获得)', required: false })
isShow?: boolean;
}
// 获取用户勋章列表响应
export class GetUserBadgesResponseDto implements BaseResponseDto<{
badges: UserBadgeDto[];
total: number;
}> {
@ApiProperty({ description: '响应状态码', example: 0 })
code: number;
@ApiProperty({ description: '响应消息', example: 'success' })
message: string;
@ApiProperty({
description: '用户勋章数据',
example: {
badges: [
{
id: 1,
code: 'goodSleep',
name: '好眠达人',
description: '完成首次睡眠挑战',
imageUrl: 'https://example.com/badge.png',
category: 'sleep',
awardedAt: '2025-01-14T07:00:00Z',
source: 'challenge',
sourceId: 'challenge-uuid',
},
],
total: 1,
},
})
data: {
badges: UserBadgeDto[];
total: number;
};
}
// 获取所有可用勋章响应
export class GetAvailableBadgesResponseDto implements BaseResponseDto<AvailableBadgeDto[]> {
@ApiProperty({ description: '响应状态码', example: 0 })
code: number;
@ApiProperty({ description: '响应消息', example: 'success' })
message: string;
@ApiProperty({
description: '所有可用勋章列表',
example: [
{
code: 'goodSleep',
name: '好眠达人',
description: '完成首次睡眠挑战',
imageUrl: 'https://example.com/badge.png',
category: 'sleep',
sortOrder: 1,
isAwarded: true,
awardedAt: '2025-01-14T07:00:00Z',
},
],
})
data: AvailableBadgeDto[];
}
// 标记勋章已展示请求
export class MarkBadgeShownDto {
@ApiProperty({ description: '勋章代码', example: 'goodSleep' })
@IsString()
@IsNotEmpty()
badgeCode: string;
}
// 标记勋章已展示响应
export class MarkBadgeShownResponseDto implements BaseResponseDto<{ success: boolean }> {
@ApiProperty({ description: '响应状态码', example: 0 })
code: number;
@ApiProperty({ description: '响应消息', example: 'success' })
message: string;
@ApiProperty({
description: '操作结果',
example: { success: true },
})
data: { success: boolean };
}

View File

@@ -0,0 +1,90 @@
import { Column, Model, Table, DataType } from 'sequelize-typescript';
export enum BadgeCategory {
SLEEP = 'sleep',
EXERCISE = 'exercise',
DIET = 'diet',
WATER = 'water',
MOOD = 'mood',
WEIGHT = 'weight',
GENERAL = 'general',
}
@Table({
tableName: 't_badge_configs',
underscored: true,
timestamps: true,
})
export class BadgeConfig extends Model {
@Column({
type: DataType.CHAR(36),
defaultValue: DataType.UUIDV4,
primaryKey: true,
})
declare id: string;
@Column({
type: DataType.STRING(64),
allowNull: false,
unique: true,
comment: '勋章唯一标识码',
})
declare code: string;
@Column({
type: DataType.STRING(128),
allowNull: false,
comment: '勋章名称',
})
declare name: string;
@Column({
type: DataType.TEXT,
allowNull: true,
comment: '勋章描述',
})
declare description: string;
@Column({
type: DataType.STRING(512),
allowNull: false,
comment: '勋章图片URL',
})
declare imageUrl: string;
@Column({
type: DataType.ENUM(...Object.values(BadgeCategory)),
allowNull: false,
defaultValue: BadgeCategory.GENERAL,
comment: '勋章分类',
})
declare category: BadgeCategory;
@Column({
type: DataType.BOOLEAN,
allowNull: false,
defaultValue: true,
comment: '是否启用',
})
declare isActive: boolean;
@Column({
type: DataType.INTEGER,
allowNull: false,
defaultValue: 0,
comment: '排序顺序',
})
declare sortOrder: number;
@Column({
type: DataType.DATE,
defaultValue: DataType.NOW,
})
declare createdAt: Date;
@Column({
type: DataType.DATE,
defaultValue: DataType.NOW,
})
declare updatedAt: Date;
}

View File

@@ -0,0 +1,105 @@
import { Column, Model, Table, DataType, ForeignKey, BelongsTo } from 'sequelize-typescript';
import { User } from './user.model';
import { BadgeConfig } from './badge-config.model';
export enum BadgeSource {
CHALLENGE = 'challenge',
MANUAL = 'manual',
SYSTEM = 'system',
}
@Table({
tableName: 't_user_badges',
underscored: true,
timestamps: true,
indexes: [
{
unique: true,
fields: ['user_id', 'badge_code'],
name: 'unique_user_badge',
},
{
fields: ['user_id'],
},
{
fields: ['badge_code'],
},
{
fields: ['awarded_at'],
},
],
})
export class UserBadge extends Model {
@Column({
type: DataType.INTEGER,
primaryKey: true,
autoIncrement: true,
})
declare id: number;
@ForeignKey(() => User)
@Column({
type: DataType.STRING,
allowNull: false,
comment: '用户ID',
})
declare userId: string;
@ForeignKey(() => BadgeConfig)
@Column({
type: DataType.STRING(64),
allowNull: false,
comment: '勋章代码',
})
declare badgeCode: string;
@Column({
type: DataType.DATE,
allowNull: false,
defaultValue: DataType.NOW,
comment: '获得时间',
})
declare awardedAt: Date;
@Column({
type: DataType.ENUM(...Object.values(BadgeSource)),
allowNull: false,
defaultValue: BadgeSource.SYSTEM,
comment: '授予来源',
})
declare source: BadgeSource;
@Column({
type: DataType.STRING(128),
allowNull: true,
comment: '来源ID如挑战ID',
})
declare sourceId: string;
@Column({
type: DataType.JSON,
allowNull: true,
comment: '额外元数据',
})
declare metadata: Record<string, any>;
@Column({
type: DataType.BOOLEAN,
allowNull: false,
defaultValue: false,
comment: '是否已展示过客户端展示勋章获得动画后设置为true',
})
declare isShow: boolean;
@Column({
type: DataType.DATE,
defaultValue: DataType.NOW,
})
declare createdAt: Date;
@BelongsTo(() => User)
declare user: User;
@BelongsTo(() => BadgeConfig, 'badgeCode')
declare badge: BadgeConfig;
}

View File

@@ -0,0 +1,236 @@
import { Injectable, Logger, ConflictException } from '@nestjs/common';
import { InjectModel } from '@nestjs/sequelize';
import { BadgeConfig } from '../models/badge-config.model';
import { UserBadge, BadgeSource } from '../models/user-badge.model';
import { Op } from 'sequelize';
@Injectable()
export class BadgeService {
private readonly logger = new Logger(BadgeService.name);
constructor(
@InjectModel(BadgeConfig)
private readonly badgeConfigModel: typeof BadgeConfig,
@InjectModel(UserBadge)
private readonly userBadgeModel: typeof UserBadge,
) {}
/**
* 授予用户勋章
* @param userId 用户ID
* @param badgeCode 勋章代码
* @param options 额外选项
*/
async awardBadge(
userId: string,
badgeCode: string,
options?: {
source?: BadgeSource;
sourceId?: string;
metadata?: Record<string, any>;
},
): Promise<UserBadge | null> {
try {
// 检查勋章配置是否存在且启用
const badgeConfig = await this.badgeConfigModel.findOne({
where: { code: badgeCode, isActive: true },
});
if (!badgeConfig) {
this.logger.warn(`勋章配置不存在或未启用: ${badgeCode}`);
return null;
}
// 检查用户是否已经拥有该勋章
const existingBadge = await this.userBadgeModel.findOne({
where: { userId, badgeCode },
});
if (existingBadge) {
this.logger.log(`用户 ${userId} 已拥有勋章 ${badgeCode},跳过授予`);
return existingBadge;
}
// 创建用户勋章记录
const userBadge = await this.userBadgeModel.create({
userId,
badgeCode,
awardedAt: new Date(),
source: options?.source || BadgeSource.SYSTEM,
sourceId: options?.sourceId,
metadata: options?.metadata,
});
this.logger.log(`成功授予用户 ${userId} 勋章 ${badgeCode}`);
return userBadge;
} catch (error) {
this.logger.error(`授予勋章失败: ${error instanceof Error ? error.message : '未知错误'}`);
// 如果是唯一约束冲突,说明已经拥有该勋章
if (error.name === 'SequelizeUniqueConstraintError') {
return null;
}
throw error;
}
}
/**
* 获取用户所有勋章
* @param userId 用户ID
*/
async getUserBadges(userId: string): Promise<Array<{
id: number;
code: string;
name: string;
description: string;
imageUrl: string;
category: string;
awardedAt: Date;
source: string;
sourceId: string;
metadata: Record<string, any>;
isShow: boolean;
}>> {
const userBadges = await this.userBadgeModel.findAll({
where: { userId },
include: [
{
model: BadgeConfig,
as: 'badge',
attributes: ['code', 'name', 'description', 'imageUrl', 'category'],
},
],
order: [['awardedAt', 'DESC']],
});
return userBadges.map((ub) => ({
id: ub.id,
code: ub.badgeCode,
name: ub.badge?.name || '',
description: ub.badge?.description || '',
imageUrl: ub.badge?.imageUrl || '',
category: ub.badge?.category || '',
awardedAt: ub.awardedAt,
source: ub.source,
sourceId: ub.sourceId,
metadata: ub.metadata,
isShow: ub.isShow,
}));
}
/**
* 获取所有可用勋章(包含用户是否已获得信息)
* @param userId 用户ID可选
*/
async getAvailableBadges(userId?: string): Promise<Array<{
code: string;
name: string;
description: string;
imageUrl: string;
category: string;
sortOrder: number;
isAwarded: boolean;
awardedAt?: Date;
isShow?: boolean;
}>> {
// 获取所有启用的勋章配置
const badgeConfigs = await this.badgeConfigModel.findAll({
where: { isActive: true },
order: [['sortOrder', 'ASC'], ['createdAt', 'ASC']],
});
if (!userId) {
// 如果没有提供用户ID返回所有勋章标记为未获得
return badgeConfigs.map((bc) => ({
code: bc.code,
name: bc.name,
description: bc.description,
imageUrl: bc.imageUrl,
category: bc.category,
sortOrder: bc.sortOrder,
isAwarded: false,
isShow: false
}));
}
// 获取用户已拥有的勋章
const userBadges = await this.userBadgeModel.findAll({
where: {
userId,
badgeCode: {
[Op.in]: badgeConfigs.map((bc) => bc.code),
},
},
});
const userBadgeMap = new Map(
userBadges.map((ub) => [ub.badgeCode, { awardedAt: ub.awardedAt, isShow: ub.isShow }]),
);
return badgeConfigs.map((bc) => {
const badgeInfo = userBadgeMap.get(bc.code);
return {
code: bc.code,
name: bc.name,
description: bc.description,
imageUrl: bc.imageUrl,
category: bc.category,
sortOrder: bc.sortOrder,
isAwarded: !!badgeInfo,
awardedAt: badgeInfo?.awardedAt || undefined,
isShow: badgeInfo?.isShow || false,
};
});
}
/**
* 检查用户是否拥有指定勋章
* @param userId 用户ID
* @param badgeCode 勋章代码
*/
async hasBadge(userId: string, badgeCode: string): Promise<boolean> {
const count = await this.userBadgeModel.count({
where: { userId, badgeCode },
});
return count > 0;
}
/**
* 获取用户勋章数量
* @param userId 用户ID
*/
async getUserBadgeCount(userId: string): Promise<number> {
return await this.userBadgeModel.count({
where: { userId },
});
}
/**
* 标记勋章已展示
* @param userId 用户ID
* @param badgeCode 勋章代码
*/
async markBadgeAsShown(userId: string, badgeCode: string): Promise<boolean> {
try {
const userBadge = await this.userBadgeModel.findOne({
where: { userId, badgeCode },
});
if (!userBadge) {
this.logger.warn(`用户 ${userId} 不拥有勋章 ${badgeCode},无法标记为已展示`);
return false;
}
if (userBadge.isShow) {
this.logger.log(`用户 ${userId} 的勋章 ${badgeCode} 已经标记为展示过,跳过更新`);
return true;
}
await userBadge.update({ isShow: true });
this.logger.log(`成功标记用户 ${userId} 的勋章 ${badgeCode} 为已展示`);
return true;
} catch (error) {
this.logger.error(`标记勋章已展示失败: ${error instanceof Error ? error.message : '未知错误'}`);
throw error;
}
}
}

View File

@@ -35,6 +35,7 @@ import { RestorePurchaseDto, RestorePurchaseResponseDto } from './dto/restore-pu
import { GetUserActivityHistoryResponseDto } from './dto/user-activity.dto'; import { GetUserActivityHistoryResponseDto } from './dto/user-activity.dto';
import { UpdateWeightRecordDto, WeightRecordResponseDto, DeleteWeightRecordResponseDto } from './dto/weight-record.dto'; import { UpdateWeightRecordDto, WeightRecordResponseDto, DeleteWeightRecordResponseDto } from './dto/weight-record.dto';
import { UpdateBodyMeasurementDto, UpdateBodyMeasurementResponseDto, GetBodyMeasurementHistoryResponseDto, GetBodyMeasurementAnalysisDto, GetBodyMeasurementAnalysisResponseDto } from './dto/body-measurement.dto'; import { UpdateBodyMeasurementDto, UpdateBodyMeasurementResponseDto, GetBodyMeasurementHistoryResponseDto, GetBodyMeasurementAnalysisDto, GetBodyMeasurementAnalysisResponseDto } from './dto/body-measurement.dto';
import { GetUserBadgesResponseDto, GetAvailableBadgesResponseDto, MarkBadgeShownDto, MarkBadgeShownResponseDto } from './dto/badge.dto';
import { Public } from '../common/decorators/public.decorator'; import { Public } from '../common/decorators/public.decorator';
import { CurrentUser } from '../common/decorators/current-user.decorator'; import { CurrentUser } from '../common/decorators/current-user.decorator';
@@ -393,4 +394,53 @@ export class UsersController {
return this.usersService.getBodyMeasurementAnalysis(user.sub, period); return this.usersService.getBodyMeasurementAnalysis(user.sub, period);
} }
// ==================== 勋章相关接口 ====================
/**
* 获取用户勋章列表
*/
@UseGuards(JwtAuthGuard)
@Get('badges')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '获取用户勋章列表' })
@ApiResponse({ status: 200, description: '成功获取用户勋章列表', type: GetUserBadgesResponseDto })
async getUserBadges(
@CurrentUser() user: AccessTokenPayload,
): Promise<GetUserBadgesResponseDto> {
this.logger.log(`获取用户勋章列表 - 用户ID: ${user.sub}`);
return this.usersService.getUserBadges(user.sub);
}
/**
* 获取所有可用勋章(包含用户是否已获得)
*/
@UseGuards(JwtAuthGuard)
@Get('badges/available')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '获取所有可用勋章' })
@ApiResponse({ status: 200, description: '成功获取所有可用勋章', type: GetAvailableBadgesResponseDto })
async getAvailableBadges(
@CurrentUser() user: AccessTokenPayload,
): Promise<GetAvailableBadgesResponseDto> {
this.logger.log(`获取可用勋章列表 - 用户ID: ${user.sub}`);
return this.usersService.getAvailableBadges(user.sub);
}
/**
* 标记勋章已展示
*/
@UseGuards(JwtAuthGuard)
@Post('badges/mark-shown')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '标记勋章已展示(客户端展示勋章动画后调用)' })
@ApiBody({ type: MarkBadgeShownDto })
@ApiResponse({ status: 200, description: '成功标记勋章已展示', type: MarkBadgeShownResponseDto })
async markBadgeAsShown(
@Body() markBadgeShownDto: MarkBadgeShownDto,
@CurrentUser() user: AccessTokenPayload,
): Promise<MarkBadgeShownResponseDto> {
this.logger.log(`标记勋章已展示 - 用户ID: ${user.sub}, 勋章代码: ${markBadgeShownDto.badgeCode}`);
return this.usersService.markBadgeAsShown(user.sub, markBadgeShownDto.badgeCode);
}
} }

View File

@@ -6,6 +6,8 @@ import { User } from "./models/user.model";
import { UserProfile } from "./models/user-profile.model"; import { UserProfile } from "./models/user-profile.model";
import { UserWeightHistory } from "./models/user-weight-history.model"; import { UserWeightHistory } from "./models/user-weight-history.model";
import { UserBodyMeasurementHistory } from "./models/user-body-measurement-history.model"; import { UserBodyMeasurementHistory } from "./models/user-body-measurement-history.model";
import { BadgeConfig } from "./models/badge-config.model";
import { UserBadge } from "./models/user-badge.model";
import { UserDietHistory } from "./models/user-diet-history.model"; import { UserDietHistory } from "./models/user-diet-history.model";
import { ApplePurchaseService } from "./services/apple-purchase.service"; import { ApplePurchaseService } from "./services/apple-purchase.service";
@@ -20,6 +22,7 @@ import { UserPurchase } from "./models/user-purchase.model";
import { PurchaseRestoreLog } from "./models/purchase-restore-log.model"; import { PurchaseRestoreLog } from "./models/purchase-restore-log.model";
import { RevenueCatEvent } from "./models/revenue-cat-event.model"; import { RevenueCatEvent } from "./models/revenue-cat-event.model";
import { CosService } from './cos.service'; import { CosService } from './cos.service';
import { BadgeService } from './services/badge.service';
import { ActivityLogsModule } from '../activity-logs/activity-logs.module'; import { ActivityLogsModule } from '../activity-logs/activity-logs.module';
@Module({ @Module({
@@ -33,6 +36,8 @@ import { ActivityLogsModule } from '../activity-logs/activity-logs.module';
UserProfile, UserProfile,
UserWeightHistory, UserWeightHistory,
UserBodyMeasurementHistory, UserBodyMeasurementHistory,
BadgeConfig,
UserBadge,
UserDietHistory, UserDietHistory,
UserActivity, UserActivity,
@@ -44,7 +49,7 @@ import { ActivityLogsModule } from '../activity-logs/activity-logs.module';
}), }),
], ],
controllers: [UsersController], controllers: [UsersController],
providers: [UsersService, ApplePurchaseService, EncryptionService, AppleAuthService, CosService, UserActivityService], providers: [UsersService, ApplePurchaseService, EncryptionService, AppleAuthService, CosService, UserActivityService, BadgeService],
exports: [UsersService, AppleAuthService, UserActivityService], exports: [UsersService, AppleAuthService, UserActivityService, BadgeService],
}) })
export class UsersModule { } export class UsersModule { }

View File

@@ -41,6 +41,7 @@ import { UserActivityService } from './services/user-activity.service';
import { GetUserActivityHistoryResponseDto } from './dto/user-activity.dto'; import { GetUserActivityHistoryResponseDto } from './dto/user-activity.dto';
import { ActivityActionType, ActivityEntityType } from '../activity-logs/models/activity-log.model'; import { ActivityActionType, ActivityEntityType } from '../activity-logs/models/activity-log.model';
import { BadgeService } from './services/badge.service';
@@ -74,6 +75,7 @@ export class UsersService {
private sequelize: Sequelize, private sequelize: Sequelize,
private readonly activityLogsService: ActivityLogsService, private readonly activityLogsService: ActivityLogsService,
private readonly userActivityService: UserActivityService, private readonly userActivityService: UserActivityService,
private readonly badgeService: BadgeService,
) { } ) { }
async getProfile(user: AccessTokenPayload): Promise<UserResponseDto> { async getProfile(user: AccessTokenPayload): Promise<UserResponseDto> {
@@ -2561,4 +2563,85 @@ export class UsersService {
return Math.floor(Date.now() / 1000) % 100000; return Math.floor(Date.now() / 1000) % 100000;
} }
} }
/**
* 获取用户勋章列表
*/
async getUserBadges(userId: string): Promise<any> {
try {
const badges = await this.badgeService.getUserBadges(userId);
const total = badges.length;
return {
code: ResponseCode.SUCCESS,
message: 'success',
data: {
badges,
total,
},
};
} catch (error) {
this.logger.error(`获取用户勋章列表失败: ${error instanceof Error ? error.message : '未知错误'}`);
return {
code: ResponseCode.ERROR,
message: `获取用户勋章列表失败: ${error instanceof Error ? error.message : '未知错误'}`,
data: {
badges: [],
total: 0,
},
};
}
}
/**
* 获取所有可用勋章(包含用户是否已获得)
*/
async getAvailableBadges(userId?: string): Promise<any> {
try {
const badges = await this.badgeService.getAvailableBadges(userId);
return {
code: ResponseCode.SUCCESS,
message: 'success',
data: badges,
};
} catch (error) {
this.logger.error(`获取可用勋章列表失败: ${error instanceof Error ? error.message : '未知错误'}`);
return {
code: ResponseCode.ERROR,
message: `获取可用勋章列表失败: ${error instanceof Error ? error.message : '未知错误'}`,
data: [],
};
}
}
/**
* 标记勋章已展示
*/
async markBadgeAsShown(userId: string, badgeCode: string): Promise<any> {
try {
const success = await this.badgeService.markBadgeAsShown(userId, badgeCode);
if (!success) {
return {
code: ResponseCode.ERROR,
message: '勋章不存在或标记失败',
data: { success: false },
};
}
return {
code: ResponseCode.SUCCESS,
message: 'success',
data: { success: true },
};
} catch (error) {
this.logger.error(`标记勋章已展示失败: ${error instanceof Error ? error.message : '未知错误'}`);
return {
code: ResponseCode.ERROR,
message: `标记勋章已展示失败: ${error instanceof Error ? error.message : '未知错误'}`,
data: { success: false },
};
}
}
} }