feat:新增活动日志模块,包括控制器、服务、模型及数据传输对象,更新应用模块以引入新模块,并在打卡和训练计划模块中集成活动日志记录功能。

This commit is contained in:
richarjiang
2025-08-14 15:43:29 +08:00
parent 24924e5d81
commit bc8a52852d
15 changed files with 373 additions and 20 deletions

View File

@@ -0,0 +1,33 @@
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
import { ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
import { ActivityLogsService } from './activity-logs.service';
import { JwtAuthGuard } from '../common/guards/jwt-auth.guard';
import { CurrentUser } from '../common/decorators/current-user.decorator';
import { AccessTokenPayload } from '../users/services/apple-auth.service';
import { GetActivityLogsQueryDto, GetActivityLogsResponseDto } from './dto/activity-log.dto';
@ApiTags('activity-logs')
@Controller('activity-logs')
@UseGuards(JwtAuthGuard)
export class ActivityLogsController {
constructor(private readonly service: ActivityLogsService) { }
@Get()
@ApiOperation({ summary: '获取活动记录列表(分页)' })
@ApiQuery({ name: 'page', required: false })
@ApiQuery({ name: 'pageSize', required: false })
@ApiResponse({ type: GetActivityLogsResponseDto })
async list(@CurrentUser() user: AccessTokenPayload, @Query() query: GetActivityLogsQueryDto): Promise<GetActivityLogsResponseDto> {
const data = await this.service.list({
userId: user.sub,
page: query.page,
pageSize: query.pageSize,
entityType: query.entityType,
action: query.action,
entityId: query.entityId,
});
return data;
}
}

View File

@@ -0,0 +1,16 @@
import { Module, forwardRef } from '@nestjs/common';
import { SequelizeModule } from '@nestjs/sequelize';
import { ActivityLogsService } from './activity-logs.service';
import { ActivityLogsController } from './activity-logs.controller';
import { ActivityLog } from './models/activity-log.model';
import { UsersModule } from '../users/users.module';
@Module({
imports: [SequelizeModule.forFeature([ActivityLog]), forwardRef(() => UsersModule)],
providers: [ActivityLogsService],
controllers: [ActivityLogsController],
exports: [ActivityLogsService],
})
export class ActivityLogsModule { }

View File

@@ -0,0 +1,55 @@
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/sequelize';
import { ActivityActionType, ActivityEntityType, ActivityLog } from './models/activity-log.model';
@Injectable()
export class ActivityLogsService {
constructor(
@InjectModel(ActivityLog)
private readonly activityLogModel: typeof ActivityLog,
) { }
async record(params: {
userId: string;
entityType: ActivityEntityType;
action: ActivityActionType;
entityId?: string | null;
changes?: Record<string, any> | null;
metadata?: Record<string, any> | null;
}): Promise<void> {
await this.activityLogModel.create({
userId: params.userId,
entityType: params.entityType,
action: params.action,
entityId: params.entityId ?? null,
changes: params.changes ?? null,
metadata: params.metadata ?? null,
});
}
async list(params: {
userId: string;
page?: number;
pageSize?: number;
entityType?: ActivityEntityType;
action?: ActivityActionType;
entityId?: string;
}) {
const page = params.page && params.page > 0 ? params.page : 1;
const pageSize = params.pageSize && params.pageSize > 0 ? Math.min(100, params.pageSize) : 20;
const where: any = { userId: params.userId };
if (params.entityType) where.entityType = params.entityType;
if (params.action) where.action = params.action;
if (params.entityId) where.entityId = params.entityId;
const { rows, count } = await this.activityLogModel.findAndCountAll({
where,
limit: pageSize,
offset: (page - 1) * pageSize,
order: [['created_at', 'DESC']],
});
return { total: count, items: rows.map(r => r.toJSON()) };
}
}

View File

@@ -0,0 +1,50 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsInt, IsNotEmpty, IsOptional, IsString, Max, Min } from 'class-validator';
import { ActivityActionType, ActivityEntityType } from '../models/activity-log.model';
export class GetActivityLogsQueryDto {
@ApiProperty({ description: '分页页码(从1开始)', required: false, example: 1 })
@IsOptional()
@IsInt()
@Min(1)
page?: number;
@ApiProperty({ description: '分页大小(1-100)', required: false, example: 20 })
@IsOptional()
@IsInt()
@Min(1)
@Max(100)
pageSize?: number;
@ApiProperty({ enum: ActivityEntityType, required: false, description: '按实体类型过滤' })
@IsOptional()
@IsEnum(ActivityEntityType)
entityType?: ActivityEntityType;
@ApiProperty({ enum: ActivityActionType, required: false, description: '按动作过滤' })
@IsOptional()
@IsEnum(ActivityActionType)
action?: ActivityActionType;
@ApiProperty({ required: false, description: '实体ID过滤' })
@IsOptional()
@IsString()
entityId?: string;
}
export class ActivityLogItemDto {
@ApiProperty() id: string;
@ApiProperty({ enum: ActivityEntityType }) entityType: ActivityEntityType;
@ApiProperty({ enum: ActivityActionType }) action: ActivityActionType;
@ApiProperty({ required: false }) entityId?: string | null;
@ApiProperty({ required: false }) changes?: Record<string, any> | null;
@ApiProperty({ required: false }) metadata?: Record<string, any> | null;
@ApiProperty() createdAt: Date;
}
export class GetActivityLogsResponseDto {
@ApiProperty({ description: '总数' }) total: number;
@ApiProperty({ type: [ActivityLogItemDto] }) items: ActivityLogItemDto[];
}

View File

@@ -0,0 +1,67 @@
import { BelongsTo, Column, DataType, ForeignKey, Index, Model, Table } from 'sequelize-typescript';
import { User } from '../../users/models/user.model';
export enum ActivityEntityType {
USER = 'USER',
USER_PROFILE = 'USER_PROFILE',
CHECKIN = 'CHECKIN',
TRAINING_PLAN = 'TRAINING_PLAN',
}
export enum ActivityActionType {
CREATE = 'CREATE',
UPDATE = 'UPDATE',
DELETE = 'DELETE',
}
@Table({
tableName: 't_activity_logs',
underscored: true,
})
export class ActivityLog extends Model {
@Column({
type: DataType.UUID,
primaryKey: true,
defaultValue: DataType.UUIDV4,
})
declare id: string;
@ForeignKey(() => User)
@Column({ type: DataType.STRING, allowNull: false, comment: '用户ID' })
declare userId: string;
@BelongsTo(() => User)
declare user?: User;
@Column({
type: DataType.ENUM('USER', 'USER_PROFILE', 'CHECKIN', 'TRAINING_PLAN'),
allowNull: false,
comment: '实体类型',
})
declare entityType: ActivityEntityType;
@Column({
type: DataType.ENUM('CREATE', 'UPDATE', 'DELETE'),
allowNull: false,
comment: '动作类型',
})
declare action: ActivityActionType;
@Column({ type: DataType.STRING, allowNull: true, comment: '实体ID' })
declare entityId: string | null;
@Column({ type: DataType.JSON, allowNull: true, comment: '变更详情或前后快照' })
declare changes: Record<string, any> | null;
@Column({ type: DataType.JSON, allowNull: true, comment: '附加信息(来源、设备等)' })
declare metadata: Record<string, any> | null;
@Column({ type: DataType.DATE, defaultValue: DataType.NOW, comment: '发生时间' })
declare createdAt: Date;
@Column({ type: DataType.DATE, defaultValue: DataType.NOW })
declare updatedAt: Date;
}