Initial WhaleTown V2 backend
This commit is contained in:
424
src/core/db/user_profiles/base_user_profiles.service.ts
Normal file
424
src/core/db/user_profiles/base_user_profiles.service.ts
Normal 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)}`;
|
||||
}
|
||||
}
|
||||
491
src/core/db/user_profiles/user_profiles.dto.ts
Normal file
491
src/core/db/user_profiles/user_profiles.dto.ts
Normal 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;
|
||||
}
|
||||
403
src/core/db/user_profiles/user_profiles.entity.ts
Normal file
403
src/core/db/user_profiles/user_profiles.entity.ts
Normal 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;
|
||||
}
|
||||
225
src/core/db/user_profiles/user_profiles.module.ts
Normal file
225
src/core/db/user_profiles/user_profiles.module.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
621
src/core/db/user_profiles/user_profiles.service.ts
Normal file
621
src/core/db/user_profiles/user_profiles.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
697
src/core/db/user_profiles/user_profiles_memory.service.ts
Normal file
697
src/core/db/user_profiles/user_profiles_memory.service.ts
Normal 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()
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user