feat: 优化项目目录结构

This commit is contained in:
2025-12-12 17:25:26 +08:00
parent ae627d0496
commit b89f83291e
235 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
import { IsString, Length, Matches } from 'class-validator';
export class UpdateUserPasswordDto {
@IsString({ message: '密码不得为空' })
@Length(6, 32, { message: '密码长度只能为6~32' })
@Matches(
/^(?=.*[a-zA-Z])(?=.*\d)[a-zA-Z\d!@#$%^&*()_+\-=\[\]{};:'",.<>/?]{6,32}$/,
{ message: '密码必须包含字母和数字且长度在6~32之间' },
)
password: string;
}

View File

@@ -0,0 +1,31 @@
import {
Column,
CreateDateColumn,
DeleteDateColumn,
Entity,
Index,
PrimaryGeneratedColumn,
} from 'typeorm';
@Entity()
@Index(['sessionId', 'userId'])
export class UserSession {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ length: 36 })
sessionId: string;
@Column({ length: 36 })
userId: string;
@CreateDateColumn({ precision: 3 })
createdAt: Date;
@DeleteDateColumn({ nullable: true, precision: 3 })
deletedAt: Date;
}
/**
* 考虑是否使用sessionId代替id以节省存储空间
*/

View File

@@ -0,0 +1,91 @@
import { Role } from 'src/auth/role.enum';
import {
BeforeInsert,
Column,
CreateDateColumn,
DeleteDateColumn,
Entity,
Index,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { v4 as uuidv4 } from 'uuid';
@Entity()
@Index('IDX_user_userid', ['userId'], { unique: true })
@Index('IDX_user_username', ['username'], { unique: true })
@Index('IDX_user_email', ['email'], {
unique: true,
where: 'email IS NOT NULL',
})
@Index('IDX_user_phone', ['phone'], {
unique: true,
where: 'phone IS NOT NULL',
})
export class User {
@PrimaryGeneratedColumn('uuid')
userId: string;
@Column({ length: 32 })
username: string;
@Column({ length: 30 })
nickname: string;
@BeforeInsert()
generateDefaults() {
if (!this.username) {
this.username = `user_${uuidv4().replace(/-/g, '').slice(0, 27)}`;
}
if (!this.nickname) {
this.nickname = `用户_${uuidv4().replace(/-/g, '').slice(0, 8)}`;
}
}
@Column({ nullable: true, type: 'char', length: 32 })
salt: string;
@Column({ nullable: true, type: 'char', length: 64 })
password_hash: string;
@Column({
nullable: true,
length: 254,
transformer: {
to: (value: string | null) => value?.trim() || null,
from: (value: string | null) => value,
},
}) // RFC 5321
email: string | null;
@Column({
nullable: true,
length: 20,
transformer: {
to: (value: string | null) => value?.trim() || null,
from: (value: string | null) => value,
},
}) // China Mainland
phone: string | null;
@Column({
nullable: true,
transformer: {
to: (value: string | null) => value?.trim() || null,
from: (value: string | null) => value,
},
})
avatar: string;
@CreateDateColumn({ precision: 3 })
createdAt: Date;
@UpdateDateColumn({ precision: 3 })
updatedAt: Date;
@DeleteDateColumn({ nullable: true, precision: 3 })
deletedAt: Date;
@Column('simple-array', { default: '' })
roles: Role[];
}

View File

@@ -0,0 +1,46 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Injectable } from '@nestjs/common';
import { UserSession } from '../entities/user-session.entity';
import { Repository } from 'typeorm';
@Injectable()
export class UserSessionService {
constructor(
@InjectRepository(UserSession)
private readonly userSessionRepository: Repository<UserSession>,
) {}
async createSession(userId: string, sessionId: string): Promise<UserSession> {
const session = this.userSessionRepository.create({
userId,
sessionId,
});
return await this.userSessionRepository.save(session);
}
async isSessionValid(userId: string, sessionId: string): Promise<boolean> {
const session = await this.userSessionRepository.findOne({
where: {
userId,
sessionId,
deletedAt: null,
},
});
return !!session;
}
async invalidateSession(userId: string, sessionId: string): Promise<void> {
const session = await this.userSessionRepository.findOne({
where: {
userId,
sessionId,
deletedAt: null,
},
});
if (session) {
await this.userSessionRepository.softDelete(session.id);
}
}
}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UserController } from './user.controller';
describe('UserController', () => {
let controller: UserController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UserController],
}).compile();
controller = module.get<UserController>(UserController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@@ -0,0 +1,26 @@
import { Body, Controller, Get, Put, Request, UseGuards } from '@nestjs/common';
import { UserService } from './user.service';
import { AuthGuard } from '@nestjs/passport';
import { UpdateUserPasswordDto } from './dto/update-user-password.dto';
import { AuthService } from 'src/auth/auth.service';
@Controller('user')
export class UserController {
constructor(
private readonly userService: UserService,
private readonly authService: AuthService,
) {}
@UseGuards(AuthGuard('jwt'))
@Get('me')
async getMe(@Request() req) {
const { user } = req;
return this.userService.findOne({ userId: user.userId });
}
@UseGuards(AuthGuard('jwt'))
@Put('password')
async update(@Request() req, @Body() dto: UpdateUserPasswordDto) {
return this.userService.setPassword(req.user.userId, dto.password);
}
}

View File

@@ -0,0 +1,19 @@
import { forwardRef, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { UserController } from './user.controller';
import { UserService } from './user.service';
import { UserSession } from './entities/user-session.entity';
import { AuthModule } from 'src/auth/auth.module';
import { UserSessionService } from './services/user-session.service';
@Module({
imports: [
TypeOrmModule.forFeature([User, UserSession]),
forwardRef(() => AuthModule), // 解决循环依赖问题
],
controllers: [UserController],
providers: [UserService, UserSessionService],
exports: [UserService, UserSessionService],
})
export class UserModule {}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UserService } from './user.service';
describe('UserService', () => {
let service: UserService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UserService],
}).compile();
service = module.get<UserService>(UserService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@@ -0,0 +1,149 @@
import {
BadRequestException,
ConflictException,
Injectable,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { QueryFailedError, Repository } from 'typeorm';
import { createHash } from 'crypto';
import { v4 as uuid } from 'uuid';
type UserFindOptions = Partial<
Pick<User, 'userId' | 'username' | 'phone' | 'email'>
>;
@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
) {}
/**
* @deprecated 尽量不使用该方法
*/
async findOne(
options: UserFindOptions | UserFindOptions[],
additionalOptions?: { withDeleted?: boolean },
): Promise<User | null> {
if (Object.keys(options).length === 0) {
throw new BadRequestException('查询条件不能为空');
}
return this.userRepository.findOne({
where: options,
withDeleted: additionalOptions?.withDeleted || false,
});
}
async findById(userId: string): Promise<User | null> {
return this.userRepository.findOne({
where: {
userId,
},
});
}
async create(user: Partial<User>): Promise<User> {
try {
const newUser = this.userRepository.create(user);
return await this.userRepository.save(newUser);
} catch (error) {
if (error instanceof QueryFailedError) {
throw new ConflictException(this.getDuplicateErrorMessage(error));
}
throw new BadRequestException('创建用户失败');
}
}
async update(userId: string, user: Partial<User>): Promise<User> {
const existingUser = await this.userRepository.findOne({
where: { userId },
});
if (!existingUser) {
throw new BadRequestException('User not found');
}
try {
Object.assign(existingUser, user);
return await this.userRepository.save(existingUser);
} catch (error) {
if (error instanceof QueryFailedError) {
throw new ConflictException(this.getDuplicateErrorMessage(error));
}
}
}
async delete(userId: string, soft: boolean) {
const existingUser = await this.userRepository.findOne({
where: { userId },
withDeleted: true,
});
if (!existingUser) {
throw new BadRequestException('用户不存在');
}
if (existingUser.deletedAt && soft) {
throw new BadRequestException('账户已注销,不得重复操作');
}
if (!existingUser.deletedAt && !soft) {
throw new BadRequestException('账号未注销,请先注销再执行删除操作');
}
return soft
? await this.userRepository.softDelete(existingUser.userId)
: await this.userRepository.delete(existingUser.userId);
}
hashPassword(password: string, salt: string): string {
return createHash('sha256').update(`${password}${salt}`).digest('hex');
}
generateSalt(): string {
return uuid().replace(/-/g, '');
}
async setPassword(userId: string, password: string): Promise<User> {
const user = await this.userRepository.findOne({ where: { userId } });
if (!user) {
throw new BadRequestException('User not found');
}
const salt = this.generateSalt();
user.password_hash = this.hashPassword(password, salt);
user.salt = salt;
return this.userRepository.save(user);
}
private getDuplicateErrorMessage(error: QueryFailedError): string {
// 根据具体的错误信息返回友好的提示
if (error.message.includes('IDX_user_username')) {
return '账户名已被使用';
}
if (error.message.includes('IDX_user_email')) {
return '邮箱已被使用';
}
if (error.message.includes('IDX_user_phone')) {
return '手机号已被使用';
}
return '该登陆方式异常,请更换其他登陆方式或联系管理员';
}
async list(page = 1, pageSize = 20) {
const queryBuilder = this.userRepository.createQueryBuilder('user');
queryBuilder.withDeleted();
queryBuilder.orderBy('user.createdAt', 'DESC');
queryBuilder.skip((page - 1) * pageSize);
queryBuilder.take(pageSize);
const [items, total] = await queryBuilder.getManyAndCount();
return {
items,
total,
page,
pageSize,
};
}
}