Initial WhaleTown V2 backend
This commit is contained in:
203
src/core/db/users/base_users.service.ts
Normal file
203
src/core/db/users/base_users.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
173
src/core/db/users/user_status.enum.ts
Normal file
173
src/core/db/users/user_status.enum.ts
Normal 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);
|
||||
}
|
||||
188
src/core/db/users/users.constants.ts
Normal file
188
src/core/db/users/users.constants.ts
Normal 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('; ');
|
||||
}
|
||||
}
|
||||
275
src/core/db/users/users.dto.ts
Normal file
275
src/core/db/users/users.dto.ts
Normal 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;
|
||||
}
|
||||
497
src/core/db/users/users.entity.ts
Normal file
497
src/core/db/users/users.entity.ts
Normal 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;
|
||||
}
|
||||
75
src/core/db/users/users.module.ts
Normal file
75
src/core/db/users/users.module.ts
Normal 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'],
|
||||
};
|
||||
}
|
||||
}
|
||||
714
src/core/db/users/users.service.ts
Normal file
714
src/core/db/users/users.service.ts
Normal 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()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
766
src/core/db/users/users_memory.service.ts
Normal file
766
src/core/db/users/users_memory.service.ts
Normal 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()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user