forked from datawhale/whale-town-end
feat(sql, auth, email, dto):重构邮箱验证流程,引入基于内存的用户服务,并改进 API 响应处理
* 新增完整的 API 状态码文档,并对测试模式进行特殊处理(`206 Partial Content`) * 重组 DTO 结构,引入 `app.dto.ts` 与 `error_response.dto.ts`,以实现统一、规范的响应格式 * 重构登录相关 DTO,优化命名与结构,提升可维护性 * 实现基于内存的用户服务(`users_memory.service.ts`),用于开发与测试环境 * 更新邮件服务,增强验证码生成逻辑,并支持测试模式自动识别 * 增强登录控制器与服务层的错误处理能力,统一响应行为 * 优化核心登录服务,强化参数校验并集成邮箱验证流程 * 新增 `@types/express` 依赖,提升 TypeScript 类型支持与开发体验 * 改进 `main.ts`,优化应用初始化流程与配置管理 * 在所有服务中统一错误处理机制,采用标准化的错误响应格式 * 实现测试模式(`206`)与生产环境邮件发送(`200`)之间的无缝切换
This commit is contained in:
@@ -4,23 +4,61 @@
|
||||
* 功能描述:
|
||||
* - 整合用户相关的实体、服务和控制器
|
||||
* - 配置TypeORM实体和Repository
|
||||
* - 支持数据库和内存存储的动态切换 by angjustinl 2025-12-17
|
||||
* - 导出用户服务供其他模块使用
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* 存储模式:by angjustinl 2025-12-17
|
||||
* - 数据库模式:使用TypeORM连接MySQL数据库
|
||||
* - 内存模式:使用Map存储,适用于开发和测试
|
||||
*
|
||||
* @author moyin angjustinl
|
||||
* @version 1.0.1
|
||||
* @since 2025-12-17
|
||||
*/
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
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';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Users])
|
||||
],
|
||||
providers: [UsersService],
|
||||
exports: [UsersService, TypeOrmModule],
|
||||
})
|
||||
export class UsersModule {}
|
||||
@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'],
|
||||
};
|
||||
}
|
||||
}
|
||||
349
src/core/db/users/users_memory.service.ts
Normal file
349
src/core/db/users/users_memory.service.ts
Normal file
@@ -0,0 +1,349 @@
|
||||
/**
|
||||
* 用户内存存储服务类
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供基于内存的用户数据存储
|
||||
* - 作为数据库连接失败时的回退方案
|
||||
* - 实现与UsersService相同的接口
|
||||
*
|
||||
* 使用场景:
|
||||
* - 开发环境无数据库时的快速启动
|
||||
* - 测试环境的轻量级存储
|
||||
* - 数据库故障时的临时降级
|
||||
*
|
||||
* 注意事项:
|
||||
* - 数据仅存储在内存中,重启后丢失
|
||||
* - 不适用于生产环境
|
||||
* - 性能优异但无持久化保证
|
||||
*
|
||||
* @author angjustinl
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-17
|
||||
*/
|
||||
|
||||
import { Injectable, ConflictException, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { Users } from './users.entity';
|
||||
import { CreateUserDto } from './users.dto';
|
||||
import { validate } from 'class-validator';
|
||||
import { plainToClass } from 'class-transformer';
|
||||
|
||||
@Injectable()
|
||||
export class UsersMemoryService {
|
||||
private users: Map<bigint, Users> = new Map();
|
||||
private currentId: bigint = BigInt(1);
|
||||
|
||||
/**
|
||||
* 创建新用户
|
||||
*
|
||||
* @param createUserDto 创建用户的数据传输对象
|
||||
* @returns 创建的用户实体
|
||||
* @throws ConflictException 当用户名、邮箱或手机号已存在时
|
||||
* @throws BadRequestException 当数据验证失败时
|
||||
*/
|
||||
async create(createUserDto: CreateUserDto): Promise<Users> {
|
||||
// 验证DTO
|
||||
const dto = plainToClass(CreateUserDto, createUserDto);
|
||||
const validationErrors = await validate(dto);
|
||||
|
||||
if (validationErrors.length > 0) {
|
||||
const errorMessages = validationErrors.map(error =>
|
||||
Object.values(error.constraints || {}).join(', ')
|
||||
).join('; ');
|
||||
throw new BadRequestException(`数据验证失败: ${errorMessages}`);
|
||||
}
|
||||
|
||||
// 检查用户名是否已存在
|
||||
if (createUserDto.username) {
|
||||
const existingUser = await this.findByUsername(createUserDto.username);
|
||||
if (existingUser) {
|
||||
throw new ConflictException('用户名已存在');
|
||||
}
|
||||
}
|
||||
|
||||
// 检查邮箱是否已存在
|
||||
if (createUserDto.email) {
|
||||
const existingEmail = await this.findByEmail(createUserDto.email);
|
||||
if (existingEmail) {
|
||||
throw new ConflictException('邮箱已存在');
|
||||
}
|
||||
}
|
||||
|
||||
// 检查手机号是否已存在
|
||||
if (createUserDto.phone) {
|
||||
const existingPhone = Array.from(this.users.values()).find(
|
||||
u => u.phone === createUserDto.phone
|
||||
);
|
||||
if (existingPhone) {
|
||||
throw new ConflictException('手机号已存在');
|
||||
}
|
||||
}
|
||||
|
||||
// 检查GitHub ID是否已存在
|
||||
if (createUserDto.github_id) {
|
||||
const existingGithub = await this.findByGithubId(createUserDto.github_id);
|
||||
if (existingGithub) {
|
||||
throw new ConflictException('GitHub ID已存在');
|
||||
}
|
||||
}
|
||||
|
||||
// 创建用户实体
|
||||
const user = new Users();
|
||||
user.id = this.currentId++;
|
||||
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 || 1;
|
||||
user.email_verified = createUserDto.email_verified || false;
|
||||
user.created_at = new Date();
|
||||
user.updated_at = new Date();
|
||||
|
||||
// 保存到内存
|
||||
this.users.set(user.id, user);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询所有用户
|
||||
*
|
||||
* @param limit 限制返回数量,默认100
|
||||
* @param offset 偏移量,默认0
|
||||
* @returns 用户列表
|
||||
*/
|
||||
async findAll(limit: number = 100, offset: number = 0): Promise<Users[]> {
|
||||
const allUsers = Array.from(this.users.values())
|
||||
.sort((a, b) => b.created_at.getTime() - a.created_at.getTime());
|
||||
|
||||
return allUsers.slice(offset, offset + limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询用户
|
||||
*
|
||||
* @param id 用户ID
|
||||
* @returns 用户实体
|
||||
* @throws NotFoundException 当用户不存在时
|
||||
*/
|
||||
async findOne(id: bigint): Promise<Users> {
|
||||
const user = this.users.get(id);
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException(`ID为 ${id} 的用户不存在`);
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户名查询用户
|
||||
*
|
||||
* @param username 用户名
|
||||
* @returns 用户实体或null
|
||||
*/
|
||||
async findByUsername(username: string): Promise<Users | null> {
|
||||
const user = Array.from(this.users.values()).find(
|
||||
u => u.username === username
|
||||
);
|
||||
return user || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据邮箱查询用户
|
||||
*
|
||||
* @param email 邮箱
|
||||
* @returns 用户实体或null
|
||||
*/
|
||||
async findByEmail(email: string): Promise<Users | null> {
|
||||
const user = Array.from(this.users.values()).find(
|
||||
u => u.email === email
|
||||
);
|
||||
return user || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据GitHub ID查询用户
|
||||
*
|
||||
* @param githubId GitHub ID
|
||||
* @returns 用户实体或null
|
||||
*/
|
||||
async findByGithubId(githubId: string): Promise<Users | null> {
|
||||
const user = Array.from(this.users.values()).find(
|
||||
u => u.github_id === githubId
|
||||
);
|
||||
return user || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户信息
|
||||
*
|
||||
* @param id 用户ID
|
||||
* @param updateData 更新的数据
|
||||
* @returns 更新后的用户实体
|
||||
* @throws NotFoundException 当用户不存在时
|
||||
* @throws ConflictException 当更新的数据与其他用户冲突时
|
||||
*/
|
||||
async update(id: bigint, updateData: Partial<CreateUserDto>): Promise<Users> {
|
||||
// 检查用户是否存在
|
||||
const existingUser = await this.findOne(id);
|
||||
|
||||
// 检查更新数据的唯一性约束
|
||||
if (updateData.username && updateData.username !== existingUser.username) {
|
||||
const usernameExists = await this.findByUsername(updateData.username);
|
||||
if (usernameExists) {
|
||||
throw new ConflictException('用户名已存在');
|
||||
}
|
||||
}
|
||||
|
||||
if (updateData.email && updateData.email !== existingUser.email) {
|
||||
const emailExists = await this.findByEmail(updateData.email);
|
||||
if (emailExists) {
|
||||
throw new ConflictException('邮箱已存在');
|
||||
}
|
||||
}
|
||||
|
||||
if (updateData.phone && updateData.phone !== existingUser.phone) {
|
||||
const phoneExists = Array.from(this.users.values()).find(
|
||||
u => u.phone === updateData.phone && u.id !== id
|
||||
);
|
||||
if (phoneExists) {
|
||||
throw new ConflictException('手机号已存在');
|
||||
}
|
||||
}
|
||||
|
||||
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('GitHub ID已存在');
|
||||
}
|
||||
}
|
||||
|
||||
// 更新用户数据
|
||||
Object.assign(existingUser, updateData);
|
||||
existingUser.updated_at = new Date();
|
||||
|
||||
this.users.set(id, existingUser);
|
||||
|
||||
return existingUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
*
|
||||
* @param id 用户ID
|
||||
* @returns 删除操作结果
|
||||
* @throws NotFoundException 当用户不存在时
|
||||
*/
|
||||
async remove(id: bigint): Promise<{ affected: number; message: string }> {
|
||||
// 检查用户是否存在
|
||||
await this.findOne(id);
|
||||
|
||||
// 执行删除
|
||||
const deleted = this.users.delete(id);
|
||||
|
||||
return {
|
||||
affected: deleted ? 1 : 0,
|
||||
message: `成功删除ID为 ${id} 的用户`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 软删除用户(内存模式下与硬删除相同)
|
||||
*
|
||||
* @param id 用户ID
|
||||
* @returns 被删除的用户实体
|
||||
*/
|
||||
async softRemove(id: bigint): Promise<Users> {
|
||||
const user = await this.findOne(id);
|
||||
this.users.delete(id);
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计用户数量
|
||||
*
|
||||
* @param conditions 查询条件(内存模式下简化处理)
|
||||
* @returns 用户数量
|
||||
*/
|
||||
async count(conditions?: 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量创建用户
|
||||
*
|
||||
* @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 角色值
|
||||
* @returns 用户列表
|
||||
*/
|
||||
async findByRole(role: number): Promise<Users[]> {
|
||||
return Array.from(this.users.values())
|
||||
.filter(u => u.role === role)
|
||||
.sort((a, b) => b.created_at.getTime() - a.created_at.getTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索用户(根据用户名或昵称)
|
||||
*
|
||||
* @param keyword 搜索关键词
|
||||
* @param limit 限制数量
|
||||
* @returns 用户列表
|
||||
*/
|
||||
async search(keyword: string, limit: number = 20): Promise<Users[]> {
|
||||
const lowerKeyword = keyword.toLowerCase();
|
||||
|
||||
return Array.from(this.users.values())
|
||||
.filter(u =>
|
||||
u.username.toLowerCase().includes(lowerKeyword) ||
|
||||
u.nickname.toLowerCase().includes(lowerKeyword)
|
||||
)
|
||||
.sort((a, b) => b.created_at.getTime() - a.created_at.getTime())
|
||||
.slice(0, limit);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user