feat: 生成活动接口

This commit is contained in:
richarjiang
2025-08-21 14:28:15 +08:00
parent 94e1b124df
commit 73f53ac5e4
8 changed files with 413 additions and 2 deletions

View File

@@ -0,0 +1,78 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsOptional, IsInt, Min, Max, IsDateString, IsEnum } from 'class-validator';
import { ResponseCode } from 'src/base.dto';
import { ActivityType, ActivityLevel } from '../models/user-activity.model';
export class CreateUserActivityDto {
@ApiProperty({
description: '活跃类型',
enum: ActivityType,
example: ActivityType.LOGIN
})
@IsEnum(ActivityType)
activityType!: ActivityType;
@ApiProperty({ description: '活跃日期 YYYY-MM-DD', example: '2024-01-15' })
@IsDateString()
activityDate!: string;
@ApiProperty({
description: '活跃等级',
enum: ActivityLevel,
example: ActivityLevel.LOW,
minimum: 0,
maximum: 3
})
@IsEnum(ActivityLevel)
level!: ActivityLevel;
@ApiProperty({ description: '备注', required: false })
@IsOptional()
@IsString()
remark?: string;
}
export class UserActivityDto {
@ApiProperty({ description: '活跃记录ID' })
id!: number;
@ApiProperty({ description: '用户ID' })
userId!: string;
@ApiProperty({ description: '活跃类型', enum: ActivityType })
activityType!: ActivityType;
@ApiProperty({ description: '活跃日期' })
activityDate!: string;
@ApiProperty({ description: '活跃等级', enum: ActivityLevel })
level!: ActivityLevel;
@ApiProperty({ description: '备注', required: false })
remark?: string;
@ApiProperty({ description: '创建时间' })
createdAt!: Date;
@ApiProperty({ description: '更新时间' })
updatedAt!: Date;
}
export class UserActivitySummaryDto {
@ApiProperty({ description: '日期 YYYY-MM-DD' })
date!: string;
@ApiProperty({ description: '活跃等级', enum: ActivityLevel })
level!: ActivityLevel;
}
export class GetUserActivityHistoryResponseDto {
@ApiProperty({ description: '响应码' })
code!: ResponseCode;
@ApiProperty({ description: '响应消息' })
message!: string;
@ApiProperty({ description: '用户活跃历史数据', type: [UserActivitySummaryDto] })
data!: UserActivitySummaryDto[];
}

View File

@@ -0,0 +1,108 @@
import { DataTypes, Model, Optional } from 'sequelize';
import { Column, Table, Model as SequelizeModel, DataType, ForeignKey, BelongsTo } from 'sequelize-typescript';
import { User } from './user.model';
export enum ActivityType {
LOGIN = 1, // 登录
WORKOUT = 2, // 训练
DIET_RECORD = 3, // 饮食记录
WEIGHT_RECORD = 4, // 体重记录
PROFILE_UPDATE = 5, // 资料更新
CHECKIN = 6, // 打卡
}
// 活跃类型显示名称映射
export const ActivityTypeNames: Record<ActivityType, string> = {
[ActivityType.LOGIN]: '登录',
[ActivityType.WORKOUT]: '训练',
[ActivityType.DIET_RECORD]: '饮食记录',
[ActivityType.WEIGHT_RECORD]: '体重记录',
[ActivityType.PROFILE_UPDATE]: '资料更新',
[ActivityType.CHECKIN]: '打卡',
};
// 获取活跃类型显示名称的辅助函数
export function getActivityTypeName(activityType: ActivityType): string {
return ActivityTypeNames[activityType] || '未知';
}
export enum ActivityLevel {
NONE = 0, // 无活跃
LOW = 1, // 低活跃
MEDIUM = 2, // 中活跃
HIGH = 3, // 高活跃
}
export interface UserActivityAttributes {
id: number;
userId: string;
activityType: ActivityType;
activityDate: string; // YYYY-MM-DD 格式
level: ActivityLevel; // 活跃等级
remark?: string;
createdAt: Date;
updatedAt: Date;
}
export interface UserActivityCreationAttributes extends Optional<UserActivityAttributes, 'id' | 'createdAt' | 'updatedAt' | 'remark'> { }
@Table({
tableName: 'user_activities',
timestamps: true,
indexes: [
{
unique: true,
fields: ['userId', 'activityDate', 'activityType']
},
{
fields: ['userId', 'activityDate']
}
]
})
export class UserActivity extends SequelizeModel<UserActivityAttributes, UserActivityCreationAttributes> {
@Column({
type: DataType.INTEGER,
primaryKey: true,
autoIncrement: true,
})
declare id: number;
@ForeignKey(() => User)
@Column({
type: DataType.STRING,
allowNull: false,
})
userId!: string;
@Column({
type: DataType.TINYINT,
allowNull: false,
comment: '活跃类型1-登录2-训练3-饮食记录4-体重记录5-资料更新6-打卡',
})
activityType!: ActivityType;
@Column({
type: DataType.DATEONLY,
allowNull: false,
comment: '活跃日期 YYYY-MM-DD',
})
activityDate!: string;
@Column({
type: DataType.TINYINT,
allowNull: false,
defaultValue: ActivityLevel.LOW,
comment: '活跃等级0-无活跃1-低活跃2-中活跃3-高活跃',
})
level!: ActivityLevel;
@Column({
type: DataType.TEXT,
allowNull: true,
comment: '备注信息',
})
remark?: string;
@BelongsTo(() => User)
user!: User;
}

View File

@@ -0,0 +1,137 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectModel } from '@nestjs/sequelize';
import { UserActivity, ActivityType, ActivityLevel } from '../models/user-activity.model';
import { CreateUserActivityDto, UserActivitySummaryDto } from '../dto/user-activity.dto';
import { Op } from 'sequelize';
import * as dayjs from 'dayjs';
@Injectable()
export class UserActivityService {
private readonly logger = new Logger(UserActivityService.name);
constructor(
@InjectModel(UserActivity)
private userActivityModel: typeof UserActivity,
) { }
/**
* 记录用户活跃
*/
async recordActivity(userId: string, activityDto: CreateUserActivityDto): Promise<UserActivity> {
try {
// 使用 upsert 避免重复记录同一天同一类型的活跃
const [activity, created] = await this.userActivityModel.upsert({
userId,
...activityDto,
});
this.logger.log(`记录用户活跃 - 用户: ${userId}, 类型: ${activityDto.activityType}, 日期: ${activityDto.activityDate}, 是否新建: ${created}`);
return activity;
} catch (error) {
this.logger.error(`记录用户活跃失败: ${error.message}`, error.stack);
throw error;
}
}
/**
* 检查今日是否有登录记录,如果没有则创建
*/
async checkAndRecordTodayLogin(userId: string): Promise<void> {
const today = dayjs().format('YYYY-MM-DD'); // YYYY-MM-DD 格式
try {
const existingLogin = await this.userActivityModel.findOne({
where: {
userId,
activityDate: today,
activityType: ActivityType.LOGIN,
},
});
if (!existingLogin) {
await this.recordActivity(userId, {
activityType: ActivityType.LOGIN,
activityDate: today,
level: ActivityLevel.LOW, // 登录默认为低活跃
remark: '用户拉取profile接口自动记录',
});
}
} catch (error) {
this.logger.error(`检查并记录今日登录失败: ${error.message}`, error.stack);
// 不抛出错误,避免影响主要业务流程
}
}
/**
* 获取用户最近六个月的活跃情况
*/
async getUserActivityHistory(userId: string): Promise<UserActivitySummaryDto[]> {
const startDate = dayjs().subtract(6, 'month').format('YYYY-MM-DD');
const today = dayjs().format('YYYY-MM-DD');
try {
// 获取用户在指定时间范围内的活跃记录,按日期分组,取每日最高活跃等级
const activities = await this.userActivityModel.findAll({
where: {
userId,
activityDate: {
[Op.gte]: startDate,
[Op.lte]: today,
},
},
attributes: [
'activityDate',
[this.userActivityModel.sequelize!.fn('MAX', this.userActivityModel.sequelize!.col('level')), 'maxLevel'],
],
group: ['activityDate'],
order: [['activityDate', 'ASC']],
raw: true,
});
// 生成完整的日期范围最近6个月的每一天
const result: UserActivitySummaryDto[] = [];
let cursor = dayjs(startDate, 'YYYY-MM-DD');
const end = dayjs(today, 'YYYY-MM-DD');
while (!cursor.isAfter(end)) {
const dateStr = cursor.format('YYYY-MM-DD');
const activity = activities.find((a: any) => a.activityDate === dateStr);
result.push({
date: dateStr,
level: activity ? parseInt((activity as any).maxLevel) : ActivityLevel.NONE, // 无活跃
});
cursor = cursor.add(1, 'day');
}
this.logger.log(`获取用户活跃历史 - 用户: ${userId}, 记录数: ${result.length}`);
return result;
} catch (error) {
this.logger.error(`获取用户活跃历史失败: ${error.message}`, error.stack);
throw error;
}
}
/**
* 更新用户某日的活跃等级(如果新等级更高)
*/
async updateActivityLevel(userId: string, activityDate: string, activityType: ActivityType, newLevel: ActivityLevel): Promise<void> {
try {
const activity = await this.userActivityModel.findOne({
where: {
userId,
activityDate,
activityType,
},
});
if (activity && activity.level < newLevel) {
await activity.update({ level: newLevel });
this.logger.log(`更新用户活跃等级 - 用户: ${userId}, 日期: ${activityDate}, 类型: ${activityType}, 新等级: ${newLevel}`);
}
} catch (error) {
this.logger.error(`更新用户活跃等级失败: ${error.message}`, error.stack);
}
}
}

View File

@@ -29,6 +29,7 @@ import { CreateDietRecordDto, UpdateDietRecordDto, GetDietHistoryQueryDto, DietR
import { GuestLoginDto, GuestLoginResponseDto, RefreshGuestTokenDto, RefreshGuestTokenResponseDto } from './dto/guest-login.dto';
import { AppStoreServerNotificationDto, ProcessNotificationResponseDto } from './dto/app-store-notification.dto';
import { RestorePurchaseDto, RestorePurchaseResponseDto } from './dto/restore-purchase.dto';
import { GetUserActivityHistoryResponseDto } from './dto/user-activity.dto';
import { Public } from '../common/decorators/public.decorator';
import { CurrentUser } from '../common/decorators/current-user.decorator';
import { AccessTokenPayload } from './services/apple-auth.service';
@@ -317,4 +318,21 @@ export class UsersController {
}
}
}
// ==================== 用户活跃记录相关接口 ====================
/**
* 获取用户最近六个月的活跃情况
*/
@UseGuards(JwtAuthGuard)
@Get('activity-history')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '获取用户最近六个月的活跃情况' })
@ApiResponse({ status: 200, description: '成功获取用户活跃历史', type: GetUserActivityHistoryResponseDto })
async getUserActivityHistory(
@CurrentUser() user: AccessTokenPayload,
): Promise<GetUserActivityHistoryResponseDto> {
this.logger.log(`获取用户活跃历史 - 用户ID: ${user.sub}`);
return this.usersService.getUserActivityHistory(user.sub);
}
}

View File

@@ -7,6 +7,8 @@ import { UserProfile } from "./models/user-profile.model";
import { UserWeightHistory } from "./models/user-weight-history.model";
import { UserDietHistory } from "./models/user-diet-history.model";
import { ApplePurchaseService } from "./services/apple-purchase.service";
import { UserActivity } from "./models/user-activity.model";
import { UserActivityService } from "./services/user-activity.service";
import { EncryptionService } from "../common/encryption.service";
import { AppleAuthService } from "./services/apple-auth.service";
import { JwtModule } from '@nestjs/jwt';
@@ -28,6 +30,7 @@ import { ActivityLogsModule } from '../activity-logs/activity-logs.module';
UserProfile,
UserWeightHistory,
UserDietHistory,
UserActivity,
]),
forwardRef(() => ActivityLogsModule),
JwtModule.register({
@@ -36,7 +39,7 @@ import { ActivityLogsModule } from '../activity-logs/activity-logs.module';
}),
],
controllers: [UsersController],
providers: [UsersService, ApplePurchaseService, EncryptionService, AppleAuthService, CosService],
providers: [UsersService, ApplePurchaseService, EncryptionService, AppleAuthService, CosService, UserActivityService],
exports: [UsersService, AppleAuthService],
})
export class UsersModule { }

View File

@@ -33,6 +33,8 @@ import { BlockedTransaction, BlockReason } from './models/blocked-transaction.mo
import { UserWeightHistory, WeightUpdateSource } from './models/user-weight-history.model';
import { UserDietHistory, DietRecordSource, MealType } from './models/user-diet-history.model';
import { ActivityLogsService } from '../activity-logs/activity-logs.service';
import { UserActivityService } from './services/user-activity.service';
import { GetUserActivityHistoryResponseDto } from './dto/user-activity.dto';
import { ActivityActionType, ActivityEntityType } from '../activity-logs/models/activity-log.model';
import { CreateDietRecordDto, UpdateDietRecordDto, GetDietHistoryQueryDto, DietRecordResponseDto, DietHistoryResponseDto, NutritionSummaryDto } from './dto/diet-record.dto';
@@ -64,6 +66,7 @@ export class UsersService {
@InjectConnection()
private sequelize: Sequelize,
private readonly activityLogsService: ActivityLogsService,
private readonly userActivityService: UserActivityService,
) { }
async getProfile(user: AccessTokenPayload): Promise<UserResponseDto> {
@@ -94,6 +97,9 @@ export class UsersService {
where: { userId: existingUser.id },
defaults: { userId: existingUser.id },
});
// 检查并记录今日登录活跃
await this.userActivityService.checkAndRecordTodayLogin(existingUser.id);
const returnData = {
...existingUser.toJSON(),
maxUsageCount: DEFAULT_FREE_USAGE_COUNT,
@@ -2334,4 +2340,26 @@ export class UsersService {
this.logger.error(`关联 RevenueCat 用户失败: ${error instanceof Error ? error.message : '未知错误'}`);
}
}
/**
* 获取用户最近六个月的活跃情况
*/
async getUserActivityHistory(userId: string): Promise<GetUserActivityHistoryResponseDto> {
try {
const activityHistory = await this.userActivityService.getUserActivityHistory(userId);
return {
code: ResponseCode.SUCCESS,
message: 'success',
data: activityHistory,
};
} catch (error) {
this.logger.error(`获取用户活跃历史失败: ${error instanceof Error ? error.message : '未知错误'}`);
return {
code: ResponseCode.ERROR,
message: `获取用户活跃历史失败: ${error instanceof Error ? error.message : '未知错误'}`,
data: [],
};
}
}
}