diff --git a/src/activity-logs/activity-logs.controller.ts b/src/activity-logs/activity-logs.controller.ts new file mode 100644 index 0000000..7ed2afb --- /dev/null +++ b/src/activity-logs/activity-logs.controller.ts @@ -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 { + 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; + } +} + + diff --git a/src/activity-logs/activity-logs.module.ts b/src/activity-logs/activity-logs.module.ts new file mode 100644 index 0000000..1686b5e --- /dev/null +++ b/src/activity-logs/activity-logs.module.ts @@ -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 { } + + diff --git a/src/activity-logs/activity-logs.service.ts b/src/activity-logs/activity-logs.service.ts new file mode 100644 index 0000000..cc7b53f --- /dev/null +++ b/src/activity-logs/activity-logs.service.ts @@ -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 | null; + metadata?: Record | null; + }): Promise { + 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()) }; + } +} + + diff --git a/src/activity-logs/dto/activity-log.dto.ts b/src/activity-logs/dto/activity-log.dto.ts new file mode 100644 index 0000000..3931695 --- /dev/null +++ b/src/activity-logs/dto/activity-log.dto.ts @@ -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 | null; + @ApiProperty({ required: false }) metadata?: Record | null; + @ApiProperty() createdAt: Date; +} + +export class GetActivityLogsResponseDto { + @ApiProperty({ description: '总数' }) total: number; + @ApiProperty({ type: [ActivityLogItemDto] }) items: ActivityLogItemDto[]; +} + + diff --git a/src/activity-logs/models/activity-log.model.ts b/src/activity-logs/models/activity-log.model.ts new file mode 100644 index 0000000..92b0378 --- /dev/null +++ b/src/activity-logs/models/activity-log.model.ts @@ -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 | null; + + @Column({ type: DataType.JSON, allowNull: true, comment: '附加信息(来源、设备等)' }) + declare metadata: Record | null; + + @Column({ type: DataType.DATE, defaultValue: DataType.NOW, comment: '发生时间' }) + declare createdAt: Date; + + @Column({ type: DataType.DATE, defaultValue: DataType.NOW }) + declare updatedAt: Date; +} + + diff --git a/src/app.module.ts b/src/app.module.ts index c6be767..ae0fb7f 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -10,6 +10,7 @@ import { AiCoachModule } from './ai-coach/ai-coach.module'; import { TrainingPlansModule } from './training-plans/training-plans.module'; import { ArticlesModule } from './articles/articles.module'; import { RecommendationsModule } from './recommendations/recommendations.module'; +import { ActivityLogsModule } from './activity-logs/activity-logs.module'; @Module({ imports: [ @@ -25,6 +26,7 @@ import { RecommendationsModule } from './recommendations/recommendations.module' TrainingPlansModule, ArticlesModule, RecommendationsModule, + ActivityLogsModule, ], controllers: [AppController], providers: [AppService], diff --git a/src/checkins/checkins.module.ts b/src/checkins/checkins.module.ts index 9670c2f..57c7676 100644 --- a/src/checkins/checkins.module.ts +++ b/src/checkins/checkins.module.ts @@ -4,9 +4,10 @@ import { CheckinsService } from './checkins.service'; import { CheckinsController } from './checkins.controller'; import { Checkin } from './models/checkin.model'; import { UsersModule } from '../users/users.module'; +import { ActivityLogsModule } from '../activity-logs/activity-logs.module'; @Module({ - imports: [SequelizeModule.forFeature([Checkin]), UsersModule], + imports: [SequelizeModule.forFeature([Checkin]), UsersModule, ActivityLogsModule], providers: [CheckinsService], controllers: [CheckinsController], }) diff --git a/src/checkins/checkins.service.ts b/src/checkins/checkins.service.ts index b9d7be7..9477cf9 100644 --- a/src/checkins/checkins.service.ts +++ b/src/checkins/checkins.service.ts @@ -5,6 +5,8 @@ import { CreateCheckinDto, UpdateCheckinDto, CompleteCheckinDto, RemoveCheckinDt import { ResponseCode } from '../base.dto'; import * as dayjs from 'dayjs'; import { Op } from 'sequelize'; +import { ActivityLogsService } from '../activity-logs/activity-logs.service'; +import { ActivityActionType, ActivityEntityType } from '../activity-logs/models/activity-log.model'; @Injectable() export class CheckinsService { @@ -13,6 +15,7 @@ export class CheckinsService { constructor( @InjectModel(Checkin) private readonly checkinModel: typeof Checkin, + private readonly activityLogsService: ActivityLogsService, ) { } async create(dto: CreateCheckinDto): Promise { @@ -28,6 +31,14 @@ export class CheckinsService { status: CheckinStatus.PENDING, }); + await this.activityLogsService.record({ + userId: record.userId, + entityType: ActivityEntityType.CHECKIN, + action: ActivityActionType.CREATE, + entityId: record.id, + changes: record.toJSON(), + }); + return { code: ResponseCode.SUCCESS, message: 'success', data: record.toJSON() }; } @@ -40,18 +51,26 @@ export class CheckinsService { throw new ForbiddenException('无权操作该打卡记录'); } - if (dto.workoutId !== undefined) record.workoutId = dto.workoutId; - if (dto.planId !== undefined) record.planId = dto.planId; - if (dto.title !== undefined) record.title = dto.title; - if (dto.checkinDate !== undefined) record.checkinDate = dto.checkinDate as any; - if (dto.startedAt !== undefined) record.startedAt = dto.startedAt ? new Date(dto.startedAt) : null; - if (dto.notes !== undefined) record.notes = dto.notes; - if (dto.metrics !== undefined) record.metrics = dto.metrics as any; - if (dto.status !== undefined) record.status = dto.status; - if (dto.completedAt !== undefined) record.completedAt = dto.completedAt ? new Date(dto.completedAt) : null; - if (dto.durationSeconds !== undefined) record.durationSeconds = dto.durationSeconds; + const changes: Record = {}; + if (dto.workoutId !== undefined) { record.workoutId = dto.workoutId; changes.workoutId = dto.workoutId; } + if (dto.planId !== undefined) { record.planId = dto.planId; changes.planId = dto.planId; } + if (dto.title !== undefined) { record.title = dto.title; changes.title = dto.title; } + if (dto.checkinDate !== undefined) { record.checkinDate = dto.checkinDate as any; changes.checkinDate = dto.checkinDate; } + if (dto.startedAt !== undefined) { record.startedAt = dto.startedAt ? new Date(dto.startedAt) : null; changes.startedAt = dto.startedAt; } + if (dto.notes !== undefined) { record.notes = dto.notes; changes.notes = dto.notes; } + if (dto.metrics !== undefined) { record.metrics = dto.metrics as any; changes.metrics = dto.metrics; } + if (dto.status !== undefined) { record.status = dto.status; changes.status = dto.status; } + if (dto.completedAt !== undefined) { record.completedAt = dto.completedAt ? new Date(dto.completedAt) : null; changes.completedAt = dto.completedAt; } + if (dto.durationSeconds !== undefined) { record.durationSeconds = dto.durationSeconds; changes.durationSeconds = dto.durationSeconds; } await record.save(); + await this.activityLogsService.record({ + userId: record.userId, + entityType: ActivityEntityType.CHECKIN, + action: ActivityActionType.UPDATE, + entityId: record.id, + changes, + }); return { code: ResponseCode.SUCCESS, message: 'success', data: record.toJSON() }; } @@ -71,6 +90,19 @@ export class CheckinsService { if (dto.metrics !== undefined) record.metrics = dto.metrics as any; await record.save(); + await this.activityLogsService.record({ + userId: record.userId, + entityType: ActivityEntityType.CHECKIN, + action: ActivityActionType.UPDATE, + entityId: record.id, + changes: { + status: record.status, + completedAt: record.completedAt, + durationSeconds: record.durationSeconds, + notes: record.notes, + metrics: record.metrics, + }, + }); return { code: ResponseCode.SUCCESS, message: 'success', data: record.toJSON() }; } @@ -83,6 +115,13 @@ export class CheckinsService { throw new ForbiddenException('无权操作该打卡记录'); } await record.destroy(); + await this.activityLogsService.record({ + userId: record.userId, + entityType: ActivityEntityType.CHECKIN, + action: ActivityActionType.DELETE, + entityId: record.id, + changes: null, + }); return { code: ResponseCode.SUCCESS, message: 'success', data: { id: dto.id } }; } diff --git a/src/recommendations/dto/recommendation.dto.ts b/src/recommendations/dto/recommendation.dto.ts index 52f202a..977e41b 100644 --- a/src/recommendations/dto/recommendation.dto.ts +++ b/src/recommendations/dto/recommendation.dto.ts @@ -7,8 +7,8 @@ export enum RecommendationType { } export class GetRecommendationsQueryDto { - @ApiProperty({ required: false, description: '数量,默认10' }) - limit?: number = 10; + // @ApiProperty({ required: false, description: '数量,默认10' }) + // limit?: number = 10; } export interface RecommendationCard { diff --git a/src/recommendations/recommendations.service.ts b/src/recommendations/recommendations.service.ts index b240bb7..a3a4a46 100644 --- a/src/recommendations/recommendations.service.ts +++ b/src/recommendations/recommendations.service.ts @@ -11,7 +11,7 @@ export class RecommendationsService { // 为你推荐:混合文章与每日打卡卡片 async list(query: GetRecommendationsQueryDto): Promise { - const limit = Math.min(50, Math.max(1, Number(query.limit || 10))); + const limit = 10 // 取最新文章若干 const articlesRes = await this.articlesService.query({ page: 1, pageSize: limit } as any); diff --git a/src/training-plans/training-plans.module.ts b/src/training-plans/training-plans.module.ts index 3fa4b50..3f34389 100644 --- a/src/training-plans/training-plans.module.ts +++ b/src/training-plans/training-plans.module.ts @@ -4,10 +4,12 @@ import { TrainingPlansService } from './training-plans.service'; import { TrainingPlansController } from './training-plans.controller'; import { TrainingPlan } from './models/training-plan.model'; import { UsersModule } from '../users/users.module'; +import { ActivityLogsModule } from '../activity-logs/activity-logs.module'; @Module({ imports: [ UsersModule, + ActivityLogsModule, SequelizeModule.forFeature([TrainingPlan]), ], controllers: [TrainingPlansController], diff --git a/src/training-plans/training-plans.service.ts b/src/training-plans/training-plans.service.ts index 30cc3f4..619eedc 100644 --- a/src/training-plans/training-plans.service.ts +++ b/src/training-plans/training-plans.service.ts @@ -2,12 +2,15 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectModel } from '@nestjs/sequelize'; import { TrainingPlan } from './models/training-plan.model'; import { CreateTrainingPlanDto } from './dto/training-plan.dto'; +import { ActivityLogsService } from '../activity-logs/activity-logs.service'; +import { ActivityActionType, ActivityEntityType } from '../activity-logs/models/activity-log.model'; @Injectable() export class TrainingPlansService { constructor( @InjectModel(TrainingPlan) private trainingPlanModel: typeof TrainingPlan, + private readonly activityLogsService: ActivityLogsService, ) { } async create(userId: string, dto: CreateTrainingPlanDto) { @@ -25,11 +28,27 @@ export class TrainingPlansService { startWeightKg: dto.startWeightKg ?? null, preferredTimeOfDay: dto.preferredTimeOfDay ?? '', }); + await this.activityLogsService.record({ + userId, + entityType: ActivityEntityType.TRAINING_PLAN, + action: ActivityActionType.CREATE, + entityId: plan.id, + changes: plan.toJSON(), + }); return plan.toJSON(); } async remove(userId: string, id: string) { const count = await this.trainingPlanModel.destroy({ where: { id, userId } }); + if (count > 0) { + await this.activityLogsService.record({ + userId, + entityType: ActivityEntityType.TRAINING_PLAN, + action: ActivityActionType.DELETE, + entityId: id, + changes: null, + }); + } return { success: count > 0 }; } diff --git a/src/users/cos.service.ts b/src/users/cos.service.ts index 687c3eb..53887bc 100644 --- a/src/users/cos.service.ts +++ b/src/users/cos.service.ts @@ -19,7 +19,7 @@ export class CosService { this.bucket = this.configService.get('COS_BUCKET') || ''; this.region = this.configService.get('COS_REGION') || 'ap-guangzhou'; this.cdnDomain = this.configService.get('COS_CDN_DOMAIN') || 'https://cdn.richarjiang.com'; - this.allowPrefix = this.configService.get('COS_ALLOW_PREFIX') || 'tennis-uploads/*'; + this.allowPrefix = this.configService.get('COS_ALLOW_PREFIX') || 'uploads/*'; if (!this.secretId || !this.secretKey || !this.bucket) { throw new Error('腾讯云COS配置缺失:TENCENT_SECRET_ID, TENCENT_SECRET_KEY, COS_BUCKET 是必需的'); diff --git a/src/users/users.module.ts b/src/users/users.module.ts index 671186b..1bb216e 100644 --- a/src/users/users.module.ts +++ b/src/users/users.module.ts @@ -1,4 +1,4 @@ -import { Module } from "@nestjs/common"; +import { Module, forwardRef } from "@nestjs/common"; import { SequelizeModule } from "@nestjs/sequelize"; import { UsersController } from "./users.controller"; import { UsersService } from "./users.service"; @@ -14,6 +14,7 @@ import { UserPurchase } from "./models/user-purchase.model"; import { PurchaseRestoreLog } from "./models/purchase-restore-log.model"; import { RevenueCatEvent } from "./models/revenue-cat-event.model"; import { CosService } from './cos.service'; +import { ActivityLogsModule } from '../activity-logs/activity-logs.module'; @Module({ imports: [ @@ -26,6 +27,7 @@ import { CosService } from './cos.service'; UserProfile, UserWeightHistory, ]), + forwardRef(() => ActivityLogsModule), JwtModule.register({ secret: process.env.JWT_ACCESS_SECRET || 'your-access-token-secret-key', signOptions: { expiresIn: '30d' }, diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 59e6de9..4732630 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -31,6 +31,8 @@ import { RestorePurchaseDto, RestorePurchaseResponseDto, RestoredPurchaseInfo, A import { PurchaseRestoreLog, RestoreStatus, RestoreSource } from './models/purchase-restore-log.model'; import { BlockedTransaction, BlockReason } from './models/blocked-transaction.model'; import { UserWeightHistory, WeightUpdateSource } from './models/user-weight-history.model'; +import { ActivityLogsService } from '../activity-logs/activity-logs.service'; +import { ActivityActionType, ActivityEntityType } from '../activity-logs/models/activity-log.model'; const DEFAULT_FREE_USAGE_COUNT = 10; @@ -57,6 +59,7 @@ export class UsersService { private userWeightHistoryModel: typeof UserWeightHistory, @InjectConnection() private sequelize: Sequelize, + private readonly activityLogsService: ActivityLogsService, ) { } async getProfile(user: AccessTokenPayload): Promise { @@ -134,17 +137,23 @@ export class UsersService { throw new NotFoundException(`ID为${userId}的用户不存在`); } + const profileChanges: Record = {}; + const userChanges: Record = {}; if (name) { user.name = name; + userChanges.name = name; } if (avatar) { user.avatar = avatar; + userChanges.avatar = avatar; } if (gender) { user.gender = gender; + userChanges.gender = gender; } if (birthDate) { user.birthDate = dayjs(birthDate * 1000).startOf('day').toDate(); + userChanges.birthDate = birthDate; } this.logger.log(`updateUser user: ${JSON.stringify(user, null, 2)}`); @@ -157,9 +166,9 @@ export class UsersService { where: { userId }, defaults: { userId }, }); - if (dailyStepsGoal !== undefined) profile.dailyStepsGoal = dailyStepsGoal as any; - if (dailyCaloriesGoal !== undefined) profile.dailyCaloriesGoal = dailyCaloriesGoal as any; - if (pilatesPurposes !== undefined) profile.pilatesPurposes = pilatesPurposes as any; + if (dailyStepsGoal !== undefined) { profile.dailyStepsGoal = dailyStepsGoal as any; profileChanges.dailyStepsGoal = dailyStepsGoal; } + if (dailyCaloriesGoal !== undefined) { profile.dailyCaloriesGoal = dailyCaloriesGoal as any; profileChanges.dailyCaloriesGoal = dailyCaloriesGoal; } + if (pilatesPurposes !== undefined) { profile.pilatesPurposes = pilatesPurposes as any; profileChanges.pilatesPurposes = pilatesPurposes; } if (weight !== undefined) { profile.weight = weight as any; try { @@ -167,11 +176,32 @@ export class UsersService { } catch (e) { this.logger.error(`记录体重历史失败: ${e instanceof Error ? e.message : String(e)}`); } + profileChanges.weight = weight; } - if (height !== undefined) profile.height = height as any; + if (height !== undefined) { profile.height = height as any; profileChanges.height = height; } await profile.save(); } + // 记录用户基础与扩展信息更新 + if (Object.keys(userChanges).length > 0) { + await this.activityLogsService.record({ + userId, + entityType: ActivityEntityType.USER, + action: ActivityActionType.UPDATE, + entityId: userId, + changes: userChanges, + }); + } + if (Object.keys(profileChanges).length > 0) { + await this.activityLogsService.record({ + userId, + entityType: ActivityEntityType.USER_PROFILE, + action: ActivityActionType.UPDATE, + entityId: userId, + changes: profileChanges, + }); + } + return { code: ResponseCode.SUCCESS, message: 'success', @@ -190,6 +220,14 @@ export class UsersService { await profile.save({ transaction: t }); await this.userWeightHistoryModel.create({ userId, weight, source: WeightUpdateSource.Vision }, { transaction: t }); await t.commit(); + await this.activityLogsService.record({ + userId, + entityType: ActivityEntityType.USER_PROFILE, + action: ActivityActionType.UPDATE, + entityId: userId, + changes: { weight }, + metadata: { source: 'vision' }, + }); } catch (e) { await t.rollback(); this.logger.error(`addWeightByVision error: ${e instanceof Error ? e.message : String(e)}`); @@ -285,6 +323,16 @@ export class UsersService { } : undefined, }; + // 登录行为也可视为活动(可选,记录创建或登录行为) + await this.activityLogsService.record({ + userId, + entityType: ActivityEntityType.USER, + action: ActivityActionType.UPDATE, + entityId: userId, + changes: { lastLogin: user.lastLogin }, + metadata: { source: 'appleLogin' }, + }); + return { code: ResponseCode.SUCCESS, message: 'success', @@ -387,6 +435,16 @@ export class UsersService { this.logger.log(`用户账号删除成功: ${userId}`); + // 记录删除账户行为 + await this.activityLogsService.record({ + userId, + entityType: ActivityEntityType.USER, + action: ActivityActionType.DELETE, + entityId: userId, + changes: null, + metadata: { reason: 'deleteAccount' }, + }); + return { code: ResponseCode.SUCCESS, message: '账号删除成功', @@ -476,6 +534,15 @@ export class UsersService { } : undefined, }; + await this.activityLogsService.record({ + userId: guestUserId, + entityType: ActivityEntityType.USER, + action: ActivityActionType.UPDATE, + entityId: guestUserId, + changes: { lastLogin: user.lastLogin }, + metadata: { source: 'guestLogin' }, + }); + return { code: ResponseCode.SUCCESS, message: 'success',