Initial WhaleTown V2 backend

This commit is contained in:
2026-07-20 02:00:52 +08:00
commit c995891c1f
265 changed files with 75689 additions and 0 deletions

View File

@@ -0,0 +1,28 @@
CREATE TABLE IF NOT EXISTS `user_assets` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` BIGINT NOT NULL COMMENT '关联users.id',
`asset_type` VARCHAR(40) NOT NULL COMMENT '资产类型skin/room_decor/item/badge等',
`asset_id` VARCHAR(100) NOT NULL COMMENT '资产ID',
`source` VARCHAR(50) NOT NULL DEFAULT 'system' COMMENT '发放来源',
`metadata` JSON NULL COMMENT '资产扩展数据',
`acquired_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '获得时间',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_user_assets_user_type_asset_unique` (`user_id`, `asset_type`, `asset_id`),
KEY `idx_user_assets_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `room_decor_placements` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` BIGINT NOT NULL COMMENT '关联users.id',
`decor_id` VARCHAR(100) NOT NULL COMMENT '房间摆件ID',
`placed` BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否已摆放',
`position_x` FLOAT NULL COMMENT '房间内X坐标',
`position_y` FLOAT NULL COMMENT '房间内Y坐标',
`scale` FLOAT NOT NULL DEFAULT 1 COMMENT '摆件缩放',
`z_index` INT NOT NULL DEFAULT 0 COMMENT '摆放层级',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_room_decor_placements_user_decor_unique` (`user_id`, `decor_id`),
KEY `idx_room_decor_placements_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@@ -0,0 +1,42 @@
INSERT IGNORE INTO `user_assets` (`user_id`, `asset_type`, `asset_id`, `source`, `metadata`, `acquired_at`)
SELECT
`user_id`,
'skin' AS `asset_type`,
`skin_id` AS `asset_id`,
`source`,
NULL AS `metadata`,
`created_at` AS `acquired_at`
FROM `user_skins`;
INSERT IGNORE INTO `user_assets` (`user_id`, `asset_type`, `asset_id`, `source`, `metadata`, `acquired_at`)
SELECT
`user_id`,
'room_decor' AS `asset_type`,
`decor_id` AS `asset_id`,
`source`,
NULL AS `metadata`,
`created_at` AS `acquired_at`
FROM `user_room_decors`;
INSERT IGNORE INTO `room_decor_placements` (
`user_id`,
`decor_id`,
`placed`,
`position_x`,
`position_y`,
`scale`,
`z_index`,
`created_at`,
`updated_at`
)
SELECT
`user_id`,
`decor_id`,
`placed`,
`position_x`,
`position_y`,
`scale`,
`z_index`,
`created_at`,
`updated_at`
FROM `user_room_decors`;

View File

@@ -0,0 +1,29 @@
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
export type PlayerAssetType = 'skin' | 'room_decor';
@Entity('user_assets')
@Index('idx_user_assets_user_id', ['user_id'])
@Index('idx_user_assets_user_type_asset_unique', ['user_id', 'asset_type', 'asset_id'], { unique: true })
export class PlayerAssets {
@PrimaryGeneratedColumn({ type: 'bigint', comment: '主键ID' })
id: bigint;
@Column({ type: 'bigint', nullable: false, comment: '关联users.id' })
user_id: bigint;
@Column({ type: 'varchar', length: 40, nullable: false, comment: '资产类型skin/room_decor/item/badge等' })
asset_type: PlayerAssetType;
@Column({ type: 'varchar', length: 100, nullable: false, comment: '资产ID' })
asset_id: string;
@Column({ type: 'varchar', length: 50, nullable: false, default: 'system', comment: '发放来源' })
source: string;
@Column({ type: 'json', nullable: true, comment: '资产扩展数据' })
metadata?: Record<string, unknown> | null;
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP', comment: '获得时间' })
acquired_at: Date;
}

View File

@@ -0,0 +1,48 @@
import { DynamicModule, Global, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PlayerAssets } from './player_assets.entity';
import { PlayerAssetsMemoryService } from './player_assets_memory.service';
import { PlayerAssetsService } from './player_assets.service';
import { RoomDecorPlacements } from './room_decor_placements.entity';
import { RoomDecorPlacementsMemoryService } from './room_decor_placements_memory.service';
import { RoomDecorPlacementsService } from './room_decor_placements.service';
@Global()
@Module({})
export class PlayerAssetsModule {
static forDatabase(): DynamicModule {
return {
module: PlayerAssetsModule,
imports: [TypeOrmModule.forFeature([PlayerAssets, RoomDecorPlacements])],
providers: [
PlayerAssetsService,
RoomDecorPlacementsService,
{ provide: 'IPlayerAssetsService', useClass: PlayerAssetsService },
{ provide: 'IRoomDecorPlacementsService', useClass: RoomDecorPlacementsService },
],
exports: [PlayerAssetsService, RoomDecorPlacementsService, 'IPlayerAssetsService', 'IRoomDecorPlacementsService'],
};
}
static forMemory(): DynamicModule {
return {
module: PlayerAssetsModule,
providers: [
PlayerAssetsMemoryService,
RoomDecorPlacementsMemoryService,
{ provide: 'IPlayerAssetsService', useClass: PlayerAssetsMemoryService },
{ provide: 'IRoomDecorPlacementsService', useClass: RoomDecorPlacementsMemoryService },
],
exports: [PlayerAssetsMemoryService, RoomDecorPlacementsMemoryService, 'IPlayerAssetsService', 'IRoomDecorPlacementsService'],
};
}
static forRoot(useMemory?: boolean): DynamicModule {
const shouldUseMemory = useMemory ?? (
process.env.NODE_ENV === 'test' ||
process.env.USE_MEMORY_STORAGE === 'true' ||
!process.env.DB_HOST
);
return shouldUseMemory ? this.forMemory() : this.forDatabase();
}
}

View File

@@ -0,0 +1,73 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { PlayerAssets, PlayerAssetType } from './player_assets.entity';
@Injectable()
export class PlayerAssetsService {
constructor(
@InjectRepository(PlayerAssets)
private readonly playerAssetsRepository: Repository<PlayerAssets>,
) {}
async grantAsset(userId: bigint, assetType: PlayerAssetType, assetId: string, source = 'system', metadata?: Record<string, unknown>): Promise<PlayerAssets> {
const normalizedAssetId = this.normalizeAssetId(assetId);
const existing = await this.playerAssetsRepository.findOne({
where: { user_id: userId, asset_type: assetType, asset_id: normalizedAssetId },
});
if (existing) {
return existing;
}
const row = new PlayerAssets();
row.user_id = userId;
row.asset_type = assetType;
row.asset_id = normalizedAssetId;
row.source = source || 'system';
row.metadata = metadata ?? null;
row.acquired_at = new Date();
return await this.playerAssetsRepository.save(row);
}
async hasAsset(userId: bigint, assetType: PlayerAssetType, assetId: string): Promise<boolean> {
const normalizedAssetId = (assetId || '').trim();
if (!normalizedAssetId) {
return false;
}
const count = await this.playerAssetsRepository.count({
where: { user_id: userId, asset_type: assetType, asset_id: normalizedAssetId },
});
return count > 0;
}
async listAssets(userId: bigint, assetType?: PlayerAssetType): Promise<PlayerAssets[]> {
return await this.playerAssetsRepository.find({
where: assetType ? { user_id: userId, asset_type: assetType } : { user_id: userId },
order: { acquired_at: 'ASC', id: 'ASC' },
});
}
async listAssetIds(userId: bigint, assetType: PlayerAssetType): Promise<string[]> {
const rows = await this.listAssets(userId, assetType);
return rows.map((row) => row.asset_id);
}
async hasAssetFromSource(userId: bigint, assetType: PlayerAssetType, source: string): Promise<boolean> {
const normalizedSource = (source || '').trim();
if (!normalizedSource) {
return false;
}
const count = await this.playerAssetsRepository.count({
where: { user_id: userId, asset_type: assetType, source: normalizedSource },
});
return count > 0;
}
private normalizeAssetId(assetId: string): string {
const normalized = (assetId || '').trim();
if (!/^[A-Za-z0-9_:-]{1,100}$/.test(normalized)) {
throw new BadRequestException('资产ID格式不正确');
}
return normalized;
}
}

View File

@@ -0,0 +1,73 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { PlayerAssets, PlayerAssetType } from './player_assets.entity';
@Injectable()
export class PlayerAssetsMemoryService {
private assets: Map<bigint, PlayerAssets> = new Map();
private userAssetIndex: Map<string, bigint> = new Map();
private currentId: bigint = BigInt(1);
async grantAsset(userId: bigint, assetType: PlayerAssetType, assetId: string, source = 'system', metadata?: Record<string, unknown>): Promise<PlayerAssets> {
const normalizedAssetId = this.normalizeAssetId(assetId);
const key = this.indexKey(userId, assetType, normalizedAssetId);
const existingId = this.userAssetIndex.get(key);
if (existingId) {
return this.assets.get(existingId) as PlayerAssets;
}
const row = new PlayerAssets();
row.id = this.currentId++;
row.user_id = userId;
row.asset_type = assetType;
row.asset_id = normalizedAssetId;
row.source = source || 'system';
row.metadata = metadata ?? null;
row.acquired_at = new Date();
this.assets.set(row.id, row);
this.userAssetIndex.set(key, row.id);
return row;
}
async hasAsset(userId: bigint, assetType: PlayerAssetType, assetId: string): Promise<boolean> {
const normalizedAssetId = (assetId || '').trim();
return normalizedAssetId ? this.userAssetIndex.has(this.indexKey(userId, assetType, normalizedAssetId)) : false;
}
async listAssets(userId: bigint, assetType?: PlayerAssetType): Promise<PlayerAssets[]> {
return Array.from(this.assets.values())
.filter((row) => row.user_id === userId && (!assetType || row.asset_type === assetType))
.sort((a, b) => {
const acquiredDiff = a.acquired_at.getTime() - b.acquired_at.getTime();
return acquiredDiff !== 0 ? acquiredDiff : Number(a.id - b.id);
});
}
async listAssetIds(userId: bigint, assetType: PlayerAssetType): Promise<string[]> {
const rows = await this.listAssets(userId, assetType);
return rows.map((row) => row.asset_id);
}
async hasAssetFromSource(userId: bigint, assetType: PlayerAssetType, source: string): Promise<boolean> {
const normalizedSource = (source || '').trim();
if (!normalizedSource) {
return false;
}
return Array.from(this.assets.values()).some((row) => (
row.user_id === userId &&
row.asset_type === assetType &&
row.source === normalizedSource
));
}
private indexKey(userId: bigint, assetType: PlayerAssetType, assetId: string): string {
return `${userId.toString()}:${assetType}:${assetId}`;
}
private normalizeAssetId(assetId: string): string {
const normalized = (assetId || '').trim();
if (!/^[A-Za-z0-9_:-]{1,100}$/.test(normalized)) {
throw new BadRequestException('资产ID格式不正确');
}
return normalized;
}
}

View File

@@ -0,0 +1,36 @@
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
@Entity('room_decor_placements')
@Index('idx_room_decor_placements_user_id', ['user_id'])
@Index('idx_room_decor_placements_user_decor_unique', ['user_id', 'decor_id'], { unique: true })
export class RoomDecorPlacements {
@PrimaryGeneratedColumn({ type: 'bigint', comment: '主键ID' })
id: bigint;
@Column({ type: 'bigint', nullable: false, comment: '关联users.id' })
user_id: bigint;
@Column({ type: 'varchar', length: 100, nullable: false, comment: '房间摆件ID' })
decor_id: string;
@Column({ type: 'boolean', nullable: false, default: false, comment: '是否已摆放' })
placed: boolean;
@Column({ type: 'float', nullable: true, comment: '房间内X坐标' })
position_x: number | null;
@Column({ type: 'float', nullable: true, comment: '房间内Y坐标' })
position_y: number | null;
@Column({ type: 'float', nullable: false, default: 1, comment: '摆件缩放' })
scale: number;
@Column({ type: 'int', nullable: false, default: 0, comment: '摆放层级' })
z_index: number;
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP', comment: '创建时间' })
created_at: Date;
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP', onUpdate: 'CURRENT_TIMESTAMP', comment: '更新时间' })
updated_at: Date;
}

View File

@@ -0,0 +1,48 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { SaveRoomDecorPlacementDto } from '../../../business/room_decor/dto/save_room_decor_placement.dto';
import { RoomDecorPlacements } from './room_decor_placements.entity';
@Injectable()
export class RoomDecorPlacementsService {
constructor(
@InjectRepository(RoomDecorPlacements)
private readonly placementsRepository: Repository<RoomDecorPlacements>,
) {}
async listPlacements(userId: bigint): Promise<RoomDecorPlacements[]> {
return await this.placementsRepository.find({
where: { user_id: userId },
order: { created_at: 'ASC', id: 'ASC' },
});
}
async savePlacement(userId: bigint, placement: SaveRoomDecorPlacementDto): Promise<RoomDecorPlacements> {
const decorId = this.normalizeDecorId(placement.decor_id);
let row = await this.placementsRepository.findOne({
where: { user_id: userId, decor_id: decorId },
});
if (!row) {
row = new RoomDecorPlacements();
row.user_id = userId;
row.decor_id = decorId;
row.created_at = new Date();
}
row.placed = placement.placed;
row.position_x = placement.placed ? Number(placement.position_x ?? row.position_x ?? 0) : null;
row.position_y = placement.placed ? Number(placement.position_y ?? row.position_y ?? 0) : null;
row.scale = Number(placement.scale ?? row.scale ?? 1);
row.z_index = Number(placement.z_index ?? row.z_index ?? 0);
row.updated_at = new Date();
return await this.placementsRepository.save(row);
}
private normalizeDecorId(decorId: string): string {
const normalized = (decorId || '').trim();
if (!/^[A-Za-z0-9_:-]{1,100}$/.test(normalized)) {
throw new BadRequestException('摆件ID格式不正确');
}
return normalized;
}
}

View File

@@ -0,0 +1,53 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { SaveRoomDecorPlacementDto } from '../../../business/room_decor/dto/save_room_decor_placement.dto';
import { RoomDecorPlacements } from './room_decor_placements.entity';
@Injectable()
export class RoomDecorPlacementsMemoryService {
private placements: Map<bigint, RoomDecorPlacements> = new Map();
private userDecorIndex: Map<string, bigint> = new Map();
private currentId: bigint = BigInt(1);
async listPlacements(userId: bigint): Promise<RoomDecorPlacements[]> {
return Array.from(this.placements.values())
.filter((row) => row.user_id === userId)
.sort((a, b) => {
const createdDiff = a.created_at.getTime() - b.created_at.getTime();
return createdDiff !== 0 ? createdDiff : Number(a.id - b.id);
});
}
async savePlacement(userId: bigint, placement: SaveRoomDecorPlacementDto): Promise<RoomDecorPlacements> {
const decorId = this.normalizeDecorId(placement.decor_id);
const key = this.indexKey(userId, decorId);
const existingId = this.userDecorIndex.get(key);
const row = existingId ? this.placements.get(existingId) as RoomDecorPlacements : new RoomDecorPlacements();
if (!existingId) {
row.id = this.currentId++;
row.user_id = userId;
row.decor_id = decorId;
row.created_at = new Date();
this.userDecorIndex.set(key, row.id);
this.placements.set(row.id, row);
}
row.placed = placement.placed;
row.position_x = placement.placed ? Number(placement.position_x ?? row.position_x ?? 0) : null;
row.position_y = placement.placed ? Number(placement.position_y ?? row.position_y ?? 0) : null;
row.scale = Number(placement.scale ?? row.scale ?? 1);
row.z_index = Number(placement.z_index ?? row.z_index ?? 0);
row.updated_at = new Date();
return row;
}
private indexKey(userId: bigint, decorId: string): string {
return `${userId.toString()}:${decorId}`;
}
private normalizeDecorId(decorId: string): string {
const normalized = (decorId || '').trim();
if (!/^[A-Za-z0-9_:-]{1,100}$/.test(normalized)) {
throw new BadRequestException('摆件ID格式不正确');
}
return normalized;
}
}

View File

@@ -0,0 +1,424 @@
/**
* 用户档案基础服务类
*
* 功能描述:
* - 提供用户档案服务的基础功能和通用方法
* - 定义日志记录和性能监控的标准模式
* - 实现错误处理和异常管理的统一规范
* - 支持双模式运行的基础架构
*
* 职责分离:
* - 日志管理:统一的日志记录格式和级别
* - 性能监控:操作耗时统计和性能指标
* - 错误处理:标准化的异常处理模式
* - 工具方法:通用的辅助功能和验证逻辑
*
* 继承关系:
* - UserProfilesService extends BaseUserProfilesService (MySQL实现)
* - UserProfilesMemoryService extends BaseUserProfilesService (内存实现)
*
* 最近修改:
* - 2026-01-08: 功能新增 - 创建用户档案基础服务类 (修改者: moyin)
*
* @author moyin
* @version 1.0.0
* @since 2026-01-08
* @lastModified 2026-01-08
*/
import { Logger } from '@nestjs/common';
/**
* 用户档案基础服务抽象类
*
* 职责:
* - 提供所有用户档案服务的通用基础功能
* - 定义标准的日志记录和性能监控模式
* - 实现统一的错误处理和异常管理
* - 支持MySQL和内存两种存储模式
*
* 设计模式:
* - 模板方法模式:定义通用的操作流程
* - 策略模式:支持不同的存储实现策略
* - 观察者模式:统一的日志和监控机制
*
* 使用场景:
* - 作为具体用户档案服务的基类
* - 提供标准化的日志和监控功能
* - 实现通用的工具方法和验证逻辑
*/
export abstract class BaseUserProfilesService {
/**
* 日志记录器
*
* 功能:
* - 记录用户档案操作的详细日志
* - 支持不同级别的日志输出
* - 提供结构化的日志格式
* - 便于问题排查和性能分析
*/
protected readonly logger = new Logger(BaseUserProfilesService.name);
/**
* 记录操作开始日志
*
* 功能描述:
* 统一记录操作开始的日志信息,包含操作类型、参数和时间戳
*
* @param operation 操作名称
* @param params 操作参数
*
* @example
* ```typescript
* this.logStart('创建用户档案', {
* userId: '123',
* currentMap: 'plaza'
* });
* ```
*/
protected logStart(operation: string, params: Record<string, any>): void {
this.logger.log(`开始${operation}`, {
operation: this.formatOperationName(operation),
...params,
timestamp: new Date().toISOString()
});
}
/**
* 记录操作成功日志
*
* 功能描述:
* 统一记录操作成功的日志信息,包含结果数据和性能指标
*
* @param operation 操作名称
* @param result 操作结果
* @param duration 操作耗时(毫秒)
*
* @example
* ```typescript
* this.logSuccess('创建用户档案', {
* profileId: '456'
* }, 150);
* ```
*/
protected logSuccess(operation: string, result: Record<string, any>, duration: number): void {
this.logger.log(`${operation}成功`, {
operation: this.formatOperationName(operation),
...result,
duration,
timestamp: new Date().toISOString()
});
}
/**
* 记录操作警告日志
*
* 功能描述:
* 统一记录操作警告的日志信息,用于记录非致命性问题
*
* @param operation 操作名称
* @param warning 警告信息
* @param params 相关参数
*
* @example
* ```typescript
* this.logWarning('更新用户位置', '用户档案不存在', {
* userId: '123'
* });
* ```
*/
protected logWarning(operation: string, warning: string, params: Record<string, any>): void {
this.logger.warn(`${operation}警告:${warning}`, {
operation: this.formatOperationName(operation),
warning,
...params,
timestamp: new Date().toISOString()
});
}
/**
* 记录操作错误日志
*
* 功能描述:
* 统一记录操作错误的日志信息,包含错误详情和堆栈信息
*
* @param operation 操作名称
* @param error 错误信息
* @param params 相关参数
* @param duration 操作耗时(毫秒)
* @param stack 错误堆栈(可选)
*
* @example
* ```typescript
* this.logError('创建用户档案', '数据库连接失败', {
* userId: '123'
* }, 500, error.stack);
* ```
*/
protected logError(
operation: string,
error: string,
params: Record<string, any>,
duration: number,
stack?: string
): void {
this.logger.error(`${operation}失败:${error}`, {
operation: this.formatOperationName(operation),
error,
...params,
duration,
timestamp: new Date().toISOString()
}, stack);
}
/**
* 处理搜索异常
*
* 功能描述:
* 专门处理搜索操作的异常,返回空结果而不抛出异常
*
* 设计理念:
* - 搜索失败不应该影响用户体验
* - 返回空结果比抛出异常更友好
* - 记录错误日志便于问题排查
*
* @param error 异常对象
* @param operation 操作名称
* @param params 操作参数
* @returns 空数组
*
* @example
* ```typescript
* try {
* return await this.searchProfiles(keyword);
* } catch (error) {
* return this.handleSearchError(error, '搜索用户档案', { keyword });
* }
* ```
*/
protected handleSearchError<T>(
error: any,
operation: string,
params: Record<string, any>
): T[] {
this.logError(
operation,
error instanceof Error ? error.message : String(error),
params,
0, // 搜索异常不计算耗时
error instanceof Error ? error.stack : undefined
);
// 搜索异常返回空数组,不影响用户体验
return [];
}
/**
* 格式化操作名称
*
* 功能描述:
* 将中文操作名称转换为英文标识符,便于日志分析和监控
*
* @param operation 中文操作名称
* @returns 英文操作标识符
*
* @example
* ```typescript
* this.formatOperationName('创建用户档案'); // 返回: 'createUserProfile'
* this.formatOperationName('更新用户位置'); // 返回: 'updateUserPosition'
* ```
*/
private formatOperationName(operation: string): string {
const operationMap: Record<string, string> = {
'创建用户档案': 'createUserProfile',
'查询用户档案': 'findUserProfile',
'更新用户档案': 'updateUserProfile',
'更新用户位置': 'updateUserPosition',
'删除用户档案': 'removeUserProfile',
'搜索用户档案': 'searchUserProfiles',
'查询地图用户': 'findUsersByMap',
'批量更新状态': 'batchUpdateStatus',
'统计用户数量': 'countUserProfiles'
};
return operationMap[operation] || operation.toLowerCase().replace(/\s+/g, '_');
}
/**
* 验证用户ID格式
*
* 功能描述:
* 验证用户ID是否为有效的bigint格式
*
* @param userId 用户ID
* @returns 是否有效
*
* @example
* ```typescript
* if (!this.isValidUserId(userId)) {
* throw new BadRequestException('用户ID格式无效');
* }
* ```
*/
protected isValidUserId(userId: any): userId is bigint {
try {
const id = BigInt(userId);
return id > 0;
} catch {
return false;
}
}
/**
* 验证坐标格式
*
* 功能描述:
* 验证位置坐标是否为有效的数字格式
*
* @param coordinate 坐标值
* @returns 是否有效
*
* @example
* ```typescript
* if (!this.isValidCoordinate(posX) || !this.isValidCoordinate(posY)) {
* throw new BadRequestException('坐标格式无效');
* }
* ```
*/
protected isValidCoordinate(coordinate: any): coordinate is number {
return typeof coordinate === 'number' &&
!isNaN(coordinate) &&
isFinite(coordinate);
}
/**
* 验证地图名称格式
*
* 功能描述:
* 验证地图名称是否符合规范要求
*
* @param mapName 地图名称
* @returns 是否有效
*
* @example
* ```typescript
* if (!this.isValidMapName(currentMap)) {
* throw new BadRequestException('地图名称格式无效');
* }
* ```
*/
protected isValidMapName(mapName: any): mapName is string {
return typeof mapName === 'string' &&
mapName.length > 0 &&
mapName.length <= 50 &&
/^[a-zA-Z0-9_-]+$/.test(mapName); // 只允许字母、数字、下划线、连字符
}
/**
* 清理敏感数据
*
* 功能描述:
* 从日志数据中移除敏感信息,保护用户隐私
*
* @param data 原始数据
* @returns 清理后的数据
*
* @example
* ```typescript
* const safeData = this.sanitizeLogData({
* userId: '123',
* email: 'user@example.com',
* password: 'secret123'
* });
* // 返回: { userId: '123', email: 'u***@example.com', password: '***' }
* ```
*/
protected sanitizeLogData(data: Record<string, any>): Record<string, any> {
const sensitiveFields = ['password', 'token', 'secret', 'key'];
const emailFields = ['email'];
const sanitized = { ...data };
for (const [key, value] of Object.entries(sanitized)) {
const lowerKey = key.toLowerCase();
// 完全隐藏敏感字段
if (sensitiveFields.some(field => lowerKey.includes(field))) {
sanitized[key] = '***';
}
// 部分隐藏邮箱字段
else if (emailFields.some(field => lowerKey.includes(field)) && typeof value === 'string') {
sanitized[key] = this.maskEmail(value);
}
}
return sanitized;
}
/**
* 邮箱脱敏处理
*
* 功能描述:
* 对邮箱地址进行脱敏处理,保护用户隐私
*
* @param email 邮箱地址
* @returns 脱敏后的邮箱
*
* @example
* ```typescript
* this.maskEmail('user@example.com'); // 返回: 'u***@example.com'
* this.maskEmail('longusername@test.org'); // 返回: 'l***@test.org'
* ```
*/
private maskEmail(email: string): string {
if (!email || !email.includes('@')) {
return '***';
}
const [username, domain] = email.split('@');
if (username.length <= 1) {
return `***@${domain}`;
}
return `${username[0]}***@${domain}`;
}
/**
* 计算操作耗时
*
* 功能描述:
* 计算操作的执行时间,用于性能监控
*
* @param startTime 开始时间戳
* @returns 耗时(毫秒)
*
* @example
* ```typescript
* const startTime = Date.now();
* // ... 执行操作
* const duration = this.calculateDuration(startTime);
* this.logSuccess('操作完成', { result }, duration);
* ```
*/
protected calculateDuration(startTime: number): number {
return Date.now() - startTime;
}
/**
* 生成操作ID
*
* 功能描述:
* 生成唯一的操作ID用于跟踪和关联日志
*
* @returns 操作ID
*
* @example
* ```typescript
* const operationId = this.generateOperationId();
* this.logger.log('开始操作', { operationId, ...params });
* ```
*/
protected generateOperationId(): string {
return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
}
}

View File

@@ -0,0 +1,491 @@
/**
* 用户档案数据传输对象模块
*
* 功能描述:
* - 定义用户档案相关的数据传输对象
* - 提供数据验证和类型约束
* - 支持位置信息的创建和更新操作
* - 实现完整的数据传输层抽象
*
* 职责分离:
* - 数据验证使用class-validator进行输入验证
* - 类型定义TypeScript类型安全保证
* - 数据转换:支持前端到后端的数据映射
* - 接口规范统一的API数据格式
*
* 依赖模块:
* - class-validator: 数据验证装饰器
* - class-transformer: 数据转换装饰器
*
* 最近修改:
* - 2026-01-08: 功能新增 - 创建用户档案DTO支持位置广播系统 (修改者: moyin)
*
* @author moyin
* @version 1.0.0
* @since 2026-01-08
* @lastModified 2026-01-08
*/
import { IsString, IsNumber, IsOptional, IsNotEmpty, IsObject, IsInt, Min, Max, Length } from 'class-validator';
import { Type } from 'class-transformer';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
/**
* 创建用户档案DTO
*
* 职责:
* - 定义创建用户档案时的必需和可选字段
* - 提供完整的数据验证规则
* - 支持位置信息的初始化
*
* 验证规则:
* - user_id: 必需,正整数
* - current_map: 必需非空字符串长度1-50
* - pos_x, pos_y: 必需,数字类型
* - 其他字段: 可选,有相应的格式验证
*/
export class CreateUserProfileDto {
/**
* 关联用户ID
*
* 验证规则:
* - 必需字段,不能为空
* - 必须是正整数
* - 用于关联users表的主键
*/
@ApiProperty({
description: '关联的用户ID',
example: 1,
type: 'integer'
})
@IsNotEmpty({ message: '用户ID不能为空' })
@Type(() => Number)
user_id: bigint;
/**
* 用户简介
*
* 验证规则:
* - 可选字段
* - 字符串类型最大长度500
* - 支持多语言和特殊字符
*/
@ApiPropertyOptional({
description: '用户自我介绍',
example: '热爱编程的全栈开发者,喜欢探索新技术',
maxLength: 500
})
@IsOptional()
@IsString({ message: '简介必须是字符串' })
@Length(0, 500, { message: '简介长度不能超过500个字符' })
bio?: string;
/**
* 简历内容
*
* 验证规则:
* - 可选字段
* - 字符串类型,支持长文本
* - 可以包含结构化信息
*/
@ApiPropertyOptional({
description: '详细简历内容',
example: '5年全栈开发经验精通React、Node.js、Python等技术栈...'
})
@IsOptional()
@IsString({ message: '简历内容必须是字符串' })
resume_content?: string;
/**
* 标签信息
*
* 验证规则:
* - 可选字段
* - 对象类型,支持嵌套结构
* - 用于存储兴趣、技能等标签
*/
@ApiPropertyOptional({
description: '用户标签信息',
example: {
interests: ['游戏', '编程', '音乐'],
skills: ['JavaScript', 'Python', 'React'],
personality: ['外向', '创新', '团队合作']
}
})
@IsOptional()
@IsObject({ message: '标签信息必须是对象格式' })
tags?: Record<string, any>;
/**
* 社交链接
*
* 验证规则:
* - 可选字段
* - 对象类型,键值对格式
* - 值必须是字符串URL格式
*/
@ApiPropertyOptional({
description: '社交媒体链接',
example: {
github: 'https://github.com/username',
twitter: 'https://twitter.com/username',
linkedin: 'https://linkedin.com/in/username'
}
})
@IsOptional()
@IsObject({ message: '社交链接必须是对象格式' })
social_links?: Record<string, string>;
/**
* 皮肤ID
*
* 验证规则:
* - 可选字段
* - 整数类型范围1-999999
* - 关联皮肤资源库
*/
@ApiPropertyOptional({
description: '角色皮肤ID',
example: 'classic_whale',
maxLength: 100
})
@IsOptional()
@IsString({ message: '皮肤ID必须是字符串' })
@Length(1, 100, { message: '皮肤ID长度需在1-100字符之间' })
skin_id?: string;
/**
* 当前地图
*
* 验证规则:
* - 必需字段,默认值'plaza'
* - 字符串类型长度1-50
* - 不能为空字符串
*/
@ApiProperty({
description: '当前所在地图',
example: 'plaza',
default: 'plaza',
minLength: 1,
maxLength: 50
})
@IsString({ message: '地图名称必须是字符串' })
@IsNotEmpty({ message: '地图名称不能为空' })
@Length(1, 50, { message: '地图名称长度必须在1-50个字符之间' })
current_map: string = 'plaza';
/**
* X坐标
*
* 验证规则:
* - 必需字段默认值0
* - 数字类型,支持小数
* - 坐标范围由具体地图决定
*/
@ApiProperty({
description: 'X轴坐标位置',
example: 100.5,
default: 0,
type: 'number'
})
@IsNumber({}, { message: 'X坐标必须是数字' })
@Type(() => Number)
pos_x: number = 0;
/**
* Y坐标
*
* 验证规则:
* - 必需字段默认值0
* - 数字类型,支持小数
* - 坐标范围由具体地图决定
*/
@ApiProperty({
description: 'Y轴坐标位置',
example: 200.3,
default: 0,
type: 'number'
})
@IsNumber({}, { message: 'Y坐标必须是数字' })
@Type(() => Number)
pos_y: number = 0;
/**
* 用户状态
*
* 验证规则:
* - 可选字段默认值0离线
* - 整数类型范围0-255
* - 0: 离线1: 在线2: 忙碌3: 隐身
*/
@ApiPropertyOptional({
description: '用户状态',
example: 1,
default: 0,
minimum: 0,
maximum: 255,
enum: [0, 1, 2, 3],
enumName: 'UserProfileStatus'
})
@IsOptional()
@IsInt({ message: '用户状态必须是整数' })
@Min(0, { message: '用户状态不能小于0' })
@Max(255, { message: '用户状态不能大于255' })
status?: number = 0;
}
/**
* 更新用户档案DTO
*
* 职责:
* - 定义更新用户档案时的可选字段
* - 继承创建DTO的验证规则
* - 支持部分字段更新
*
* 特点:
* - 所有字段都是可选的
* - 保持与创建DTO相同的验证规则
* - 支持灵活的部分更新操作
*/
export class UpdateUserProfileDto {
/**
* 用户简介(可选更新)
*/
@ApiPropertyOptional({
description: '用户自我介绍',
example: '更新后的自我介绍',
maxLength: 500
})
@IsOptional()
@IsString({ message: '简介必须是字符串' })
@Length(0, 500, { message: '简介长度不能超过500个字符' })
bio?: string;
/**
* 简历内容(可选更新)
*/
@ApiPropertyOptional({
description: '详细简历内容',
example: '更新后的简历内容'
})
@IsOptional()
@IsString({ message: '简历内容必须是字符串' })
resume_content?: string;
/**
* 标签信息(可选更新)
*/
@ApiPropertyOptional({
description: '用户标签信息',
example: {
interests: ['新的兴趣'],
skills: ['新的技能']
}
})
@IsOptional()
@IsObject({ message: '标签信息必须是对象格式' })
tags?: Record<string, any>;
/**
* 社交链接(可选更新)
*/
@ApiPropertyOptional({
description: '社交媒体链接',
example: {
github: 'https://github.com/newusername'
}
})
@IsOptional()
@IsObject({ message: '社交链接必须是对象格式' })
social_links?: Record<string, string>;
/**
* 皮肤ID可选更新
*/
@ApiPropertyOptional({
description: '角色皮肤ID',
example: 'human_whale_directional_v2_8x4',
maxLength: 100
})
@IsOptional()
@IsString({ message: '皮肤ID必须是字符串' })
@Length(1, 100, { message: '皮肤ID长度需在1-100字符之间' })
skin_id?: string;
/**
* 当前地图(可选更新)
*/
@ApiPropertyOptional({
description: '当前所在地图',
example: 'forest',
minLength: 1,
maxLength: 50
})
@IsOptional()
@IsString({ message: '地图名称必须是字符串' })
@IsNotEmpty({ message: '地图名称不能为空' })
@Length(1, 50, { message: '地图名称长度必须在1-50个字符之间' })
current_map?: string;
/**
* X坐标可选更新
*/
@ApiPropertyOptional({
description: 'X轴坐标位置',
example: 150.7,
type: 'number'
})
@IsOptional()
@IsNumber({}, { message: 'X坐标必须是数字' })
@Type(() => Number)
pos_x?: number;
/**
* Y坐标可选更新
*/
@ApiPropertyOptional({
description: 'Y轴坐标位置',
example: 250.9,
type: 'number'
})
@IsOptional()
@IsNumber({}, { message: 'Y坐标必须是数字' })
@Type(() => Number)
pos_y?: number;
/**
* 用户状态(可选更新)
*/
@ApiPropertyOptional({
description: '用户状态',
example: 2,
minimum: 0,
maximum: 255,
enum: [0, 1, 2, 3]
})
@IsOptional()
@IsInt({ message: '用户状态必须是整数' })
@Min(0, { message: '用户状态不能小于0' })
@Max(255, { message: '用户状态不能大于255' })
status?: number;
}
/**
* 位置更新DTO
*
* 职责:
* - 专门用于位置广播系统的位置更新
* - 只包含位置相关的核心字段
* - 提供高性能的位置数据传输
*
* 使用场景:
* - WebSocket位置更新消息
* - 批量位置同步操作
* - 位置广播系统的核心数据结构
*/
export class UpdatePositionDto {
/**
* 当前地图
*/
@ApiProperty({
description: '当前所在地图',
example: 'plaza',
minLength: 1,
maxLength: 50
})
@IsString({ message: '地图名称必须是字符串' })
@IsNotEmpty({ message: '地图名称不能为空' })
@Length(1, 50, { message: '地图名称长度必须在1-50个字符之间' })
current_map: string;
/**
* X坐标
*/
@ApiProperty({
description: 'X轴坐标位置',
example: 100.5,
type: 'number'
})
@IsNumber({}, { message: 'X坐标必须是数字' })
@Type(() => Number)
pos_x: number;
/**
* Y坐标
*/
@ApiProperty({
description: 'Y轴坐标位置',
example: 200.3,
type: 'number'
})
@IsNumber({}, { message: 'Y坐标必须是数字' })
@Type(() => Number)
pos_y: number;
}
/**
* 用户档案查询DTO
*
* 职责:
* - 定义查询用户档案时的过滤条件
* - 支持分页和排序参数
* - 提供灵活的查询选项
*/
export class QueryUserProfileDto {
/**
* 地图过滤
*/
@ApiPropertyOptional({
description: '按地图过滤用户',
example: 'plaza'
})
@IsOptional()
@IsString({ message: '地图名称必须是字符串' })
current_map?: string;
/**
* 状态过滤
*/
@ApiPropertyOptional({
description: '按状态过滤用户',
example: 1,
enum: [0, 1, 2, 3]
})
@IsOptional()
@IsInt({ message: '状态必须是整数' })
@Min(0, { message: '状态不能小于0' })
@Max(255, { message: '状态不能大于255' })
status?: number;
/**
* 分页大小
*/
@ApiPropertyOptional({
description: '每页数量',
example: 20,
default: 20,
minimum: 1,
maximum: 100
})
@IsOptional()
@IsInt({ message: '分页大小必须是整数' })
@Min(1, { message: '分页大小不能小于1' })
@Max(100, { message: '分页大小不能超过100' })
@Type(() => Number)
limit?: number = 20;
/**
* 偏移量
*/
@ApiPropertyOptional({
description: '偏移量',
example: 0,
default: 0,
minimum: 0
})
@IsOptional()
@IsInt({ message: '偏移量必须是整数' })
@Min(0, { message: '偏移量不能小于0' })
@Type(() => Number)
offset?: number = 0;
}

View File

@@ -0,0 +1,403 @@
/**
* 用户档案数据实体模块
*
* 功能描述:
* - 定义用户档案表的实体映射和字段约束
* - 提供用户档案数据的持久化存储结构
* - 支持用户位置信息和档案数据存储
* - 实现完整的用户档案数据模型和关系映射
*
* 职责分离:
* - 数据映射TypeORM实体与数据库表的映射关系
* - 约束定义:字段类型、长度、唯一性等约束规则
* - 关系管理:与其他实体的关联关系定义
* - 索引优化:数据库查询性能优化策略
*
* 依赖模块:
* - TypeORM: ORM框架提供数据库映射功能
* - MySQL: 底层数据库存储
*
* 数据库表user_profiles
* 存储引擎InnoDB
* 字符集utf8mb4
*
* 最近修改:
* - 2026-01-08: 功能新增 - 创建用户档案实体,支持位置广播系统 (修改者: moyin)
*
* @author moyin
* @version 1.0.0
* @since 2026-01-08
* @lastModified 2026-01-08
*/
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
/**
* 用户档案实体类
*
* 职责:
* - 映射数据库user_profiles表的结构和约束
* - 定义用户档案数据的字段类型和验证规则
* - 提供用户位置信息和档案数据的完整数据模型
*
* 主要功能:
* - 用户基础档案信息存储
* - 用户位置信息管理current_map, pos_x, pos_y
* - 用户状态和活跃度跟踪
* - 自动时间戳记录和更新
*
* 数据完整性:
* - 主键约束id字段自增主键
* - 外键约束user_id关联users表
* - 非空约束user_id, current_map, pos_x, pos_y
* - 默认值current_map='plaza', pos_x=0, pos_y=0
*
* 使用场景:
* - 用户档案信息查询和更新
* - 位置广播系统的位置数据存储
* - 用户活跃度统计和分析
* - 游戏内用户状态管理
*
* 索引策略:
* - 主键索引id (自动创建)
* - 唯一索引user_id (用户唯一档案)
* - 普通索引current_map (用于地图查询)
* - 复合索引current_map + status (用于活跃用户查询)
*/
@Entity('user_profiles')
export class UserProfiles {
/**
* 档案主键ID
*
* 数据库设计:
* - 类型BIGINT支持大量档案数据
* - 约束:主键、非空、自增
* - 范围1 ~ 9,223,372,036,854,775,807
*
* 业务规则:
* - 系统自动生成,不可手动指定
* - 全局唯一标识符,用于档案关联
* - 作为其他表的外键引用
*/
@PrimaryGeneratedColumn({
type: 'bigint',
comment: '主键ID'
})
id: bigint;
/**
* 关联用户ID
*
* 数据库设计:
* - 类型BIGINT与users表id字段对应
* - 约束:非空、唯一索引
* - 外键关联users表的主键
*
* 业务规则:
* - 每个用户只能有一个档案记录
* - 用于关联用户基础信息和档案信息
* - 删除用户时需要同步处理档案数据
*
* 性能考虑:
* - 建立唯一索引,确保一对一关系
* - 用于JOIN查询用户完整信息
*/
@Column({
type: 'bigint',
nullable: false,
unique: true,
comment: '关联users.id'
})
user_id: bigint;
/**
* 用户简介
*
* 数据库设计:
* - 类型VARCHAR(500),支持较长的自我介绍
* - 约束:允许空,无唯一性要求
* - 字符集utf8mb4支持emoji表情
*
* 业务规则:
* - 用户自定义的个人简介信息
* - 支持多语言和特殊字符
* - 长度限制最多500个字符
* - 可用于用户搜索和推荐
*/
@Column({
type: 'varchar',
length: 500,
nullable: true,
comment: '自我介绍'
})
bio?: string;
/**
* 简历内容
*
* 数据库设计:
* - 类型TEXT支持大量文本内容
* - 约束:允许空,无长度限制
* - 存储:适合存储结构化的简历信息
*
* 业务规则:
* - 用户的详细简历或经历信息
* - 支持富文本或结构化数据
* - 可用于职业匹配和推荐
* - 隐私敏感,需要权限控制
*/
@Column({
type: 'text',
nullable: true,
comment: '个人详细简历'
})
resume_content?: string;
/**
* 标签信息
*
* 数据库设计:
* - 类型JSON支持结构化标签数据
* - 约束:允许空,灵活的数据结构
* - 存储JSON格式便于查询和过滤
*
* 业务规则:
* - 用户的兴趣标签、技能标签等
* - 支持多维度标签分类
* - 用于用户匹配和内容推荐
* - 支持动态添加和删除标签
*
* 数据格式示例:
* ```json
* {
* "interests": ["游戏", "编程", "音乐"],
* "skills": ["JavaScript", "Python", "React"],
* "personality": ["外向", "创新", "团队合作"]
* }
* ```
*/
@Column({
type: 'json',
nullable: true,
comment: '身份标签信息'
})
tags?: Record<string, any>;
/**
* 社交链接
*
* 数据库设计:
* - 类型JSON支持多个社交平台链接
* - 约束:允许空,灵活的数据结构
* - 存储JSON格式便于扩展新平台
*
* 业务规则:
* - 用户的各种社交媒体链接
* - 支持GitHub、Twitter、LinkedIn等平台
* - 用于用户社交网络建立
* - 需要验证链接的有效性
*
* 数据格式示例:
* ```json
* {
* "github": "https://github.com/username",
* "twitter": "https://twitter.com/username",
* "linkedin": "https://linkedin.com/in/username",
* "website": "https://personal-website.com"
* }
* ```
*/
@Column({
type: 'json',
nullable: true,
comment: '社交链接信息'
})
social_links?: Record<string, string>;
/**
* 皮肤ID
*
* 数据库设计:
* - 类型INT整数类型
* - 约束允许空默认值null
* - 范围:支持大量皮肤选择
*
* 业务规则:
* - 用户选择的游戏皮肤或主题
* - 关联皮肤资源库的ID
* - 影响游戏内角色外观
* - 支持皮肤商城和个性化定制
*/
@Column({
type: 'varchar',
length: 100,
nullable: true,
comment: '角色外观皮肤ID'
})
skin_id?: string;
/**
* 当前地图
*
* 数据库设计:
* - 类型VARCHAR(50),支持地图名称
* - 约束:非空、默认值'plaza'
* - 索引:用于地图用户查询
*
* 业务规则:
* - 用户当前所在的游戏地图
* - 用于位置广播系统的地图过滤
* - 影响用户可见性和交互范围
* - 默认为广场(plaza),新用户的起始位置
*
* 位置广播系统:
* - 核心字段,用于确定用户所在区域
* - 同一地图的用户可以相互看到位置
* - 切换地图时需要更新此字段
*/
@Column({
type: 'varchar',
length: 50,
nullable: false,
default: 'plaza',
comment: '当前所在地图'
})
current_map: string;
/**
* X坐标位置
*
* 数据库设计:
* - 类型FLOAT支持小数坐标
* - 约束非空、默认值0
* - 精度:单精度浮点数,满足游戏精度需求
*
* 业务规则:
* - 用户在当前地图的X轴坐标
* - 用于位置广播系统的精确定位
* - 坐标范围由具体地图决定
* - 默认值0表示地图中心或起始点
*
* 位置广播系统:
* - 核心字段,用于计算用户间距离
* - 实时更新,频繁读写操作
* - 需要与Redis缓存保持同步
*/
@Column({
type: 'float',
nullable: false,
default: 0,
comment: 'X坐标横轴'
})
pos_x: number;
/**
* Y坐标位置
*
* 数据库设计:
* - 类型FLOAT支持小数坐标
* - 约束非空、默认值0
* - 精度:单精度浮点数,满足游戏精度需求
*
* 业务规则:
* - 用户在当前地图的Y轴坐标
* - 用于位置广播系统的精确定位
* - 坐标范围由具体地图决定
* - 默认值0表示地图中心或起始点
*
* 位置广播系统:
* - 核心字段,用于计算用户间距离
* - 实时更新,频繁读写操作
* - 需要与Redis缓存保持同步
*/
@Column({
type: 'float',
nullable: false,
default: 0,
comment: 'Y坐标纵轴'
})
pos_y: number;
/**
* 用户状态
*
* 数据库设计:
* - 类型TINYINT节省存储空间
* - 约束非空、默认值0
* - 范围0-255支持多种状态
*
* 业务规则:
* - 用户当前的活动状态
* - 0: 离线1: 在线2: 忙碌3: 隐身等
* - 影响位置广播的可见性
* - 用于用户活跃度统计
*
* 位置广播系统:
* - 影响位置信息的广播范围
* - 隐身用户不参与位置广播
* - 离线用户需要清理位置缓存
*/
@Column({
type: 'tinyint',
nullable: false,
default: 0,
comment: '状态0-离线1-在线2-忙碌3-隐身'
})
status: number;
/**
* 最后登录时间
*
* 数据库设计:
* - 类型DATETIME精确到秒
* - 约束:允许空,新用户可能为空
* - 时区使用系统时区建议UTC
*
* 业务规则:
* - 记录用户最后一次登录的时间
* - 用于用户活跃度分析
* - 支持长时间未登录用户的清理
* - 影响位置数据的有效性判断
*
* 位置广播系统:
* - 用于判断位置数据的时效性
* - 长时间未登录的用户位置数据可能过期
* - 支持基于登录时间的数据清理策略
*/
@Column({
type: 'datetime',
nullable: true,
comment: '最后登录时间'
})
last_login_at?: Date;
/**
* 最后位置更新时间
*
* 数据库设计:
* - 类型DATETIME精确到秒
* - 约束允许空默认值null
* - 时区使用系统时区建议UTC
*
* 业务规则:
* - 记录用户位置最后更新的时间
* - 用于位置数据的缓存失效判断
* - 支持位置更新频率的统计分析
* - 用于清理过期的位置缓存数据
*
* 位置广播系统:
* - 核心字段,用于缓存同步策略
* - 判断Redis中位置数据是否需要更新
* - 支持增量同步和数据一致性保证
* - 用于性能监控和优化
*
* 注意此字段需要通过ALTER TABLE添加到现有表中
*/
@Column({
type: 'datetime',
nullable: true,
default: null,
comment: '最后位置更新时间,用于位置广播系统'
})
last_position_update?: Date;
}

View File

@@ -0,0 +1,225 @@
/**
* 用户档案模块
*
* 功能描述:
* - 提供用户档案数据访问的完整模块配置
* - 支持MySQL和内存两种存储模式的动态切换
* - 集成TypeORM实体和服务的依赖注入
* - 为位置广播系统提供数据持久化支持
*
* 职责分离:
* - 模块配置:定义模块的导入、提供者和导出
* - 依赖注入:配置服务和存储库的注入关系
* - 存储模式:支持数据库和内存两种存储实现
* - 接口抽象:提供统一的服务接口供业务层使用
*
* 存储模式:
* - 数据库模式使用TypeORM连接MySQL数据库
* - 内存模式使用Map存储适用于开发和测试
*
* 最近修改:
* - 2026-01-08: 功能新增 - 创建用户档案模块,支持位置广播系统 (修改者: moyin)
*
* @author moyin
* @version 1.0.0
* @since 2026-01-08
* @lastModified 2026-01-08
*/
import { Module, DynamicModule, Global } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserProfiles } from './user_profiles.entity';
import { UserProfilesService } from './user_profiles.service';
import { UserProfilesMemoryService } from './user_profiles_memory.service';
/**
* 用户档案模块类
*
* 职责:
* - 配置用户档案相关的服务和实体
* - 提供数据库和内存两种存储模式
* - 支持动态模块配置和依赖注入
* - 为位置广播系统提供数据访问层
*
* 模块特性:
* - 动态模块:支持运行时配置选择
* - 双模式支持:数据库模式和内存模式
* - 接口统一:提供一致的服务接口
* - 可测试性:内存模式便于单元测试
*
* 使用场景:
* - 生产环境:使用数据库模式,数据持久化
* - 开发测试:使用内存模式,快速启动
* - 单元测试:使用内存模式,隔离测试
* - 故障降级:数据库故障时切换到内存模式
*/
@Global()
@Module({})
export class UserProfilesModule {
/**
* 配置数据库模式的用户档案模块
*
* 功能描述:
* 创建使用MySQL数据库的用户档案模块配置
*
* 技术实现:
* 1. 导入TypeORM模块并注册UserProfiles实体
* 2. 提供UserProfilesService作为数据访问服务
* 3. 导出服务供其他模块使用
* 4. 配置依赖注入关系
*
* 适用场景:
* - 生产环境部署
* - 需要数据持久化的场景
* - 多实例部署的数据共享
* - 大数据量的用户档案管理
*
* @returns 配置了数据库模式的动态模块
*
* @example
* ```typescript
* // 在AppModule中使用数据库模式
* @Module({
* imports: [
* UserProfilesModule.forDatabase(),
* // 其他模块...
* ],
* })
* export class AppModule {}
* ```
*/
static forDatabase(): DynamicModule {
return {
module: UserProfilesModule,
imports: [
// 导入TypeORM模块注册UserProfiles实体
TypeOrmModule.forFeature([UserProfiles])
],
providers: [
// 提供MySQL数据库实现的用户档案服务
UserProfilesService,
{
// 使用接口名称作为注入令牌,便于依赖注入
provide: 'IUserProfilesService',
useClass: UserProfilesService,
},
],
exports: [
// 导出服务供其他模块使用
UserProfilesService,
'IUserProfilesService',
],
};
}
/**
* 配置内存模式的用户档案模块
*
* 功能描述:
* 创建使用内存存储的用户档案模块配置
*
* 技术实现:
* 1. 提供UserProfilesMemoryService作为内存存储服务
* 2. 使用Map数据结构进行内存数据管理
* 3. 导出服务供其他模块使用
* 4. 配置统一的服务接口
*
* 适用场景:
* - 开发环境快速启动
* - 单元测试和集成测试
* - 演示和原型开发
* - 数据库故障时的降级方案
*
* 性能特点:
* - 启动速度快,无需数据库连接
* - 读写性能高,直接内存访问
* - 数据易失,重启后数据丢失
* - 内存占用,大数据量时需注意
*
* @returns 配置了内存模式的动态模块
*
* @example
* ```typescript
* // 在测试模块中使用内存模式
* @Module({
* imports: [
* UserProfilesModule.forMemory(),
* // 其他测试模块...
* ],
* })
* export class TestModule {}
* ```
*/
static forMemory(): DynamicModule {
return {
module: UserProfilesModule,
providers: [
// 提供内存存储实现的用户档案服务
UserProfilesMemoryService,
{
// 使用接口名称作为注入令牌,保持接口一致性
provide: 'IUserProfilesService',
useClass: UserProfilesMemoryService,
},
],
exports: [
// 导出服务供其他模块使用
UserProfilesMemoryService,
'IUserProfilesService',
],
};
}
/**
* 根据配置自动选择存储模式
*
* 功能描述:
* 根据环境变量或配置参数自动选择数据库或内存模式
*
* 技术实现:
* 1. 读取环境变量或配置参数
* 2. 根据配置选择对应的存储模式
* 3. 返回相应的动态模块配置
* 4. 支持运行时模式切换
*
* 配置规则:
* - DB_HOST存在且不为空使用数据库模式
* - DB_HOST不存在或为空使用内存模式
* - NODE_ENV=test强制使用内存模式
* - USE_MEMORY_STORAGE=true强制使用内存模式
*
* @param useMemory 是否强制使用内存模式(可选)
* @returns 自动选择的动态模块配置
*
* @example
* ```typescript
* // 在AppModule中使用自动模式选择
* @Module({
* imports: [
* UserProfilesModule.forRoot(),
* // 其他模块...
* ],
* })
* export class AppModule {}
*
* // 强制使用内存模式
* UserProfilesModule.forRoot(true);
* ```
*/
static forRoot(useMemory?: boolean): DynamicModule {
// 自动检测存储模式
const shouldUseMemory = useMemory ?? (
process.env.NODE_ENV === 'test' ||
process.env.USE_MEMORY_STORAGE === 'true' ||
!process.env.DB_HOST
);
// 根据检测结果选择对应的模块配置
if (shouldUseMemory) {
return this.forMemory();
} else {
return this.forDatabase();
}
}
}

View File

@@ -0,0 +1,621 @@
/**
* 用户档案服务类
*
* 功能描述:
* - 提供用户档案数据的增删改查技术实现
* - 处理位置信息的持久化和存储操作
* - 数据格式验证和约束检查
* - 支持完整的用户档案生命周期管理
*
* 职责分离:
* - 数据持久化通过TypeORM操作MySQL数据库
* - 数据验证:数据格式和约束完整性检查
* - 异常处理:统一的错误处理和日志记录
* - 性能监控:操作耗时统计和性能优化
*
* 位置广播系统集成:
* - 位置数据的持久化存储
* - 支持位置更新时间戳管理
* - 提供地图用户查询功能
* - 实现位置数据的批量操作
*
* 最近修改:
* - 2026-01-08: 功能新增 - 创建用户档案服务,支持位置广播系统 (修改者: moyin)
*
* @author moyin
* @version 1.0.0
* @since 2026-01-08
* @lastModified 2026-01-08
*/
import { Injectable, ConflictException, NotFoundException, BadRequestException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, FindOptionsWhere } from 'typeorm';
import { UserProfiles } from './user_profiles.entity';
import { CreateUserProfileDto, UpdateUserProfileDto, UpdatePositionDto, QueryUserProfileDto } from './user_profiles.dto';
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';
import { BaseUserProfilesService } from './base_user_profiles.service';
@Injectable()
export class UserProfilesService extends BaseUserProfilesService {
constructor(
@InjectRepository(UserProfiles)
private readonly userProfilesRepository: Repository<UserProfiles>,
) {
super(); // 调用基类构造函数
}
/**
* 创建新用户档案
*
* 技术实现:
* 1. 验证输入数据的格式和完整性
* 2. 使用class-validator进行DTO数据验证
* 3. 检查用户ID的唯一性约束
* 4. 创建用户档案实体并设置默认值
* 5. 保存用户档案数据到数据库
* 6. 记录操作日志和性能指标
* 7. 返回创建成功的用户档案实体
*
* @param createUserProfileDto 创建用户档案的数据传输对象
* @returns 创建成功的用户档案实体包含自动生成的ID和时间戳
* @throws BadRequestException 当数据验证失败或输入格式错误时
* @throws ConflictException 当用户ID已存在档案时
*
* @example
* ```typescript
* const newProfile = await userProfilesService.create({
* user_id: BigInt(1),
* current_map: 'plaza',
* pos_x: 0,
* pos_y: 0,
* bio: '新用户'
* });
* console.log(`用户档案创建成功ID: ${newProfile.id}`);
* ```
*/
async create(createUserProfileDto: CreateUserProfileDto): Promise<UserProfiles> {
const startTime = Date.now();
this.logger.log('开始创建用户档案', {
operation: 'create',
userId: createUserProfileDto.user_id.toString(),
currentMap: createUserProfileDto.current_map,
timestamp: new Date().toISOString()
});
try {
// 验证DTO
const dto = plainToClass(CreateUserProfileDto, createUserProfileDto);
const validationErrors = await validate(dto);
if (validationErrors.length > 0) {
const errorMessages = validationErrors.map(error =>
Object.values(error.constraints || {}).join(', ')
).join('; ');
this.logger.warn('用户档案创建失败:数据验证失败', {
operation: 'create',
userId: createUserProfileDto.user_id.toString(),
validationErrors: errorMessages
});
throw new BadRequestException(`数据验证失败: ${errorMessages}`);
}
// 检查用户ID是否已存在档案
const existingProfile = await this.userProfilesRepository.findOne({
where: { user_id: createUserProfileDto.user_id }
});
if (existingProfile) {
this.logger.warn('用户档案创建失败用户ID已存在档案', {
operation: 'create',
userId: createUserProfileDto.user_id.toString(),
existingProfileId: existingProfile.id.toString()
});
throw new ConflictException('该用户已存在档案记录');
}
// 创建用户档案实体
const userProfile = new UserProfiles();
userProfile.user_id = createUserProfileDto.user_id;
userProfile.bio = createUserProfileDto.bio || null;
userProfile.resume_content = createUserProfileDto.resume_content || null;
userProfile.tags = createUserProfileDto.tags || null;
userProfile.social_links = createUserProfileDto.social_links || null;
userProfile.skin_id = createUserProfileDto.skin_id || null;
userProfile.current_map = createUserProfileDto.current_map || 'plaza';
userProfile.pos_x = createUserProfileDto.pos_x || 0;
userProfile.pos_y = createUserProfileDto.pos_y || 0;
userProfile.status = createUserProfileDto.status || 0;
userProfile.last_position_update = new Date(); // 设置初始位置更新时间
// 保存到数据库
const savedProfile = await this.userProfilesRepository.save(userProfile);
const duration = Date.now() - startTime;
this.logger.log('用户档案创建成功', {
operation: 'create',
profileId: savedProfile.id.toString(),
userId: savedProfile.user_id.toString(),
currentMap: savedProfile.current_map,
duration,
timestamp: new Date().toISOString()
});
return savedProfile;
} catch (error) {
const duration = Date.now() - startTime;
if (error instanceof BadRequestException || error instanceof ConflictException) {
throw error;
}
this.logger.error('用户档案创建系统异常', {
operation: 'create',
userId: createUserProfileDto.user_id.toString(),
error: error instanceof Error ? error.message : String(error),
duration,
timestamp: new Date().toISOString()
}, error instanceof Error ? error.stack : undefined);
throw new BadRequestException('用户档案创建失败,请稍后重试');
}
}
/**
* 根据ID查询用户档案
*
* @param id 档案ID
* @returns 用户档案实体
* @throws NotFoundException 当档案不存在时
*/
async findOne(id: bigint): Promise<UserProfiles> {
const profile = await this.userProfilesRepository.findOne({
where: { id }
});
if (!profile) {
throw new NotFoundException(`ID为 ${id} 的用户档案不存在`);
}
return profile;
}
/**
* 根据用户ID查询用户档案
*
* @param userId 用户ID
* @returns 用户档案实体或null
*/
async findByUserId(userId: bigint): Promise<UserProfiles | null> {
return await this.userProfilesRepository.findOne({
where: { user_id: userId }
});
}
/**
* 根据地图查询用户档案列表
*
* 功能描述:
* 查询指定地图中的所有用户档案,支持状态过滤和分页
*
* 业务逻辑:
* 1. 构建查询条件(地图、状态)
* 2. 应用分页参数
* 3. 按最后位置更新时间排序
* 4. 返回查询结果
*
* 位置广播系统应用:
* - 获取同一地图的所有在线用户
* - 支持位置广播的目标用户筛选
* - 提供地图用户统计功能
*
* @param mapId 地图ID
* @param status 用户状态过滤(可选)
* @param limit 限制数量默认50
* @param offset 偏移量默认0
* @returns 用户档案列表
*
* @example
* ```typescript
* // 获取plaza地图中的所有在线用户
* const onlineUsers = await userProfilesService.findByMap('plaza', 1, 20, 0);
*
* // 获取forest地图中的所有用户不限状态
* const allUsers = await userProfilesService.findByMap('forest');
* ```
*/
async findByMap(mapId: string, status?: number, limit: number = 50, offset: number = 0): Promise<UserProfiles[]> {
const startTime = Date.now();
this.logger.log('开始查询地图用户档案', {
operation: 'findByMap',
mapId,
status,
limit,
offset,
timestamp: new Date().toISOString()
});
try {
// 构建查询条件
const whereCondition: FindOptionsWhere<UserProfiles> = {
current_map: mapId
};
// 添加状态过滤
if (status !== undefined) {
whereCondition.status = status;
}
const profiles = await this.userProfilesRepository.find({
where: whereCondition,
take: limit,
skip: offset,
order: { last_position_update: 'DESC' }
});
const duration = Date.now() - startTime;
this.logger.log('地图用户档案查询成功', {
operation: 'findByMap',
mapId,
status,
resultCount: profiles.length,
duration,
timestamp: new Date().toISOString()
});
return profiles;
} catch (error) {
const duration = Date.now() - startTime;
this.logger.error('地图用户档案查询异常', {
operation: 'findByMap',
mapId,
status,
error: error instanceof Error ? error.message : String(error),
duration,
timestamp: new Date().toISOString()
}, error instanceof Error ? error.stack : undefined);
// 查询异常返回空数组而不抛出异常
return [];
}
}
/**
* 更新用户档案信息
*
* @param id 档案ID
* @param updateData 更新的数据
* @returns 更新后的用户档案实体
* @throws NotFoundException 当档案不存在时
*/
async update(id: bigint, updateData: UpdateUserProfileDto): Promise<UserProfiles> {
const startTime = Date.now();
this.logger.log('开始更新用户档案信息', {
operation: 'update',
profileId: id.toString(),
updateFields: Object.keys(updateData),
timestamp: new Date().toISOString()
});
try {
// 检查档案是否存在
const existingProfile = await this.findOne(id);
// 合并更新数据
Object.assign(existingProfile, updateData);
// 保存更新后的档案信息
const updatedProfile = await this.userProfilesRepository.save(existingProfile);
const duration = Date.now() - startTime;
this.logger.log('用户档案信息更新成功', {
operation: 'update',
profileId: id.toString(),
userId: updatedProfile.user_id.toString(),
updateFields: Object.keys(updateData),
duration,
timestamp: new Date().toISOString()
});
return updatedProfile;
} catch (error) {
const duration = Date.now() - startTime;
if (error instanceof NotFoundException) {
throw error;
}
this.logger.error('用户档案更新系统异常', {
operation: 'update',
profileId: id.toString(),
updateData,
error: error instanceof Error ? error.message : String(error),
duration,
timestamp: new Date().toISOString()
}, error instanceof Error ? error.stack : undefined);
throw new BadRequestException('用户档案更新失败,请稍后重试');
}
}
/**
* 更新用户位置信息
*
* 功能描述:
* 专门用于位置广播系统的位置更新操作,高性能优化
*
* 技术实现:
* 1. 根据用户ID查找档案记录
* 2. 更新位置相关字段(地图、坐标)
* 3. 自动更新位置更新时间戳
* 4. 执行数据库更新操作
* 5. 记录位置更新日志
*
* 性能优化:
* - 只更新位置相关字段,减少数据传输
* - 使用部分更新,避免全量数据操作
* - 批量操作支持,提高并发性能
*
* @param userId 用户ID
* @param positionData 位置数据
* @returns 更新后的用户档案实体
* @throws NotFoundException 当用户档案不存在时
*
* @example
* ```typescript
* // 更新用户位置
* const updatedProfile = await userProfilesService.updatePosition(
* BigInt(1),
* {
* current_map: 'forest',
* pos_x: 150.5,
* pos_y: 200.3
* }
* );
* ```
*/
async updatePosition(userId: bigint, positionData: UpdatePositionDto): Promise<UserProfiles> {
const startTime = Date.now();
this.logger.log('开始更新用户位置', {
operation: 'updatePosition',
userId: userId.toString(),
currentMap: positionData.current_map,
posX: positionData.pos_x,
posY: positionData.pos_y,
timestamp: new Date().toISOString()
});
try {
// 查找用户档案
const profile = await this.userProfilesRepository.findOne({
where: { user_id: userId }
});
if (!profile) {
this.logger.warn('用户位置更新失败:档案不存在', {
operation: 'updatePosition',
userId: userId.toString()
});
throw new NotFoundException(`用户ID ${userId} 的档案不存在`);
}
// 更新位置信息
profile.current_map = positionData.current_map;
profile.pos_x = positionData.pos_x;
profile.pos_y = positionData.pos_y;
profile.last_position_update = new Date(); // 更新位置更新时间
// 保存更新
const updatedProfile = await this.userProfilesRepository.save(profile);
const duration = Date.now() - startTime;
this.logger.log('用户位置更新成功', {
operation: 'updatePosition',
profileId: updatedProfile.id.toString(),
userId: userId.toString(),
currentMap: updatedProfile.current_map,
posX: updatedProfile.pos_x,
posY: updatedProfile.pos_y,
duration,
timestamp: new Date().toISOString()
});
return updatedProfile;
} catch (error) {
const duration = Date.now() - startTime;
if (error instanceof NotFoundException) {
throw error;
}
this.logger.error('用户位置更新系统异常', {
operation: 'updatePosition',
userId: userId.toString(),
positionData,
error: error instanceof Error ? error.message : String(error),
duration,
timestamp: new Date().toISOString()
}, error instanceof Error ? error.stack : undefined);
throw new BadRequestException('用户位置更新失败,请稍后重试');
}
}
/**
* 批量更新用户状态
*
* 功能描述:
* 批量更新多个用户的状态,用于系统维护和状态同步
*
* @param userIds 用户ID列表
* @param status 目标状态
* @returns 更新的记录数量
*/
async batchUpdateStatus(userIds: bigint[], status: number): Promise<number> {
const startTime = Date.now();
this.logger.log('开始批量更新用户状态', {
operation: 'batchUpdateStatus',
userCount: userIds.length,
targetStatus: status,
timestamp: new Date().toISOString()
});
try {
const result = await this.userProfilesRepository.update(
{ user_id: { $in: userIds } as any },
{ status }
);
const duration = Date.now() - startTime;
this.logger.log('批量更新用户状态成功', {
operation: 'batchUpdateStatus',
userCount: userIds.length,
targetStatus: status,
affectedRows: result.affected || 0,
duration,
timestamp: new Date().toISOString()
});
return result.affected || 0;
} catch (error) {
const duration = Date.now() - startTime;
this.logger.error('批量更新用户状态异常', {
operation: 'batchUpdateStatus',
userCount: userIds.length,
targetStatus: status,
error: error instanceof Error ? error.message : String(error),
duration,
timestamp: new Date().toISOString()
}, error instanceof Error ? error.stack : undefined);
throw new BadRequestException('批量更新用户状态失败,请稍后重试');
}
}
/**
* 查询用户档案列表
*
* @param queryDto 查询条件
* @returns 用户档案列表
*/
async findAll(queryDto: QueryUserProfileDto = {}): Promise<UserProfiles[]> {
const { current_map, status, limit = 20, offset = 0 } = queryDto;
// 构建查询条件
const whereCondition: FindOptionsWhere<UserProfiles> = {};
if (current_map) {
whereCondition.current_map = current_map;
}
if (status !== undefined) {
whereCondition.status = status;
}
return await this.userProfilesRepository.find({
where: whereCondition,
take: limit,
skip: offset,
order: { last_position_update: 'DESC' }
});
}
/**
* 统计用户档案数量
*
* @param conditions 查询条件
* @returns 档案数量
*/
async count(conditions?: FindOptionsWhere<UserProfiles>): Promise<number> {
return await this.userProfilesRepository.count({ where: conditions });
}
/**
* 删除用户档案
*
* @param id 档案ID
* @returns 删除操作结果
* @throws NotFoundException 当档案不存在时
*/
async remove(id: bigint): Promise<{ affected: number; message: string }> {
const startTime = Date.now();
this.logger.log('开始删除用户档案', {
operation: 'remove',
profileId: id.toString(),
timestamp: new Date().toISOString()
});
try {
// 检查档案是否存在
await this.findOne(id);
// 执行删除操作
const result = await this.userProfilesRepository.delete({ id });
const deleteResult = {
affected: result.affected || 0,
message: `成功删除ID为 ${id} 的用户档案`
};
const duration = Date.now() - startTime;
this.logger.log('用户档案删除成功', {
operation: 'remove',
profileId: id.toString(),
affected: deleteResult.affected,
duration,
timestamp: new Date().toISOString()
});
return deleteResult;
} catch (error) {
const duration = Date.now() - startTime;
if (error instanceof NotFoundException) {
throw error;
}
this.logger.error('用户档案删除系统异常', {
operation: 'remove',
profileId: id.toString(),
error: error instanceof Error ? error.message : String(error),
duration,
timestamp: new Date().toISOString()
}, error instanceof Error ? error.stack : undefined);
throw new BadRequestException('用户档案删除失败,请稍后重试');
}
}
/**
* 检查用户档案是否存在
*
* @param userId 用户ID
* @returns 是否存在
*/
async existsByUserId(userId: bigint): Promise<boolean> {
const count = await this.userProfilesRepository.count({
where: { user_id: userId }
});
return count > 0;
}
}

View File

@@ -0,0 +1,697 @@
/**
* 用户档案内存服务类
*
* 功能描述:
* - 提供用户档案数据的内存存储实现
* - 使用Map数据结构进行高性能数据管理
* - 支持完整的CRUD操作和位置信息管理
* - 为开发测试环境提供零依赖的数据存储方案
*
* 职责分离:
* - 数据存储使用Map进行内存数据管理
* - ID生成线程安全的自增ID生成机制
* - 数据验证:数据完整性和唯一性约束检查
* - 性能监控:操作耗时统计和日志记录
*
* 技术特点:
* - 高性能直接内存访问无IO开销
* - 零依赖:无需数据库连接,快速启动
* - 完整功能:实现与数据库服务相同的接口
* - 易测试:便于单元测试和集成测试
*
* 使用场景:
* - 开发环境快速启动和调试
* - 单元测试和集成测试
* - 演示和原型开发
* - 数据库故障时的降级方案
*
* 最近修改:
* - 2026-01-08: 功能新增 - 创建用户档案内存服务,支持位置广播系统 (修改者: moyin)
*
* @author moyin
* @version 1.0.0
* @since 2026-01-08
* @lastModified 2026-01-08
*/
import { Injectable, ConflictException, NotFoundException, BadRequestException } from '@nestjs/common';
import { UserProfiles } from './user_profiles.entity';
import { CreateUserProfileDto, UpdateUserProfileDto, UpdatePositionDto, QueryUserProfileDto } from './user_profiles.dto';
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';
import { BaseUserProfilesService } from './base_user_profiles.service';
@Injectable()
export class UserProfilesMemoryService extends BaseUserProfilesService {
/**
* 内存数据存储
*
* 数据结构:
* - Key: bigint类型的档案ID
* - Value: UserProfiles实体对象
* - 特点:支持快速查找和更新操作
*/
private profiles: Map<bigint, UserProfiles> = new Map();
/**
* 用户ID到档案ID的映射
*
* 数据结构:
* - Key: bigint类型的用户ID
* - Value: bigint类型的档案ID
* - 用途支持根据用户ID快速查找档案
*/
private userIdToProfileId: Map<bigint, bigint> = new Map();
/**
* 当前ID计数器
*
* 功能:
* - 生成唯一的档案ID
* - 自增机制确保ID唯一性
* - 线程安全的ID生成
*/
private CURRENT_ID: bigint = BigInt(1);
/**
* ID生成锁
*
* 功能:
* - 防止并发ID生成冲突
* - 简单的锁机制实现
* - 确保ID生成的原子性
*/
private readonly ID_LOCK = new Set<string>();
/**
* 创建新用户档案
*
* 技术实现:
* 1. 验证输入数据的格式和完整性
* 2. 检查用户ID的唯一性约束
* 3. 生成唯一的档案ID
* 4. 创建用户档案实体对象
* 5. 存储到内存Map中
* 6. 建立用户ID到档案ID的映射
* 7. 记录操作日志和性能指标
*
* @param createUserProfileDto 创建用户档案的数据传输对象
* @returns 创建成功的用户档案实体
* @throws BadRequestException 当数据验证失败时
* @throws ConflictException 当用户ID已存在档案时
*/
async create(createUserProfileDto: CreateUserProfileDto): Promise<UserProfiles> {
const startTime = Date.now();
this.logStart('创建用户档案', {
userId: createUserProfileDto.user_id.toString(),
currentMap: createUserProfileDto.current_map
});
try {
// 验证DTO
const dto = plainToClass(CreateUserProfileDto, createUserProfileDto);
const validationErrors = await validate(dto);
if (validationErrors.length > 0) {
const errorMessages = validationErrors.map(error =>
Object.values(error.constraints || {}).join(', ')
).join('; ');
this.logWarning('创建用户档案', '数据验证失败', {
userId: createUserProfileDto.user_id.toString(),
validationErrors: errorMessages
});
throw new BadRequestException(`数据验证失败: ${errorMessages}`);
}
// 检查用户ID是否已存在档案
if (this.userIdToProfileId.has(createUserProfileDto.user_id)) {
const existingProfileId = this.userIdToProfileId.get(createUserProfileDto.user_id);
this.logWarning('创建用户档案', '用户ID已存在档案', {
userId: createUserProfileDto.user_id.toString(),
existingProfileId: existingProfileId?.toString()
});
throw new ConflictException('该用户已存在档案记录');
}
// 生成唯一ID
const profileId = this.generateUniqueId();
// 创建用户档案实体
const userProfile = new UserProfiles();
userProfile.id = profileId;
userProfile.user_id = createUserProfileDto.user_id;
userProfile.bio = createUserProfileDto.bio || null;
userProfile.resume_content = createUserProfileDto.resume_content || null;
userProfile.tags = createUserProfileDto.tags || null;
userProfile.social_links = createUserProfileDto.social_links || null;
userProfile.skin_id = createUserProfileDto.skin_id || null;
userProfile.current_map = createUserProfileDto.current_map || 'plaza';
userProfile.pos_x = createUserProfileDto.pos_x || 0;
userProfile.pos_y = createUserProfileDto.pos_y || 0;
userProfile.status = createUserProfileDto.status || 0;
userProfile.last_position_update = new Date();
// 存储到内存
this.profiles.set(profileId, userProfile);
this.userIdToProfileId.set(createUserProfileDto.user_id, profileId);
const duration = this.calculateDuration(startTime);
this.logSuccess('创建用户档案', {
profileId: profileId.toString(),
userId: userProfile.user_id.toString(),
currentMap: userProfile.current_map
}, duration);
return userProfile;
} catch (error) {
const duration = this.calculateDuration(startTime);
if (error instanceof BadRequestException || error instanceof ConflictException) {
throw error;
}
this.logError('创建用户档案',
error instanceof Error ? error.message : String(error),
{ userId: createUserProfileDto.user_id.toString() },
duration,
error instanceof Error ? error.stack : undefined
);
throw new BadRequestException('用户档案创建失败,请稍后重试');
}
}
/**
* 根据ID查询用户档案
*
* 业务逻辑:
* 1. 从内存Map中根据ID快速查找档案
* 2. 验证档案是否存在
* 3. 记录查询操作和结果
*
* @param id 档案ID
* @returns 用户档案实体
* @throws NotFoundException 当档案不存在时
*/
async findOne(id: bigint): Promise<UserProfiles> {
const startTime = Date.now();
this.logStart('查询用户档案', { profileId: id.toString() });
try {
const profile = this.profiles.get(id);
if (!profile) {
this.logWarning('查询用户档案', '档案不存在', { profileId: id.toString() });
throw new NotFoundException(`ID为 ${id} 的用户档案不存在`);
}
const duration = this.calculateDuration(startTime);
this.logSuccess('查询用户档案', {
profileId: id.toString(),
userId: profile.user_id.toString()
}, duration);
return profile;
} catch (error) {
const duration = this.calculateDuration(startTime);
if (error instanceof NotFoundException) {
throw error;
}
this.logError('查询用户档案',
error instanceof Error ? error.message : String(error),
{ profileId: id.toString() },
duration,
error instanceof Error ? error.stack : undefined
);
throw new BadRequestException('用户档案查询失败,请稍后重试');
}
}
/**
* 根据用户ID查询用户档案
*
* @param userId 用户ID
* @returns 用户档案实体或null
*/
async findByUserId(userId: bigint): Promise<UserProfiles | null> {
const profileId = this.userIdToProfileId.get(userId);
if (!profileId) {
return null;
}
return this.profiles.get(profileId) || null;
}
/**
* 根据地图查询用户档案列表
*
* @param mapId 地图ID
* @param status 用户状态过滤(可选)
* @param limit 限制数量默认50
* @param offset 偏移量默认0
* @returns 用户档案列表
*/
async findByMap(mapId: string, status?: number, limit: number = 50, offset: number = 0): Promise<UserProfiles[]> {
const startTime = Date.now();
this.logStart('查询地图用户档案', { mapId, status, limit, offset });
try {
// 过滤符合条件的档案
const filteredProfiles = Array.from(this.profiles.values()).filter(profile => {
if (profile.current_map !== mapId) {
return false;
}
if (status !== undefined && profile.status !== status) {
return false;
}
return true;
});
// 按最后位置更新时间排序
filteredProfiles.sort((a, b) => {
const timeA = a.last_position_update?.getTime() || 0;
const timeB = b.last_position_update?.getTime() || 0;
return timeB - timeA; // 降序排列
});
// 应用分页
const result = filteredProfiles.slice(offset, offset + limit);
const duration = this.calculateDuration(startTime);
this.logSuccess('查询地图用户档案', {
mapId,
status,
resultCount: result.length,
totalCount: filteredProfiles.length
}, duration);
return result;
} catch (error) {
const duration = this.calculateDuration(startTime);
return this.handleSearchError(error, '查询地图用户档案', {
mapId,
status,
duration
});
}
}
/**
* 更新用户档案信息
*
* @param id 档案ID
* @param updateData 更新的数据
* @returns 更新后的用户档案实体
* @throws NotFoundException 当档案不存在时
*/
async update(id: bigint, updateData: UpdateUserProfileDto): Promise<UserProfiles> {
const startTime = Date.now();
this.logStart('更新用户档案信息', {
profileId: id.toString(),
updateFields: Object.keys(updateData)
});
try {
// 检查档案是否存在
const existingProfile = await this.findOne(id);
// 合并更新数据
Object.assign(existingProfile, updateData);
// 更新内存中的数据
this.profiles.set(id, existingProfile);
const duration = this.calculateDuration(startTime);
this.logSuccess('更新用户档案信息', {
profileId: id.toString(),
userId: existingProfile.user_id.toString(),
updateFields: Object.keys(updateData)
}, duration);
return existingProfile;
} catch (error) {
const duration = this.calculateDuration(startTime);
if (error instanceof NotFoundException) {
throw error;
}
this.logError('更新用户档案信息',
error instanceof Error ? error.message : String(error),
{ profileId: id.toString(), updateData },
duration,
error instanceof Error ? error.stack : undefined
);
throw new BadRequestException('用户档案更新失败,请稍后重试');
}
}
/**
* 更新用户位置信息
*
* @param userId 用户ID
* @param positionData 位置数据
* @returns 更新后的用户档案实体
* @throws NotFoundException 当用户档案不存在时
*/
async updatePosition(userId: bigint, positionData: UpdatePositionDto): Promise<UserProfiles> {
const startTime = Date.now();
this.logStart('更新用户位置', {
userId: userId.toString(),
currentMap: positionData.current_map,
posX: positionData.pos_x,
posY: positionData.pos_y
});
try {
// 查找用户档案
const profileId = this.userIdToProfileId.get(userId);
if (!profileId) {
this.logWarning('更新用户位置', '档案不存在', { userId: userId.toString() });
throw new NotFoundException(`用户ID ${userId} 的档案不存在`);
}
const profile = this.profiles.get(profileId);
if (!profile) {
this.logWarning('更新用户位置', '档案数据不存在', {
userId: userId.toString(),
profileId: profileId.toString()
});
throw new NotFoundException(`用户ID ${userId} 的档案不存在`);
}
// 更新位置信息
profile.current_map = positionData.current_map;
profile.pos_x = positionData.pos_x;
profile.pos_y = positionData.pos_y;
profile.last_position_update = new Date();
// 更新内存中的数据
this.profiles.set(profileId, profile);
const duration = this.calculateDuration(startTime);
this.logSuccess('更新用户位置', {
profileId: profileId.toString(),
userId: userId.toString(),
currentMap: profile.current_map,
posX: profile.pos_x,
posY: profile.pos_y
}, duration);
return profile;
} catch (error) {
const duration = this.calculateDuration(startTime);
if (error instanceof NotFoundException) {
throw error;
}
this.logError('更新用户位置',
error instanceof Error ? error.message : String(error),
{ userId: userId.toString(), positionData },
duration,
error instanceof Error ? error.stack : undefined
);
throw new BadRequestException('用户位置更新失败,请稍后重试');
}
}
/**
* 批量更新用户状态
*
* @param userIds 用户ID列表
* @param status 目标状态
* @returns 更新的记录数量
*/
async batchUpdateStatus(userIds: bigint[], status: number): Promise<number> {
const startTime = Date.now();
this.logStart('批量更新用户状态', {
userCount: userIds.length,
targetStatus: status
});
try {
let updatedCount = 0;
for (const userId of userIds) {
const profileId = this.userIdToProfileId.get(userId);
if (profileId) {
const profile = this.profiles.get(profileId);
if (profile) {
profile.status = status;
this.profiles.set(profileId, profile);
updatedCount++;
}
}
}
const duration = this.calculateDuration(startTime);
this.logSuccess('批量更新用户状态', {
userCount: userIds.length,
targetStatus: status,
updatedCount
}, duration);
return updatedCount;
} catch (error) {
const duration = this.calculateDuration(startTime);
this.logError('批量更新用户状态',
error instanceof Error ? error.message : String(error),
{ userCount: userIds.length, targetStatus: status },
duration,
error instanceof Error ? error.stack : undefined
);
throw new BadRequestException('批量更新用户状态失败,请稍后重试');
}
}
/**
* 查询用户档案列表
*
* @param queryDto 查询条件
* @returns 用户档案列表
*/
async findAll(queryDto: QueryUserProfileDto = {}): Promise<UserProfiles[]> {
const { current_map, status, limit = 20, offset = 0 } = queryDto;
// 过滤符合条件的档案
const filteredProfiles = Array.from(this.profiles.values()).filter(profile => {
if (current_map && profile.current_map !== current_map) {
return false;
}
if (status !== undefined && profile.status !== status) {
return false;
}
return true;
});
// 按最后位置更新时间排序
filteredProfiles.sort((a, b) => {
const timeA = a.last_position_update?.getTime() || 0;
const timeB = b.last_position_update?.getTime() || 0;
return timeB - timeA;
});
// 应用分页
return filteredProfiles.slice(offset, offset + limit);
}
/**
* 统计用户档案数量
*
* @param conditions 查询条件
* @returns 档案数量
*/
async count(conditions?: any): Promise<number> {
if (!conditions) {
return this.profiles.size;
}
// 简单的条件过滤统计
let count = 0;
for (const profile of this.profiles.values()) {
let match = true;
for (const [key, value] of Object.entries(conditions)) {
if ((profile as any)[key] !== value) {
match = false;
break;
}
}
if (match) {
count++;
}
}
return count;
}
/**
* 删除用户档案
*
* 业务逻辑:
* 1. 验证目标档案是否存在
* 2. 从内存Map中删除档案记录
* 3. 删除用户ID到档案ID的映射
* 4. 记录删除操作和结果
* 5. 返回删除操作的统计信息
*
* @param id 档案ID
* @returns 删除操作结果
* @throws NotFoundException 当档案不存在时
*/
async remove(id: bigint): Promise<{ affected: number; message: string }> {
const startTime = Date.now();
this.logStart('删除用户档案', { profileId: id.toString() });
try {
// 检查档案是否存在
const profile = await this.findOne(id);
// 删除档案记录
this.profiles.delete(id);
this.userIdToProfileId.delete(profile.user_id);
const deleteResult = {
affected: 1,
message: `成功删除ID为 ${id} 的用户档案`
};
const duration = this.calculateDuration(startTime);
this.logSuccess('删除用户档案', {
profileId: id.toString(),
userId: profile.user_id.toString(),
affected: deleteResult.affected
}, duration);
return deleteResult;
} catch (error) {
const duration = this.calculateDuration(startTime);
if (error instanceof NotFoundException) {
throw error;
}
this.logError('删除用户档案',
error instanceof Error ? error.message : String(error),
{ profileId: id.toString() },
duration,
error instanceof Error ? error.stack : undefined
);
throw new BadRequestException('用户档案删除失败,请稍后重试');
}
}
/**
* 检查用户档案是否存在
*
* @param userId 用户ID
* @returns 是否存在
*/
async existsByUserId(userId: bigint): Promise<boolean> {
return this.userIdToProfileId.has(userId);
}
/**
* 生成唯一ID
*
* 功能描述:
* 生成唯一的档案ID确保线程安全和ID唯一性
*
* 技术实现:
* 1. 使用简单的锁机制防止并发冲突
* 2. 自增ID生成确保唯一性
* 3. 释放锁,允许其他操作继续
*
* @returns 唯一的档案ID
*/
private generateUniqueId(): bigint {
const lockKey = 'id_generation';
// 简单的锁机制
while (this.ID_LOCK.has(lockKey)) {
// 等待锁释放(简单的自旋锁)
}
this.ID_LOCK.add(lockKey);
try {
const id = this.CURRENT_ID;
this.CURRENT_ID = this.CURRENT_ID + BigInt(1);
return id;
} finally {
this.ID_LOCK.delete(lockKey);
}
}
/**
* 清空所有数据
*
* 功能描述:
* 清空内存中的所有档案数据,用于测试环境的数据重置
*
* 注意:此方法仅用于测试环境,生产环境请勿使用
*/
async clearAll(): Promise<void> {
this.profiles.clear();
this.userIdToProfileId.clear();
this.CURRENT_ID = BigInt(1);
this.logger.warn('清空所有用户档案数据', {
operation: 'clearAll',
timestamp: new Date().toISOString()
});
}
/**
* 获取内存使用统计
*
* 功能描述:
* 获取当前内存存储的统计信息,用于监控和调试
*
* @returns 内存使用统计
*/
getMemoryStats(): {
profileCount: number;
userIdMappingCount: number;
currentId: string;
} {
return {
profileCount: this.profiles.size,
userIdMappingCount: this.userIdToProfileId.size,
currentId: this.CURRENT_ID.toString()
};
}
}

View File

@@ -0,0 +1,25 @@
-- 创建用户钱包和鲸币流水表
CREATE TABLE IF NOT EXISTS `user_wallets` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` bigint NOT NULL COMMENT '关联users.id',
`balance` int NOT NULL DEFAULT 0 COMMENT '鲸币余额',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_user_wallets_user_id_unique` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户钱包表';
CREATE TABLE IF NOT EXISTS `wallet_transactions` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` bigint NOT NULL COMMENT '关联users.id',
`type` varchar(24) NOT NULL COMMENT '流水类型grant/spend/earn/refund',
`amount` int NOT NULL COMMENT '变动数量,收入为正,支出为负',
`balance_after` int NOT NULL COMMENT '变动后余额',
`reference_type` varchar(50) NOT NULL COMMENT '业务引用类型',
`reference_id` varchar(100) NOT NULL COMMENT '业务引用ID',
`note` varchar(255) DEFAULT NULL COMMENT '流水备注',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `idx_wallet_transactions_user_id` (`user_id`),
KEY `idx_wallet_transactions_reference` (`reference_type`, `reference_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='钱包鲸币流水表';

View File

@@ -0,0 +1,41 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity('user_wallets')
export class UserWallets {
@PrimaryGeneratedColumn({
type: 'bigint',
comment: '主键ID',
})
id: bigint;
@Column({
type: 'bigint',
nullable: false,
unique: true,
comment: '关联users.id',
})
user_id: bigint;
@Column({
type: 'int',
nullable: false,
default: 0,
comment: '鲸币余额',
})
balance: number;
@Column({
type: 'timestamp',
default: () => 'CURRENT_TIMESTAMP',
comment: '创建时间',
})
created_at: Date;
@Column({
type: 'timestamp',
default: () => 'CURRENT_TIMESTAMP',
onUpdate: 'CURRENT_TIMESTAMP',
comment: '更新时间',
})
updated_at: Date;
}

View File

@@ -0,0 +1,49 @@
import { DynamicModule, Global, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserWallets } from './user_wallets.entity';
import { WalletTransactions } from './wallet_transactions.entity';
import { UserWalletsMemoryService } from './user_wallets_memory.service';
import { UserWalletsService } from './user_wallets.service';
@Global()
@Module({})
export class UserWalletsModule {
static forDatabase(): DynamicModule {
return {
module: UserWalletsModule,
imports: [TypeOrmModule.forFeature([UserWallets, WalletTransactions])],
providers: [
UserWalletsService,
{
provide: 'IUserWalletsService',
useClass: UserWalletsService,
},
],
exports: [UserWalletsService, 'IUserWalletsService'],
};
}
static forMemory(): DynamicModule {
return {
module: UserWalletsModule,
providers: [
UserWalletsMemoryService,
{
provide: 'IUserWalletsService',
useClass: UserWalletsMemoryService,
},
],
exports: [UserWalletsMemoryService, 'IUserWalletsService'],
};
}
static forRoot(useMemory?: boolean): DynamicModule {
const shouldUseMemory = useMemory ?? (
process.env.NODE_ENV === 'test' ||
process.env.USE_MEMORY_STORAGE === 'true' ||
!process.env.DB_HOST
);
return shouldUseMemory ? this.forMemory() : this.forDatabase();
}
}

View File

@@ -0,0 +1,119 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { UserWallets } from './user_wallets.entity';
import { WalletTransactions } from './wallet_transactions.entity';
export const DEFAULT_INITIAL_WHALE_COINS = 1200;
export interface WalletBalancePayload {
user_id: string;
balance: number;
currency: 'whale_coin';
}
export interface SpendWalletResult {
wallet: UserWallets;
transaction: WalletTransactions;
}
export interface EarnWalletResult {
wallet: UserWallets;
transaction: WalletTransactions;
}
@Injectable()
export class UserWalletsService {
constructor(
@InjectRepository(UserWallets)
private readonly userWalletsRepository: Repository<UserWallets>,
@InjectRepository(WalletTransactions)
private readonly walletTransactionsRepository: Repository<WalletTransactions>,
) {}
async ensureWallet(userId: bigint): Promise<UserWallets> {
const existing = await this.userWalletsRepository.findOne({
where: { user_id: userId },
});
if (existing) {
return existing;
}
const wallet = new UserWallets();
wallet.user_id = userId;
wallet.balance = DEFAULT_INITIAL_WHALE_COINS;
wallet.created_at = new Date();
wallet.updated_at = new Date();
const savedWallet = await this.userWalletsRepository.save(wallet);
await this.createTransaction(userId, 'grant', DEFAULT_INITIAL_WHALE_COINS, savedWallet.balance, 'registration', 'initial_wallet', '新用户初始鲸币');
return savedWallet;
}
async getBalance(userId: bigint): Promise<WalletBalancePayload> {
const wallet = await this.ensureWallet(userId);
return {
user_id: userId.toString(),
balance: wallet.balance,
currency: 'whale_coin',
};
}
async spend(userId: bigint, amount: number, referenceType: string, referenceId: string, note?: string): Promise<SpendWalletResult> {
if (!Number.isInteger(amount) || amount < 0) {
throw new BadRequestException('鲸币消费数量不正确');
}
const wallet = await this.ensureWallet(userId);
if (wallet.balance < amount) {
throw new BadRequestException('鲸币余额不足');
}
wallet.balance -= amount;
wallet.updated_at = new Date();
const savedWallet = await this.userWalletsRepository.save(wallet);
const transaction = await this.createTransaction(userId, 'spend', -amount, savedWallet.balance, referenceType, referenceId, note || '');
return {
wallet: savedWallet,
transaction,
};
}
async earn(userId: bigint, amount: number, referenceType: string, referenceId: string, note?: string): Promise<EarnWalletResult> {
if (!Number.isInteger(amount) || amount < 0) {
throw new BadRequestException('鲸币收入数量不正确');
}
const wallet = await this.ensureWallet(userId);
wallet.balance += amount;
wallet.updated_at = new Date();
const savedWallet = await this.userWalletsRepository.save(wallet);
const transaction = await this.createTransaction(userId, 'earn', amount, savedWallet.balance, referenceType, referenceId, note || '');
return {
wallet: savedWallet,
transaction,
};
}
private async createTransaction(
userId: bigint,
type: string,
amount: number,
balanceAfter: number,
referenceType: string,
referenceId: string,
note: string,
): Promise<WalletTransactions> {
const transaction = new WalletTransactions();
transaction.user_id = userId;
transaction.type = type;
transaction.amount = amount;
transaction.balance_after = balanceAfter;
transaction.reference_type = referenceType;
transaction.reference_id = referenceId;
transaction.note = note || null;
transaction.created_at = new Date();
return await this.walletTransactionsRepository.save(transaction);
}
}

View File

@@ -0,0 +1,95 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { DEFAULT_INITIAL_WHALE_COINS, EarnWalletResult, SpendWalletResult, WalletBalancePayload } from './user_wallets.service';
import { UserWallets } from './user_wallets.entity';
import { WalletTransactions } from './wallet_transactions.entity';
@Injectable()
export class UserWalletsMemoryService {
private wallets: Map<bigint, UserWallets> = new Map();
private transactions: WalletTransactions[] = [];
private currentWalletId: bigint = BigInt(1);
private currentTransactionId: bigint = BigInt(1);
async ensureWallet(userId: bigint): Promise<UserWallets> {
const existing = this.wallets.get(userId);
if (existing) {
return existing;
}
const wallet = new UserWallets();
wallet.id = this.currentWalletId++;
wallet.user_id = userId;
wallet.balance = DEFAULT_INITIAL_WHALE_COINS;
wallet.created_at = new Date();
wallet.updated_at = new Date();
this.wallets.set(userId, wallet);
await this.createTransaction(userId, 'grant', DEFAULT_INITIAL_WHALE_COINS, wallet.balance, 'registration', 'initial_wallet', '新用户初始鲸币');
return wallet;
}
async getBalance(userId: bigint): Promise<WalletBalancePayload> {
const wallet = await this.ensureWallet(userId);
return {
user_id: userId.toString(),
balance: wallet.balance,
currency: 'whale_coin',
};
}
async spend(userId: bigint, amount: number, referenceType: string, referenceId: string, note?: string): Promise<SpendWalletResult> {
if (!Number.isInteger(amount) || amount < 0) {
throw new BadRequestException('鲸币消费数量不正确');
}
const wallet = await this.ensureWallet(userId);
if (wallet.balance < amount) {
throw new BadRequestException('鲸币余额不足');
}
wallet.balance -= amount;
wallet.updated_at = new Date();
const transaction = await this.createTransaction(userId, 'spend', -amount, wallet.balance, referenceType, referenceId, note || '');
return {
wallet,
transaction,
};
}
async earn(userId: bigint, amount: number, referenceType: string, referenceId: string, note?: string): Promise<EarnWalletResult> {
if (!Number.isInteger(amount) || amount < 0) {
throw new BadRequestException('鲸币收入数量不正确');
}
const wallet = await this.ensureWallet(userId);
wallet.balance += amount;
wallet.updated_at = new Date();
const transaction = await this.createTransaction(userId, 'earn', amount, wallet.balance, referenceType, referenceId, note || '');
return {
wallet,
transaction,
};
}
private async createTransaction(
userId: bigint,
type: string,
amount: number,
balanceAfter: number,
referenceType: string,
referenceId: string,
note: string,
): Promise<WalletTransactions> {
const transaction = new WalletTransactions();
transaction.id = this.currentTransactionId++;
transaction.user_id = userId;
transaction.type = type;
transaction.amount = amount;
transaction.balance_after = balanceAfter;
transaction.reference_type = referenceType;
transaction.reference_id = referenceId;
transaction.note = note || null;
transaction.created_at = new Date();
this.transactions.push(transaction);
return transaction;
}
}

View File

@@ -0,0 +1,72 @@
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
@Entity('wallet_transactions')
@Index('idx_wallet_transactions_user_id', ['user_id'])
@Index('idx_wallet_transactions_reference', ['reference_type', 'reference_id'])
export class WalletTransactions {
@PrimaryGeneratedColumn({
type: 'bigint',
comment: '主键ID',
})
id: bigint;
@Column({
type: 'bigint',
nullable: false,
comment: '关联users.id',
})
user_id: bigint;
@Column({
type: 'varchar',
length: 24,
nullable: false,
comment: '流水类型grant/spend/earn/refund',
})
type: string;
@Column({
type: 'int',
nullable: false,
comment: '变动数量,收入为正,支出为负',
})
amount: number;
@Column({
type: 'int',
nullable: false,
comment: '变动后余额',
})
balance_after: number;
@Column({
type: 'varchar',
length: 50,
nullable: false,
comment: '业务引用类型',
})
reference_type: string;
@Column({
type: 'varchar',
length: 100,
nullable: false,
comment: '业务引用ID',
})
reference_id: string;
@Column({
type: 'varchar',
length: 255,
nullable: true,
comment: '流水备注',
})
note?: string | null;
@Column({
type: 'timestamp',
default: () => 'CURRENT_TIMESTAMP',
comment: '创建时间',
})
created_at: Date;
}

View File

@@ -0,0 +1,203 @@
/**
* 用户服务基类
*
* 功能描述:
* - 提供统一的异常处理机制
* - 定义通用的错误处理方法
* - 统一日志记录格式
* - 敏感信息脱敏处理
*
* 职责分离:
* - 异常处理:统一的错误格式化和异常转换
* - 日志管理:结构化日志记录和敏感信息脱敏
* - 性能监控:操作成功和失败的统计记录
* - 搜索优化:搜索异常的特殊处理机制
*
* 最近修改:
* - 2026-01-15: 代码规范优化 - 为保护方法补充@example示例 (修改者: moyin)
* - 2026-01-07: 代码规范优化 - 完善注释规范,添加完整的文件头和方法注释
* - 2026-01-07: 功能新增 - 添加敏感信息脱敏处理和结构化日志记录
*
* @author moyin
* @version 1.0.2
* @since 2025-01-07
* @lastModified 2026-01-15
*/
import { Logger, ConflictException, NotFoundException, BadRequestException } from '@nestjs/common';
export abstract class BaseUsersService {
protected readonly logger = new Logger(this.constructor.name);
/**
* 统一的错误格式化方法
*
* @param error 原始错误对象
* @returns 格式化后的错误信息字符串
*
* @example
* ```typescript
* const errorMsg = this.formatError(new Error('数据库连接失败'));
* // 返回: "数据库连接失败"
* ```
*/
protected formatError(error: unknown): string {
if (error instanceof Error) {
return error.message;
}
return String(error);
}
/**
* 统一的异常处理方法
*
* @param error 原始错误
* @param operation 操作名称
* @param context 上下文信息
* @throws 处理后的标准异常
*
* @example
* ```typescript
* try {
* // 业务操作
* } catch (error) {
* this.handleServiceError(error, '创建用户', { username: 'test' });
* }
* ```
*/
protected handleServiceError(error: unknown, operation: string, context?: Record<string, any>): never {
const errorMessage = this.formatError(error);
// 记录错误日志
this.logger.error(`${operation}失败`, {
operation,
error: errorMessage,
context: context ? this.sanitizeLogData(context) : undefined,
timestamp: new Date().toISOString()
}, error instanceof Error ? error.stack : undefined);
// 如果是已知的业务异常,直接重新抛出
if (error instanceof ConflictException ||
error instanceof NotFoundException ||
error instanceof BadRequestException) {
throw error;
}
// 系统异常转换为BadRequestException
throw new BadRequestException(`${operation}失败,请稍后重试`);
}
/**
* 搜索异常的特殊处理(返回空结果而不抛出异常)
*
* @param error 原始错误
* @param operation 操作名称
* @param context 上下文信息
* @returns 空数组
*
* @example
* ```typescript
* try {
* // 搜索操作
* } catch (error) {
* return this.handleSearchError(error, '搜索用户', { keyword: 'test' });
* }
* ```
*/
protected handleSearchError(error: unknown, operation: string, context?: Record<string, any>): any[] {
const errorMessage = this.formatError(error);
this.logger.warn(`${operation}失败,返回空结果`, {
operation,
error: errorMessage,
context: context ? this.sanitizeLogData(context) : undefined,
timestamp: new Date().toISOString()
});
return [];
}
/**
* 记录操作成功日志
*
* @param operation 操作名称
* @param context 上下文信息
* @param duration 操作耗时
*
* @example
* ```typescript
* this.logSuccess('创建用户', { userId: '123', username: 'test' }, 50);
* ```
*/
protected logSuccess(operation: string, context?: Record<string, any>, duration?: number): void {
this.logger.log(`${operation}成功`, {
operation,
context: context ? this.sanitizeLogData(context) : undefined,
duration,
timestamp: new Date().toISOString()
});
}
/**
* 记录操作开始日志
*
* @param operation 操作名称
* @param context 上下文信息
*
* @example
* ```typescript
* this.logStart('创建用户', { username: 'test' });
* ```
*/
protected logStart(operation: string, context?: Record<string, any>): void {
this.logger.log(`开始${operation}`, {
operation,
context: context ? this.sanitizeLogData(context) : undefined,
timestamp: new Date().toISOString()
});
}
/**
* 脱敏处理敏感信息
*
* @param data 原始数据
* @returns 脱敏后的数据
*
* @example
* ```typescript
* const sanitized = this.sanitizeLogData({
* email: 'test@example.com',
* phone: '13800138000',
* password_hash: 'secret'
* });
* // 返回: { email: 'te***@example.com', phone: '138****00', password_hash: '[REDACTED]' }
* ```
*/
protected sanitizeLogData(data: Record<string, any>): Record<string, any> {
const sanitized = { ...data };
// 脱敏邮箱
if (sanitized.email) {
const email = sanitized.email;
const [localPart, domain] = email.split('@');
if (localPart && domain) {
sanitized.email = `${localPart.substring(0, 2)}***@${domain}`;
}
}
// 脱敏手机号
if (sanitized.phone) {
const phone = sanitized.phone;
if (phone.length > 4) {
sanitized.phone = `${phone.substring(0, 3)}****${phone.substring(phone.length - 2)}`;
}
}
// 移除密码哈希
if (sanitized.password_hash) {
sanitized.password_hash = '[REDACTED]';
}
return sanitized;
}
}

View File

@@ -0,0 +1,173 @@
/**
* 用户状态枚举Core层
*
* 功能描述:
* - 定义用户账户的各种状态
* - 提供状态检查和描述功能
* - 支持用户生命周期管理
*
* 职责分离:
* - 用户状态枚举值定义和管理
* - 状态描述和错误消息的国际化支持
* - 状态验证和转换工具函数提供
*
* 最近修改:
* - 2026-01-07: 架构优化 - 从Business层移动到Core层符合架构分层原则 (修改者: moyin)
*
* @author moyin
* @version 1.0.2
* @since 2025-12-24
* @lastModified 2026-01-07
*/
/**
* 用户状态枚举
*
* 状态说明:
* - active: 正常状态,可以正常使用所有功能
* - inactive: 未激活状态,通常是新注册用户需要邮箱验证
* - locked: 临时锁定状态,可以解锁恢复
* - banned: 永久禁用状态,需要管理员处理
* - deleted: 软删除状态,数据保留但不可使用
* - pending: 待审核状态,需要管理员审核后激活
*/
export enum UserStatus {
ACTIVE = 'active', // 正常状态
INACTIVE = 'inactive', // 未激活状态
LOCKED = 'locked', // 锁定状态
BANNED = 'banned', // 禁用状态
DELETED = 'deleted', // 删除状态
PENDING = 'pending' // 待审核状态
}
/**
* 获取用户状态的中文描述
*
* 技术实现:
* 1. 根据用户状态枚举值查找对应的中文描述
* 2. 提供用户友好的状态显示文本
* 3. 处理未知状态的默认描述
*
* @param status 用户状态
* @returns 状态描述
* @throws 无异常抛出,未知状态返回默认描述
*
* @example
* ```typescript
* const description = getUserStatusDescription(UserStatus.ACTIVE);
* // 返回: "正常"
* ```
*/
export function getUserStatusDescription(status: UserStatus): string {
const descriptions = {
[UserStatus.ACTIVE]: '正常',
[UserStatus.INACTIVE]: '未激活',
[UserStatus.LOCKED]: '已锁定',
[UserStatus.BANNED]: '已禁用',
[UserStatus.DELETED]: '已删除',
[UserStatus.PENDING]: '待审核'
};
return descriptions[status] || '未知状态';
}
/**
* 检查用户是否可以登录
*
* 技术实现:
* 1. 验证用户状态是否允许登录系统
* 2. 只有正常状态的用户可以登录
* 3. 其他状态均不允许登录
*
* @param status 用户状态
* @returns 是否可以登录
* @throws 无异常抛出
*
* @example
* ```typescript
* const canLogin = canUserLogin(UserStatus.ACTIVE);
* // 返回: true
* const cannotLogin = canUserLogin(UserStatus.LOCKED);
* // 返回: false
* ```
*/
export function canUserLogin(status: UserStatus): boolean {
// 只有正常状态的用户可以登录
return status === UserStatus.ACTIVE;
}
/**
* 获取用户状态对应的错误消息
*
* 技术实现:
* 1. 根据用户状态返回相应的错误提示信息
* 2. 为不同状态提供用户友好的错误说明
* 3. 指导用户如何解决状态问题
*
* @param status 用户状态
* @returns 错误消息
* @throws 无异常抛出,未知状态返回默认错误消息
*
* @example
* ```typescript
* const errorMsg = getUserStatusErrorMessage(UserStatus.LOCKED);
* // 返回: "账户已被锁定,请联系管理员"
* ```
*/
export function getUserStatusErrorMessage(status: UserStatus): string {
const errorMessages = {
[UserStatus.ACTIVE]: '', // 正常状态无错误
[UserStatus.INACTIVE]: '账户未激活,请先验证邮箱',
[UserStatus.LOCKED]: '账户已被锁定,请联系管理员',
[UserStatus.BANNED]: '账户已被禁用,请联系管理员',
[UserStatus.DELETED]: '账户不存在',
[UserStatus.PENDING]: '账户待审核,请等待管理员审核'
};
return errorMessages[status] || '账户状态异常';
}
/**
* 获取所有可用的用户状态
*
* 技术实现:
* 1. 返回系统中定义的所有用户状态枚举值
* 2. 用于状态选择器和验证逻辑
* 3. 支持动态状态管理功能
*
* @returns 用户状态数组
* @throws 无异常抛出
*
* @example
* ```typescript
* const allStatuses = getAllUserStatuses();
* // 返回: [UserStatus.ACTIVE, UserStatus.INACTIVE, ...]
* ```
*/
export function getAllUserStatuses(): UserStatus[] {
return Object.values(UserStatus);
}
/**
* 检查状态值是否有效
*
* 技术实现:
* 1. 验证输入的字符串是否为有效的用户状态枚举值
* 2. 提供类型安全的状态验证功能
* 3. 支持动态状态值验证和类型转换
*
* @param status 状态值
* @returns 是否为有效状态
* @throws 无异常抛出
*
* @example
* ```typescript
* const isValid = isValidUserStatus('active');
* // 返回: true
* const isInvalid = isValidUserStatus('unknown');
* // 返回: false
* ```
*/
export function isValidUserStatus(status: string): status is UserStatus {
return Object.values(UserStatus).includes(status as UserStatus);
}

View File

@@ -0,0 +1,188 @@
/**
* 用户模块常量定义
*
* 功能描述:
* - 定义用户模块中使用的常量值
* - 避免魔法数字,提高代码可维护性
* - 集中管理配置参数
*
* 职责分离:
* - 常量定义:用户角色、字段限制、查询限制等常量值
* - 错误消息:统一的错误消息定义和管理
* - 工具类:性能监控和验证工具的封装
*
* 最近修改:
* - 2026-01-15: 代码规范优化 - 补充职责分离描述 (修改者: moyin)
* - 2026-01-09: 代码质量优化 - 提取魔法数字为常量定义 (修改者: moyin)
*
* @author moyin
* @version 1.0.1
* @since 2026-01-09
* @lastModified 2026-01-15
*/
import { ValidationError } from 'class-validator';
/**
* 用户角色常量
*/
export const USER_ROLES = {
/** 普通用户角色 */
NORMAL_USER: 1,
/** 管理员角色 */
ADMIN: 9
} as const;
/**
* 字段长度限制常量
*/
export const FIELD_LIMITS = {
/** 用户名最大长度 */
USERNAME_MAX_LENGTH: 50,
/** 昵称最大长度 */
NICKNAME_MAX_LENGTH: 50,
/** 邮箱最大长度 */
EMAIL_MAX_LENGTH: 100,
/** 手机号最大长度 */
PHONE_MAX_LENGTH: 30,
/** GitHub ID最大长度 */
GITHUB_ID_MAX_LENGTH: 100,
/** 头像URL最大长度 */
AVATAR_URL_MAX_LENGTH: 255,
/** 密码哈希最大长度 */
PASSWORD_HASH_MAX_LENGTH: 255,
/** 用户状态最大长度 */
STATUS_MAX_LENGTH: 20
} as const;
/**
* 查询限制常量
*/
export const QUERY_LIMITS = {
/** 默认查询限制 */
DEFAULT_LIMIT: 100,
/** 默认搜索限制 */
DEFAULT_SEARCH_LIMIT: 20,
/** 最大查询限制 */
MAX_LIMIT: 1000
} as const;
/**
* 系统配置常量
*/
export const SYSTEM_CONFIG = {
/** ID生成超时时间毫秒 */
ID_GENERATION_TIMEOUT: 5000,
/** 锁等待间隔(毫秒) */
LOCK_WAIT_INTERVAL: 1
} as const;
/**
* 数据库常量
*/
export const DATABASE_CONSTANTS = {
/** 排序方向 */
ORDER_DESC: 'DESC' as const,
ORDER_ASC: 'ASC' as const,
/** 数据库默认值 */
CURRENT_TIMESTAMP: 'CURRENT_TIMESTAMP' as const,
/** 锁键名 */
ID_GENERATION_LOCK_KEY: 'id_generation' as const
} as const;
/**
* 测试常量
*/
export const TEST_CONSTANTS = {
/** 测试用的不存在用户ID */
NON_EXISTENT_USER_ID: 99999,
/** 测试用的无效角色 */
INVALID_ROLE: 999,
/** 测试用的用户名长度限制 */
USERNAME_LENGTH_LIMIT: 51,
/** 测试用的批量操作数量 */
BATCH_TEST_SIZE: 50,
/** 测试用的性能测试数量 */
PERFORMANCE_TEST_SIZE: 50,
/** 测试用的分页大小 */
TEST_PAGE_SIZE: 20,
/** 测试用的查询偏移量 */
TEST_OFFSET: 10
} as const;
/**
* 错误消息常量
*/
export const ERROR_MESSAGES = {
/** 用户创建失败 */
USER_CREATE_FAILED: '用户创建失败,请稍后重试',
/** 用户更新失败 */
USER_UPDATE_FAILED: '用户更新失败,请稍后重试',
/** 用户删除失败 */
USER_DELETE_FAILED: '用户删除失败,请稍后重试',
/** 用户不存在 */
USER_NOT_FOUND: '用户不存在',
/** 数据验证失败 */
VALIDATION_FAILED: '数据验证失败',
/** ID生成超时 */
ID_GENERATION_TIMEOUT: 'ID生成超时可能存在死锁',
/** 用户名已存在 */
USERNAME_EXISTS: '用户名已存在',
/** 邮箱已存在 */
EMAIL_EXISTS: '邮箱已存在',
/** 手机号已存在 */
PHONE_EXISTS: '手机号已存在',
/** GitHub ID已存在 */
GITHUB_ID_EXISTS: 'GitHub ID已存在'
} as const;
/**
* 性能监控工具类
*/
export class PerformanceMonitor {
private startTime: number;
constructor() {
this.startTime = Date.now();
}
/**
* 获取执行时长
* @returns 执行时长(毫秒)
*/
getDuration(): number {
return Date.now() - this.startTime;
}
/**
* 重置计时器
*/
reset(): void {
this.startTime = Date.now();
}
/**
* 创建新的性能监控实例
* @returns 性能监控实例
*/
static create(): PerformanceMonitor {
return new PerformanceMonitor();
}
}
/**
* 验证工具类
*/
export class ValidationUtils {
/**
* 格式化验证错误消息
*
* @param validationErrors 验证错误数组
* @returns 格式化后的错误消息字符串
*/
static formatValidationErrors(validationErrors: ValidationError[]): string {
return validationErrors.map(error =>
Object.values(error.constraints || {}).join(', ')
).join('; ');
}
}

View File

@@ -0,0 +1,275 @@
/**
* 用户数据传输对象模块
*
* 功能描述:
* - 定义用户创建和更新的数据传输对象
* - 提供完整的数据验证规则和错误提示
* - 支持多种登录方式的数据格式验证
* - 确保数据传输的安全性和完整性
*
* 职责分离:
* - 数据验证使用class-validator进行输入数据验证
* - 类型定义:定义清晰的数据结构和类型约束
* - 错误处理:提供友好的验证错误提示信息
* - 业务规则:实现用户数据的业务验证逻辑
*
* 依赖模块:
* - class-validator: 数据验证装饰器
* - class-transformer: 数据转换工具
*
* 最近修改:
* - 2026-01-07: 代码规范优化 - 完善注释规范,添加完整的文件头和字段注释
*
* @author moyin
* @version 1.0.1
* @since 2025-12-17
* @lastModified 2026-01-07
*/
import {
IsString,
IsEmail,
IsPhoneNumber,
IsInt,
Min,
Max,
IsOptional,
Length,
IsNotEmpty,
IsEnum
} from 'class-validator';
import { UserStatus } from './user_status.enum';
import { USER_ROLES, FIELD_LIMITS } from './users.constants';
/**
* 创建用户数据传输对象
*
* 职责:
* - 定义用户创建时的数据结构和验证规则
* - 确保输入数据的格式正确性和业务规则符合性
* - 提供友好的错误提示信息
*
* 主要字段:
* - username: 唯一用户名,用于登录识别
* - email: 邮箱地址,用于通知和账户找回
* - phone: 手机号码,支持全球格式
* - password_hash: 密码哈希值OAuth登录时可为空
* - nickname: 显示昵称,在游戏中展示
* - github_id: GitHub第三方登录标识
* - avatar_url: 用户头像链接
* - role: 用户角色,控制权限级别
*
* 使用场景:
* - 用户注册接口的请求体验证
* - 管理员创建用户的数据验证
* - 第三方登录用户信息同步
*
* 验证规则:
* - 必填字段username, nickname
* - 唯一性字段username, email, phone, github_id
* - 长度限制username(1-50), nickname(1-50), github_id(1-100)
* - 格式验证email格式, phone国际格式
* - 数值范围role(1-9)
*/
export class CreateUserDto {
/**
* 用户名
*
* 业务规则:
* - 必填字段,用于用户登录和唯一标识
* - 长度限制1-50个字符
* - 全局唯一性:不允许重复
* - 建议使用字母、数字、下划线组合
*
* 验证规则:
* - 非空验证:确保用户名不为空
* - 字符串类型验证
* - 长度范围验证1-50字符
*/
@IsString()
@IsNotEmpty({ message: '用户名不能为空' })
@Length(1, FIELD_LIMITS.USERNAME_MAX_LENGTH, { message: `用户名长度需在1-${FIELD_LIMITS.USERNAME_MAX_LENGTH}字符之间` })
username: string;
/**
* 邮箱地址
*
* 业务规则:
* - 可选字段,用于账户找回和通知
* - 全局唯一性:不允许重复
* - 支持标准邮箱格式验证
* - OAuth登录时可能为空
*
* 验证规则:
* - 可选字段验证
* - 邮箱格式验证符合RFC标准
* - 长度限制最大100字符数据库约束
*/
@IsOptional()
@IsEmail({}, { message: '邮箱格式不正确' })
email?: string;
/**
* 手机号码
*
* 业务规则:
* - 可选字段,用于账户找回和通知
* - 全局唯一性:不允许重复
* - 支持国际手机号格式
* - 用于短信验证和双因子认证
*
* 验证规则:
* - 可选字段验证
* - 国际手机号格式验证
* - 长度限制最大30字符数据库约束
*/
@IsOptional()
@IsPhoneNumber(null, { message: '手机号格式不正确' })
phone?: string;
/**
* 密码哈希值
*
* 业务规则:
* - 可选字段OAuth登录时为空
* - 存储加密后的密码,不存储明文
* - 用于传统用户名密码登录方式
* - 应使用bcrypt等安全哈希算法
*
* 验证规则:
* - 可选字段验证
* - 字符串类型验证
* - 长度限制最大255字符数据库约束
*
* 安全注意:
* - 传输过程中应使用HTTPS
* - 日志记录时会自动脱敏处理
*/
@IsOptional()
@IsString({ message: '密码哈希必须是字符串' })
password_hash?: string;
/**
* 用户昵称
*
* 业务规则:
* - 必填字段,用于游戏内显示
* - 长度限制1-50个字符
* - 支持中文、英文、数字等字符
* - 可以与用户名不同,更友好的显示名称
*
* 验证规则:
* - 非空验证:确保昵称不为空
* - 字符串类型验证
* - 长度范围验证1-50字符
*/
@IsString()
@IsNotEmpty({ message: '昵称不能为空' })
@Length(1, FIELD_LIMITS.NICKNAME_MAX_LENGTH, { message: `昵称长度需在1-${FIELD_LIMITS.NICKNAME_MAX_LENGTH}字符之间` })
nickname: string;
/**
* GitHub用户标识
*
* 业务规则:
* - 可选字段用于GitHub OAuth登录
* - 全局唯一性:不允许重复
* - 存储GitHub用户的唯一标识符
* - 用于关联GitHub账户信息
*
* 验证规则:
* - 可选字段验证
* - 字符串类型验证
* - 长度范围验证1-100字符
*/
@IsOptional()
@IsString({ message: 'GitHub ID必须是字符串' })
@Length(1, FIELD_LIMITS.GITHUB_ID_MAX_LENGTH, { message: `GitHub ID长度需在1-${FIELD_LIMITS.GITHUB_ID_MAX_LENGTH}字符之间` })
github_id?: string;
/**
* 用户头像链接
*
* 业务规则:
* - 可选字段,用于显示用户头像
* - 支持GitHub头像或自定义头像
* - 应为有效的HTTP/HTTPS链接
* - 建议使用CDN加速访问
*
* 验证规则:
* - 可选字段验证
* - 字符串类型验证
* - 长度限制最大255字符数据库约束
*/
@IsOptional()
@IsString({ message: '头像URL必须是字符串' })
avatar_url?: string;
/**
* 用户角色
*
* 业务规则:
* - 可选字段,默认为普通用户(1)
* - 角色级别1-普通用户9-管理员
* - 控制用户在系统中的权限范围
* - 管理员具有系统管理权限
*
* 验证规则:
* - 可选字段验证
* - 整数类型验证
* - 数值范围验证1-9之间
* - 默认值1普通用户
*
* 权限说明:
* - 1: 普通用户 - 基础游戏功能
* - 9: 管理员 - 系统管理权限
*/
@IsOptional()
@IsInt({ message: '角色必须是数字' })
@Min(USER_ROLES.NORMAL_USER, { message: `角色值最小为${USER_ROLES.NORMAL_USER}` })
@Max(USER_ROLES.ADMIN, { message: `角色值最大为${USER_ROLES.ADMIN}` })
role?: number = USER_ROLES.NORMAL_USER;
/**
* 邮箱验证状态
*
* 业务规则:
* - 可选字段默认为false未验证
* - 控制邮箱相关功能的可用性
* - OAuth登录时可直接设为true
* - 影响密码重置等安全功能
*
* 验证规则:
* - 可选字段验证
* - 布尔类型验证
* - 默认值false未验证
*/
@IsOptional()
email_verified?: boolean = false;
/**
* 用户状态
*
* 业务规则:
* - 可选字段默认为active正常状态
* - 控制用户账户的可用性和权限
* - 支持多种状态:正常、未激活、锁定、禁用等
* - 影响用户登录和API访问权限
*
* 验证规则:
* - 可选字段验证
* - 枚举类型验证
* - 默认值active正常状态
*
* 状态说明:
* - active: 正常状态,可以正常使用
* - inactive: 未激活,需要邮箱验证
* - locked: 已锁定,临时禁用
* - banned: 已禁用,管理员操作
* - deleted: 已删除,软删除状态
* - pending: 待审核,需要管理员审核
*/
@IsOptional()
@IsEnum(UserStatus, { message: '用户状态必须是有效的枚举值' })
status?: UserStatus = UserStatus.ACTIVE;
}

View File

@@ -0,0 +1,497 @@
/**
* 用户数据实体模块
*
* 功能描述:
* - 定义用户数据表的实体映射和字段约束
* - 提供用户数据的持久化存储结构
* - 支持多种登录方式的用户信息存储
* - 实现完整的用户数据模型和关系映射
*
* 职责分离:
* - 数据映射TypeORM实体与数据库表的映射关系
* - 约束定义:字段类型、长度、唯一性等约束规则
* - 关系管理:与其他实体的关联关系定义
* - 索引优化:数据库查询性能优化策略
*
* 依赖模块:
* - TypeORM: ORM框架提供数据库映射功能
* - MySQL: 底层数据库存储
*
* 数据库表users
* 存储引擎InnoDB
* 字符集utf8mb4
*
* 最近修改:
* - 2026-01-07: 代码规范优化 - 完善注释规范,添加完整的文件头和字段注释
*
* @author moyin
* @version 1.0.1
* @since 2025-12-17
* @lastModified 2026-01-07
*/
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, OneToOne } from 'typeorm';
import { UserStatus } from './user_status.enum';
import { ZulipAccounts } from '../zulip_accounts/zulip_accounts.entity';
import { FIELD_LIMITS } from './users.constants';
/**
* 用户实体类
*
* 职责:
* - 映射数据库users表的结构和约束
* - 定义用户数据的字段类型和验证规则
* - 提供用户信息的完整数据模型
*
* 主要功能:
* - 用户身份标识和认证信息存储
* - 支持传统登录和OAuth第三方登录
* - 用户基础信息和角色权限管理
* - 自动时间戳记录和更新
*
* 数据完整性:
* - 主键约束id字段自增主键
* - 唯一约束username, email, phone, github_id
* - 非空约束username, nickname, role
* - 外键关联:可扩展关联用户详情、权限等表
*
* 使用场景:
* - 用户注册和登录验证
* - 用户信息查询和更新
* - 权限验证和角色管理
* - 用户数据统计和分析
*
* 索引策略:
* - 主键索引id (自动创建)
* - 唯一索引username, email, phone, github_id
* - 普通索引role (用于角色查询)
* - 复合索引created_at + role (用于分页查询)
*/
@Entity('users')
export class Users {
/**
* 用户主键ID
*
* 数据库设计:
* - 类型BIGINT支持大量用户数据
* - 约束:主键、非空、自增
* - 范围1 ~ 9,223,372,036,854,775,807
*
* 业务规则:
* - 系统自动生成,不可手动指定
* - 全局唯一标识符,用于用户关联
* - 作为其他表的外键引用
*
* 性能考虑:
* - 自增主键,插入性能优异
* - 聚簇索引,范围查询效率高
* - BIGINT类型避免ID耗尽问题
*/
@PrimaryGeneratedColumn({
type: 'bigint',
comment: '主键ID'
})
id: bigint;
/**
* 用户名
*
* 数据库设计:
* - 类型VARCHAR(50),支持多语言字符
* - 约束:非空、唯一索引
* - 字符集utf8mb4支持emoji等特殊字符
*
* 业务规则:
* - 用户登录的唯一标识符
* - 全系统唯一,不允许重复
* - 长度限制1-50个字符
* - 建议格式:字母、数字、下划线组合
*
* 安全考虑:
* - 不应包含敏感信息
* - 避免使用易猜测的用户名
* - 支持用户名修改(需要额外验证)
*/
@Column({
type: 'varchar',
length: FIELD_LIMITS.USERNAME_MAX_LENGTH,
nullable: false,
unique: true,
comment: '唯一用户名/登录名'
})
username: string;
/**
* 邮箱地址
*
* 数据库设计:
* - 类型VARCHAR(100),支持长邮箱地址
* - 约束:允许空、唯一索引
* - 索引:用于快速邮箱查找
*
* 业务规则:
* - 用于账户找回和重要通知
* - 全系统唯一,不允许重复
* - OAuth登录时可能为空
* - 支持邮箱验证和双因子认证
*
* 隐私保护:
* - 敏感信息,日志记录时脱敏
* - 仅用于系统通知,不对外展示
* - 支持用户自主修改和验证
*/
@Column({
type: 'varchar',
length: FIELD_LIMITS.EMAIL_MAX_LENGTH,
nullable: true,
unique: true,
comment: '邮箱(用于找回/通知)'
})
email: string;
/**
* 邮箱验证状态
*
* 数据库设计:
* - 类型BOOLEAN布尔值
* - 约束非空、默认值false
* - 索引:用于查询已验证用户
*
* 业务规则:
* - false邮箱未验证
* - true邮箱已验证
* - 影响密码重置等安全功能
* - OAuth登录时可直接设为true
*
* 安全考虑:
* - 未验证邮箱限制部分功能
* - 验证后才能用于密码重置
* - 支持重新发送验证邮件
*/
@Column({
type: 'boolean',
nullable: false,
default: false,
comment: '邮箱是否已验证'
})
email_verified: boolean;
/**
* 手机号码
*
* 数据库设计:
* - 类型VARCHAR(30),支持国际号码格式
* - 约束:允许空、唯一索引
* - 格式:包含国家代码的完整号码
*
* 业务规则:
* - 用于账户找回和短信通知
* - 全系统唯一,不允许重复
* - 支持国际手机号格式(+86、+1等
* - 用于短信验证码和双因子认证
*
* 隐私保护:
* - 敏感信息,日志记录时脱敏
* - 仅用于安全验证,不对外展示
* - 支持用户自主修改和验证
*/
@Column({
type: 'varchar',
length: FIELD_LIMITS.PHONE_MAX_LENGTH,
nullable: true,
unique: true,
comment: '全球电话号码(用于找回/通知)'
})
phone: string;
/**
* 密码哈希值
*
* 数据库设计:
* - 类型VARCHAR(255),支持各种哈希算法
* - 约束允许空OAuth登录时
* - 存储:加密后的哈希值,不存储明文
*
* 业务规则:
* - 传统用户名密码登录方式使用
* - OAuth第三方登录时此字段为空
* - 使用bcrypt等安全哈希算法
* - 支持密码强度验证和定期更新
*
* 安全措施:
* - 绝不存储明文密码
* - 使用盐值防止彩虹表攻击
* - 日志系统自动脱敏处理
* - 传输过程使用HTTPS加密
* - 支持密码重置和修改功能
*/
@Column({
type: 'varchar',
length: FIELD_LIMITS.PASSWORD_HASH_MAX_LENGTH,
nullable: true,
comment: '密码哈希OAuth登录为空'
})
password_hash: string;
/**
* 用户昵称
*
* 数据库设计:
* - 类型VARCHAR(50),支持多语言字符
* - 约束:非空,无唯一性要求
* - 字符集utf8mb4支持emoji表情
*
* 业务规则:
* - 游戏内显示的友好名称
* - 允许重复,提高用户体验
* - 长度限制1-50个字符
* - 支持中文、英文、数字、表情符号
*
* 显示规则:
* - 游戏内头顶显示名称
* - 聊天消息发送者标识
* - 排行榜和用户列表显示
* - 支持用户随时修改
*/
@Column({
type: 'varchar',
length: FIELD_LIMITS.NICKNAME_MAX_LENGTH,
nullable: false,
comment: '显示昵称(头顶显示)'
})
nickname: string;
/**
* GitHub用户标识
*
* 数据库设计:
* - 类型VARCHAR(100)存储GitHub用户ID
* - 约束:允许空、唯一索引
* - 用途GitHub OAuth登录关联
*
* 业务规则:
* - GitHub第三方登录的唯一标识
* - 全系统唯一,不允许重复
* - 用于关联GitHub账户信息
* - 支持GitHub头像和基础信息同步
*
* OAuth集成
* - 存储GitHub返回的用户ID
* - 用于后续API调用身份验证
* - 支持账户绑定和解绑操作
* - 可扩展支持其他OAuth提供商
*/
@Column({
type: 'varchar',
length: FIELD_LIMITS.GITHUB_ID_MAX_LENGTH,
nullable: true,
unique: true,
comment: 'GitHub OpenID第三方登录用'
})
github_id: string;
/**
* 用户头像链接
*
* 数据库设计:
* - 类型VARCHAR(255)支持长URL
* - 约束:允许空,无唯一性要求
* - 存储完整的HTTP/HTTPS链接
*
* 业务规则:
* - 用户头像图片的访问链接
* - 支持GitHub头像或自定义上传
* - 建议使用CDN加速访问
* - 支持多种图片格式jpg、png、gif等
*
* 性能优化:
* - 建议使用图片CDN服务
* - 支持多尺寸头像适配
* - 缓存策略优化加载速度
* - 默认头像兜底机制
*/
@Column({
type: 'varchar',
length: FIELD_LIMITS.AVATAR_URL_MAX_LENGTH,
nullable: true,
comment: 'GitHub头像或自定义头像URL'
})
avatar_url: string;
/**
* 用户角色
*
* 数据库设计:
* - 类型TINYINT节省存储空间
* - 约束非空、默认值1
* - 范围1-9支持角色扩展
*
* 业务规则:
* - 控制用户在系统中的权限级别
* - 1普通用户基础游戏功能
* - 9管理员系统管理权限
* - 支持角色升级和降级操作
*
* 权限设计:
* - 基于角色的访问控制RBAC
* - 支持细粒度权限配置
* - 可扩展更多角色类型
* - 权限验证中间件集成
*
* 扩展性:
* - 预留2-8角色级别供未来使用
* - 支持角色权限动态配置
* - 可关联角色权限表进行扩展
*/
@Column({
type: 'tinyint',
nullable: false,
default: 1,
comment: '角色1-普通9-管理员'
})
role: number;
/**
* 用户状态
*
* 数据库设计:
* - 类型VARCHAR(20),存储状态枚举值
* - 约束:非空、默认值'active'
* - 索引:用于状态查询和统计
*
* 业务规则:
* - 控制用户账户的可用性和权限
* - active正常状态可以正常使用
* - inactive未激活需要邮箱验证
* - locked已锁定临时禁用
* - banned已禁用管理员操作
* - deleted已删除软删除状态
* - pending待审核需要管理员审核
*
* 安全控制:
* - 登录时检查状态权限
* - API访问时验证状态
* - 状态变更记录审计日志
* - 支持批量状态管理
*
* 应用场景:
* - 账户安全管理
* - 用户生命周期控制
* - 违规用户处理
* - 系统维护和升级
*/
@Column({
type: 'varchar',
length: FIELD_LIMITS.STATUS_MAX_LENGTH,
nullable: true,
default: UserStatus.ACTIVE,
comment: '用户状态active-正常inactive-未激活locked-锁定banned-禁用deleted-删除pending-待审核'
})
status?: UserStatus;
/**
* 创建时间
*
* 数据库设计:
* - 类型DATETIME精确到秒
* - 约束:非空、默认当前时间
* - 时区使用系统时区建议UTC
*
* 业务规则:
* - 记录用户注册的准确时间
* - 用于用户数据统计和分析
* - 支持按时间范围查询用户
* - 不可修改,保证数据完整性
*
* 应用场景:
* - 用户注册趋势分析
* - 新用户欢迎流程触发
* - 数据审计和合规要求
* - 用户生命周期管理
*/
@CreateDateColumn({
type: 'datetime',
nullable: false,
default: () => 'CURRENT_TIMESTAMP',
comment: '注册时间'
})
created_at: Date;
/**
* 更新时间
*
* 数据库设计:
* - 类型DATETIME精确到秒
* - 约束:非空、自动更新
* - 触发:任何字段更新时自动刷新
*
* 业务规则:
* - 记录用户信息最后修改时间
* - 数据库级别自动维护
* - 用于数据同步和缓存失效
* - 支持增量数据同步
*
* 应用场景:
* - 数据变更审计
* - 缓存更新策略
* - 数据同步时间戳
* - 用户活跃度分析
*/
@UpdateDateColumn({
type: 'datetime',
nullable: false,
default: () => 'CURRENT_TIMESTAMP',
onUpdate: 'CURRENT_TIMESTAMP',
comment: '更新时间'
})
updated_at: Date;
/**
* 删除时间
*
* 数据库设计:
* - 类型DATETIME精确到秒
* - 约束:允许空,软删除时手动设置
* - 索引:用于过滤已删除记录
*
* 业务规则:
* - null正常状态未删除
* - 有值:已软删除,记录删除时间
* - 软删除的记录在查询时需要手动过滤
* - 支持数据恢复和审计追踪
*
* 应用场景:
* - 数据安全删除,避免误删
* - 数据审计和合规要求
* - 支持数据恢复功能
* - 删除操作的时间追踪
*/
// @Column({
// type: 'datetime',
// nullable: true,
// default: null,
// comment: '软删除时间null表示未删除'
// })
// deleted_at?: Date;
/**
* 关联的Zulip账号
*
* 关系设计:
* - 类型一对一关系OneToOne
* - 外键在ZulipAccounts表中
* - 级联:不设置级联删除,保证数据安全
*
* 业务规则:
* - 每个游戏用户最多关联一个Zulip账号
* - 支持延迟加载,提高查询性能
* - 可选关联不是所有用户都有Zulip账号
*
* 使用场景:
* - 游戏内聊天功能集成
* - 跨平台消息同步
* - 用户身份验证和权限管理
*/
@OneToOne(() => ZulipAccounts, zulipAccount => zulipAccount.gameUser)
zulipAccount?: ZulipAccounts;
}

View File

@@ -0,0 +1,75 @@
/**
* 用户模块
*
* 功能描述:
* - 整合用户相关的实体、服务和控制器
* - 配置TypeORM实体和Repository
* - 支持数据库和内存存储的动态切换
* - 导出用户服务供其他模块使用
*
* 职责分离:
* - 模块配置:动态模块的创建和依赖注入配置
* - 存储切换:数据库模式和内存模式的灵活切换
* - 服务导出:统一的服务接口导出和类型安全
* - 依赖管理:模块间依赖关系的清晰定义
*
* 存储模式:
* - 数据库模式使用TypeORM连接MySQL数据库
* - 内存模式使用Map存储适用于开发和测试
*
* 最近修改:
* - 2026-01-07: 代码规范优化 - 完善注释规范,添加完整的文件头和方法注释
* - 2025-12-17: 功能新增 - 添加双存储模式支持by angjustinl
*
* @author moyin
* @version 1.0.1
* @since 2025-12-17
* @lastModified 2026-01-07
*/
import { Module, DynamicModule, Global } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Users } from './users.entity';
import { UsersService } from './users.service';
import { UsersMemoryService } from './users_memory.service';
@Global()
@Module({})
export class UsersModule {
/**
* 创建数据库模式的用户模块
*
* @returns 配置了TypeORM的动态模块
*/
static forDatabase(): DynamicModule {
return {
module: UsersModule,
imports: [TypeOrmModule.forFeature([Users])],
providers: [
{
provide: 'UsersService',
useClass: UsersService,
},
],
exports: ['UsersService', TypeOrmModule],
};
}
/**
* 创建内存模式的用户模块
*
* @returns 配置了内存存储的动态模块
*/
static forMemory(): DynamicModule {
return {
module: UsersModule,
providers: [
{
provide: 'UsersService',
useClass: UsersMemoryService,
},
],
exports: ['UsersService'],
};
}
}

View File

@@ -0,0 +1,714 @@
/**
* 用户服务类
*
* 功能描述:
* - 提供用户数据的增删改查技术实现
* - 处理数据持久化和存储操作
* - 数据格式验证和约束检查
* - 支持完整的数据生命周期管理
*
* 职责分离:
* - 数据持久化通过TypeORM操作MySQL数据库
* - 数据验证:数据格式和约束完整性检查
* - 异常处理:统一的错误处理和日志记录
* - 性能监控:操作耗时统计和性能优化
*
* 最近修改:
* - 2026-01-07: 代码规范优化 - 完善注释规范,添加完整的文件头和方法注释
* - 2026-01-07: 功能优化 - 添加完整的日志记录系统和详细的技术实现注释
* - 2026-01-07: 性能优化 - 优化异常处理和性能监控机制
*
* @author moyin
* @version 1.0.1
* @since 2025-12-17
* @lastModified 2026-01-07
*/
import { Injectable, ConflictException, NotFoundException, BadRequestException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, FindOptionsWhere } from 'typeorm';
import { Users } from './users.entity';
import { CreateUserDto } from './users.dto';
import { UserStatus } from './user_status.enum';
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';
import { BaseUsersService } from './base_users.service';
import { USER_ROLES, QUERY_LIMITS, ERROR_MESSAGES, DATABASE_CONSTANTS, ValidationUtils, PerformanceMonitor } from './users.constants';
@Injectable()
export class UsersService extends BaseUsersService {
constructor(
@InjectRepository(Users)
private readonly usersRepository: Repository<Users>,
) {
super(); // 调用基类构造函数
}
/**
* 创建新用户
*
* 技术实现:
* 1. 验证输入数据的格式和完整性
* 2. 创建用户实体并设置默认值
* 3. 保存用户数据到数据库
* 4. 记录操作日志和性能指标
*
* @param createUserDto 创建用户的数据传输对象,包含用户基本信息
* @returns 创建成功的用户实体包含自动生成的ID和时间戳
* @throws BadRequestException 当数据验证失败或输入格式错误时
*
* @example
* ```typescript
* const newUser = await usersService.create({
* username: 'testuser',
* email: 'test@example.com',
* nickname: '测试用户',
* password_hash: 'hashed_password'
* });
* console.log(`用户创建成功ID: ${newUser.id}`);
* ```
*/
async create(createUserDto: CreateUserDto): Promise<Users> {
const monitor = PerformanceMonitor.create();
this.logger.log('开始创建用户', {
operation: 'create',
username: createUserDto.username,
email: createUserDto.email,
timestamp: new Date().toISOString()
});
try {
// 验证DTO
await this.validateCreateUserDto(createUserDto);
// 创建用户实体
const user = this.buildUserEntity(createUserDto);
// 保存到数据库
const savedUser = await this.usersRepository.save(user);
this.logger.log('用户创建成功', {
operation: 'create',
userId: savedUser.id.toString(),
username: savedUser.username,
email: savedUser.email,
duration: monitor.getDuration(),
timestamp: new Date().toISOString()
});
return savedUser;
} catch (error) {
if (error instanceof BadRequestException) {
throw error;
}
this.logger.error('用户创建系统异常', {
operation: 'create',
username: createUserDto.username,
email: createUserDto.email,
error: error instanceof Error ? error.message : String(error),
duration: monitor.getDuration(),
timestamp: new Date().toISOString()
}, error instanceof Error ? error.stack : undefined);
throw new BadRequestException(ERROR_MESSAGES.USER_CREATE_FAILED);
}
}
/**
* 验证创建用户DTO
*
* @param createUserDto 用户数据
* @throws BadRequestException 当数据验证失败时
*/
private async validateCreateUserDto(createUserDto: CreateUserDto): Promise<void> {
const dto = plainToClass(CreateUserDto, createUserDto);
const validationErrors = await validate(dto);
if (validationErrors.length > 0) {
const errorMessages = ValidationUtils.formatValidationErrors(validationErrors);
this.logger.warn('用户创建失败:数据验证失败', {
operation: 'create',
username: createUserDto.username,
email: createUserDto.email,
validationErrors: errorMessages
});
throw new BadRequestException(`${ERROR_MESSAGES.VALIDATION_FAILED}: ${errorMessages}`);
}
}
/**
* 构建用户实体
*
* @param createUserDto 用户数据
* @returns 用户实体
*/
private buildUserEntity(createUserDto: CreateUserDto): Users {
const user = new Users();
user.username = createUserDto.username;
user.email = createUserDto.email || null;
user.phone = createUserDto.phone || null;
user.password_hash = createUserDto.password_hash || null;
user.nickname = createUserDto.nickname;
user.github_id = createUserDto.github_id || null;
user.avatar_url = createUserDto.avatar_url || null;
user.role = createUserDto.role || USER_ROLES.NORMAL_USER;
user.email_verified = createUserDto.email_verified || false;
user.status = createUserDto.status || UserStatus.ACTIVE;
return user;
}
/**
* 创建新用户(带重复检查)
*
* 技术实现:
* 1. 检查用户名、邮箱、手机号、GitHub ID的唯一性约束
* 2. 如果所有检查都通过调用create方法创建用户
* 3. 记录操作日志和性能指标
*
* @param createUserDto 创建用户的数据传输对象
* @returns 创建的用户实体
* @throws ConflictException 当用户名、邮箱、手机号或GitHub ID已存在时
* @throws BadRequestException 当数据验证失败时
*
* @example
* ```typescript
* const newUser = await usersService.createWithDuplicateCheck({
* username: 'testuser',
* email: 'test@example.com',
* nickname: '测试用户'
* });
* ```
*/
async createWithDuplicateCheck(createUserDto: CreateUserDto): Promise<Users> {
const monitor = PerformanceMonitor.create();
this.logStart('创建用户(带重复检查)', {
username: createUserDto.username,
email: createUserDto.email,
phone: createUserDto.phone,
github_id: createUserDto.github_id
});
try {
// 执行所有唯一性检查
await this.validateUniqueness(createUserDto);
// 调用普通的创建方法
const user = await this.create(createUserDto);
this.logSuccess('创建用户(带重复检查)', {
userId: user.id.toString(),
username: user.username
}, monitor.getDuration());
return user;
} catch (error) {
this.handleServiceError(error, '创建用户(带重复检查)', {
username: createUserDto.username,
duration: monitor.getDuration()
});
}
}
/**
* 验证用户数据的唯一性
*
* @param createUserDto 用户数据
* @throws ConflictException 当发现重复数据时
*/
private async validateUniqueness(createUserDto: CreateUserDto): Promise<void> {
await this.checkUsernameUniqueness(createUserDto.username);
await this.checkEmailUniqueness(createUserDto.email);
await this.checkPhoneUniqueness(createUserDto.phone);
await this.checkGithubIdUniqueness(createUserDto.github_id);
}
/**
* 检查用户名唯一性
*/
private async checkUsernameUniqueness(username?: string): Promise<void> {
if (username) {
const existingUser = await this.usersRepository.findOne({
where: { username }
});
if (existingUser) {
this.logger.warn('用户创建失败:用户名已存在', {
operation: 'uniqueness_check',
username,
existingUserId: existingUser.id.toString()
});
throw new ConflictException(ERROR_MESSAGES.USERNAME_EXISTS);
}
}
}
/**
* 检查邮箱唯一性
*/
private async checkEmailUniqueness(email?: string): Promise<void> {
if (email) {
const existingEmail = await this.usersRepository.findOne({
where: { email }
});
if (existingEmail) {
this.logger.warn('用户创建失败:邮箱已存在', {
operation: 'uniqueness_check',
email,
existingUserId: existingEmail.id.toString()
});
throw new ConflictException(ERROR_MESSAGES.EMAIL_EXISTS);
}
}
}
/**
* 检查手机号唯一性
*/
private async checkPhoneUniqueness(phone?: string): Promise<void> {
if (phone) {
const existingPhone = await this.usersRepository.findOne({
where: { phone }
});
if (existingPhone) {
this.logger.warn('用户创建失败:手机号已存在', {
operation: 'uniqueness_check',
phone,
existingUserId: existingPhone.id.toString()
});
throw new ConflictException(ERROR_MESSAGES.PHONE_EXISTS);
}
}
}
/**
* 检查GitHub ID唯一性
*/
private async checkGithubIdUniqueness(githubId?: string): Promise<void> {
if (githubId) {
const existingGithub = await this.usersRepository.findOne({
where: { github_id: githubId }
});
if (existingGithub) {
this.logger.warn('用户创建失败GitHub ID已存在', {
operation: 'uniqueness_check',
github_id: githubId,
existingUserId: existingGithub.id.toString()
});
throw new ConflictException(ERROR_MESSAGES.GITHUB_ID_EXISTS);
}
}
}
/**
* 查询所有用户
*
* @param limit 限制返回数量默认100
* @param offset 偏移量默认0
* @param includeDeleted 是否包含已删除用户默认false
* @returns 用户列表
*/
async findAll(limit: number = QUERY_LIMITS.DEFAULT_LIMIT, offset: number = 0, includeDeleted: boolean = false): Promise<Users[]> {
// 注意软删除功能暂未实现includeDeleted参数预留用于未来扩展
const whereCondition = {};
return await this.usersRepository.find({
where: whereCondition,
take: limit,
skip: offset,
order: { created_at: DATABASE_CONSTANTS.ORDER_DESC }
});
}
/**
* 根据ID查询用户
*
* @param id 用户ID
* @param includeDeleted 是否包含已删除用户默认false
* @returns 用户实体
* @throws NotFoundException 当用户不存在时
*/
async findOne(id: bigint, includeDeleted: boolean = false): Promise<Users> {
// 注意软删除功能暂未实现includeDeleted参数预留用于未来扩展
const whereCondition = { id };
const user = await this.usersRepository.findOne({
where: whereCondition
});
if (!user) {
throw new NotFoundException(`ID为 ${id} 的用户不存在`);
}
return user;
}
/**
* 根据用户名查询用户
*
* @param username 用户名
* @param includeDeleted 是否包含已删除用户默认false
* @returns 用户实体或null
*/
async findByUsername(username: string, includeDeleted: boolean = false): Promise<Users | null> {
// 注意软删除功能暂未实现includeDeleted参数预留用于未来扩展
const whereCondition = { username };
return await this.usersRepository.findOne({
where: whereCondition
});
}
/**
* 根据邮箱查询用户
*
* @param email 邮箱
* @param includeDeleted 是否包含已删除用户默认false
* @returns 用户实体或null
*/
async findByEmail(email: string, includeDeleted: boolean = false): Promise<Users | null> {
// 注意软删除功能暂未实现includeDeleted参数预留用于未来扩展
const whereCondition = { email };
return await this.usersRepository.findOne({
where: whereCondition
});
}
/**
* 根据GitHub ID查询用户
*
* @param githubId GitHub ID
* @param includeDeleted 是否包含已删除用户默认false
* @returns 用户实体或null
*/
async findByGithubId(githubId: string, includeDeleted: boolean = false): Promise<Users | null> {
// 注意软删除功能暂未实现includeDeleted参数预留用于未来扩展
const whereCondition = { github_id: githubId };
return await this.usersRepository.findOne({
where: whereCondition
});
}
/**
* 更新用户信息
*
* 功能描述:
* 更新指定用户的信息,包含完整的数据验证和唯一性检查
*
* 业务逻辑:
* 1. 验证用户是否存在
* 2. 检查更新字段的唯一性约束用户名、邮箱、手机号、GitHub ID
* 3. 合并更新数据到现有用户实体
* 4. 保存更新后的用户信息
* 5. 记录操作日志
*
* @param id 用户ID必须是有效的已存在用户
* @param updateData 更新的数据,支持部分字段更新
* @returns 更新后的用户实体
* @throws NotFoundException 当用户不存在时
* @throws ConflictException 当更新的数据与其他用户冲突时
*
* @example
* ```typescript
* const updatedUser = await usersService.update(BigInt(1), {
* nickname: '新昵称',
* email: 'new@example.com'
* });
* ```
*/
async update(id: bigint, updateData: Partial<CreateUserDto>): Promise<Users> {
const monitor = PerformanceMonitor.create();
this.logger.log('开始更新用户信息', {
operation: 'update',
userId: id.toString(),
updateFields: Object.keys(updateData),
timestamp: new Date().toISOString()
});
try {
// 1. 检查用户是否存在 - 确保要更新的用户确实存在
const existingUser = await this.findOne(id);
// 2. 检查更新数据的唯一性约束 - 防止违反数据库唯一约束
await this.checkUpdateUniqueness(id, updateData);
// 3. 合并更新数据 - 使用Object.assign将新数据合并到现有实体
Object.assign(existingUser, updateData);
// 4. 保存更新后的用户信息 - TypeORM会自动更新updated_at字段
const updatedUser = await this.usersRepository.save(existingUser);
this.logger.log('用户信息更新成功', {
operation: 'update',
userId: id.toString(),
updateFields: Object.keys(updateData),
duration: monitor.getDuration(),
timestamp: new Date().toISOString()
});
return updatedUser;
} catch (error) {
if (error instanceof NotFoundException || error instanceof ConflictException) {
throw error;
}
this.logger.error('用户更新系统异常', {
operation: 'update',
userId: id.toString(),
updateData,
error: error instanceof Error ? error.message : String(error),
duration: monitor.getDuration(),
timestamp: new Date().toISOString()
}, error instanceof Error ? error.stack : undefined);
throw new BadRequestException(ERROR_MESSAGES.USER_UPDATE_FAILED);
}
}
/**
* 删除用户
*
* 功能描述:
* 物理删除指定的用户记录,数据将从数据库中永久移除
*
* 业务逻辑:
* 1. 验证用户是否存在
* 2. 执行物理删除操作
* 3. 返回删除结果统计
* 4. 记录删除操作日志
*
* 注意事项:
* - 这是物理删除,数据无法恢复
* - 如需保留数据,请使用 softRemove 方法
* - 删除前请确认用户没有关联的重要数据
*
* @param id 用户ID必须是有效的已存在用户
* @returns 删除操作结果,包含影响行数和操作消息
* @throws NotFoundException 当用户不存在时
*
* @example
* ```typescript
* const result = await usersService.remove(BigInt(1));
* console.log(`删除了 ${result.affected} 个用户`);
* ```
*/
async remove(id: bigint): Promise<{ affected: number; message: string }> {
const monitor = PerformanceMonitor.create();
this.logger.log('开始删除用户', {
operation: 'remove',
userId: id.toString(),
timestamp: new Date().toISOString()
});
try {
// 1. 检查用户是否存在 - 确保要删除的用户确实存在
await this.findOne(id);
// 2. 执行删除操作 - 使用where条件来处理bigint类型
const result = await this.usersRepository.delete({ id });
const deleteResult = {
affected: result.affected || 0,
message: `成功删除ID为 ${id} 的用户`
};
this.logger.log('用户删除成功', {
operation: 'remove',
userId: id.toString(),
affected: deleteResult.affected,
duration: monitor.getDuration(),
timestamp: new Date().toISOString()
});
return deleteResult;
} catch (error) {
if (error instanceof NotFoundException) {
throw error;
}
this.logger.error('用户删除系统异常', {
operation: 'remove',
userId: id.toString(),
error: error instanceof Error ? error.message : String(error),
duration: monitor.getDuration(),
timestamp: new Date().toISOString()
}, error instanceof Error ? error.stack : undefined);
throw new BadRequestException(ERROR_MESSAGES.USER_DELETE_FAILED);
}
}
/**
* 检查更新数据的唯一性约束
*
* @param id 用户ID
* @param updateData 更新数据
* @throws ConflictException 当发现冲突时
*/
private async checkUpdateUniqueness(id: bigint, updateData: Partial<CreateUserDto>): Promise<void> {
const existingUser = await this.findOne(id);
if (updateData.username && updateData.username !== existingUser.username) {
await this.checkUsernameUniqueness(updateData.username);
}
if (updateData.email && updateData.email !== existingUser.email) {
await this.checkEmailUniqueness(updateData.email);
}
if (updateData.phone && updateData.phone !== existingUser.phone) {
await this.checkPhoneUniqueness(updateData.phone);
}
if (updateData.github_id && updateData.github_id !== existingUser.github_id) {
await this.checkGithubIdUniqueness(updateData.github_id);
}
}
/**
* 软删除用户
*
* @param id 用户ID
* @returns 软删除操作结果
*/
async softRemove(id: bigint): Promise<Users> {
const user = await this.findOne(id);
// 注意:软删除功能暂未实现,当前仅返回用户实体
return user;
}
/**
* 统计用户数量
*
* @param conditions 查询条件
* @returns 用户数量
*/
async count(conditions?: FindOptionsWhere<Users>): Promise<number> {
return await this.usersRepository.count({ where: conditions });
}
/**
* 检查用户是否存在
*
* @param id 用户ID
* @returns 是否存在
*/
async exists(id: bigint): Promise<boolean> {
const count = await this.usersRepository.count({ where: { id } });
return count > 0;
}
/**
* 批量创建用户
*
* @param createUserDtos 用户数据数组
* @returns 创建的用户列表
*/
async createBatch(createUserDtos: CreateUserDto[]): Promise<Users[]> {
const users: Users[] = [];
for (const dto of createUserDtos) {
const user = await this.create(dto);
users.push(user);
}
return users;
}
/**
* 根据角色查询用户
*
* @param role 角色值
* @param includeDeleted 是否包含已删除用户默认false
* @returns 用户列表
*/
async findByRole(role: number, includeDeleted: boolean = false): Promise<Users[]> {
// 注意软删除功能暂未实现includeDeleted参数预留用于未来扩展
const whereCondition = { role };
return await this.usersRepository.find({
where: whereCondition,
order: { created_at: DATABASE_CONSTANTS.ORDER_DESC }
});
}
/**
* 搜索用户(根据用户名或昵称)
*
* 功能描述:
* 根据关键词在用户名和昵称字段中进行模糊搜索,支持部分匹配
*
* 业务逻辑:
* 1. 使用QueryBuilder构建复杂查询
* 2. 对用户名和昵称字段进行LIKE模糊匹配
* 3. 按创建时间倒序排列结果
* 4. 限制返回数量防止性能问题
*
* 性能考虑:
* - 使用数据库索引优化查询性能
* - 限制返回数量避免大数据量问题
* - 建议在用户名和昵称字段上建立索引
*
* @param keyword 搜索关键词,支持中文、英文、数字等字符
* @param limit 限制数量默认20条建议不超过100
* @returns 匹配的用户列表,按创建时间倒序排列
*
* @example
* ```typescript
* // 搜索包含"张三"的用户
* const users = await usersService.search('张三', 10);
*
* // 搜索包含"admin"的用户
* const adminUsers = await usersService.search('admin');
* ```
*/
async search(keyword: string, limit: number = QUERY_LIMITS.DEFAULT_SEARCH_LIMIT, includeDeleted: boolean = false): Promise<Users[]> {
const monitor = PerformanceMonitor.create();
this.logStart('搜索用户', { keyword, limit, includeDeleted });
try {
// 1. 构建查询 - 使用QueryBuilder支持复杂的WHERE条件
const queryBuilder = this.usersRepository.createQueryBuilder('user');
// 添加搜索条件 - 在用户名和昵称中进行模糊匹配
let whereClause = 'user.username LIKE :keyword OR user.nickname LIKE :keyword';
// 注意软删除功能暂未实现includeDeleted参数预留用于未来扩展
const result = await queryBuilder
.where(whereClause, {
keyword: `%${keyword}%` // 前后加%实现模糊匹配
})
.orderBy('user.created_at', DATABASE_CONSTANTS.ORDER_DESC) // 按创建时间倒序
.limit(limit) // 限制返回数量
.getMany();
this.logSuccess('搜索用户', {
keyword,
limit,
includeDeleted,
resultCount: result.length
}, monitor.getDuration());
return result;
} catch (error) {
// 搜索异常使用特殊处理,返回空数组而不抛出异常
return this.handleSearchError(error, '搜索用户', {
keyword,
limit,
includeDeleted,
duration: monitor.getDuration()
});
}
}
}

View File

@@ -0,0 +1,766 @@
/**
* 用户内存存储服务类
*
* 功能描述:
* - 提供基于内存的用户数据存储技术实现
* - 作为数据库连接失败时的回退方案
* - 实现与UsersService相同的接口
* - 支持完整的CRUD操作和数据管理
*
* 职责分离:
* - 数据存储使用Map进行内存数据管理
* - ID生成线程安全的自增ID生成机制
* - 数据验证:数据完整性和唯一性约束检查
* - 异常处理:统一的错误处理和日志记录
*
* 使用场景:
* - 开发环境无数据库时的快速启动
* - 测试环境的轻量级存储
* - 数据库故障时的临时降级
*
* 注意事项:
* - 数据仅存储在内存中,重启后丢失
* - 不适用于生产环境
* - 性能优异但无持久化保证
*
* 最近修改:
* - 2026-01-08: 架构分层优化 - 修正导入路径确保Core层不依赖Business层 (修改者: moyin)
* - 2026-01-08: 代码质量优化 - 重构create方法提取私有方法减少代码重复 (修改者: moyin)
* - 2026-01-07: 代码规范优化 - 完善注释规范,添加完整的文件头和方法注释
* - 2026-01-07: 功能新增 - 添加createWithDuplicateCheck方法保持与数据库服务一致
* - 2026-01-07: 功能优化 - 添加日志记录系统,统一异常处理和性能监控
*
* @author moyin
* @version 1.0.3
* @since 2025-12-17
* @lastModified 2026-01-08
*/
import { Injectable, ConflictException, NotFoundException, BadRequestException } from '@nestjs/common';
import { Users } from './users.entity';
import { CreateUserDto } from './users.dto';
import { UserStatus } from './user_status.enum';
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';
import { BaseUsersService } from './base_users.service';
import { USER_ROLES, QUERY_LIMITS, SYSTEM_CONFIG, ERROR_MESSAGES, DATABASE_CONSTANTS, ValidationUtils, PerformanceMonitor } from './users.constants';
@Injectable()
export class UsersMemoryService extends BaseUsersService {
private users: Map<bigint, Users> = new Map();
private CURRENT_ID: bigint = BigInt(USER_ROLES.NORMAL_USER);
private readonly ID_LOCK = new Set<string>(); // 简单的ID生成锁
constructor() {
super(); // 调用基类构造函数
}
/**
* 根据条件查找用户
*
* @param predicate 查找条件
* @returns 匹配的用户或null
*/
private findUserByCondition(predicate: (user: Users) => boolean): Users | null {
const user = Array.from(this.users.values()).find(predicate);
return user || null;
}
/**
* 获取用户
*
* @param id 用户ID
* @returns 用户实体或undefined
*/
private getUser(id: bigint): Users | undefined {
return this.users.get(id);
}
/**
* 保存用户
*
* @param user 用户实体
*/
private saveUser(user: Users): void {
this.users.set(user.id, user);
}
/**
* 线程安全的ID生成方法
*
* 技术实现:
* 1. 检查ID生成锁的状态避免并发冲突
* 2. 使用超时机制防止死锁情况
* 3. 获取锁后安全地递增ID计数器
* 4. 确保锁在任何情况下都会被正确释放
* 5. 返回新生成的唯一ID
*
* @returns 新的唯一ID保证全局唯一性
* @throws Error 当ID生成超时或发生死锁时
*
* @example
* ```typescript
* const newId = await this.generateId();
* console.log(`生成新ID: ${newId}`);
* ```
*/
private async generateId(): Promise<bigint> {
const lockKey = DATABASE_CONSTANTS.ID_GENERATION_LOCK_KEY;
const maxWaitTime = SYSTEM_CONFIG.ID_GENERATION_TIMEOUT;
const startTime = Date.now();
// 改进的锁机制,添加超时保护
while (this.ID_LOCK.has(lockKey)) {
if (Date.now() - startTime > maxWaitTime) {
throw new Error(ERROR_MESSAGES.ID_GENERATION_TIMEOUT);
}
// 使用 Promise 避免忙等待
await new Promise(resolve => setTimeout(resolve, SYSTEM_CONFIG.LOCK_WAIT_INTERVAL));
}
this.ID_LOCK.add(lockKey);
try {
const newId = this.CURRENT_ID++;
return newId;
} finally {
// 确保锁一定会被释放
this.ID_LOCK.delete(lockKey);
}
}
/**
* 创建新用户
*
* 技术实现:
* 1. 验证输入数据的格式和完整性
* 2. 检查用户名、邮箱、手机号、GitHub ID的唯一性
* 3. 创建用户实体并分配唯一ID
* 4. 设置默认值和时间戳
* 5. 保存到内存存储并记录操作日志
*
* @param createUserDto 创建用户的数据传输对象,包含用户基本信息
* @returns 创建成功的用户实体,不包含敏感信息
* @throws ConflictException 当用户名、邮箱、手机号或GitHub ID已存在时
* @throws BadRequestException 当数据验证失败时
*
* @example
* const newUser = await userService.create({
* username: 'testuser',
* email: 'test@example.com',
* nickname: '测试用户'
* });
*/
async create(createUserDto: CreateUserDto): Promise<Users> {
const monitor = PerformanceMonitor.create();
this.logStart('创建用户', { username: createUserDto.username });
try {
// 验证DTO
await this.validateUserDto(createUserDto);
// 检查唯一性约束
await this.checkUniquenessConstraints(createUserDto);
// 创建用户实体
const user = await this.createUserEntity(createUserDto);
// 保存到内存
this.saveUser(user);
this.logSuccess('创建用户', {
userId: user.id.toString(),
username: user.username
}, monitor.getDuration());
return user;
} catch (error) {
this.handleServiceError(error, '创建用户', {
username: createUserDto.username,
duration: monitor.getDuration()
});
}
}
/**
* 验证用户DTO数据
*
* @param createUserDto 用户数据
* @throws BadRequestException 当数据验证失败时
*/
private async validateUserDto(createUserDto: CreateUserDto): Promise<void> {
const dto = plainToClass(CreateUserDto, createUserDto);
const validationErrors = await validate(dto);
if (validationErrors.length > 0) {
const errorMessages = ValidationUtils.formatValidationErrors(validationErrors);
throw new BadRequestException(`数据验证失败: ${errorMessages}`);
}
}
/**
* 检查唯一性约束
*
* @param createUserDto 用户数据
* @throws ConflictException 当发现重复数据时
*/
private async checkUniquenessConstraints(createUserDto: CreateUserDto): Promise<void> {
// 检查用户名是否已存在
if (createUserDto.username) {
const existingUser = await this.findByUsername(createUserDto.username);
if (existingUser) {
throw new ConflictException(ERROR_MESSAGES.USERNAME_EXISTS);
}
}
// 检查邮箱是否已存在
if (createUserDto.email) {
const existingEmail = await this.findByEmail(createUserDto.email);
if (existingEmail) {
throw new ConflictException(ERROR_MESSAGES.EMAIL_EXISTS);
}
}
// 检查手机号是否已存在
if (createUserDto.phone) {
const existingPhone = this.findUserByCondition(
u => u.phone === createUserDto.phone
);
if (existingPhone) {
throw new ConflictException(ERROR_MESSAGES.PHONE_EXISTS);
}
}
// 检查GitHub ID是否已存在
if (createUserDto.github_id) {
const existingGithub = await this.findByGithubId(createUserDto.github_id);
if (existingGithub) {
throw new ConflictException(ERROR_MESSAGES.GITHUB_ID_EXISTS);
}
}
}
/**
* 创建用户实体
*
* @param createUserDto 用户数据
* @returns 创建的用户实体
*/
private async createUserEntity(createUserDto: CreateUserDto): Promise<Users> {
const user = new Users();
user.id = await this.generateId();
user.username = createUserDto.username;
user.email = createUserDto.email || null;
user.phone = createUserDto.phone || null;
user.password_hash = createUserDto.password_hash || null;
user.nickname = createUserDto.nickname;
user.github_id = createUserDto.github_id || null;
user.avatar_url = createUserDto.avatar_url || null;
user.role = createUserDto.role || USER_ROLES.NORMAL_USER;
user.email_verified = createUserDto.email_verified || false;
user.status = createUserDto.status || UserStatus.ACTIVE;
user.created_at = new Date();
user.updated_at = new Date();
return user;
}
/**
* 查询所有用户
*
* 业务逻辑:
* 1. 获取内存中的所有用户数据
* 2. 按创建时间倒序排列(最新的在前)
* 3. 应用分页参数进行数据切片
* 4. 记录查询操作和性能指标
*
* @param limit 限制返回数量默认100用于分页控制
* @param offset 偏移量默认0用于分页控制
* @returns 用户列表,按创建时间倒序排列
*
* @example
* // 获取前10个用户
* const users = await userService.findAll(10, 0);
*
* // 获取第二页用户每页20个
* const secondPageUsers = await userService.findAll(20, 20);
*/
async findAll(limit: number = QUERY_LIMITS.DEFAULT_LIMIT, offset: number = 0, includeDeleted: boolean = false): Promise<Users[]> {
const monitor = PerformanceMonitor.create();
this.logStart('查询所有用户', { limit, offset, includeDeleted });
try {
let allUsers = Array.from(this.users.values());
// 注意软删除功能暂未实现includeDeleted参数预留用于未来扩展
// 按创建时间倒序排列
allUsers.sort((a, b) => b.created_at.getTime() - a.created_at.getTime());
const result = allUsers.slice(offset, offset + limit);
this.logSuccess('查询所有用户', {
resultCount: result.length,
totalCount: allUsers.length,
includeDeleted
}, monitor.getDuration());
return result;
} catch (error) {
this.handleServiceError(error, '查询所有用户', {
limit,
offset,
includeDeleted,
duration: monitor.getDuration()
});
}
}
/**
* 根据ID查询用户
*
* 业务逻辑:
* 1. 从内存Map中根据ID快速查找用户
* 2. 验证用户是否存在
* 3. 记录查询操作和结果
* 4. 如果用户不存在则抛出404异常
*
* @param id 用户ID必须是有效的bigint类型
* @returns 用户实体,包含完整的用户信息
* @throws NotFoundException 当指定ID的用户不存在时
*
* @example
* try {
* const user = await userService.findOne(BigInt(123));
* console.log(user.username);
* } catch (error) {
* // 处理用户不存在的情况
* }
*/
async findOne(id: bigint, includeDeleted: boolean = false): Promise<Users> {
const monitor = PerformanceMonitor.create();
this.logStart('查询用户', { userId: id.toString(), includeDeleted });
try {
const user = this.getUser(id);
if (!user) {
throw new NotFoundException(`ID为 ${id} 的用户不存在`);
}
this.logSuccess('查询用户', {
userId: id.toString(),
username: user.username,
includeDeleted
}, monitor.getDuration());
return user;
} catch (error) {
this.handleServiceError(error, '查询用户', {
userId: id.toString(),
includeDeleted,
duration: monitor.getDuration()
});
}
}
/**
* 根据用户名查询用户
*
* @param username 用户名
* @param includeDeleted 是否包含已删除用户默认false
* @returns 用户实体或null
*/
async findByUsername(username: string, includeDeleted: boolean = false): Promise<Users | null> {
return this.findUserByCondition(u => u.username === username);
}
/**
* 根据邮箱查询用户
*
* @param email 邮箱
* @param includeDeleted 是否包含已删除用户默认false
* @returns 用户实体或null
*/
async findByEmail(email: string, includeDeleted: boolean = false): Promise<Users | null> {
return this.findUserByCondition(u => u.email === email);
}
/**
* 根据GitHub ID查询用户
*
* @param githubId GitHub ID
* @param includeDeleted 是否包含已删除用户默认false
* @returns 用户实体或null
*/
async findByGithubId(githubId: string, includeDeleted: boolean = false): Promise<Users | null> {
return this.findUserByCondition(u => u.github_id === githubId);
}
/**
* 检查更新数据的唯一性约束
*
* @param id 用户ID
* @param updateData 更新数据
* @param existingUser 现有用户
* @throws ConflictException 当发现冲突时
*/
private async checkUpdateUniquenessConstraints(
id: bigint,
updateData: Partial<CreateUserDto>,
existingUser: Users
): Promise<void> {
if (updateData.username && updateData.username !== existingUser.username) {
const usernameExists = await this.findByUsername(updateData.username);
if (usernameExists) {
throw new ConflictException(ERROR_MESSAGES.USERNAME_EXISTS);
}
}
if (updateData.email && updateData.email !== existingUser.email) {
const emailExists = await this.findByEmail(updateData.email);
if (emailExists) {
throw new ConflictException(ERROR_MESSAGES.EMAIL_EXISTS);
}
}
if (updateData.phone && updateData.phone !== existingUser.phone) {
const phoneExists = this.findUserByCondition(
u => u.phone === updateData.phone && u.id !== id
);
if (phoneExists) {
throw new ConflictException(ERROR_MESSAGES.PHONE_EXISTS);
}
}
if (updateData.github_id && updateData.github_id !== existingUser.github_id) {
const githubExists = await this.findByGithubId(updateData.github_id);
if (githubExists && githubExists.id !== id) {
throw new ConflictException(ERROR_MESSAGES.GITHUB_ID_EXISTS);
}
}
}
/**
* 更新用户信息
*
* 业务逻辑:
* 1. 验证目标用户是否存在
* 2. 检查更新数据的唯一性约束用户名、邮箱、手机号、GitHub ID
* 3. 应用更新数据到现有用户实体
* 4. 更新时间戳并保存到内存
* 5. 记录更新操作和性能指标
*
* @param id 用户ID必须是有效的bigint类型
* @param updateData 更新的数据,可以是部分用户信息
* @returns 更新后的用户实体,包含最新的信息和时间戳
* @throws NotFoundException 当指定ID的用户不存在时
* @throws ConflictException 当更新的数据与其他用户产生唯一性冲突时
*
* @example
* const updatedUser = await userService.update(BigInt(123), {
* nickname: '新昵称',
* email: 'newemail@example.com'
* });
*/
async update(id: bigint, updateData: Partial<CreateUserDto>): Promise<Users> {
const monitor = PerformanceMonitor.create();
this.logStart('更新用户', {
userId: id.toString(),
updateFields: Object.keys(updateData)
});
try {
// 检查用户是否存在
const existingUser = await this.findOne(id);
// 检查更新数据的唯一性约束
await this.checkUpdateUniquenessConstraints(id, updateData, existingUser);
// 更新用户数据
Object.assign(existingUser, updateData);
existingUser.updated_at = new Date();
this.saveUser(existingUser);
this.logSuccess('更新用户', {
userId: id.toString(),
username: existingUser.username
}, monitor.getDuration());
return existingUser;
} catch (error) {
this.handleServiceError(error, '更新用户', {
userId: id.toString(),
duration: monitor.getDuration()
});
}
}
/**
* 删除用户
*
* 业务逻辑:
* 1. 验证目标用户是否存在
* 2. 从内存Map中删除用户记录
* 3. 记录删除操作和结果
* 4. 返回删除操作的统计信息
*
* @param id 用户ID必须是有效的bigint类型
* @returns 删除操作结果,包含影响的记录数和操作消息
* @throws NotFoundException 当指定ID的用户不存在时
*
* @example
* const result = await userService.remove(BigInt(123));
* console.log(result.message); // "成功删除ID为 123 的用户"
*/
async remove(id: bigint): Promise<{ affected: number; message: string }> {
const monitor = PerformanceMonitor.create();
this.logStart('删除用户', { userId: id.toString() });
try {
// 检查用户是否存在
const user = await this.findOne(id);
// 执行删除
const deleted = this.users.delete(id);
const result = {
affected: deleted ? 1 : 0,
message: `成功删除ID为 ${id} 的用户`
};
this.logSuccess('删除用户', {
userId: id.toString(),
username: user.username
}, monitor.getDuration());
return result;
} catch (error) {
this.handleServiceError(error, '删除用户', {
userId: id.toString(),
duration: monitor.getDuration()
});
}
}
/**
* 软删除用户(内存模式下设置删除时间)
*
* @param id 用户ID
* @returns 被软删除的用户实体
*/
async softRemove(id: bigint): Promise<Users> {
const user = await this.findOne(id);
// 注意:软删除功能暂未实现,当前仅返回用户实体
this.saveUser(user);
return user;
}
/**
* 统计用户数量
*
* @param conditions 查询条件(内存模式下简化处理)
* @returns 用户数量
*/
async count(conditions?: Record<string, any>): Promise<number> {
if (!conditions) {
return this.users.size;
}
// 简化的条件过滤
let count = 0;
for (const user of this.users.values()) {
let match = true;
for (const [key, value] of Object.entries(conditions)) {
if ((user as any)[key] !== value) {
match = false;
break;
}
}
if (match) count++;
}
return count;
}
/**
* 检查用户是否存在
*
* @param id 用户ID
* @returns 是否存在
*/
async exists(id: bigint): Promise<boolean> {
return this.users.has(id);
}
/**
* 创建新用户(带重复检查)
*
* 业务逻辑:
* 1. 检查用户名、邮箱、手机号、GitHub ID的唯一性
* 2. 如果所有检查都通过调用create方法创建用户
* 3. 记录操作日志和性能指标
*
* @param createUserDto 创建用户的数据传输对象
* @returns 创建的用户实体
* @throws ConflictException 当用户名、邮箱、手机号或GitHub ID已存在时
* @throws BadRequestException 当数据验证失败时
*/
async createWithDuplicateCheck(createUserDto: CreateUserDto): Promise<Users> {
const monitor = PerformanceMonitor.create();
this.logStart('创建用户(带重复检查)', {
username: createUserDto.username,
email: createUserDto.email,
phone: createUserDto.phone,
github_id: createUserDto.github_id
});
try {
// 执行所有唯一性检查
await this.checkUniquenessConstraints(createUserDto);
// 调用普通的创建方法
const user = await this.create(createUserDto);
this.logSuccess('创建用户(带重复检查)', {
userId: user.id.toString(),
username: user.username
}, monitor.getDuration());
return user;
} catch (error) {
this.handleServiceError(error, '创建用户(带重复检查)', {
username: createUserDto.username,
duration: monitor.getDuration()
});
}
}
/**
* 批量创建用户
*
* 业务逻辑:
* 1. 遍历用户数据数组
* 2. 对每个用户数据调用create方法
* 3. 收集所有创建成功的用户
* 4. 记录批量操作的统计信息和性能指标
* 5. 如果某个用户创建失败,整个操作会中断并抛出异常
*
* @param createUserDtos 用户数据数组每个元素都是CreateUserDto类型
* @returns 创建成功的用户列表,顺序与输入数组一致
* @throws ConflictException 当任何用户的唯一性约束冲突时
* @throws BadRequestException 当任何用户的数据验证失败时
*
* @example
* const users = await userService.createBatch([
* { username: 'user1', email: 'user1@example.com', nickname: '用户1' },
* { username: 'user2', email: 'user2@example.com', nickname: '用户2' }
* ]);
*/
async createBatch(createUserDtos: CreateUserDto[]): Promise<Users[]> {
const monitor = PerformanceMonitor.create();
this.logStart('批量创建用户', { count: createUserDtos.length });
try {
const users: Users[] = [];
const createdUsers: Users[] = []; // 用于回滚的记录
try {
for (const dto of createUserDtos) {
const user = await this.create(dto);
users.push(user);
createdUsers.push(user);
}
this.logSuccess('批量创建用户', {
createdCount: users.length
}, monitor.getDuration());
return users;
} catch (error) {
// 回滚已创建的用户
for (const user of createdUsers) {
this.users.delete(user.id);
}
throw error;
}
} catch (error) {
this.handleServiceError(error, '批量创建用户', {
count: createUserDtos.length,
duration: monitor.getDuration()
});
}
}
/**
* 根据角色查询用户
*
* @param role 角色值
* @param includeDeleted 是否包含已删除用户默认false
* @returns 用户列表
*/
async findByRole(role: number, includeDeleted: boolean = false): Promise<Users[]> {
return Array.from(this.users.values())
.filter(u => u.role === role)
.sort((a, b) => b.created_at.getTime() - a.created_at.getTime());
}
/**
* 搜索用户(根据用户名或昵称)
*
* 业务逻辑:
* 1. 将搜索关键词转换为小写以实现大小写不敏感搜索
* 2. 遍历所有用户,匹配用户名或昵称中包含关键词的用户
* 3. 按创建时间倒序排列搜索结果
* 4. 限制返回结果数量以提高性能
* 5. 记录搜索操作和性能指标
*
* @param keyword 搜索关键词,支持部分匹配,大小写不敏感
* @param limit 限制返回数量默认20防止结果过多影响性能
* @returns 匹配的用户列表,按创建时间倒序排列
*
* @example
* // 搜索用户名或昵称包含"admin"的用户
* const users = await userService.search('admin', 10);
*
* // 搜索所有包含"测试"的用户
* const testUsers = await userService.search('测试');
*/
async search(keyword: string, limit: number = QUERY_LIMITS.DEFAULT_SEARCH_LIMIT, includeDeleted: boolean = false): Promise<Users[]> {
const monitor = PerformanceMonitor.create();
this.logStart('搜索用户', { keyword, limit, includeDeleted });
try {
const lowerKeyword = keyword.toLowerCase();
const results = Array.from(this.users.values())
.filter(u => {
// 注意软删除功能暂未实现includeDeleted参数预留用于未来扩展
// 检查关键词匹配
return u.username.toLowerCase().includes(lowerKeyword) ||
u.nickname.toLowerCase().includes(lowerKeyword);
})
.sort((a, b) => b.created_at.getTime() - a.created_at.getTime())
.slice(0, limit);
this.logSuccess('搜索用户', {
keyword,
resultCount: results.length,
includeDeleted
}, monitor.getDuration());
return results;
} catch (error) {
// 搜索异常使用特殊处理,返回空数组而不抛出异常
return this.handleSearchError(error, '搜索用户', {
keyword,
limit,
includeDeleted,
duration: monitor.getDuration()
});
}
}
}

View File

@@ -0,0 +1,391 @@
/**
* Zulip账号关联数据访问服务基类
*
* 功能描述:
* - 提供统一的数据访问操作基础功能
* - 集成高性能日志系统,支持结构化日志记录
* - 定义通用的数据转换方法和性能监控
* - 为所有Zulip账号数据访问服务提供基础功能支持
*
* 职责分离:
* - 数据访问:统一处理数据访问相关的基础操作
* - 日志管理集成AppLoggerService提供高性能日志记录
* - 性能监控:提供操作耗时统计和性能指标收集
* - 数据转换:统一数据格式化和转换逻辑
* - 基础服务:为子类提供通用的数据访问方法
*
* 注意:业务异常处理已转移到 src/core/zulip_core/services/zulip_accounts_business.service.ts
*
* 最近修改:
* - 2026-01-12: 架构优化 - 移除业务异常处理,专注数据访问功能 (修改者: moyin)
* - 2026-01-12: 代码质量优化 - 添加列表响应构建工具方法,彻底消除所有重复代码 (修改者: moyin)
* - 2026-01-12: 代码质量优化 - 添加数组映射工具方法,进一步减少重复代码 (修改者: moyin)
* - 2026-01-12: 代码质量优化 - 添加BigInt转换和DTO转换的抽象方法减少重复代码 (修改者: moyin)
* - 2026-01-12: 性能优化 - 集成AppLoggerService添加性能监控和结构化日志
* - 2026-01-07: 代码规范优化 - 修复文件命名规范,将短横线改为下划线分隔
* - 2026-01-07: 代码规范优化 - 完善文件头注释和方法三级注释
* - 2026-01-07: 功能完善 - 增加搜索异常的特殊处理逻辑
* - 2026-01-07: 架构优化 - 统一异常处理机制和日志记录格式
* - 2025-01-07: 初始创建 - 创建基础服务类和异常处理框架
*
* @author angjustinl
* @version 2.0.0
* @since 2025-01-07
* @lastModified 2026-01-12
*/
import { Inject } from '@nestjs/common';
import { AppLoggerService, LogContext } from '../../utils/logger/logger.service';
export abstract class BaseZulipAccountsService {
protected readonly logger: AppLoggerService;
protected readonly moduleName: string;
constructor(
@Inject(AppLoggerService) logger: AppLoggerService,
moduleName: string = 'ZulipAccountsService'
) {
this.logger = logger;
this.moduleName = moduleName;
}
/**
* 统一的错误格式化方法
*
* 数据访问逻辑:
* 1. 检查错误对象类型判断是否为Error实例
* 2. 如果是Error实例提取message属性作为错误信息
* 3. 如果不是Error实例将错误对象转换为字符串
* 4. 返回格式化后的错误信息字符串
*
* @param error 原始错误对象可能是Error实例或其他类型
* @returns 格式化后的错误信息字符串,用于日志记录
* @throws 无异常抛出,该方法保证返回字符串
*/
protected formatError(error: unknown): string {
if (error instanceof Error) {
return error.message;
}
return String(error);
}
/**
* 统一的数据访问错误处理方法
*
* 数据访问逻辑:
* 1. 格式化原始错误信息,提取可读的错误描述
* 2. 使用AppLoggerService记录结构化错误日志
* 3. 重新抛出原始错误,不进行业务异常转换
* 4. 确保错误信息被正确记录用于调试
*
* @param error 原始错误对象,数据访问过程中发生的异常
* @param operation 操作名称,用于日志记录和错误追踪
* @param context 上下文信息,包含相关的数据访问参数
* @returns 永不返回,该方法总是抛出异常
* @throws 重新抛出原始错误
*/
protected handleDataAccessError(error: unknown, operation: string, context?: Record<string, any>): never {
const errorMessage = this.formatError(error);
// 使用AppLoggerService记录结构化错误日志
const logContext: LogContext = {
module: this.moduleName,
operation,
error: errorMessage,
context,
timestamp: new Date().toISOString()
};
this.logger.error(`${operation}失败`, logContext, error instanceof Error ? error.stack : undefined);
// 重新抛出原始错误,不进行业务异常转换
throw error;
}
/**
* 搜索异常的特殊处理(返回空结果而不抛出异常)
*
* 数据访问逻辑:
* 1. 格式化错误信息,提取可读的错误描述
* 2. 使用AppLoggerService记录警告级别的结构化日志
* 3. 返回空数组而不是抛出异常,保证搜索接口的可用性
* 4. 记录完整的上下文信息,便于问题排查和监控
* 5. 使用warn级别日志区别于error级别的严重异常
*
* @param error 原始错误对象,搜索过程中发生的异常
* @param operation 操作名称,用于日志记录和问题定位
* @param context 上下文信息,包含搜索条件和相关参数
* @returns 空数组,确保搜索接口始终返回有效的数组结果
*/
protected handleSearchError(error: unknown, operation: string, context?: Record<string, any>): any[] {
const errorMessage = this.formatError(error);
// 使用AppLoggerService记录结构化警告日志
const logContext: LogContext = {
module: this.moduleName,
operation,
error: errorMessage,
context,
timestamp: new Date().toISOString()
};
this.logger.warn(`${operation}失败,返回空结果`, logContext);
return [];
}
/**
* 记录操作成功日志
*
* 业务逻辑:
* 1. 构建标准化的成功日志信息,包含操作名称和结果
* 2. 使用AppLoggerService记录结构化日志信息
* 3. 记录上下文信息,便于业务流程追踪和性能分析
* 4. 可选记录操作耗时,用于性能监控和优化
* 5. 添加时间戳,确保日志的时序性和可追溯性
* 6. 使用info级别日志标识正常的业务操作完成
*
* @param operation 操作名称,描述具体的业务操作类型
* @param context 上下文信息,包含操作相关的业务数据
* @param duration 操作耗时(毫秒),用于性能监控,可选参数
* @returns 无返回值,仅记录日志
*
* @example
* // 记录简单操作成功
* this.logSuccess('创建用户', { userId: '12345', username: 'test' });
*
* @example
* // 记录带耗时的操作成功
* const startTime = Date.now();
* // ... 执行业务逻辑
* const duration = Date.now() - startTime;
* this.logSuccess('复杂查询', { criteria, resultCount: 100 }, duration);
*/
protected logSuccess(operation: string, context?: Record<string, any>, duration?: number): void {
const logContext: LogContext = {
module: this.moduleName,
operation,
context,
duration,
timestamp: new Date().toISOString()
};
this.logger.info(`${operation}成功`, logContext);
}
/**
* 记录操作开始日志
*
* 业务逻辑:
* 1. 构建标准化的操作开始日志信息,标记业务流程起点
* 2. 使用AppLoggerService记录结构化日志信息
* 3. 记录上下文信息,包含操作的输入参数和相关数据
* 4. 添加时间戳,便于与成功/失败日志进行时序关联
* 5. 使用info级别日志标识正常的业务操作开始
* 6. 为后续的性能分析和问题排查提供起始点标记
*
* @param operation 操作名称,描述即将执行的业务操作类型
* @param context 上下文信息,包含操作的输入参数和相关数据
* @returns 无返回值,仅记录日志
*
* @example
* // 记录数据库操作开始
* this.logStart('创建用户', {
* gameUserId: '12345',
* email: 'user@example.com'
* });
*
* @example
* // 记录复杂业务流程开始
* this.logStart('用户认证流程', {
* userId: user.id,
* authMethod: 'oauth',
* clientIp: request.ip
* });
*/
protected logStart(operation: string, context?: Record<string, any>): void {
const logContext: LogContext = {
module: this.moduleName,
operation,
context,
timestamp: new Date().toISOString()
};
this.logger.info(`开始${operation}`, logContext);
}
/**
* 创建性能监控器
*
* 功能描述:
* 创建一个性能监控器对象,用于测量操作耗时和记录性能指标
*
* 业务逻辑:
* 1. 记录操作开始时间戳
* 2. 返回包含结束方法的监控器对象
* 3. 结束方法自动计算耗时并记录日志
* 4. 支持成功和失败两种结束状态
*
* @param operation 操作名称
* @param context 操作上下文
* @returns 性能监控器对象
*
* @example
* ```typescript
* const monitor = this.createPerformanceMonitor('创建用户', { userId: '123' });
* try {
* const result = await this.repository.create(data);
* monitor.success({ result: 'created' });
* return result;
* } catch (error) {
* monitor.error(error);
* throw error;
* }
* ```
*/
protected createPerformanceMonitor(operation: string, context?: Record<string, any>) {
const startTime = Date.now();
this.logStart(operation, context);
return {
success: (additionalContext?: Record<string, any>) => {
const duration = Date.now() - startTime;
this.logSuccess(operation, { ...context, ...additionalContext }, duration);
},
error: (error: unknown, additionalContext?: Record<string, any>) => {
const duration = Date.now() - startTime;
this.handleDataAccessError(error, operation, {
...context,
...additionalContext,
duration
});
}
};
}
/**
* 解析游戏用户ID为BigInt类型
*
* 数据转换逻辑:
* 1. 将字符串类型的游戏用户ID转换为BigInt类型
* 2. 统一处理ID转换逻辑避免重复代码
* 3. 提供类型安全的转换方法
*
* @param gameUserId 游戏用户ID字符串
* @returns BigInt类型的游戏用户ID
* @throws Error 当ID格式无效时
*/
protected parseGameUserId(gameUserId: string): bigint {
try {
return BigInt(gameUserId);
} catch (error) {
throw new Error(`无效的游戏用户ID格式: ${gameUserId}`);
}
}
/**
* 批量解析ID数组为BigInt类型
*
* 数据转换逻辑:
* 1. 将字符串ID数组转换为BigInt数组
* 2. 统一处理批量ID转换逻辑
* 3. 提供类型安全的批量转换方法
*
* @param ids 字符串ID数组
* @returns BigInt类型的ID数组
* @throws Error 当任何ID格式无效时
*/
protected parseIds(ids: string[]): bigint[] {
try {
return ids.map(id => BigInt(id));
} catch (error) {
throw new Error(`无效的ID格式: ${ids.join(', ')}`);
}
}
/**
* 解析单个ID为BigInt类型
*
* 数据转换逻辑:
* 1. 将字符串类型的ID转换为BigInt类型
* 2. 统一处理单个ID转换逻辑
* 3. 提供类型安全的转换方法
*
* @param id 字符串ID
* @returns BigInt类型的ID
* @throws Error 当ID格式无效时
*/
protected parseId(id: string): bigint {
try {
return BigInt(id);
} catch (error) {
throw new Error(`无效的ID格式: ${id}`);
}
}
/**
* 抽象方法将实体转换为响应DTO
*
* 功能描述:
* 子类必须实现此方法将数据库实体转换为API响应DTO
*
* @param entity 数据库实体对象
* @returns 响应DTO对象
*
* @example
* ```typescript
* // 在子类中实现
* protected toResponseDto(account: ZulipAccounts): ZulipAccountResponseDto {
* return {
* id: account.id.toString(),
* gameUserId: account.gameUserId.toString(),
* // ... 其他字段
* };
* }
* ```
*/
protected abstract toResponseDto(entity: any): any;
/**
* 将实体数组转换为响应DTO数组
*
* 功能描述:
* 统一处理实体数组到DTO数组的转换减少重复代码
*
* @param entities 实体数组
* @returns 响应DTO数组
*
* @example
* ```typescript
* const accounts = await this.repository.findMany();
* const responseAccounts = this.toResponseDtoArray(accounts);
* ```
*/
protected toResponseDtoArray(entities: any[]): any[] {
return entities.map(entity => this.toResponseDto(entity));
}
/**
* 构建列表响应对象
*
* 功能描述:
* 统一构建列表响应对象,减少重复的对象构建代码
*
* @param entities 实体数组
* @returns 标准的列表响应对象
*
* @example
* ```typescript
* const accounts = await this.repository.findMany();
* return this.buildListResponse(accounts);
* ```
*/
protected buildListResponse(entities: any[]): any {
const responseAccounts = this.toResponseDtoArray(entities);
return {
accounts: responseAccounts,
total: responseAccounts.length,
count: responseAccounts.length,
};
}
}

View File

@@ -0,0 +1,260 @@
/**
* Zulip账号关联缓存配置
*
* 功能描述:
* - 定义Zulip账号关联模块的缓存策略和配置
* - 提供不同类型数据的缓存TTL设置
* - 支持环境相关的缓存配置调整
* - 提供缓存键命名规范和管理工具
*
* 职责分离:
* - 缓存策略:定义不同数据类型的缓存时间和策略
* - 键管理:提供统一的缓存键命名规范
* - 环境适配:根据环境调整缓存配置
* - 性能优化:平衡缓存效果和内存使用
*
* 最近修改:
* - 2026-01-12: 初始创建 - 定义缓存配置和策略
*
* @author angjustinl
* @version 1.0.0
* @since 2026-01-12
* @lastModified 2026-01-12
*/
import { CacheModuleOptions } from '@nestjs/cache-manager';
/**
* 缓存配置常量
*/
export const CACHE_CONFIG = {
// 缓存键前缀
PREFIX: 'zulip_accounts',
// TTL配置
TTL: {
// 账号基础信息缓存 - 5分钟
ACCOUNT_INFO: 300,
// 统计数据缓存 - 1分钟变化频繁
STATISTICS: 60,
// 验证状态缓存 - 10分钟
VERIFICATION_STATUS: 600,
// 错误账号列表缓存 - 2分钟需要及时更新
ERROR_ACCOUNTS: 120,
// 批量查询结果缓存 - 3分钟
BATCH_QUERY: 180,
},
// 缓存大小限制
MAX_ITEMS: {
// 生产环境
PRODUCTION: 5000,
// 开发环境
DEVELOPMENT: 1000,
// 测试环境
TEST: 500,
},
} as const;
/**
* 缓存键类型枚举
*/
export enum CacheKeyType {
GAME_USER = 'game_user',
ZULIP_USER = 'zulip_user',
ZULIP_EMAIL = 'zulip_email',
ACCOUNT_ID = 'account_id',
STATISTICS = 'stats',
VERIFICATION_LIST = 'verification_list',
ERROR_LIST = 'error_list',
BATCH_QUERY = 'batch_query',
}
/**
* 缓存配置工厂
*/
export class ZulipAccountsCacheConfigFactory {
/**
* 创建缓存模块配置
*
* @param environment 环境名称
* @returns 缓存模块配置
*/
static createCacheConfig(environment: string = 'development'): CacheModuleOptions {
const maxItems = this.getMaxItemsByEnvironment(environment);
return {
ttl: CACHE_CONFIG.TTL.ACCOUNT_INFO, // 默认TTL
max: maxItems,
// 可以添加更多配置,如存储引擎等
};
}
/**
* 根据环境获取最大缓存项数
*
* @param environment 环境名称
* @returns 最大缓存项数
* @private
*/
private static getMaxItemsByEnvironment(environment: string): number {
switch (environment) {
case 'production':
return CACHE_CONFIG.MAX_ITEMS.PRODUCTION;
case 'test':
return CACHE_CONFIG.MAX_ITEMS.TEST;
default:
return CACHE_CONFIG.MAX_ITEMS.DEVELOPMENT;
}
}
/**
* 构建缓存键
*
* @param type 缓存键类型
* @param identifier 标识符
* @param suffix 后缀(可选)
* @returns 完整的缓存键
*/
static buildCacheKey(
type: CacheKeyType,
identifier?: string | number,
suffix?: string
): string {
const parts = [CACHE_CONFIG.PREFIX, type.toString()];
if (identifier !== undefined) {
parts.push(String(identifier));
}
if (suffix) {
parts.push(suffix);
}
return parts.join(':');
}
/**
* 获取指定类型的TTL
*
* @param type 缓存键类型
* @returns TTL
*/
static getTTLByType(type: CacheKeyType): number {
switch (type) {
case CacheKeyType.STATISTICS:
return CACHE_CONFIG.TTL.STATISTICS;
case CacheKeyType.VERIFICATION_LIST:
return CACHE_CONFIG.TTL.VERIFICATION_STATUS;
case CacheKeyType.ERROR_LIST:
return CACHE_CONFIG.TTL.ERROR_ACCOUNTS;
case CacheKeyType.BATCH_QUERY:
return CACHE_CONFIG.TTL.BATCH_QUERY;
default:
return CACHE_CONFIG.TTL.ACCOUNT_INFO;
}
}
/**
* 生成缓存键模式(用于批量删除)
*
* @param type 缓存键类型
* @returns 缓存键模式
*/
static getCacheKeyPattern(type: CacheKeyType): string {
return `${CACHE_CONFIG.PREFIX}:${type}:*`;
}
}
/**
* 缓存管理工具类
*/
export class ZulipAccountsCacheManager {
/**
* 获取所有相关的缓存键(用于清除)
*
* @param gameUserId 游戏用户ID
* @param zulipUserId Zulip用户ID
* @param zulipEmail Zulip邮箱
* @returns 相关的缓存键列表
*/
static getRelatedCacheKeys(
gameUserId?: string,
zulipUserId?: number,
zulipEmail?: string
): string[] {
const keys: string[] = [];
// 统计数据缓存(总是需要清除)
keys.push(ZulipAccountsCacheConfigFactory.buildCacheKey(CacheKeyType.STATISTICS));
// 验证和错误列表缓存(可能受影响)
keys.push(ZulipAccountsCacheConfigFactory.buildCacheKey(CacheKeyType.VERIFICATION_LIST));
keys.push(ZulipAccountsCacheConfigFactory.buildCacheKey(CacheKeyType.ERROR_LIST));
// 具体记录的缓存
if (gameUserId) {
keys.push(
ZulipAccountsCacheConfigFactory.buildCacheKey(CacheKeyType.GAME_USER, gameUserId),
ZulipAccountsCacheConfigFactory.buildCacheKey(CacheKeyType.GAME_USER, gameUserId, 'with_user')
);
}
if (zulipUserId) {
keys.push(
ZulipAccountsCacheConfigFactory.buildCacheKey(CacheKeyType.ZULIP_USER, zulipUserId),
ZulipAccountsCacheConfigFactory.buildCacheKey(CacheKeyType.ZULIP_USER, zulipUserId, 'with_user')
);
}
if (zulipEmail) {
keys.push(
ZulipAccountsCacheConfigFactory.buildCacheKey(CacheKeyType.ZULIP_EMAIL, zulipEmail),
ZulipAccountsCacheConfigFactory.buildCacheKey(CacheKeyType.ZULIP_EMAIL, zulipEmail, 'with_user')
);
}
return keys;
}
/**
* 检查缓存键是否有效
*
* @param key 缓存键
* @returns 是否有效
*/
static isValidCacheKey(key: string): boolean {
return key.startsWith(CACHE_CONFIG.PREFIX + ':');
}
/**
* 解析缓存键
*
* @param key 缓存键
* @returns 解析结果
*/
static parseCacheKey(key: string): {
prefix: string;
type: string;
identifier?: string;
suffix?: string;
} | null {
if (!this.isValidCacheKey(key)) {
return null;
}
const parts = key.split(':');
return {
prefix: parts[0],
type: parts[1],
identifier: parts[2],
suffix: parts[3],
};
}
}

View File

@@ -0,0 +1,65 @@
/**
* Zulip账号关联模块常量定义
*
* 功能描述:
* - 定义模块中使用的所有常量和配置值
* - 提供统一的常量管理和维护
* - 避免魔法数字和硬编码值
* - 便于配置调整和环境适配
*
* 职责分离:
* - 常量定义:集中管理所有模块常量
* - 配置管理:提供可配置的默认值
* - 类型安全:确保常量的类型正确性
*
* 最近修改:
* - 2026-01-07: 代码规范优化 - 提取魔法数字为常量,提高代码质量 (修改者: moyin)
* - 2026-01-07: 代码规范优化 - 注释规范检查和修正 (修改者: moyin)
* - 2026-01-07: 代码规范优化 - 使用统一的常量文件,提高代码质量
* - 2026-01-07: 功能新增 - 添加状态枚举和类型定义
* - 2026-01-07: 初始创建 - 提取模块中的常量定义,统一管理
*
* @author angjustinl
* @version 1.0.1
* @since 2026-01-07
* @lastModified 2026-01-07
*/
// 时间相关常量
export const MILLISECONDS_PER_HOUR = 60 * 60 * 1000;
export const MILLISECONDS_PER_DAY = 24 * MILLISECONDS_PER_HOUR;
// 验证相关常量
export const DEFAULT_VERIFICATION_MAX_AGE = 24 * MILLISECONDS_PER_HOUR; // 24小时验证间隔
export const DEFAULT_VERIFICATION_HOURS = 24;
export const DEFAULT_VERIFICATION_INTERVAL = DEFAULT_VERIFICATION_MAX_AGE;
// 重试相关常量
export const DEFAULT_MAX_RETRY_COUNT = 3; // 默认最大重试次数
export const HIGH_RETRY_THRESHOLD = 5; // 高重试次数阈值
// 查询限制常量
export const VERIFICATION_QUERY_LIMIT = 100; // 验证查询限制
export const ERROR_ACCOUNTS_QUERY_LIMIT = 50; // 错误账号查询限制
export const DEFAULT_ERROR_ACCOUNTS_LIMIT = 50; // 默认错误账号限制
// 业务规则常量
export const DEFAULT_MAX_AGE_DAYS = 7; // 默认最大年龄天数
// 长度限制常量
export const MAX_FULL_NAME_LENGTH = 100; // 用户全名最大长度
export const MAX_SHORT_NAME_LENGTH = 50; // 用户短名称最大长度
export const MIN_FULL_NAME_LENGTH = 2; // 用户全名最小长度
// 数据库配置常量
export const REQUIRED_DB_ENV_VARS = ['DB_HOST', 'DB_PORT', 'DB_USERNAME', 'DB_PASSWORD', 'DB_NAME'];
// 状态枚举
export const ACCOUNT_STATUS = {
ACTIVE: 'active' as const,
INACTIVE: 'inactive' as const,
SUSPENDED: 'suspended' as const,
ERROR: 'error' as const,
} as const;
export type AccountStatus = typeof ACCOUNT_STATUS[keyof typeof ACCOUNT_STATUS];

View File

@@ -0,0 +1,275 @@
/**
* Zulip账号关联数据传输对象
*
* 功能描述:
* - 定义API请求和响应的数据结构和验证规则
* - 提供统一的数据传输格式和类型约束
* - 支持Swagger文档自动生成和API接口描述
* - 实现数据验证、转换和序列化功能
*
* 职责分离:
* - 数据结构定义定义所有API相关的数据传输对象
* - 验证规则:通过装饰器定义字段验证和约束规则
* - 文档生成提供Swagger API文档的元数据信息
* - 类型安全:确保前后端数据交互的类型一致性
*
* 最近修改:
* - 2026-01-07: 代码规范优化 - 完善文件头注释和移除未使用的导入
* - 2026-01-07: 功能完善 - 优化DTO字段验证规则和文档描述
* - 2025-01-07: 架构优化 - 统一数据传输对象的设计模式
* - 2025-01-07: 初始创建 - 创建基础的DTO类和验证规则
* - 2025-01-07: 功能实现 - 实现完整的请求响应DTO定义
*
* @author angjustinl
* @version 1.1.0
* @since 2025-01-07
* @lastModified 2026-01-07
*/
import { IsString, IsNumber, IsEmail, IsEnum, IsOptional, IsBoolean } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
/**
* 创建Zulip账号关联请求DTO
*/
export class CreateZulipAccountDto {
@ApiProperty({ description: '游戏用户ID', example: '12345' })
@IsString()
gameUserId: string;
@ApiProperty({ description: 'Zulip用户ID', example: 67890 })
@IsNumber()
zulipUserId: number;
@ApiProperty({ description: 'Zulip邮箱地址', example: 'user@example.com' })
@IsEmail()
zulipEmail: string;
@ApiProperty({ description: 'Zulip用户全名', example: '张三' })
@IsString()
zulipFullName: string;
@ApiProperty({ description: '加密的Zulip API Key' })
@IsString()
zulipApiKeyEncrypted: string;
@ApiPropertyOptional({
description: '账号状态',
enum: ['active', 'inactive', 'suspended', 'error'],
default: 'active'
})
@IsOptional()
@IsEnum(['active', 'inactive', 'suspended', 'error'])
status?: 'active' | 'inactive' | 'suspended' | 'error';
@ApiPropertyOptional({ description: '最后验证时间' })
@IsOptional()
lastVerifiedAt?: Date;
}
/**
* 更新Zulip账号关联请求DTO
*/
export class UpdateZulipAccountDto {
@ApiPropertyOptional({ description: 'Zulip用户全名', example: '李四' })
@IsOptional()
@IsString()
zulipFullName?: string;
@ApiPropertyOptional({ description: '加密的Zulip API Key' })
@IsOptional()
@IsString()
zulipApiKeyEncrypted?: string;
@ApiPropertyOptional({
description: '账号状态',
enum: ['active', 'inactive', 'suspended', 'error']
})
@IsOptional()
@IsEnum(['active', 'inactive', 'suspended', 'error'])
status?: 'active' | 'inactive' | 'suspended' | 'error';
@ApiPropertyOptional({ description: '错误信息' })
@IsOptional()
@IsString()
errorMessage?: string;
@ApiPropertyOptional({ description: '重试次数', example: 0 })
@IsOptional()
@IsNumber()
retryCount?: number;
@ApiPropertyOptional({ description: '最后验证时间' })
@IsOptional()
lastVerifiedAt?: Date;
}
/**
* Zulip账号关联查询DTO
*/
export class QueryZulipAccountDto {
@ApiPropertyOptional({ description: '游戏用户ID', example: '12345' })
@IsOptional()
@IsString()
gameUserId?: string;
@ApiPropertyOptional({ description: 'Zulip用户ID', example: 67890 })
@IsOptional()
@IsNumber()
zulipUserId?: number;
@ApiPropertyOptional({ description: 'Zulip邮箱地址', example: 'user@example.com' })
@IsOptional()
@IsEmail()
zulipEmail?: string;
@ApiPropertyOptional({
description: '账号状态',
enum: ['active', 'inactive', 'suspended', 'error']
})
@IsOptional()
@IsEnum(['active', 'inactive', 'suspended', 'error'])
status?: 'active' | 'inactive' | 'suspended' | 'error';
@ApiPropertyOptional({ description: '是否包含游戏用户信息', default: false })
@IsOptional()
@IsBoolean()
includeGameUser?: boolean;
}
/**
* Zulip账号关联响应DTO
*/
export class ZulipAccountResponseDto {
@ApiProperty({ description: '关联记录ID', example: '1' })
id: string;
@ApiProperty({ description: '游戏用户ID', example: '12345' })
gameUserId: string;
@ApiProperty({ description: 'Zulip用户ID', example: 67890 })
zulipUserId: number;
@ApiProperty({ description: 'Zulip邮箱地址', example: 'user@example.com' })
zulipEmail: string;
@ApiProperty({ description: 'Zulip用户全名', example: '张三' })
zulipFullName: string;
@ApiProperty({
description: '账号状态',
enum: ['active', 'inactive', 'suspended', 'error']
})
status: 'active' | 'inactive' | 'suspended' | 'error';
@ApiPropertyOptional({ description: '最后验证时间' })
lastVerifiedAt?: string;
@ApiPropertyOptional({ description: '最后同步时间' })
lastSyncedAt?: string;
@ApiPropertyOptional({ description: '错误信息' })
errorMessage?: string;
@ApiProperty({ description: '重试次数', example: 0 })
retryCount: number;
@ApiProperty({ description: '创建时间' })
createdAt: string;
@ApiProperty({ description: '更新时间' })
updatedAt: string;
@ApiPropertyOptional({ description: '关联的游戏用户信息' })
gameUser?: any;
}
/**
* Zulip账号关联列表响应DTO
*/
export class ZulipAccountListResponseDto {
@ApiProperty({ description: '账号关联列表', type: [ZulipAccountResponseDto] })
accounts: ZulipAccountResponseDto[];
@ApiProperty({ description: '总数', example: 100 })
total: number;
@ApiProperty({ description: '当前页数量', example: 10 })
count: number;
}
/**
* 账号状态统计响应DTO
*/
export class ZulipAccountStatsResponseDto {
@ApiProperty({ description: '正常状态账号数', example: 85 })
active: number;
@ApiProperty({ description: '未激活账号数', example: 10 })
inactive: number;
@ApiProperty({ description: '暂停状态账号数', example: 3 })
suspended: number;
@ApiProperty({ description: '错误状态账号数', example: 2 })
error: number;
@ApiProperty({ description: '总账号数', example: 100 })
total: number;
}
/**
* 批量操作请求DTO
*/
export class BatchUpdateStatusDto {
@ApiProperty({ description: '账号ID列表', example: ['1', '2', '3'] })
@IsString({ each: true })
ids: string[];
@ApiProperty({
description: '新状态',
enum: ['active', 'inactive', 'suspended', 'error']
})
@IsEnum(['active', 'inactive', 'suspended', 'error'])
status: 'active' | 'inactive' | 'suspended' | 'error';
}
/**
* 批量操作响应DTO
*/
export class BatchUpdateResponseDto {
@ApiProperty({ description: '操作是否成功' })
success: boolean;
@ApiProperty({ description: '更新的记录数', example: 3 })
updatedCount: number;
@ApiPropertyOptional({ description: '错误信息' })
error?: string;
}
/**
* 账号验证请求DTO
*/
export class VerifyAccountDto {
@ApiProperty({ description: '游戏用户ID', example: '12345' })
@IsString()
gameUserId: string;
}
/**
* 账号验证响应DTO
*/
export class VerifyAccountResponseDto {
@ApiProperty({ description: '验证是否成功' })
success: boolean;
@ApiProperty({ description: '账号是否有效' })
isValid: boolean;
@ApiPropertyOptional({ description: '验证时间' })
verifiedAt?: string;
@ApiPropertyOptional({ description: '错误信息' })
error?: string;
}

View File

@@ -0,0 +1,476 @@
/**
* Zulip账号关联实体
*
* 功能描述:
* - 存储游戏用户与Zulip账号的关联关系
* - 管理Zulip账号的基本信息和状态
* - 提供账号验证和同步功能
* - 支持多种状态管理和业务判断方法
*
* 职责分离:
* - 数据模型定义:定义数据库表结构和字段约束
* - 业务方法:提供账号状态判断和操作方法
* - 关联关系管理与Users表的一对一关系
*
* 最近修改:
* - 2026-01-07: 代码规范优化 - 使用统一的常量文件,提高代码质量
* - 2026-01-07: 代码规范优化 - 完善文件头注释和方法注释规范
* - 2026-01-07: 功能新增 - 添加数据库唯一约束和复合索引
* - 2026-01-07: 功能新增 - 新增多个业务判断方法(isHealthy, canBeDeleted等)
*
* @author angjustinl
* @version 1.1.1
* @since 2025-01-05
* @lastModified 2026-01-07
*/
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, OneToOne, JoinColumn, Index } from 'typeorm';
import { Users } from '../users/users.entity';
import {
DEFAULT_MAX_AGE_DAYS,
DEFAULT_VERIFICATION_HOURS,
DEFAULT_MAX_RETRY_COUNT,
HIGH_RETRY_THRESHOLD,
MILLISECONDS_PER_HOUR,
MILLISECONDS_PER_DAY,
} from './zulip_accounts.constants';
@Entity('zulip_accounts')
@Index(['gameUserId']) // 普通索引,不是唯一索引
@Index(['zulipUserId'], { unique: true })
@Index(['zulipEmail'], { unique: true })
@Index(['status']) // 单独的status索引
@Index(['createdAt']) // 单独的created_at索引
@Index(['status', 'lastVerifiedAt']) // 复合索引用于查询优化
@Index(['status', 'updatedAt']) // 复合索引用于查询优化
export class ZulipAccounts {
/**
* 主键ID
*/
@PrimaryGeneratedColumn('increment', { type: 'bigint' })
id: bigint;
/**
* 关联的游戏用户ID
*/
@Column({ type: 'bigint', name: 'game_user_id', comment: '关联的游戏用户ID' })
gameUserId: bigint;
/**
* Zulip用户ID
*/
@Column({ type: 'int', name: 'zulip_user_id', comment: 'Zulip服务器上的用户ID' })
zulipUserId: number;
/**
* Zulip用户邮箱
*/
@Column({ type: 'varchar', length: 255, name: 'zulip_email', comment: 'Zulip账号邮箱地址' })
zulipEmail: string;
/**
* Zulip用户全名
*/
@Column({ type: 'varchar', length: 100, name: 'zulip_full_name', comment: 'Zulip账号全名' })
zulipFullName: string;
/**
* Zulip API Key加密存储
*/
@Column({ type: 'text', name: 'zulip_api_key_encrypted', comment: '加密存储的Zulip API Key' })
zulipApiKeyEncrypted: string;
/**
* 账号状态
* - active: 正常激活状态
* - inactive: 未激活状态
* - suspended: 暂停状态
* - error: 错误状态
*/
@Column({
type: 'enum',
enum: ['active', 'inactive', 'suspended', 'error'],
default: 'active',
comment: '账号状态active-正常inactive-未激活suspended-暂停error-错误'
})
status: 'active' | 'inactive' | 'suspended' | 'error';
/**
* 最后验证时间
*/
@Column({ type: 'timestamp', name: 'last_verified_at', nullable: true, comment: '最后一次验证Zulip账号的时间' })
lastVerifiedAt: Date | null;
/**
* 最后同步时间
*/
@Column({ type: 'timestamp', name: 'last_synced_at', nullable: true, comment: '最后一次同步数据的时间' })
lastSyncedAt: Date | null;
/**
* 错误信息
*/
@Column({ type: 'text', name: 'error_message', nullable: true, comment: '最后一次操作的错误信息' })
errorMessage: string | null;
/**
* 重试次数
*/
@Column({ type: 'int', name: 'retry_count', default: 0, comment: '创建或同步失败的重试次数' })
retryCount: number;
/**
* 创建时间
*/
@CreateDateColumn({ name: 'created_at', comment: '记录创建时间' })
createdAt: Date;
/**
* 更新时间
*/
@UpdateDateColumn({ name: 'updated_at', comment: '记录最后更新时间' })
updatedAt: Date;
/**
* 关联的游戏用户
*/
@OneToOne(() => Users, user => user.zulipAccount)
@JoinColumn({ name: 'game_user_id' })
gameUser: Users;
/**
* 检查账号是否处于正常状态
*
* 业务逻辑:
* 1. 检查账号状态是否为'active'
* 2. 返回布尔值表示是否正常
*
* @returns boolean 是否为正常状态
*
* @example
* ```typescript
* const account = new ZulipAccounts();
* account.status = 'active';
* console.log(account.isActive()); // true
* ```
*/
isActive(): boolean {
return this.status === 'active';
}
/**
* 检查账号是否健康(正常且重试次数不多)
*
* 业务逻辑:
* 1. 检查账号状态是否为'active'
* 2. 检查重试次数是否小于默认阈值
* 3. 两个条件都满足才认为健康
*
* @returns boolean 是否健康
*
* @example
* ```typescript
* const account = new ZulipAccounts();
* account.status = 'active';
* account.retryCount = 1;
* console.log(account.isHealthy()); // true
* ```
*/
isHealthy(): boolean {
return this.status === 'active' && this.retryCount < DEFAULT_MAX_RETRY_COUNT;
}
/**
* 检查账号是否可以被删除
*
* 业务逻辑:
* 1. 如果账号状态不是'active',可以删除
* 2. 如果重试次数超过高阈值,可以删除
* 3. 满足任一条件即可删除
*
* @returns boolean 是否可以删除
*
* @example
* ```typescript
* const account = new ZulipAccounts();
* account.status = 'error';
* account.retryCount = 6;
* console.log(account.canBeDeleted()); // true
* ```
*/
canBeDeleted(): boolean {
return this.status !== 'active' || this.retryCount > HIGH_RETRY_THRESHOLD;
}
/**
* 检查账号数据是否过期
*
* 业务逻辑:
* 1. 获取当前时间
* 2. 计算与最后更新时间的差值
* 3. 比较差值是否超过最大年龄限制
*
* @param maxAge 最大年龄毫秒默认7天
* @returns boolean 是否过期
*
* @example
* ```typescript
* const account = new ZulipAccounts();
* account.updatedAt = new Date(Date.now() - 8 * 24 * 60 * 60 * 1000);
* console.log(account.isStale()); // true (超过7天)
* ```
*/
isStale(maxAge: number = DEFAULT_MAX_AGE_DAYS * MILLISECONDS_PER_DAY): boolean {
const now = new Date();
const timeDiff = now.getTime() - this.updatedAt.getTime();
return timeDiff > maxAge;
}
/**
* 检查账号是否需要重新验证
*
* 业务逻辑:
* 1. 如果从未验证过,需要验证
* 2. 计算距离上次验证的时间差
* 3. 比较时间差是否超过最大验证间隔
*
* @param maxAge 最大验证间隔毫秒默认24小时
* @returns boolean 是否需要重新验证
*
* @example
* ```typescript
* const account = new ZulipAccounts();
* account.lastVerifiedAt = null;
* console.log(account.needsVerification()); // true
* ```
*/
needsVerification(maxAge: number = DEFAULT_VERIFICATION_HOURS * MILLISECONDS_PER_HOUR): boolean {
if (!this.lastVerifiedAt) {
return true;
}
const now = new Date();
const timeDiff = now.getTime() - this.lastVerifiedAt.getTime();
return timeDiff > maxAge;
}
/**
* 检查是否应该重试操作
*
* 业务逻辑:
* 1. 检查账号状态是否为'error'
* 2. 检查重试次数是否小于最大重试次数
* 3. 两个条件都满足才应该重试
*
* @param maxRetryCount 最大重试次数默认3次
* @returns boolean 是否应该重试
*
* @example
* ```typescript
* const account = new ZulipAccounts();
* account.status = 'error';
* account.retryCount = 2;
* console.log(account.shouldRetry()); // true
* ```
*/
shouldRetry(maxRetryCount: number = DEFAULT_MAX_RETRY_COUNT): boolean {
return this.status === 'error' && this.retryCount < maxRetryCount;
}
/**
* 更新验证时间
*
* 业务逻辑:
* 1. 设置最后验证时间为当前时间
* 2. 更新记录的最后修改时间
* 3. 用于标记账号验证操作的完成
*
* @returns void 无返回值,直接修改实体属性
*
* @example
* ```typescript
* const account = new ZulipAccounts();
* account.updateVerificationTime();
* console.log(account.lastVerifiedAt); // 当前时间
* ```
*/
updateVerificationTime(): void {
this.lastVerifiedAt = new Date();
this.updatedAt = new Date();
}
/**
* 更新同步时间
*
* 业务逻辑:
* 1. 设置最后同步时间为当前时间
* 2. 更新记录的最后修改时间
* 3. 用于标记数据同步操作的完成
*
* @returns void 无返回值,直接修改实体属性
*
* @example
* ```typescript
* const account = new ZulipAccounts();
* account.updateSyncTime();
* console.log(account.lastSyncedAt); // 当前时间
* ```
*/
updateSyncTime(): void {
this.lastSyncedAt = new Date();
this.updatedAt = new Date();
}
/**
* 设置错误状态
*
* 业务逻辑:
* 1. 将账号状态设置为'error'
* 2. 记录具体的错误信息
* 3. 增加重试计数器
* 4. 更新最后修改时间
*
* @param errorMessage 错误信息,描述具体的错误原因
* @returns void 无返回值,直接修改实体属性
*
* @example
* ```typescript
* const account = new ZulipAccounts();
* account.setError('API连接超时');
* console.log(account.status); // 'error'
* console.log(account.retryCount); // 增加1
* ```
*/
setError(errorMessage: string): void {
this.status = 'error';
this.errorMessage = errorMessage;
this.retryCount += 1;
this.updatedAt = new Date();
}
/**
* 清除错误状态
*
* 业务逻辑:
* 1. 检查当前状态是否为'error'
* 2. 如果是错误状态,恢复为'active'状态
* 3. 清空错误信息
* 4. 更新最后修改时间
*
* @returns void 无返回值,直接修改实体属性
*
* @example
* ```typescript
* const account = new ZulipAccounts();
* account.status = 'error';
* account.clearError();
* console.log(account.status); // 'active'
* console.log(account.errorMessage); // null
* ```
*/
clearError(): void {
if (this.status === 'error') {
this.status = 'active';
this.errorMessage = null;
this.updatedAt = new Date();
}
}
/**
* 重置重试计数
*
* 业务逻辑:
* 1. 将重试次数重置为0
* 2. 更新最后修改时间
* 3. 用于成功操作后清除重试记录
*
* @returns void 无返回值,直接修改实体属性
*
* @example
* ```typescript
* const account = new ZulipAccounts();
* account.retryCount = 3;
* account.resetRetryCount();
* console.log(account.retryCount); // 0
* ```
*/
resetRetryCount(): void {
this.retryCount = 0;
this.updatedAt = new Date();
}
/**
* 激活账号
*
* 业务逻辑:
* 1. 将账号状态设置为'active'
* 2. 清空错误信息
* 3. 重置重试计数为0
* 4. 更新最后修改时间
*
* @returns void 无返回值,直接修改实体属性
*
* @example
* ```typescript
* const account = new ZulipAccounts();
* account.status = 'suspended';
* account.activate();
* console.log(account.status); // 'active'
* ```
*/
activate(): void {
this.status = 'active';
this.errorMessage = null;
this.retryCount = 0;
this.updatedAt = new Date();
}
/**
* 暂停账号
*
* 业务逻辑:
* 1. 将账号状态设置为'suspended'
* 2. 如果提供了原因,记录到错误信息中
* 3. 更新最后修改时间
*
* @param reason 暂停原因,可选参数,用于记录暂停的具体原因
* @returns void 无返回值,直接修改实体属性
*
* @example
* ```typescript
* const account = new ZulipAccounts();
* account.suspend('违反使用规则');
* console.log(account.status); // 'suspended'
* console.log(account.errorMessage); // '违反使用规则'
* ```
*/
suspend(reason?: string): void {
this.status = 'suspended';
if (reason) {
this.errorMessage = reason;
}
this.updatedAt = new Date();
}
/**
* 停用账号
*
* 业务逻辑:
* 1. 将账号状态设置为'inactive'
* 2. 更新最后修改时间
* 3. 用于临时停用账号但保留数据
*
* @returns void 无返回值,直接修改实体属性
*
* @example
* ```typescript
* const account = new ZulipAccounts();
* account.deactivate();
* console.log(account.status); // 'inactive'
* ```
*/
deactivate(): void {
this.status = 'inactive';
this.updatedAt = new Date();
}
}

View File

@@ -0,0 +1,190 @@
/**
* Zulip账号关联数据模块
*
* 功能描述:
* - 提供Zulip账号关联数据的访问接口和服务注册
* - 封装TypeORM实体和Repository的依赖注入配置
* - 为业务层提供统一的数据访问服务接口
* - 支持数据库和内存模式的动态切换和环境适配
* - 集成缓存和日志系统,提升性能和可观测性
*
* 职责分离:
* - 模块配置:管理依赖注入和服务提供者的注册
* - 环境适配:根据配置自动选择数据库或内存存储模式
* - 服务导出:为其他模块提供数据访问服务的统一接口
* - 全局注册:通过@Global装饰器实现全局模块共享
* - 依赖管理:集成缓存、日志等基础设施服务
*
* 最近修改:
* - 2026-01-12: 性能优化 - 集成缓存模块和AppLoggerService提升性能和可观测性
* - 2026-01-07: 代码规范优化 - 使用统一的常量文件,提高代码质量
* - 2026-01-07: 代码规范优化 - 完善文件头注释和方法三级注释
* - 2026-01-07: 功能完善 - 优化环境检测逻辑和模块配置
* - 2025-01-07: 架构优化 - 实现动态模块配置和环境自适应
* - 2025-01-05: 功能扩展 - 添加内存模式支持和自动切换机制
*
* @author angjustinl
* @version 1.2.0
* @since 2025-01-05
* @lastModified 2026-01-12
*/
import { Module, DynamicModule, Global } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CacheModule } from '@nestjs/cache-manager';
import { ZulipAccounts } from './zulip_accounts.entity';
import { ZulipAccountsRepository } from './zulip_accounts.repository';
import { ZulipAccountsMemoryRepository } from './zulip_accounts_memory.repository';
import { ZulipAccountsService } from './zulip_accounts.service';
import { ZulipAccountsMemoryService } from './zulip_accounts_memory.service';
import { AppLoggerService } from '../../utils/logger/logger.service';
import { REQUIRED_DB_ENV_VARS } from './zulip_accounts.constants';
/**
* 检查数据库配置是否完整
*
* 业务逻辑:
* 1. 遍历所有必需的数据库环境变量名称
* 2. 检查每个环境变量是否在process.env中存在且有值
* 3. 只有当所有必需变量都存在时才返回true
* 4. 用于决定使用数据库模式还是内存模式
*
* @returns 是否配置了完整的数据库连接信息
*
* @example
* // 检查数据库配置
* if (isDatabaseConfigured()) {
* console.log('使用数据库模式');
* } else {
* console.log('使用内存模式');
* }
*/
function isDatabaseConfigured(): boolean {
return REQUIRED_DB_ENV_VARS.every(varName => process.env[varName]);
}
@Global()
@Module({})
export class ZulipAccountsModule {
/**
* 创建数据库模式的Zulip账号模块
*
* 业务逻辑:
* 1. 导入TypeORM模块并注册ZulipAccounts实体
* 2. 集成缓存模块提供数据缓存能力
* 3. 注册数据库版本的Repository和Service实现
* 4. 配置依赖注入的提供者和别名映射
* 5. 导出服务接口供其他模块使用
* 6. 确保TypeORM功能的完整集成和事务支持
* 7. 集成AppLoggerService提供结构化日志
*
* @returns 配置了TypeORM和缓存的动态模块包含数据库访问功能
*
* @example
* // 在应用模块中使用数据库模式
* @Module({
* imports: [ZulipAccountsModule.forDatabase()],
* })
* export class AppModule {}
*/
static forDatabase(): DynamicModule {
return {
module: ZulipAccountsModule,
imports: [
TypeOrmModule.forFeature([ZulipAccounts]),
CacheModule.register({
ttl: 300, // 5分钟默认TTL
max: 1000, // 最大缓存项数
}),
],
providers: [
ZulipAccountsRepository,
AppLoggerService,
{
provide: 'ZulipAccountsService',
useClass: ZulipAccountsService,
},
],
exports: [
ZulipAccountsRepository,
'ZulipAccountsService',
TypeOrmModule,
AppLoggerService,
],
};
}
/**
* 创建内存模式的Zulip账号模块
*
* 业务逻辑:
* 1. 注册内存版本的Repository和Service实现
* 2. 集成基础缓存模块(内存模式也可以使用缓存)
* 3. 配置依赖注入的提供者,使用内存存储类
* 4. 不依赖TypeORM和数据库连接
* 5. 适用于开发、测试和演示环境
* 6. 提供与数据库模式相同的接口和功能
* 7. 集成AppLoggerService提供结构化日志
*
* @returns 配置了内存存储和缓存的动态模块,无需数据库连接
*
* @example
* // 在测试环境中使用内存模式
* @Module({
* imports: [ZulipAccountsModule.forMemory()],
* })
* export class TestModule {}
*/
static forMemory(): DynamicModule {
return {
module: ZulipAccountsModule,
imports: [
CacheModule.register({
ttl: 300, // 5分钟默认TTL
max: 500, // 内存模式使用较小的缓存
}),
],
providers: [
AppLoggerService,
{
provide: 'ZulipAccountsRepository',
useClass: ZulipAccountsMemoryRepository,
},
{
provide: 'ZulipAccountsService',
useClass: ZulipAccountsMemoryService,
},
],
exports: [
'ZulipAccountsRepository',
'ZulipAccountsService',
AppLoggerService,
],
};
}
/**
* 根据环境自动选择模式
*
* 业务逻辑:
* 1. 调用isDatabaseConfigured()检查数据库配置完整性
* 2. 如果数据库配置完整,返回数据库模式的动态模块
* 3. 如果数据库配置不完整,返回内存模式的动态模块
* 4. 实现环境自适应,简化模块配置和部署流程
* 5. 确保应用在不同环境下都能正常启动和运行
*
* @returns 根据环境配置自动选择的动态模块
*
* @example
* // 在主模块中使用自动模式选择
* @Module({
* imports: [ZulipAccountsModule.forRoot()],
* })
* export class AppModule {}
*/
static forRoot(): DynamicModule {
return isDatabaseConfigured()
? ZulipAccountsModule.forDatabase()
: ZulipAccountsModule.forMemory();
}
}

View File

@@ -0,0 +1,429 @@
/**
* Zulip账号关联性能监控工具
*
* 功能描述:
* - 提供性能监控和指标收集功能
* - 支持操作耗时统计和性能基准对比
* - 集成告警机制和性能阈值监控
* - 提供性能报告和分析工具
*
* 职责分离:
* - 性能监控:记录和统计各种操作的性能指标
* - 阈值管理:定义和管理性能阈值和告警规则
* - 指标收集:收集和聚合性能数据
* - 报告生成:生成性能报告和分析结果
*
* 最近修改:
* - 2026-01-12: 初始创建 - 实现性能监控和指标收集功能
*
* @author angjustinl
* @version 1.0.0
* @since 2026-01-12
* @lastModified 2026-01-12
*/
import { AppLoggerService } from '../../utils/logger/logger.service';
/**
* 性能指标接口
*/
export interface PerformanceMetric {
/** 操作名称 */
operation: string;
/** 执行时长(毫秒) */
duration: number;
/** 开始时间 */
startTime: number;
/** 结束时间 */
endTime: number;
/** 是否成功 */
success: boolean;
/** 上下文信息 */
context?: Record<string, any>;
/** 错误信息(如果失败) */
error?: string;
}
/**
* 性能统计信息
*/
export interface PerformanceStats {
/** 操作名称 */
operation: string;
/** 总调用次数 */
totalCalls: number;
/** 成功次数 */
successCalls: number;
/** 失败次数 */
failureCalls: number;
/** 成功率 */
successRate: number;
/** 平均耗时 */
avgDuration: number;
/** 最小耗时 */
minDuration: number;
/** 最大耗时 */
maxDuration: number;
/** P95耗时 */
p95Duration: number;
/** P99耗时 */
p99Duration: number;
/** 最后更新时间 */
lastUpdated: Date;
}
/**
* 性能阈值配置
*/
export const PERFORMANCE_THRESHOLDS = {
// 数据库操作阈值(毫秒)
DATABASE: {
QUERY_SINGLE: 50, // 单条查询
QUERY_BATCH: 200, // 批量查询
INSERT: 100, // 插入操作
UPDATE: 80, // 更新操作
DELETE: 60, // 删除操作
TRANSACTION: 300, // 事务操作
},
// 缓存操作阈值(毫秒)
CACHE: {
GET: 5, // 缓存读取
SET: 10, // 缓存写入
DELETE: 8, // 缓存删除
},
// 业务操作阈值(毫秒)
BUSINESS: {
CREATE_ACCOUNT: 500, // 创建账号
VERIFY_ACCOUNT: 200, // 验证账号
BATCH_UPDATE: 1000, // 批量更新
STATISTICS: 300, // 统计查询
},
// API接口阈值毫秒
API: {
SIMPLE_QUERY: 100, // 简单查询接口
COMPLEX_QUERY: 500, // 复杂查询接口
CREATE_OPERATION: 800, // 创建操作接口
UPDATE_OPERATION: 600, // 更新操作接口
},
} as const;
/**
* 性能监控器类
*/
export class ZulipAccountsPerformanceMonitor {
private static instance: ZulipAccountsPerformanceMonitor;
private metrics: Map<string, PerformanceMetric[]> = new Map();
private stats: Map<string, PerformanceStats> = new Map();
private logger: AppLoggerService;
private constructor(logger: AppLoggerService) {
this.logger = logger;
}
/**
* 获取单例实例
*/
static getInstance(logger: AppLoggerService): ZulipAccountsPerformanceMonitor {
if (!ZulipAccountsPerformanceMonitor.instance) {
ZulipAccountsPerformanceMonitor.instance = new ZulipAccountsPerformanceMonitor(logger);
}
return ZulipAccountsPerformanceMonitor.instance;
}
/**
* 创建性能监控器
*
* @param operation 操作名称
* @param context 上下文信息
* @returns 性能监控器对象
*/
createMonitor(operation: string, context?: Record<string, any>) {
const startTime = Date.now();
return {
/**
* 记录成功完成
*/
success: (additionalContext?: Record<string, any>) => {
const endTime = Date.now();
const duration = endTime - startTime;
const metric: PerformanceMetric = {
operation,
duration,
startTime,
endTime,
success: true,
context: { ...context, ...additionalContext },
};
this.recordMetric(metric);
this.checkThreshold(metric);
},
/**
* 记录失败完成
*/
error: (error: unknown, additionalContext?: Record<string, any>) => {
const endTime = Date.now();
const duration = endTime - startTime;
const errorMessage = error instanceof Error ? error.message : String(error);
const metric: PerformanceMetric = {
operation,
duration,
startTime,
endTime,
success: false,
context: { ...context, ...additionalContext },
error: errorMessage,
};
this.recordMetric(metric);
this.checkThreshold(metric);
},
};
}
/**
* 记录性能指标
*
* @param metric 性能指标
* @private
*/
private recordMetric(metric: PerformanceMetric): void {
// 存储原始指标
if (!this.metrics.has(metric.operation)) {
this.metrics.set(metric.operation, []);
}
const operationMetrics = this.metrics.get(metric.operation)!;
operationMetrics.push(metric);
// 保持最近1000条记录
if (operationMetrics.length > 1000) {
operationMetrics.shift();
}
// 更新统计信息
this.updateStats(metric.operation);
// 记录日志
this.logger.debug('性能指标记录', {
module: 'ZulipAccountsPerformanceMonitor',
operation: 'recordMetric',
metric: {
operation: metric.operation,
duration: metric.duration,
success: metric.success,
},
});
}
/**
* 更新统计信息
*
* @param operation 操作名称
* @private
*/
private updateStats(operation: string): void {
const metrics = this.metrics.get(operation) || [];
if (metrics.length === 0) return;
const successMetrics = metrics.filter(m => m.success);
const durations = metrics.map(m => m.duration).sort((a, b) => a - b);
const stats: PerformanceStats = {
operation,
totalCalls: metrics.length,
successCalls: successMetrics.length,
failureCalls: metrics.length - successMetrics.length,
successRate: (successMetrics.length / metrics.length) * 100,
avgDuration: durations.reduce((sum, d) => sum + d, 0) / durations.length,
minDuration: durations[0],
maxDuration: durations[durations.length - 1],
p95Duration: durations[Math.floor(durations.length * 0.95)],
p99Duration: durations[Math.floor(durations.length * 0.99)],
lastUpdated: new Date(),
};
this.stats.set(operation, stats);
}
/**
* 检查性能阈值
*
* @param metric 性能指标
* @private
*/
private checkThreshold(metric: PerformanceMetric): void {
const threshold = this.getThreshold(metric.operation);
if (!threshold) return;
if (metric.duration > threshold) {
this.logger.warn('性能阈值超标', {
module: 'ZulipAccountsPerformanceMonitor',
operation: 'checkThreshold',
metric: {
operation: metric.operation,
duration: metric.duration,
threshold,
exceeded: metric.duration - threshold,
},
context: metric.context,
});
}
}
/**
* 获取操作的性能阈值
*
* @param operation 操作名称
* @returns 阈值毫秒或null
* @private
*/
private getThreshold(operation: string): number | null {
// 根据操作名称匹配阈值
if (operation.includes('query') || operation.includes('find')) {
if (operation.includes('batch') || operation.includes('many')) {
return PERFORMANCE_THRESHOLDS.DATABASE.QUERY_BATCH;
}
return PERFORMANCE_THRESHOLDS.DATABASE.QUERY_SINGLE;
}
if (operation.includes('create')) {
return PERFORMANCE_THRESHOLDS.DATABASE.INSERT;
}
if (operation.includes('update')) {
return PERFORMANCE_THRESHOLDS.DATABASE.UPDATE;
}
if (operation.includes('delete')) {
return PERFORMANCE_THRESHOLDS.DATABASE.DELETE;
}
if (operation.includes('transaction')) {
return PERFORMANCE_THRESHOLDS.DATABASE.TRANSACTION;
}
if (operation.includes('cache')) {
return PERFORMANCE_THRESHOLDS.CACHE.GET;
}
if (operation.includes('statistics')) {
return PERFORMANCE_THRESHOLDS.BUSINESS.STATISTICS;
}
// 默认阈值
return 1000;
}
/**
* 获取操作的统计信息
*
* @param operation 操作名称
* @returns 统计信息或null
*/
getStats(operation: string): PerformanceStats | null {
return this.stats.get(operation) || null;
}
/**
* 获取所有统计信息
*
* @returns 所有统计信息
*/
getAllStats(): PerformanceStats[] {
return Array.from(this.stats.values());
}
/**
* 获取性能报告
*
* @returns 性能报告
*/
getPerformanceReport(): {
summary: {
totalOperations: number;
avgSuccessRate: number;
slowestOperations: Array<{ operation: string; avgDuration: number }>;
};
details: PerformanceStats[];
} {
const allStats = this.getAllStats();
const summary = {
totalOperations: allStats.length,
avgSuccessRate: allStats.reduce((sum, s) => sum + s.successRate, 0) / allStats.length || 0,
slowestOperations: allStats
.sort((a, b) => b.avgDuration - a.avgDuration)
.slice(0, 5)
.map(s => ({ operation: s.operation, avgDuration: s.avgDuration })),
};
return {
summary,
details: allStats,
};
}
/**
* 清除历史数据
*
* @param operation 操作名称(可选,不提供则清除所有)
*/
clearHistory(operation?: string): void {
if (operation) {
this.metrics.delete(operation);
this.stats.delete(operation);
} else {
this.metrics.clear();
this.stats.clear();
}
this.logger.info('性能监控历史数据已清除', {
module: 'ZulipAccountsPerformanceMonitor',
operation: 'clearHistory',
clearedOperation: operation || 'all',
});
}
}
/**
* 性能监控装饰器
*
* @param operation 操作名称
* @returns 方法装饰器
*/
export function PerformanceMonitor(operation: string) {
return function (_target: any, propertyName: string, descriptor: PropertyDescriptor) {
const method = descriptor.value;
descriptor.value = async function (...args: any[]) {
const logger = (this as any).logger as AppLoggerService;
if (!logger) {
// 如果没有logger直接执行原方法
return method.apply(this, args);
}
const monitor = ZulipAccountsPerformanceMonitor
.getInstance(logger)
.createMonitor(operation, { method: propertyName });
try {
const result = await method.apply(this, args);
monitor.success();
return result;
} catch (error) {
monitor.error(error);
throw error;
}
};
return descriptor;
};
}

View File

@@ -0,0 +1,623 @@
/**
* Zulip账号关联数据访问层
*
* 功能描述:
* - 提供Zulip账号关联数据的CRUD操作
* - 封装复杂查询逻辑和数据库交互
* - 实现数据访问层的业务逻辑抽象
* - 支持事务操作确保数据一致性
* - 优化查询性能和批量操作效率
* - 集成AppLoggerService提供结构化日志
*
* 职责分离:
* - 数据访问:负责所有数据库操作和查询
* - 事务管理:处理需要原子性的复合操作
* - 查询优化:提供高效的数据库查询方法
* - 性能监控:记录查询耗时和性能指标
* - 并发控制:使用悲观锁防止竞态条件
*
* 最近修改:
* - 2026-01-15: 代码规范优化 - 清理未使用的导入FindOptionsWhere (修改者: moyin)
* - 2026-01-12: 性能优化 - 集成AppLoggerService优化查询和批量操作
* - 2026-01-07: 代码规范优化 - 使用统一的常量文件,提高代码质量
* - 2026-01-07: 代码规范优化 - 完善文件头注释和方法三级注释
* - 2026-01-07: 功能新增 - 添加事务支持防止并发竞态条件
*
* @author angjustinl
* @version 1.2.1
* @since 2025-01-05
* @lastModified 2026-01-15
*/
import { Injectable, Inject } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource, SelectQueryBuilder } from 'typeorm';
import { ZulipAccounts } from './zulip_accounts.entity';
import { AppLoggerService } from '../../utils/logger/logger.service';
import {
DEFAULT_VERIFICATION_INTERVAL,
DEFAULT_MAX_RETRY_COUNT,
VERIFICATION_QUERY_LIMIT,
ERROR_ACCOUNTS_QUERY_LIMIT,
} from './zulip_accounts.constants';
import {
CreateZulipAccountData,
UpdateZulipAccountData,
ZulipAccountQueryOptions,
StatusStatistics,
IZulipAccountsRepository,
} from './zulip_accounts.types';
// 保持向后兼容的类型别名
export type CreateZulipAccountDto = CreateZulipAccountData;
export type UpdateZulipAccountDto = UpdateZulipAccountData;
export { ZulipAccountQueryOptions };
@Injectable()
export class ZulipAccountsRepository implements IZulipAccountsRepository {
private readonly logger: AppLoggerService;
constructor(
@InjectRepository(ZulipAccounts)
private readonly repository: Repository<ZulipAccounts>,
private readonly dataSource: DataSource,
@Inject(AppLoggerService) logger: AppLoggerService,
) {
this.logger = logger;
this.logger.info('ZulipAccountsRepository初始化完成', {
module: 'ZulipAccountsRepository',
operation: 'constructor'
});
}
/**
* 创建新的Zulip账号关联带事务支持和性能监控
*
* 业务逻辑:
* 1. 开启数据库事务确保原子性
* 2. 使用悲观锁检查游戏用户ID是否已存在关联
* 3. 检查Zulip用户ID是否已被使用
* 4. 检查Zulip邮箱是否已被使用
* 5. 创建新的关联记录并保存
* 6. 记录操作日志和性能指标
* 7. 提交事务或回滚
*
* @param createDto 创建数据
* @returns Promise<ZulipAccounts> 创建的关联记录
* @throws Error 当唯一性约束冲突时
*
* @example
* ```typescript
* const account = await repository.create({
* gameUserId: BigInt(12345),
* zulipUserId: 67890,
* zulipEmail: 'user@example.com',
* zulipFullName: '用户名',
* zulipApiKeyEncrypted: 'encrypted_key'
* });
* ```
*/
async create(createDto: CreateZulipAccountDto): Promise<ZulipAccounts> {
const startTime = Date.now();
this.logger.info('开始创建Zulip账号关联', {
module: 'ZulipAccountsRepository',
operation: 'create',
gameUserId: createDto.gameUserId.toString(),
zulipUserId: createDto.zulipUserId,
zulipEmail: createDto.zulipEmail
});
return await this.dataSource.transaction(async manager => {
try {
// 使用悲观锁在事务中检查唯一性约束
const existingByGameUser = await manager
.createQueryBuilder(ZulipAccounts, 'za')
.where('za.gameUserId = :gameUserId', { gameUserId: createDto.gameUserId })
.setLock('pessimistic_write')
.getOne();
if (existingByGameUser) {
throw new Error(`Game user ${createDto.gameUserId} already has a Zulip account`);
}
const existingByZulipUser = await manager
.createQueryBuilder(ZulipAccounts, 'za')
.where('za.zulipUserId = :zulipUserId', { zulipUserId: createDto.zulipUserId })
.setLock('pessimistic_write')
.getOne();
if (existingByZulipUser) {
throw new Error(`Zulip user ${createDto.zulipUserId} is already linked`);
}
const existingByEmail = await manager
.createQueryBuilder(ZulipAccounts, 'za')
.where('za.zulipEmail = :zulipEmail', { zulipEmail: createDto.zulipEmail })
.setLock('pessimistic_write')
.getOne();
if (existingByEmail) {
throw new Error(`Zulip email ${createDto.zulipEmail} is already linked`);
}
// 创建实体
const zulipAccount = manager.create(ZulipAccounts, createDto);
const result = await manager.save(zulipAccount);
const duration = Date.now() - startTime;
this.logger.info('创建Zulip账号关联成功', {
module: 'ZulipAccountsRepository',
operation: 'create',
gameUserId: createDto.gameUserId.toString(),
accountId: result.id.toString(),
duration
});
return result;
} catch (error) {
const duration = Date.now() - startTime;
this.logger.error('创建Zulip账号关联失败', {
module: 'ZulipAccountsRepository',
operation: 'create',
gameUserId: createDto.gameUserId.toString(),
error: error instanceof Error ? error.message : String(error),
duration
}, error instanceof Error ? error.stack : undefined);
throw error;
}
});
}
/**
* 根据游戏用户ID查找Zulip账号关联
*
* 业务逻辑:
* 1. 根据includeGameUser参数决定是否加载关联的游戏用户信息
* 2. 构建查询条件使用gameUserId作为查询键
* 3. 执行数据库查询返回匹配的记录或null
* 4. 如果需要关联信息通过relations参数加载
*
* @param gameUserId 游戏用户IDBigInt类型
* @param includeGameUser 是否包含游戏用户信息默认false
* @returns Promise<ZulipAccounts | null> 关联记录或null
*
* @example
* ```typescript
* const account = await repository.findByGameUserId(BigInt(12345), true);
* if (account) {
* console.log('用户邮箱:', account.zulipEmail);
* console.log('游戏用户:', account.gameUser?.username);
* }
* ```
*/
async findByGameUserId(gameUserId: bigint, includeGameUser: boolean = false): Promise<ZulipAccounts | null> {
const relations = includeGameUser ? ['gameUser'] : [];
return await this.repository.findOne({
where: { gameUserId },
relations,
});
}
/**
* 根据Zulip用户ID查找账号关联
*
* 业务逻辑:
* 1. 根据includeGameUser参数决定是否加载关联的游戏用户信息
* 2. 构建查询条件使用zulipUserId作为查询键
* 3. 执行数据库查询返回匹配的记录或null
* 4. 如果需要关联信息通过relations参数加载
*
* @param zulipUserId Zulip用户ID数字类型
* @param includeGameUser 是否包含游戏用户信息默认false
* @returns Promise<ZulipAccounts | null> 关联记录或null
*
* @example
* ```typescript
* const account = await repository.findByZulipUserId(67890, false);
* if (account) {
* console.log('关联的游戏用户ID:', account.gameUserId.toString());
* }
* ```
*/
async findByZulipUserId(zulipUserId: number, includeGameUser: boolean = false): Promise<ZulipAccounts | null> {
const relations = includeGameUser ? ['gameUser'] : [];
return await this.repository.findOne({
where: { zulipUserId },
relations,
});
}
/**
* 根据Zulip邮箱查找账号关联
*
* @param zulipEmail Zulip邮箱
* @param includeGameUser 是否包含游戏用户信息
* @returns Promise<ZulipAccounts | null> 关联记录或null
*/
async findByZulipEmail(zulipEmail: string, includeGameUser: boolean = false): Promise<ZulipAccounts | null> {
const relations = includeGameUser ? ['gameUser'] : [];
return await this.repository.findOne({
where: { zulipEmail },
relations,
});
}
/**
* 根据ID查找Zulip账号关联
*
* @param id 关联记录ID
* @param includeGameUser 是否包含游戏用户信息
* @returns Promise<ZulipAccounts | null> 关联记录或null
*/
async findById(id: bigint, includeGameUser: boolean = false): Promise<ZulipAccounts | null> {
const relations = includeGameUser ? ['gameUser'] : [];
return await this.repository.findOne({
where: { id },
relations,
});
}
/**
* 更新Zulip账号关联
*
* @param id 关联记录ID
* @param updateDto 更新数据
* @returns Promise<ZulipAccounts | null> 更新后的记录或null
*/
async update(id: bigint, updateDto: UpdateZulipAccountDto): Promise<ZulipAccounts | null> {
const result = await this.repository.update({ id }, updateDto);
if (result.affected === 0) {
return null;
}
return await this.findById(id);
}
/**
* 根据游戏用户ID更新Zulip账号关联
*
* @param gameUserId 游戏用户ID
* @param updateDto 更新数据
* @returns Promise<ZulipAccounts | null> 更新后的记录或null
*/
async updateByGameUserId(gameUserId: bigint, updateDto: UpdateZulipAccountDto): Promise<ZulipAccounts | null> {
const result = await this.repository.update({ gameUserId }, updateDto);
if (result.affected === 0) {
return null;
}
return await this.findByGameUserId(gameUserId);
}
/**
* 删除Zulip账号关联
*
* @param id 关联记录ID
* @returns Promise<boolean> 是否删除成功
*/
async delete(id: bigint): Promise<boolean> {
const result = await this.repository.delete({ id });
return result.affected > 0;
}
/**
* 根据游戏用户ID删除Zulip账号关联
*
* @param gameUserId 游戏用户ID
* @returns Promise<boolean> 是否删除成功
*/
async deleteByGameUserId(gameUserId: bigint): Promise<boolean> {
const result = await this.repository.delete({ gameUserId });
return result.affected > 0;
}
/**
* 查询多个Zulip账号关联优化版本
*
* 业务逻辑:
* 1. 构建基础查询构建器
* 2. 根据查询选项动态添加WHERE条件
* 3. 支持关联查询和分页
* 4. 使用索引优化查询性能
* 5. 记录查询日志和性能指标
*
* @param options 查询选项
* @returns Promise<ZulipAccounts[]> 关联记录列表
*/
async findMany(options: ZulipAccountQueryOptions = {}): Promise<ZulipAccounts[]> {
const startTime = Date.now();
this.logger.debug('开始查询多个Zulip账号关联', {
module: 'ZulipAccountsRepository',
operation: 'findMany',
options
});
try {
const queryBuilder = this.createBaseQueryBuilder('za');
// 动态添加WHERE条件
this.applyQueryConditions(queryBuilder, options);
// 处理关联查询
if (options.includeGameUser) {
queryBuilder.leftJoinAndSelect('za.gameUser', 'user');
}
// 添加排序和分页
queryBuilder
.orderBy('za.createdAt', 'DESC')
.addOrderBy('za.id', 'DESC'); // 添加第二排序字段确保结果稳定
// 如果有分页需求,可以在这里添加
// if (options.limit) queryBuilder.limit(options.limit);
// if (options.offset) queryBuilder.offset(options.offset);
const results = await queryBuilder.getMany();
const duration = Date.now() - startTime;
this.logger.debug('查询多个Zulip账号关联完成', {
module: 'ZulipAccountsRepository',
operation: 'findMany',
resultCount: results.length,
duration
});
return results;
} catch (error) {
const duration = Date.now() - startTime;
this.logger.error('查询多个Zulip账号关联失败', {
module: 'ZulipAccountsRepository',
operation: 'findMany',
options,
error: error instanceof Error ? error.message : String(error),
duration
}, error instanceof Error ? error.stack : undefined);
throw error;
}
}
/**
* 获取需要验证的账号列表(优化查询)
*
* 业务逻辑:
* 1. 计算验证截止时间(当前时间减去最大验证间隔)
* 2. 查询状态为active的账号
* 3. 筛选从未验证或验证时间超期的账号
* 4. 按验证时间升序排序NULL值优先
* 5. 限制查询数量避免性能问题
*
* @param maxAge 最大验证间隔毫秒默认24小时
* @returns Promise<ZulipAccounts[]> 需要验证的账号列表
*
* @example
* ```typescript
* const accounts = await repository.findAccountsNeedingVerification();
* console.log(`需要验证的账号数量: ${accounts.length}`);
* ```
*/
async findAccountsNeedingVerification(maxAge: number = DEFAULT_VERIFICATION_INTERVAL): Promise<ZulipAccounts[]> {
const cutoffTime = new Date(Date.now() - maxAge);
return await this.repository
.createQueryBuilder('za')
.where('za.status = :status', { status: 'active' })
.andWhere(
'(za.last_verified_at IS NULL OR za.last_verified_at < :cutoffTime)',
{ cutoffTime }
)
.orderBy('za.last_verified_at', 'ASC', 'NULLS FIRST')
.limit(VERIFICATION_QUERY_LIMIT) // 限制查询数量,避免性能问题
.getMany();
}
/**
* 获取错误状态的账号列表(可重试的)
*
* 业务逻辑:
* 1. 查询状态为error的账号
* 2. 筛选重试次数小于最大重试次数的账号
* 3. 按更新时间升序排序,优先处理较早的错误
* 4. 限制查询数量避免性能问题
*
* @param maxRetryCount 最大重试次数默认3次
* @returns Promise<ZulipAccounts[]> 错误状态的账号列表
*
* @example
* ```typescript
* const errorAccounts = await repository.findErrorAccounts(5);
* console.log(`可重试的错误账号: ${errorAccounts.length}`);
* ```
*/
async findErrorAccounts(maxRetryCount: number = DEFAULT_MAX_RETRY_COUNT): Promise<ZulipAccounts[]> {
return await this.repository
.createQueryBuilder('za')
.where('za.status = :status', { status: 'error' })
.andWhere('za.retry_count < :maxRetryCount', { maxRetryCount })
.orderBy('za.updated_at', 'ASC')
.limit(ERROR_ACCOUNTS_QUERY_LIMIT) // 限制查询数量
.getMany();
}
/**
* 批量更新账号状态
*
* @param ids 账号ID列表
* @param status 新状态
* @returns Promise<number> 更新的记录数
*/
async batchUpdateStatus(ids: bigint[], status: 'active' | 'inactive' | 'suspended' | 'error'): Promise<number> {
const result = await this.repository
.createQueryBuilder()
.update(ZulipAccounts)
.set({ status })
.whereInIds(ids)
.execute();
return result.affected || 0;
}
/**
* 统计各状态的账号数量(优化查询)
*
* @returns Promise<StatusStatistics> 状态统计
*/
async getStatusStatistics(): Promise<StatusStatistics> {
const result = await this.repository
.createQueryBuilder('za')
.select('za.status', 'status')
.addSelect('COUNT(*)', 'count')
.groupBy('za.status')
.getRawMany();
const statistics: StatusStatistics = {
active: 0,
inactive: 0,
suspended: 0,
error: 0,
};
result.forEach(row => {
statistics[row.status] = parseInt(row.count, 10);
});
return statistics;
}
/**
* 检查邮箱是否已存在
*
* @param zulipEmail Zulip邮箱
* @param excludeId 排除的记录ID用于更新时检查
* @returns Promise<boolean> 是否已存在
*/
async existsByEmail(zulipEmail: string, excludeId?: bigint): Promise<boolean> {
const queryBuilder = this.repository
.createQueryBuilder('za')
.where('za.zulip_email = :zulipEmail', { zulipEmail });
if (excludeId) {
queryBuilder.andWhere('za.id != :excludeId', { excludeId });
}
const count = await queryBuilder.getCount();
return count > 0;
}
/**
* 检查Zulip用户ID是否已存在
*
* @param zulipUserId Zulip用户ID
* @param excludeId 排除的记录ID用于更新时检查
* @returns Promise<boolean> 是否已存在
*/
async existsByZulipUserId(zulipUserId: number, excludeId?: bigint): Promise<boolean> {
const queryBuilder = this.repository
.createQueryBuilder('za')
.where('za.zulip_user_id = :zulipUserId', { zulipUserId });
if (excludeId) {
queryBuilder.andWhere('za.id != :excludeId', { excludeId });
}
const count = await queryBuilder.getCount();
return count > 0;
}
/**
* 检查游戏用户ID是否已存在
*
* @param gameUserId 游戏用户ID
* @param excludeId 排除的记录ID用于更新时检查
* @returns Promise<boolean> 是否已存在
*/
async existsByGameUserId(gameUserId: bigint, excludeId?: bigint): Promise<boolean> {
const queryBuilder = this.repository
.createQueryBuilder('za')
.where('za.game_user_id = :gameUserId', { gameUserId });
if (excludeId) {
queryBuilder.andWhere('za.id != :excludeId', { excludeId });
}
const count = await queryBuilder.getCount();
return count > 0;
}
// ========== 辅助方法 ==========
/**
* 创建基础查询构建器
*
* @param alias 表别名
* @returns SelectQueryBuilder<ZulipAccounts>
* @private
*/
private createBaseQueryBuilder(alias: string = 'za'): SelectQueryBuilder<ZulipAccounts> {
return this.repository.createQueryBuilder(alias);
}
/**
* 应用查询条件
*
* @param queryBuilder 查询构建器
* @param options 查询选项
* @private
*/
private applyQueryConditions(
queryBuilder: SelectQueryBuilder<ZulipAccounts>,
options: ZulipAccountQueryOptions
): void {
if (options.gameUserId) {
queryBuilder.andWhere('za.gameUserId = :gameUserId', { gameUserId: options.gameUserId });
}
if (options.zulipUserId) {
queryBuilder.andWhere('za.zulipUserId = :zulipUserId', { zulipUserId: options.zulipUserId });
}
if (options.zulipEmail) {
queryBuilder.andWhere('za.zulipEmail = :zulipEmail', { zulipEmail: options.zulipEmail });
}
if (options.status) {
queryBuilder.andWhere('za.status = :status', { status: options.status });
}
}
/**
* 记录查询性能指标
*
* @param operation 操作名称
* @param startTime 开始时间
* @param resultCount 结果数量
* @private
*/
private logQueryPerformance(operation: string, startTime: number, resultCount?: number): void {
const duration = Date.now() - startTime;
this.logger.debug('查询性能指标', {
module: 'ZulipAccountsRepository',
operation,
duration,
resultCount,
timestamp: new Date().toISOString()
});
// 如果查询时间超过阈值,记录警告
if (duration > 1000) { // 1秒阈值
this.logger.warn('查询耗时过长', {
module: 'ZulipAccountsRepository',
operation,
duration,
resultCount,
threshold: 1000
});
}
}
}

View File

@@ -0,0 +1,862 @@
/**
* Zulip账号关联服务数据库版本
*
* 功能描述:
* - 提供Zulip账号关联的数据访问服务
* - 封装Repository层的数据操作
* - 提供基础的CRUD操作接口
* - 支持缓存机制提升查询性能
*
* 职责分离:
* - 数据访问封装Repository层的数据操作
* - 缓存管理:管理数据缓存策略
* - DTO转换实体对象与响应DTO之间的转换
* - 日志记录:记录数据访问操作日志
*
* 注意:业务逻辑已转移到 src/core/zulip_core/services/zulip_accounts_business.service.ts
*
* 最近修改:
* - 2026-01-15: 代码规范优化 - 清理未使用的导入NotFoundException (修改者: moyin)
* - 2026-01-12: 代码规范优化 - 修复依赖注入配置,添加@Inject装饰器确保正确的参数注入 (修改者: moyin)
* - 2026-01-12: 功能修改 - 优化create方法错误处理正确转换重复创建错误为ConflictException (修改者: moyin)
* - 2026-01-12: 架构优化 - 移除业务逻辑转移到zulip_core业务服务 (修改者: moyin)
* - 2026-01-12: 代码质量优化 - 清理重复导入,统一使用@Inject装饰器 (修改者: moyin)
*
* @author angjustinl
* @version 2.1.1
* @since 2025-01-07
* @lastModified 2026-01-15
*/
import { Injectable, Inject, ConflictException } from '@nestjs/common';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { BaseZulipAccountsService } from './base_zulip_accounts.service';
import { ZulipAccountsRepository } from './zulip_accounts.repository';
import { ZulipAccounts } from './zulip_accounts.entity';
import { AppLoggerService } from '../../utils/logger/logger.service';
import {
DEFAULT_VERIFICATION_MAX_AGE,
DEFAULT_MAX_RETRY_COUNT,
} from './zulip_accounts.constants';
import {
CreateZulipAccountDto,
UpdateZulipAccountDto,
QueryZulipAccountDto,
ZulipAccountResponseDto,
ZulipAccountListResponseDto,
ZulipAccountStatsResponseDto,
BatchUpdateResponseDto,
VerifyAccountResponseDto,
} from './zulip_accounts.dto';
@Injectable()
export class ZulipAccountsService extends BaseZulipAccountsService {
// 缓存键前缀
private static readonly CACHE_PREFIX = 'zulip_accounts';
private static readonly CACHE_TTL = 300; // 5分钟缓存
private static readonly STATS_CACHE_TTL = 60; // 统计数据1分钟缓存
constructor(
@Inject(ZulipAccountsRepository) private readonly repository: ZulipAccountsRepository,
@Inject(AppLoggerService) logger: AppLoggerService,
@Inject(CACHE_MANAGER) private readonly cacheManager: any,
) {
super(logger, 'ZulipAccountsService');
this.logger.info('ZulipAccountsService初始化完成', {
module: 'ZulipAccountsService',
operation: 'constructor',
cacheEnabled: !!this.cacheManager
});
}
/**
* 创建Zulip账号关联
*
* 数据访问逻辑:
* 1. 接收创建请求数据
* 2. 将字符串类型的gameUserId转换为BigInt类型
* 3. 调用Repository层创建账号关联记录
* 4. 清除相关缓存确保数据一致性
* 5. 将实体对象转换为响应DTO返回
*
* @param createDto 创建数据包含游戏用户ID、Zulip用户信息等
* @returns Promise<ZulipAccountResponseDto> 创建的关联记录DTO
* @throws 数据访问异常
*/
async create(createDto: CreateZulipAccountDto): Promise<ZulipAccountResponseDto> {
const monitor = this.createPerformanceMonitor('创建Zulip账号关联', {
gameUserId: createDto.gameUserId
});
try {
const account = await this.repository.create({
gameUserId: this.parseGameUserId(createDto.gameUserId),
zulipUserId: createDto.zulipUserId,
zulipEmail: createDto.zulipEmail,
zulipFullName: createDto.zulipFullName,
zulipApiKeyEncrypted: createDto.zulipApiKeyEncrypted,
status: createDto.status || 'active',
});
// 清除相关缓存
await this.clearRelatedCache(createDto.gameUserId, createDto.zulipUserId, createDto.zulipEmail);
const result = this.toResponseDto(account);
monitor.success({
accountId: account.id.toString(),
status: account.status
});
return result;
} catch (error) {
// 检查是否是重复创建错误转换为ConflictException
const errorMessage = this.formatError(error);
if (errorMessage.includes('already has a Zulip account') ||
errorMessage.includes('duplicate') ||
errorMessage.includes('unique constraint')) {
const conflictError = new ConflictException(`游戏用户 ${createDto.gameUserId} 已存在Zulip账号关联`);
monitor.error(conflictError);
throw conflictError;
} else {
monitor.error(error);
throw error;
}
}
}
/**
* 根据游戏用户ID查找关联带缓存
*
* 数据访问逻辑:
* 1. 构建缓存键并尝试从缓存获取数据
* 2. 如果缓存命中,记录日志并返回缓存数据
* 3. 如果缓存未命中,从数据库查询数据
* 4. 将查询结果存入缓存设置合适的TTL
* 5. 记录查询日志和性能指标
* 6. 将实体对象转换为响应DTO返回
*
* @param gameUserId 游戏用户ID字符串格式
* @param includeGameUser 是否包含游戏用户信息默认false
* @returns Promise<ZulipAccountResponseDto | null> 关联记录DTO或null
* @throws BadRequestException 当查询参数无效或系统异常时
*
* @example
* ```typescript
* const account = await service.findByGameUserId('12345', true);
* if (account) {
* console.log('找到关联:', account.zulipEmail);
* }
* ```
*/
async findByGameUserId(gameUserId: string, includeGameUser: boolean = false): Promise<ZulipAccountResponseDto | null> {
const cacheKey = this.buildCacheKey('game_user', gameUserId, includeGameUser);
try {
// 尝试从缓存获取
const cached = await this.cacheManager.get(cacheKey) as ZulipAccountResponseDto;
if (cached) {
this.logger.debug('缓存命中', {
module: this.moduleName,
operation: 'findByGameUserId',
gameUserId,
cacheKey
});
return cached;
}
// 缓存未命中,从数据库查询
const monitor = this.createPerformanceMonitor('根据游戏用户ID查找关联', { gameUserId });
const account = await this.repository.findByGameUserId(this.parseGameUserId(gameUserId), includeGameUser);
if (!account) {
this.logger.debug('未找到Zulip账号关联', {
module: this.moduleName,
operation: 'findByGameUserId',
gameUserId
});
monitor.success({ found: false });
return null;
}
const result = this.toResponseDto(account);
// 存入缓存
await this.cacheManager.set(cacheKey, result, ZulipAccountsService.CACHE_TTL);
monitor.success({ found: true, cached: true });
return result;
} catch (error) {
this.handleDataAccessError(error, '根据游戏用户ID查找关联', { gameUserId });
}
}
/**
* 根据Zulip用户ID查找关联
*
* 数据访问逻辑:
* 1. 记录查询操作开始日志
* 2. 调用Repository层根据Zulip用户ID查找记录
* 3. 如果未找到记录记录调试日志并返回null
* 4. 如果找到记录,记录成功日志
* 5. 将实体对象转换为响应DTO返回
* 6. 捕获异常并进行统一的错误处理
*
* @param zulipUserId Zulip用户ID数字类型
* @param includeGameUser 是否包含游戏用户信息默认false
* @returns Promise<ZulipAccountResponseDto | null> 关联记录DTO或null
* @throws BadRequestException 当查询参数无效或系统异常时
*
* @example
* ```typescript
* const account = await service.findByZulipUserId(67890);
* if (account) {
* console.log('关联的游戏用户:', account.gameUserId);
* }
* ```
*/
async findByZulipUserId(zulipUserId: number, includeGameUser: boolean = false): Promise<ZulipAccountResponseDto | null> {
this.logStart('根据Zulip用户ID查找关联', { zulipUserId });
try {
const account = await this.repository.findByZulipUserId(zulipUserId, includeGameUser);
if (!account) {
this.logger.debug('未找到Zulip账号关联', { zulipUserId });
return null;
}
this.logSuccess('根据Zulip用户ID查找关联', { zulipUserId, found: true });
return this.toResponseDto(account);
} catch (error) {
this.handleDataAccessError(error, '根据Zulip用户ID查找关联', { zulipUserId });
}
}
/**
* 根据Zulip邮箱查找关联
*
* 数据访问逻辑:
* 1. 记录查询操作开始日志
* 2. 调用Repository层根据Zulip邮箱查找记录
* 3. 如果未找到记录记录调试日志并返回null
* 4. 如果找到记录,记录成功日志
* 5. 将实体对象转换为响应DTO返回
* 6. 捕获异常并进行统一的错误处理
*
* @param zulipEmail Zulip邮箱地址字符串格式
* @param includeGameUser 是否包含游戏用户信息默认false
* @returns Promise<ZulipAccountResponseDto | null> 关联记录DTO或null
* @throws BadRequestException 当查询参数无效或系统异常时
*
* @example
* ```typescript
* const account = await service.findByZulipEmail('user@example.com');
* if (account) {
* console.log('邮箱对应的用户:', account.zulipFullName);
* }
* ```
*/
async findByZulipEmail(zulipEmail: string, includeGameUser: boolean = false): Promise<ZulipAccountResponseDto | null> {
this.logStart('根据Zulip邮箱查找关联', { zulipEmail });
try {
const account = await this.repository.findByZulipEmail(zulipEmail, includeGameUser);
if (!account) {
this.logger.debug('未找到Zulip账号关联', { zulipEmail });
return null;
}
this.logSuccess('根据Zulip邮箱查找关联', { zulipEmail, found: true });
return this.toResponseDto(account);
} catch (error) {
this.handleDataAccessError(error, '根据Zulip邮箱查找关联', { zulipEmail });
}
}
/**
* 根据ID查找关联
*
* 数据访问逻辑:
* 1. 记录查询操作开始日志
* 2. 将字符串类型的ID转换为BigInt类型
* 3. 调用Repository层根据ID查找记录
* 4. 如果未找到记录抛出NotFoundException异常
* 5. 如果找到记录,记录成功日志
* 6. 将实体对象转换为响应DTO返回
* 7. 捕获异常并进行统一的错误处理
*
* @param id 关联记录ID字符串格式
* @param includeGameUser 是否包含游戏用户信息默认false
* @returns Promise<ZulipAccountResponseDto> 关联记录DTO
* @throws NotFoundException 当记录不存在时
* @throws BadRequestException 当查询参数无效或系统异常时
*
* @example
* ```typescript
* const account = await service.findById('123', true);
* console.log('找到记录:', account.zulipEmail);
* ```
*/
async findById(id: string, includeGameUser: boolean = false): Promise<ZulipAccountResponseDto> {
const monitor = this.createPerformanceMonitor('根据ID查找关联', { id });
try {
const account = await this.repository.findById(this.parseId(id), includeGameUser);
const result = account ? this.toResponseDto(account) : null;
monitor.success({ found: !!account });
return result;
} catch (error) {
monitor.error(error);
throw error;
}
}
/**
* 更新Zulip账号关联
*
* 数据访问逻辑:
* 1. 记录更新操作开始时间和日志
* 2. 将字符串类型的ID转换为BigInt类型
* 3. 调用Repository层执行更新操作
* 4. 如果记录不存在抛出NotFoundException异常
* 5. 记录操作成功日志和耗时
* 6. 将更新后的实体转换为响应DTO返回
* 7. 捕获异常并进行统一的错误处理
*
* @param id 关联记录ID字符串格式
* @param updateDto 更新数据,包含需要修改的字段
* @returns Promise<ZulipAccountResponseDto> 更新后的记录DTO
* @throws NotFoundException 当记录不存在时
* @throws BadRequestException 当更新数据无效或系统异常时
*
* @example
* ```typescript
* const updated = await service.update('123', {
* zulipFullName: '新用户名',
* status: 'active'
* });
* ```
*/
async update(id: string, updateDto: UpdateZulipAccountDto): Promise<ZulipAccountResponseDto> {
const monitor = this.createPerformanceMonitor('更新Zulip账号关联', { id });
try {
const account = await this.repository.update(this.parseId(id), updateDto);
const result = account ? this.toResponseDto(account) : null;
monitor.success({ updated: !!account });
return result;
} catch (error) {
monitor.error(error);
throw error;
}
}
/**
* 根据游戏用户ID更新关联
*
* 数据访问逻辑:
* 1. 记录更新操作开始时间和日志
* 2. 将字符串类型的gameUserId转换为BigInt类型
* 3. 调用Repository层根据游戏用户ID执行更新
* 4. 如果记录不存在抛出NotFoundException异常
* 5. 记录操作成功日志和耗时
* 6. 将更新后的实体转换为响应DTO返回
* 7. 捕获异常并进行统一的错误处理
*
* @param gameUserId 游戏用户ID字符串格式
* @param updateDto 更新数据,包含需要修改的字段
* @returns Promise<ZulipAccountResponseDto> 更新后的记录DTO
* @throws NotFoundException 当记录不存在时
* @throws BadRequestException 当更新数据无效或系统异常时
*
* @example
* ```typescript
* const updated = await service.updateByGameUserId('12345', {
* status: 'suspended',
* errorMessage: '账号异常'
* });
* ```
*/
async updateByGameUserId(gameUserId: string, updateDto: UpdateZulipAccountDto): Promise<ZulipAccountResponseDto> {
const monitor = this.createPerformanceMonitor('根据游戏用户ID更新关联', { gameUserId });
try {
const account = await this.repository.updateByGameUserId(this.parseGameUserId(gameUserId), updateDto);
const result = account ? this.toResponseDto(account) : null;
monitor.success({ updated: !!account });
return result;
} catch (error) {
monitor.error(error);
throw error;
}
}
/**
* 删除Zulip账号关联
*
* @param id 关联记录ID
* @returns Promise<boolean> 是否删除成功
*/
async delete(id: string): Promise<boolean> {
const monitor = this.createPerformanceMonitor('删除Zulip账号关联', { id });
try {
const result = await this.repository.delete(this.parseId(id));
monitor.success({ deleted: result });
return result;
} catch (error) {
monitor.error(error);
throw error;
}
}
/**
* 根据游戏用户ID删除关联
*
* @param gameUserId 游戏用户ID
* @returns Promise<boolean> 是否删除成功
*/
async deleteByGameUserId(gameUserId: string): Promise<boolean> {
const monitor = this.createPerformanceMonitor('根据游戏用户ID删除关联', { gameUserId });
try {
const result = await this.repository.deleteByGameUserId(this.parseGameUserId(gameUserId));
monitor.success({ deleted: result });
return result;
} catch (error) {
monitor.error(error);
throw error;
}
}
/**
* 查询多个Zulip账号关联
*
* @param queryDto 查询条件
* @returns Promise<ZulipAccountListResponseDto> 关联记录列表
*/
async findMany(queryDto: QueryZulipAccountDto = {}): Promise<ZulipAccountListResponseDto> {
this.logStart('查询多个Zulip账号关联', queryDto);
try {
const options = {
gameUserId: queryDto.gameUserId ? this.parseGameUserId(queryDto.gameUserId) : undefined,
zulipUserId: queryDto.zulipUserId,
zulipEmail: queryDto.zulipEmail,
status: queryDto.status,
includeGameUser: queryDto.includeGameUser || false,
};
const accounts = await this.repository.findMany(options);
this.logSuccess('查询多个Zulip账号关联', {
count: accounts.length,
conditions: queryDto
});
return this.buildListResponse(accounts);
} catch (error) {
return {
accounts: this.handleSearchError(error, '查询多个Zulip账号关联', queryDto),
total: 0,
count: 0,
};
}
}
/**
* 获取需要验证的账号列表
*
* @param maxAge 最大验证间隔毫秒默认24小时
* @returns Promise<ZulipAccountListResponseDto> 需要验证的账号列表
*/
async findAccountsNeedingVerification(maxAge: number = DEFAULT_VERIFICATION_MAX_AGE): Promise<ZulipAccountListResponseDto> {
this.logStart('获取需要验证的账号列表', { maxAge });
try {
const accounts = await this.repository.findAccountsNeedingVerification(maxAge);
this.logSuccess('获取需要验证的账号列表', { count: accounts.length });
return this.buildListResponse(accounts);
} catch (error) {
return {
accounts: this.handleSearchError(error, '获取需要验证的账号列表', { maxAge }),
total: 0,
count: 0,
};
}
}
/**
* 获取错误状态的账号列表
*
* @param maxRetryCount 最大重试次数默认3次
* @returns Promise<ZulipAccountListResponseDto> 错误状态的账号列表
*/
async findErrorAccounts(maxRetryCount: number = DEFAULT_MAX_RETRY_COUNT): Promise<ZulipAccountListResponseDto> {
this.logStart('获取错误状态的账号列表', { maxRetryCount });
try {
const accounts = await this.repository.findErrorAccounts(maxRetryCount);
this.logSuccess('获取错误状态的账号列表', { count: accounts.length });
return this.buildListResponse(accounts);
} catch (error) {
return {
accounts: this.handleSearchError(error, '获取错误状态的账号列表', { maxRetryCount }),
total: 0,
count: 0,
};
}
}
/**
* 批量更新账号状态
*
* @param ids 账号ID列表
* @param status 新状态
* @returns Promise<BatchUpdateResponseDto> 批量更新结果
*/
async batchUpdateStatus(ids: string[], status: 'active' | 'inactive' | 'suspended' | 'error'): Promise<BatchUpdateResponseDto> {
const monitor = this.createPerformanceMonitor('批量更新账号状态', { count: ids.length, status });
try {
const bigintIds = this.parseIds(ids);
const updatedCount = await this.repository.batchUpdateStatus(bigintIds, status);
monitor.success({
requestCount: ids.length,
updatedCount,
status
});
return {
success: true,
updatedCount,
};
} catch (error) {
this.logger.error('批量更新账号状态失败', {
operation: 'batchUpdateStatus',
error: this.formatError(error),
count: ids.length,
status,
});
return {
success: false,
updatedCount: 0,
error: this.formatError(error),
};
}
}
/**
* 获取账号状态统计(带缓存)
*
* 数据访问逻辑:
* 1. 构建统计数据的缓存键
* 2. 尝试从缓存获取统计数据
* 3. 如果缓存命中,直接返回缓存数据
* 4. 如果缓存未命中,从数据库查询统计数据
* 5. 计算总数并构建完整的统计响应
* 6. 将统计结果存入缓存使用较短的TTL
* 7. 记录操作日志和性能指标
*
* @returns Promise<ZulipAccountStatsResponseDto> 状态统计
*/
async getStatusStatistics(): Promise<ZulipAccountStatsResponseDto> {
const cacheKey = this.buildCacheKey('stats');
try {
// 尝试从缓存获取
const cached = await this.cacheManager.get(cacheKey) as ZulipAccountStatsResponseDto;
if (cached) {
this.logger.debug('统计数据缓存命中', {
module: this.moduleName,
operation: 'getStatusStatistics',
cacheKey
});
return cached;
}
// 缓存未命中,从数据库查询
const monitor = this.createPerformanceMonitor('获取账号状态统计');
const statistics = await this.repository.getStatusStatistics();
const result = {
active: statistics.active || 0,
inactive: statistics.inactive || 0,
suspended: statistics.suspended || 0,
error: statistics.error || 0,
total: (statistics.active || 0) + (statistics.inactive || 0) +
(statistics.suspended || 0) + (statistics.error || 0),
};
// 存入缓存使用较短的TTL
await this.cacheManager.set(cacheKey, result, ZulipAccountsService.STATS_CACHE_TTL);
monitor.success({
total: result.total,
cached: true
});
return result;
} catch (error) {
this.handleDataAccessError(error, '获取账号状态统计');
}
}
/**
* 验证账号有效性
*
* @param gameUserId 游戏用户ID
* @returns Promise<VerifyAccountResponseDto> 验证结果
*/
async verifyAccount(gameUserId: string): Promise<VerifyAccountResponseDto> {
const monitor = this.createPerformanceMonitor('验证账号有效性', { gameUserId });
try {
// 1. 查找账号关联
const account = await this.repository.findByGameUserId(this.parseGameUserId(gameUserId));
if (!account) {
monitor.success({ isValid: false, reason: '账号关联不存在' });
return {
success: false,
isValid: false,
error: '账号关联不存在',
};
}
// 2. 检查账号状态
if (account.status !== 'active') {
monitor.success({ isValid: false, reason: `账号状态为 ${account.status}` });
return {
success: true,
isValid: false,
error: `账号状态为 ${account.status}`,
};
}
// 3. 更新验证时间
await this.repository.updateByGameUserId(this.parseGameUserId(gameUserId), {
lastVerifiedAt: new Date(),
});
monitor.success({ isValid: true });
return {
success: true,
isValid: true,
verifiedAt: new Date().toISOString(),
};
} catch (error) {
this.logger.error('验证账号有效性失败', {
operation: 'verifyAccount',
gameUserId,
error: this.formatError(error),
});
return {
success: false,
isValid: false,
error: this.formatError(error),
};
}
}
/**
* 检查邮箱是否已存在
*
* @param zulipEmail Zulip邮箱
* @param excludeId 排除的记录ID
* @returns Promise<boolean> 是否已存在
*/
async existsByEmail(zulipEmail: string, excludeId?: string): Promise<boolean> {
try {
const excludeBigintId = excludeId ? this.parseId(excludeId) : undefined;
return await this.repository.existsByEmail(zulipEmail, excludeBigintId);
} catch (error) {
this.logger.warn('检查邮箱存在性失败', {
operation: 'existsByEmail',
zulipEmail,
error: this.formatError(error),
});
return false;
}
}
/**
* 检查Zulip用户ID是否已存在
*
* @param zulipUserId Zulip用户ID
* @param excludeId 排除的记录ID
* @returns Promise<boolean> 是否已存在
*/
async existsByZulipUserId(zulipUserId: number, excludeId?: string): Promise<boolean> {
try {
const excludeBigintId = excludeId ? this.parseId(excludeId) : undefined;
return await this.repository.existsByZulipUserId(zulipUserId, excludeBigintId);
} catch (error) {
this.logger.warn('检查Zulip用户ID存在性失败', {
operation: 'existsByZulipUserId',
zulipUserId,
error: this.formatError(error),
});
return false;
}
}
/**
* 将实体转换为响应DTO
*
* @param account 账号关联实体
* @returns ZulipAccountResponseDto 响应DTO
*/
protected toResponseDto(account: ZulipAccounts): ZulipAccountResponseDto {
return {
id: account.id.toString(),
gameUserId: account.gameUserId.toString(),
zulipUserId: account.zulipUserId,
zulipEmail: account.zulipEmail,
zulipFullName: account.zulipFullName,
status: account.status,
lastVerifiedAt: account.lastVerifiedAt?.toISOString(),
lastSyncedAt: account.lastSyncedAt?.toISOString(),
errorMessage: account.errorMessage,
retryCount: account.retryCount,
createdAt: account.createdAt.toISOString(),
updatedAt: account.updatedAt.toISOString(),
gameUser: account.gameUser,
};
}
// ========== 缓存管理方法 ==========
/**
* 构建缓存键
*
* @param type 缓存类型
* @param identifier 标识符
* @param includeGameUser 是否包含游戏用户信息
* @returns 缓存键字符串
* @private
*/
private buildCacheKey(type: string, identifier?: string, includeGameUser?: boolean): string {
const parts = [ZulipAccountsService.CACHE_PREFIX, type];
if (identifier) parts.push(identifier);
if (includeGameUser) parts.push('with_user');
return parts.join(':');
}
/**
* 清除相关缓存
*
* 功能描述:
* 当数据发生变更时,清除相关的缓存项以确保数据一致性
*
* @param gameUserId 游戏用户ID
* @param zulipUserId Zulip用户ID
* @param zulipEmail Zulip邮箱
* @private
*/
private async clearRelatedCache(gameUserId?: string, zulipUserId?: number, zulipEmail?: string): Promise<void> {
const keysToDelete: string[] = [];
// 清除统计缓存
keysToDelete.push(this.buildCacheKey('stats'));
// 清除具体记录的缓存
if (gameUserId) {
keysToDelete.push(this.buildCacheKey('game_user', gameUserId, false));
keysToDelete.push(this.buildCacheKey('game_user', gameUserId, true));
}
if (zulipUserId) {
keysToDelete.push(this.buildCacheKey('zulip_user', zulipUserId.toString(), false));
keysToDelete.push(this.buildCacheKey('zulip_user', zulipUserId.toString(), true));
}
if (zulipEmail) {
keysToDelete.push(this.buildCacheKey('zulip_email', zulipEmail, false));
keysToDelete.push(this.buildCacheKey('zulip_email', zulipEmail, true));
}
// 批量删除缓存
try {
await Promise.all(keysToDelete.map(key => this.cacheManager.del(key)));
this.logger.debug('清除相关缓存', {
module: this.moduleName,
operation: 'clearRelatedCache',
keysCount: keysToDelete.length,
keys: keysToDelete
});
} catch (error) {
this.logger.warn('清除缓存失败', {
module: this.moduleName,
operation: 'clearRelatedCache',
error: this.formatError(error),
keys: keysToDelete
});
}
}
/**
* 清除所有相关缓存
*
* 功能描述:
* 清除所有与Zulip账号相关的缓存通常在批量操作后调用
*
* @returns Promise<void>
*/
async clearAllCache(): Promise<void> {
try {
// 这里可以根据实际的缓存实现来清除所有相关缓存
// 由于cache-manager没有直接的模式匹配删除我们清除已知的缓存类型
const commonKeys = [
this.buildCacheKey('stats'),
// 可以添加更多已知的缓存键模式
];
await Promise.all(commonKeys.map(key => this.cacheManager.del(key)));
this.logger.info('清除所有缓存完成', {
module: this.moduleName,
operation: 'clearAllCache',
keysCount: commonKeys.length
});
} catch (error) {
this.logger.warn('清除所有缓存失败', {
module: this.moduleName,
operation: 'clearAllCache',
error: this.formatError(error)
});
}
}
}

View File

@@ -0,0 +1,98 @@
/**
* Zulip账号关联类型定义
*
* 功能描述:
* - 定义模块中使用的所有类型和接口
* - 提供统一的类型管理和约束
* - 确保类型安全和一致性
* - 便于类型复用和维护
*
* 职责分离:
* - 类型定义:集中管理所有模块类型
* - 接口约束:定义数据结构和方法签名
* - 类型安全:确保编译时类型检查
*
* 最近修改:
* - 2026-01-07: 代码规范优化 - 完善类型定义和接口约束
* - 2026-01-07: 架构优化 - 提取统一的类型定义,改善架构分层
* - 2026-01-07: 初始创建 - 提取和统一类型定义,提高代码质量
*
* @author angjustinl
* @version 1.0.1
* @since 2026-01-07
* @lastModified 2026-01-07
*/
/**
* 账号状态枚举
*/
export type AccountStatus = 'active' | 'inactive' | 'suspended' | 'error';
/**
* 创建Zulip账号关联的数据传输对象
*/
export interface CreateZulipAccountData {
gameUserId: bigint;
zulipUserId: number;
zulipEmail: string;
zulipFullName: string;
zulipApiKeyEncrypted: string;
status?: AccountStatus;
}
/**
* 更新Zulip账号关联的数据传输对象
*/
export interface UpdateZulipAccountData {
zulipFullName?: string;
zulipApiKeyEncrypted?: string;
status?: AccountStatus;
lastVerifiedAt?: Date;
lastSyncedAt?: Date;
errorMessage?: string;
retryCount?: number;
}
/**
* Zulip账号查询选项
*/
export interface ZulipAccountQueryOptions {
gameUserId?: bigint;
zulipUserId?: number;
zulipEmail?: string;
status?: AccountStatus;
includeGameUser?: boolean;
}
/**
* 状态统计结果
*/
export interface StatusStatistics {
active: number;
inactive: number;
suspended: number;
error: number;
}
/**
* Repository接口定义
*/
export interface IZulipAccountsRepository {
create(data: CreateZulipAccountData): Promise<any>;
findByGameUserId(gameUserId: bigint, includeGameUser?: boolean): Promise<any | null>;
findByZulipUserId(zulipUserId: number, includeGameUser?: boolean): Promise<any | null>;
findByZulipEmail(zulipEmail: string, includeGameUser?: boolean): Promise<any | null>;
findById(id: bigint, includeGameUser?: boolean): Promise<any | null>;
update(id: bigint, data: UpdateZulipAccountData): Promise<any | null>;
updateByGameUserId(gameUserId: bigint, data: UpdateZulipAccountData): Promise<any | null>;
delete(id: bigint): Promise<boolean>;
deleteByGameUserId(gameUserId: bigint): Promise<boolean>;
findMany(options?: ZulipAccountQueryOptions): Promise<any[]>;
findAccountsNeedingVerification(maxAge?: number): Promise<any[]>;
findErrorAccounts(maxRetryCount?: number): Promise<any[]>;
batchUpdateStatus(ids: bigint[], status: AccountStatus): Promise<number>;
getStatusStatistics(): Promise<StatusStatistics>;
existsByEmail(email: string, excludeId?: bigint): Promise<boolean>;
existsByZulipUserId(zulipUserId: number, excludeId?: bigint): Promise<boolean>;
existsByGameUserId(gameUserId: bigint, excludeId?: bigint): Promise<boolean>;
}

View File

@@ -0,0 +1,446 @@
/**
* Zulip账号关联内存数据访问层
*
* 功能描述:
* - 提供Zulip账号关联数据的内存存储实现和CRUD操作
* - 用于开发和测试环境,无需数据库连接和配置
* - 实现与数据库版本相同的接口和查询功能
* - 支持数据导入导出、备份恢复和测试数据管理
*
* 职责分离:
* - 数据存储使用Map结构提供高效的内存数据存储
* - 查询实现:实现各种查询条件和过滤逻辑
* - 约束检查:确保数据唯一性和完整性约束
* - 测试支持:提供数据导入导出和清理功能
*
* 最近修改:
* - 2026-01-12: 代码规范优化 - 修复findAccountsNeedingVerification方法的限制逻辑与数据库版本保持一致 (修改者: moyin)
* - 2026-01-07: 代码规范优化 - 使用统一的常量文件,提高代码质量
* - 2026-01-07: 代码规范优化 - 完善文件头注释和方法三级注释
* - 2026-01-07: 功能完善 - 优化查询性能和数据管理功能
* - 2025-01-07: 架构优化 - 统一Repository层的接口设计和实现
* - 2025-01-05: 功能扩展 - 添加批量操作和统计查询功能
*
* @author angjustinl
* @version 1.1.2
* @since 2025-01-05
* @lastModified 2026-01-12
*/
import { Injectable } from '@nestjs/common';
import { ZulipAccounts } from './zulip_accounts.entity';
import {
DEFAULT_VERIFICATION_MAX_AGE,
DEFAULT_MAX_RETRY_COUNT,
DEFAULT_ERROR_ACCOUNTS_LIMIT,
} from './zulip_accounts.constants';
import {
CreateZulipAccountData,
UpdateZulipAccountData,
ZulipAccountQueryOptions,
StatusStatistics,
IZulipAccountsRepository,
} from './zulip_accounts.types';
@Injectable()
export class ZulipAccountsMemoryRepository implements IZulipAccountsRepository {
private accounts: Map<bigint, ZulipAccounts> = new Map();
private currentId: bigint = BigInt(1);
/**
* 创建新的Zulip账号关联带唯一性检查
*
* @param createData 创建数据
* @returns Promise<ZulipAccounts> 创建的关联记录
*/
async create(createData: CreateZulipAccountData): Promise<ZulipAccounts> {
// 检查唯一性约束
const existingByGameUser = await this.findByGameUserId(createData.gameUserId);
if (existingByGameUser) {
throw new Error(`Game user ${createData.gameUserId} already has a Zulip account`);
}
const existingByZulipUser = await this.findByZulipUserId(createData.zulipUserId);
if (existingByZulipUser) {
throw new Error(`Zulip user ${createData.zulipUserId} is already linked`);
}
const existingByEmail = await this.findByZulipEmail(createData.zulipEmail);
if (existingByEmail) {
throw new Error(`Zulip email ${createData.zulipEmail} is already linked`);
}
const account = new ZulipAccounts();
account.id = this.currentId++;
account.gameUserId = createData.gameUserId;
account.zulipUserId = createData.zulipUserId;
account.zulipEmail = createData.zulipEmail;
account.zulipFullName = createData.zulipFullName;
account.zulipApiKeyEncrypted = createData.zulipApiKeyEncrypted;
account.status = createData.status || 'active';
account.lastVerifiedAt = null;
account.lastSyncedAt = null;
account.errorMessage = null;
account.retryCount = 0;
account.createdAt = new Date();
account.updatedAt = new Date();
this.accounts.set(account.id, account);
return account;
}
/**
* 根据游戏用户ID查找Zulip账号关联
*
* @param gameUserId 游戏用户ID
* @param includeGameUser 是否包含游戏用户信息(内存模式忽略)
* @returns Promise<ZulipAccounts | null> 关联记录或null
*/
async findByGameUserId(gameUserId: bigint, includeGameUser: boolean = false): Promise<ZulipAccounts | null> {
for (const account of this.accounts.values()) {
if (account.gameUserId === gameUserId) {
return account;
}
}
return null;
}
/**
* 根据Zulip用户ID查找账号关联
*
* @param zulipUserId Zulip用户ID
* @param includeGameUser 是否包含游戏用户信息(内存模式忽略)
* @returns Promise<ZulipAccounts | null> 关联记录或null
*/
async findByZulipUserId(zulipUserId: number, includeGameUser: boolean = false): Promise<ZulipAccounts | null> {
for (const account of this.accounts.values()) {
if (account.zulipUserId === zulipUserId) {
return account;
}
}
return null;
}
/**
* 根据Zulip邮箱查找账号关联
*
* @param zulipEmail Zulip邮箱
* @param includeGameUser 是否包含游戏用户信息(内存模式忽略)
* @returns Promise<ZulipAccounts | null> 关联记录或null
*/
async findByZulipEmail(zulipEmail: string, includeGameUser: boolean = false): Promise<ZulipAccounts | null> {
for (const account of this.accounts.values()) {
if (account.zulipEmail === zulipEmail) {
return account;
}
}
return null;
}
/**
* 根据ID查找Zulip账号关联
*
* @param id 关联记录ID
* @param includeGameUser 是否包含游戏用户信息(内存模式忽略)
* @returns Promise<ZulipAccounts | null> 关联记录或null
*/
async findById(id: bigint, includeGameUser: boolean = false): Promise<ZulipAccounts | null> {
return this.accounts.get(id) || null;
}
/**
* 更新Zulip账号关联
*
* @param id 关联记录ID
* @param updateData 更新数据
* @returns Promise<ZulipAccounts | null> 更新后的记录或null
*/
async update(id: bigint, updateData: UpdateZulipAccountData): Promise<ZulipAccounts | null> {
const account = this.accounts.get(id);
if (!account) {
return null;
}
Object.assign(account, updateData);
account.updatedAt = new Date();
return account;
}
/**
* 根据游戏用户ID更新Zulip账号关联
*
* @param gameUserId 游戏用户ID
* @param updateData 更新数据
* @returns Promise<ZulipAccounts | null> 更新后的记录或null
*/
async updateByGameUserId(gameUserId: bigint, updateData: UpdateZulipAccountData): Promise<ZulipAccounts | null> {
const account = await this.findByGameUserId(gameUserId);
if (!account) {
return null;
}
Object.assign(account, updateData);
account.updatedAt = new Date();
return account;
}
/**
* 删除Zulip账号关联
*
* @param id 关联记录ID
* @returns Promise<boolean> 是否删除成功
*/
async delete(id: bigint): Promise<boolean> {
return this.accounts.delete(id);
}
/**
* 根据游戏用户ID删除Zulip账号关联
*
* @param gameUserId 游戏用户ID
* @returns Promise<boolean> 是否删除成功
*/
async deleteByGameUserId(gameUserId: bigint): Promise<boolean> {
for (const [id, account] of this.accounts.entries()) {
if (account.gameUserId === gameUserId) {
return this.accounts.delete(id);
}
}
return false;
}
/**
* 查询多个Zulip账号关联
*
* @param options 查询选项
* @returns Promise<ZulipAccounts[]> 关联记录列表
*/
async findMany(options: ZulipAccountQueryOptions = {}): Promise<ZulipAccounts[]> {
let results = Array.from(this.accounts.values());
if (options.gameUserId) {
results = results.filter(a => a.gameUserId === options.gameUserId);
}
if (options.zulipUserId) {
results = results.filter(a => a.zulipUserId === options.zulipUserId);
}
if (options.zulipEmail) {
results = results.filter(a => a.zulipEmail === options.zulipEmail);
}
if (options.status) {
results = results.filter(a => a.status === options.status);
}
// 按创建时间降序排序
results.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
return results;
}
/**
* 获取需要验证的账号列表
*
* 业务逻辑:
* 1. 计算验证截止时间,基于当前时间减去最大验证间隔
* 2. 筛选状态为active且需要验证的账号记录
* 3. 包含从未验证过的账号lastVerifiedAt为null
* 4. 包含验证时间超过最大间隔的账号
* 5. 按验证时间升序排序,优先处理最久未验证的账号
*
* @param maxAge 最大验证间隔毫秒默认24小时
* @returns Promise<ZulipAccounts[]> 需要验证的账号列表,按验证时间升序排序
*
* @example
* // 获取需要验证的账号默认24小时
* const accounts = await repository.findAccountsNeedingVerification();
*
* @example
* // 获取需要验证的账号自定义12小时
* const accounts = await repository.findAccountsNeedingVerification(12 * 60 * 60 * 1000);
*/
async findAccountsNeedingVerification(maxAge: number = DEFAULT_VERIFICATION_MAX_AGE): Promise<ZulipAccounts[]> {
const cutoffTime = new Date(Date.now() - maxAge);
return Array.from(this.accounts.values())
.filter(account =>
account.status === 'active' &&
(!account.lastVerifiedAt || account.lastVerifiedAt < cutoffTime)
)
.sort((a, b) => {
if (!a.lastVerifiedAt) return -1;
if (!b.lastVerifiedAt) return 1;
return a.lastVerifiedAt.getTime() - b.lastVerifiedAt.getTime();
})
.slice(0, 100); // 应用默认限制,与数据库版本保持一致
}
/**
* 获取错误状态的账号列表(可重试的)
*
* 业务逻辑:
* 1. 筛选状态为error的账号记录
* 2. 过滤重试次数小于最大重试次数的账号
* 3. 按更新时间升序排序,优先处理最早出错的账号
* 4. 限制返回数量,避免一次处理过多错误账号
* 5. 为错误恢复和重试机制提供数据支持
*
* @param maxRetryCount 最大重试次数默认3次
* @returns Promise<ZulipAccounts[]> 错误状态的账号列表限制50条记录
*
* @example
* // 获取可重试的错误账号默认3次重试限制
* const errorAccounts = await repository.findErrorAccounts();
*
* @example
* // 获取可重试的错误账号自定义5次重试限制
* const errorAccounts = await repository.findErrorAccounts(5);
*/
async findErrorAccounts(maxRetryCount: number = DEFAULT_MAX_RETRY_COUNT): Promise<ZulipAccounts[]> {
return Array.from(this.accounts.values())
.filter(account => account.status === 'error' && account.retryCount < maxRetryCount)
.sort((a, b) => a.updatedAt.getTime() - b.updatedAt.getTime())
.slice(0, DEFAULT_ERROR_ACCOUNTS_LIMIT); // 限制返回数量
}
/**
* 批量更新账号状态
*
* @param ids 账号ID列表
* @param status 新状态
* @returns Promise<number> 更新的记录数
*/
async batchUpdateStatus(ids: bigint[], status: 'active' | 'inactive' | 'suspended' | 'error'): Promise<number> {
let count = 0;
for (const id of ids) {
const account = this.accounts.get(id);
if (account) {
account.status = status;
account.updatedAt = new Date();
count++;
}
}
return count;
}
/**
* 统计各状态的账号数量
*
* @returns Promise<StatusStatistics> 状态统计
*/
async getStatusStatistics(): Promise<StatusStatistics> {
const statistics: StatusStatistics = {
active: 0,
inactive: 0,
suspended: 0,
error: 0,
};
for (const account of this.accounts.values()) {
const status = account.status;
statistics[status] = (statistics[status] || 0) + 1;
}
return statistics;
}
/**
* 检查邮箱是否已存在
*
* @param zulipEmail Zulip邮箱
* @param excludeId 排除的记录ID用于更新时检查
* @returns Promise<boolean> 是否已存在
*/
async existsByEmail(zulipEmail: string, excludeId?: bigint): Promise<boolean> {
for (const [id, account] of this.accounts.entries()) {
if (account.zulipEmail === zulipEmail && (!excludeId || id !== excludeId)) {
return true;
}
}
return false;
}
/**
* 检查Zulip用户ID是否已存在
*
* @param zulipUserId Zulip用户ID
* @param excludeId 排除的记录ID用于更新时检查
* @returns Promise<boolean> 是否已存在
*/
async existsByZulipUserId(zulipUserId: number, excludeId?: bigint): Promise<boolean> {
for (const [id, account] of this.accounts.entries()) {
if (account.zulipUserId === zulipUserId && (!excludeId || id !== excludeId)) {
return true;
}
}
return false;
}
/**
* 检查游戏用户ID是否已存在
*
* @param gameUserId 游戏用户ID
* @param excludeId 排除的记录ID用于更新时检查
* @returns Promise<boolean> 是否已存在
*/
async existsByGameUserId(gameUserId: bigint, excludeId?: bigint): Promise<boolean> {
for (const [id, account] of this.accounts.entries()) {
if (account.gameUserId === gameUserId && (!excludeId || id !== excludeId)) {
return true;
}
}
return false;
}
/**
* 导出所有数据(用于测试和备份)
*
* @returns Promise<ZulipAccounts[]> 所有账号数据
*/
async exportData(): Promise<ZulipAccounts[]> {
return Array.from(this.accounts.values());
}
/**
* 导入数据(用于测试数据初始化)
*
* @param accounts 账号数据列表
* @returns Promise<void>
*/
async importData(accounts: ZulipAccounts[]): Promise<void> {
this.accounts.clear();
let maxId = BigInt(0);
for (const account of accounts) {
this.accounts.set(account.id, account);
if (account.id > maxId) {
maxId = account.id;
}
}
this.currentId = maxId + BigInt(1);
}
/**
* 清空所有数据(用于测试)
*
* @returns Promise<void>
*/
async clearAll(): Promise<void> {
this.accounts.clear();
this.currentId = BigInt(1);
}
/**
* 获取数据统计信息
*
* @returns Promise<{ total: number; nextId: string }> 统计信息
*/
async getDataInfo(): Promise<{ total: number; nextId: string }> {
return {
total: this.accounts.size,
nextId: this.currentId.toString(),
};
}
}

View File

@@ -0,0 +1,607 @@
/**
* Zulip账号关联服务内存版本
*
* 功能描述:
* - 提供Zulip账号关联的内存存储数据访问服务
* - 用于开发和测试环境,无需数据库依赖
* - 实现与数据库版本相同的数据访问接口
* - 支持数据导入导出和测试数据管理
*
* 职责分离:
* - 数据访问通过内存Repository提供数据持久化
* - 接口兼容与数据库版本保持完全一致的API接口
* - 测试支持:提供测试环境的数据管理功能
*
* 注意:业务逻辑已转移到 src/core/zulip_core/services/zulip_accounts_business.service.ts
*
* 最近修改:
* - 2026-01-15: 代码规范优化 - 清理未使用的导入ConflictException和NotFoundException (修改者: moyin)
* - 2026-01-12: 架构优化 - 移除业务逻辑转移到zulip_core业务服务 (修改者: moyin)
* - 2026-01-12: 代码质量优化 - 修复导入语句添加缺失的AppLoggerService导入 (修改者: moyin)
* - 2026-01-12: 代码质量优化 - 修复logger初始化问题统一使用AppLoggerService (修改者: moyin)
* - 2026-01-12: 代码质量优化 - 完成所有性能监控代码优化统一使用createPerformanceMonitor方法 (修改者: moyin)
*
* @author angjustinl
* @version 2.0.1
* @since 2025-01-07
* @lastModified 2026-01-15
*/
import { Injectable, Inject } from '@nestjs/common';
import { BaseZulipAccountsService } from './base_zulip_accounts.service';
import { ZulipAccountsMemoryRepository } from './zulip_accounts_memory.repository';
import { ZulipAccounts } from './zulip_accounts.entity';
import { AppLoggerService } from '../../utils/logger/logger.service';
import {
DEFAULT_VERIFICATION_MAX_AGE,
DEFAULT_MAX_RETRY_COUNT,
} from './zulip_accounts.constants';
import {
CreateZulipAccountDto,
UpdateZulipAccountDto,
QueryZulipAccountDto,
ZulipAccountResponseDto,
ZulipAccountListResponseDto,
ZulipAccountStatsResponseDto,
BatchUpdateResponseDto,
VerifyAccountResponseDto,
} from './zulip_accounts.dto';
@Injectable()
export class ZulipAccountsMemoryService extends BaseZulipAccountsService {
constructor(
@Inject('ZulipAccountsRepository')
private readonly repository: ZulipAccountsMemoryRepository,
@Inject(AppLoggerService) logger: AppLoggerService,
) {
super(logger, 'ZulipAccountsMemoryService');
this.logger.info('ZulipAccountsMemoryService初始化完成', {
module: 'ZulipAccountsMemoryService',
operation: 'constructor'
});
}
/**
* 创建Zulip账号关联
*
* 数据访问逻辑:
* 1. 接收创建请求数据
* 2. 将字符串类型的gameUserId转换为BigInt类型
* 3. 调用内存Repository层创建账号关联记录
* 4. 记录操作日志和性能指标
* 5. 将实体对象转换为响应DTO返回
*
* @param createDto 创建数据包含游戏用户ID、Zulip用户信息等
* @returns Promise<ZulipAccountResponseDto> 创建的关联记录DTO
* @throws 数据访问异常
*/
async create(createDto: CreateZulipAccountDto): Promise<ZulipAccountResponseDto> {
const monitor = this.createPerformanceMonitor('创建Zulip账号关联', { gameUserId: createDto.gameUserId });
try {
const account = await this.repository.create({
gameUserId: this.parseGameUserId(createDto.gameUserId),
zulipUserId: createDto.zulipUserId,
zulipEmail: createDto.zulipEmail,
zulipFullName: createDto.zulipFullName,
zulipApiKeyEncrypted: createDto.zulipApiKeyEncrypted,
status: createDto.status || 'active',
});
const result = this.toResponseDto(account);
monitor.success({ accountId: account.id.toString() });
return result;
} catch (error) {
monitor.error(error);
throw error;
}
}
/**
* 根据游戏用户ID查找关联
*
* 数据访问逻辑:
* 1. 记录查询操作开始日志
* 2. 将字符串类型的gameUserId转换为BigInt类型
* 3. 调用内存Repository层根据游戏用户ID查找记录
* 4. 如果未找到记录记录调试日志并返回null
* 5. 如果找到记录,记录成功日志
* 6. 将实体对象转换为响应DTO返回
* 7. 捕获异常并进行统一的错误处理
*
* @param gameUserId 游戏用户ID字符串格式
* @param includeGameUser 是否包含游戏用户信息内存模式忽略默认false
* @returns Promise<ZulipAccountResponseDto | null> 关联记录DTO或null
* @throws BadRequestException 当查询参数无效或系统异常时
*
* @example
* ```typescript
* const account = await memoryService.findByGameUserId('12345', true);
* if (account) {
* console.log('找到关联:', account.zulipEmail);
* }
* ```
*/
async findByGameUserId(gameUserId: string, includeGameUser: boolean = false): Promise<ZulipAccountResponseDto | null> {
const monitor = this.createPerformanceMonitor('根据游戏用户ID查找关联', { gameUserId });
try {
const account = await this.repository.findByGameUserId(this.parseGameUserId(gameUserId), includeGameUser);
if (!account) {
this.logger.debug('未找到Zulip账号关联', { gameUserId });
return null;
}
const result = this.toResponseDto(account);
monitor.success({ found: true });
return result;
} catch (error) {
monitor.error(error);
throw error;
}
}
/**
* 根据Zulip用户ID查找关联
*
* 数据访问逻辑:
* 1. 记录查询操作开始日志
* 2. 调用内存Repository层根据Zulip用户ID查找记录
* 3. 如果未找到记录记录调试日志并返回null
* 4. 如果找到记录,记录成功日志
* 5. 将实体对象转换为响应DTO返回
* 6. 捕获异常并进行统一的错误处理
*
* @param zulipUserId Zulip用户ID数字类型
* @param includeGameUser 是否包含游戏用户信息内存模式忽略默认false
* @returns Promise<ZulipAccountResponseDto | null> 关联记录DTO或null
* @throws BadRequestException 当查询参数无效或系统异常时
*
* @example
* ```typescript
* const account = await memoryService.findByZulipUserId(67890);
* if (account) {
* console.log('关联的游戏用户:', account.gameUserId);
* }
* ```
*/
async findByZulipUserId(zulipUserId: number, includeGameUser: boolean = false): Promise<ZulipAccountResponseDto | null> {
this.logStart('根据Zulip用户ID查找关联', { zulipUserId });
try {
const account = await this.repository.findByZulipUserId(zulipUserId, includeGameUser);
if (!account) {
this.logger.debug('未找到Zulip账号关联', { zulipUserId });
return null;
}
this.logSuccess('根据Zulip用户ID查找关联', { zulipUserId, found: true });
return this.toResponseDto(account);
} catch (error) {
this.handleDataAccessError(error, '根据Zulip用户ID查找关联', { zulipUserId });
}
}
/**
* 根据Zulip邮箱查找关联
*
* @param zulipEmail Zulip邮箱
* @param includeGameUser 是否包含游戏用户信息(内存模式忽略)
* @returns Promise<ZulipAccountResponseDto | null> 关联记录或null
*/
async findByZulipEmail(zulipEmail: string, includeGameUser: boolean = false): Promise<ZulipAccountResponseDto | null> {
this.logStart('根据Zulip邮箱查找关联', { zulipEmail });
try {
const account = await this.repository.findByZulipEmail(zulipEmail, includeGameUser);
if (!account) {
this.logger.debug('未找到Zulip账号关联', { zulipEmail });
return null;
}
this.logSuccess('根据Zulip邮箱查找关联', { zulipEmail, found: true });
return this.toResponseDto(account);
} catch (error) {
this.handleDataAccessError(error, '根据Zulip邮箱查找关联', { zulipEmail });
}
}
/**
* 根据ID查找关联
*
* @param id 关联记录ID
* @param includeGameUser 是否包含游戏用户信息(内存模式忽略)
* @returns Promise<ZulipAccountResponseDto> 关联记录
*/
async findById(id: string, includeGameUser: boolean = false): Promise<ZulipAccountResponseDto> {
this.logStart('根据ID查找关联', { id });
try {
const account = await this.repository.findById(this.parseId(id), includeGameUser);
const result = account ? this.toResponseDto(account) : null;
this.logSuccess('根据ID查找关联', { id, found: !!account });
return result;
} catch (error) {
this.handleDataAccessError(error, '根据ID查找关联', { id });
}
}
/**
* 更新Zulip账号关联
*
* @param id 关联记录ID
* @param updateDto 更新数据
* @returns Promise<ZulipAccountResponseDto> 更新后的记录
*/
async update(id: string, updateDto: UpdateZulipAccountDto): Promise<ZulipAccountResponseDto> {
const monitor = this.createPerformanceMonitor('更新Zulip账号关联', { id });
try {
const account = await this.repository.update(this.parseId(id), updateDto);
const result = account ? this.toResponseDto(account) : null;
monitor.success({ updated: !!account });
return result;
} catch (error) {
monitor.error(error);
throw error;
}
}
/**
* 根据游戏用户ID更新关联
*
* @param gameUserId 游戏用户ID
* @param updateDto 更新数据
* @returns Promise<ZulipAccountResponseDto> 更新后的记录
*/
async updateByGameUserId(gameUserId: string, updateDto: UpdateZulipAccountDto): Promise<ZulipAccountResponseDto> {
const monitor = this.createPerformanceMonitor('根据游戏用户ID更新关联', { gameUserId });
try {
const account = await this.repository.updateByGameUserId(this.parseGameUserId(gameUserId), updateDto);
const result = account ? this.toResponseDto(account) : null;
monitor.success({ updated: !!account });
return result;
} catch (error) {
monitor.error(error);
throw error;
}
}
/**
* 删除Zulip账号关联
*
* @param id 关联记录ID
* @returns Promise<boolean> 是否删除成功
*/
async delete(id: string): Promise<boolean> {
const monitor = this.createPerformanceMonitor('删除Zulip账号关联', { id });
try {
const result = await this.repository.delete(this.parseId(id));
monitor.success({ deleted: result });
return result;
} catch (error) {
monitor.error(error);
throw error;
}
}
/**
* 根据游戏用户ID删除关联
*
* @param gameUserId 游戏用户ID
* @returns Promise<boolean> 是否删除成功
*/
async deleteByGameUserId(gameUserId: string): Promise<boolean> {
const monitor = this.createPerformanceMonitor('根据游戏用户ID删除关联', { gameUserId });
try {
const result = await this.repository.deleteByGameUserId(this.parseGameUserId(gameUserId));
monitor.success({ deleted: result });
return result;
} catch (error) {
monitor.error(error);
throw error;
}
}
/**
* 查询多个Zulip账号关联
*
* @param queryDto 查询条件
* @returns Promise<ZulipAccountListResponseDto> 关联记录列表
*/
async findMany(queryDto: QueryZulipAccountDto = {}): Promise<ZulipAccountListResponseDto> {
this.logStart('查询多个Zulip账号关联', queryDto);
try {
const options = {
gameUserId: queryDto.gameUserId ? this.parseGameUserId(queryDto.gameUserId) : undefined,
zulipUserId: queryDto.zulipUserId,
zulipEmail: queryDto.zulipEmail,
status: queryDto.status,
includeGameUser: queryDto.includeGameUser || false,
};
const accounts = await this.repository.findMany(options);
this.logSuccess('查询多个Zulip账号关联', {
count: accounts.length,
conditions: queryDto
});
return this.buildListResponse(accounts);
} catch (error) {
return {
accounts: this.handleSearchError(error, '查询多个Zulip账号关联', queryDto),
total: 0,
count: 0,
};
}
}
/**
* 获取需要验证的账号列表
*
* @param maxAge 最大验证间隔毫秒默认24小时
* @returns Promise<ZulipAccountListResponseDto> 需要验证的账号列表
*/
async findAccountsNeedingVerification(maxAge: number = DEFAULT_VERIFICATION_MAX_AGE): Promise<ZulipAccountListResponseDto> {
this.logStart('获取需要验证的账号列表', { maxAge });
try {
const accounts = await this.repository.findAccountsNeedingVerification(maxAge);
this.logSuccess('获取需要验证的账号列表', { count: accounts.length });
return this.buildListResponse(accounts);
} catch (error) {
return {
accounts: this.handleSearchError(error, '获取需要验证的账号列表', { maxAge }),
total: 0,
count: 0,
};
}
}
/**
* 获取错误状态的账号列表
*
* @param maxRetryCount 最大重试次数默认3次
* @returns Promise<ZulipAccountListResponseDto> 错误状态的账号列表
*/
async findErrorAccounts(maxRetryCount: number = DEFAULT_MAX_RETRY_COUNT): Promise<ZulipAccountListResponseDto> {
this.logStart('获取错误状态的账号列表', { maxRetryCount });
try {
const accounts = await this.repository.findErrorAccounts(maxRetryCount);
this.logSuccess('获取错误状态的账号列表', { count: accounts.length });
return this.buildListResponse(accounts);
} catch (error) {
return {
accounts: this.handleSearchError(error, '获取错误状态的账号列表', { maxRetryCount }),
total: 0,
count: 0,
};
}
}
/**
* 批量更新账号状态
*
* @param ids 账号ID列表
* @param status 新状态
* @returns Promise<BatchUpdateResponseDto> 批量更新结果
*/
async batchUpdateStatus(ids: string[], status: 'active' | 'inactive' | 'suspended' | 'error'): Promise<BatchUpdateResponseDto> {
const monitor = this.createPerformanceMonitor('批量更新账号状态', { count: ids.length, status });
try {
const bigintIds = this.parseIds(ids);
const updatedCount = await this.repository.batchUpdateStatus(bigintIds, status);
monitor.success({
requestCount: ids.length,
updatedCount,
status
});
return {
success: true,
updatedCount,
};
} catch (error) {
this.logger.error('批量更新账号状态失败', {
operation: 'batchUpdateStatus',
error: this.formatError(error),
count: ids.length,
status,
});
return {
success: false,
updatedCount: 0,
error: this.formatError(error),
};
}
}
/**
* 获取账号状态统计
*
* @returns Promise<ZulipAccountStatsResponseDto> 状态统计
*/
async getStatusStatistics(): Promise<ZulipAccountStatsResponseDto> {
this.logStart('获取账号状态统计');
try {
const statistics = await this.repository.getStatusStatistics();
const result = {
active: statistics.active || 0,
inactive: statistics.inactive || 0,
suspended: statistics.suspended || 0,
error: statistics.error || 0,
total: (statistics.active || 0) + (statistics.inactive || 0) +
(statistics.suspended || 0) + (statistics.error || 0),
};
this.logSuccess('获取账号状态统计', result);
return result;
} catch (error) {
this.handleDataAccessError(error, '获取账号状态统计');
}
}
/**
* 验证账号有效性
*
* @param gameUserId 游戏用户ID
* @returns Promise<VerifyAccountResponseDto> 验证结果
*/
async verifyAccount(gameUserId: string): Promise<VerifyAccountResponseDto> {
const monitor = this.createPerformanceMonitor('验证账号有效性', { gameUserId });
try {
// 1. 查找账号关联
const account = await this.repository.findByGameUserId(this.parseGameUserId(gameUserId));
if (!account) {
monitor.success({ isValid: false, reason: '账号关联不存在' });
return {
success: false,
isValid: false,
error: '账号关联不存在',
};
}
// 2. 检查账号状态
if (account.status !== 'active') {
monitor.success({ isValid: false, reason: `账号状态为 ${account.status}` });
return {
success: true,
isValid: false,
error: `账号状态为 ${account.status}`,
};
}
// 3. 更新验证时间
await this.repository.updateByGameUserId(this.parseGameUserId(gameUserId), {
lastVerifiedAt: new Date(),
});
monitor.success({ isValid: true });
return {
success: true,
isValid: true,
verifiedAt: new Date().toISOString(),
};
} catch (error) {
this.logger.error('验证账号有效性失败', {
operation: 'verifyAccount',
gameUserId,
error: this.formatError(error),
});
return {
success: false,
isValid: false,
error: this.formatError(error),
};
}
}
/**
* 检查邮箱是否已存在
*
* @param zulipEmail Zulip邮箱
* @param excludeId 排除的记录ID
* @returns Promise<boolean> 是否已存在
*/
async existsByEmail(zulipEmail: string, excludeId?: string): Promise<boolean> {
try {
const excludeBigintId = excludeId ? this.parseId(excludeId) : undefined;
return await this.repository.existsByEmail(zulipEmail, excludeBigintId);
} catch (error) {
this.logger.warn('检查邮箱存在性失败', {
operation: 'existsByEmail',
zulipEmail,
error: this.formatError(error),
});
return false;
}
}
/**
* 检查Zulip用户ID是否已存在
*
* @param zulipUserId Zulip用户ID
* @param excludeId 排除的记录ID
* @returns Promise<boolean> 是否已存在
*/
async existsByZulipUserId(zulipUserId: number, excludeId?: string): Promise<boolean> {
try {
const excludeBigintId = excludeId ? this.parseId(excludeId) : undefined;
return await this.repository.existsByZulipUserId(zulipUserId, excludeBigintId);
} catch (error) {
this.logger.warn('检查Zulip用户ID存在性失败', {
operation: 'existsByZulipUserId',
zulipUserId,
error: this.formatError(error),
});
return false;
}
}
/**
* 将实体转换为响应DTO
*
* @param account 账号关联实体
* @returns ZulipAccountResponseDto 响应DTO
*/
protected toResponseDto(account: ZulipAccounts): ZulipAccountResponseDto {
return {
id: account.id.toString(),
gameUserId: account.gameUserId.toString(),
zulipUserId: account.zulipUserId,
zulipEmail: account.zulipEmail,
zulipFullName: account.zulipFullName,
status: account.status,
lastVerifiedAt: account.lastVerifiedAt?.toISOString(),
lastSyncedAt: account.lastSyncedAt?.toISOString(),
errorMessage: account.errorMessage,
retryCount: account.retryCount,
createdAt: account.createdAt.toISOString(),
updatedAt: account.updatedAt.toISOString(),
gameUser: account.gameUser,
};
}
}