Initial WhaleTown V2 backend

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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