NestJS + TypeORM + MySQL搭建企业级项目

1,182 阅读4分钟

项目结构

├── auth/                 # 身份认证模块
│   ├── auth.controller.ts
│   ├── auth.module.ts
│   ├── auth.service.ts
│   └── jwt.strategy.ts
├── common/               # 公共模块 (日志、过滤器等)
│   ├── filters/
│   │   └── http-exception.filter.ts
│   ├── logger/
│   │   └── logger.service.ts
│   └── interceptors/
│       └── logging.interceptor.ts
├── database/             # 数据库模块
│   ├── entities/
│   │   ├── user.entity.ts
│   │   ├── role.entity.ts
│   │   └── permission.entity.ts
│   └── database.module.ts
├── roles/                # 角色与权限管理模块
│   ├── roles.controller.ts
│   ├── roles.module.ts
│   └── roles.service.ts
├── users/                # 用户管理模块
│   ├── users.controller.ts
│   ├── users.module.ts
│   └── users.service.ts
├── app.controller.ts     # 应用控制器
├── app.module.ts         # 主模块
└── main.ts               # 启动文件

1. 数据库模块:配置 TypeORM 和实体

src/database/database.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { Role } from './entities/role.entity';
import { Permission } from './entities/permission.entity';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'password',
      database: 'enterprise_db',
      entities: [User, Role, Permission],
      synchronize: true, // 生产环境中要关闭
    }),
    TypeOrmModule.forFeature([User, Role, Permission]),
  ],
  exports: [TypeOrmModule],
})
export class DatabaseModule {}

src/database/entities/user.entity.ts

import { Entity, Column, PrimaryGeneratedColumn, ManyToMany, JoinTable } from 'typeorm';
import { Role } from './role.entity';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  username: string;

  @Column()
  password: string;

  @ManyToMany(() => Role)
  @JoinTable()
  roles: Role[];
}

src/database/entities/role.entity.ts

import { Entity, Column, PrimaryGeneratedColumn, ManyToMany } from 'typeorm';
import { Permission } from './permission.entity';
import { User } from './user.entity';

@Entity()
export class Role {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @ManyToMany(() => Permission)
  permissions: Permission[];

  @ManyToMany(() => User, user => user.roles)
  users: User[];
}

src/database/entities/permission.entity.ts

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class Permission {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;
}

2. 用户与角色管理模块

src/users/users.service.ts

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from '../database/entities/user.entity';
import { Role } from '../database/entities/role.entity';

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
    @InjectRepository(Role)
    private rolesRepository: Repository<Role>,
  ) {}

  async findOne(username: string): Promise<User | undefined> {
    return this.usersRepository.findOne({
      where: { username },
      relations: ['roles'],
    });
  }

  async createUser(username: string, password: string, roleIds: number[]): Promise<User> {
    const user = this.usersRepository.create({ username, password });
    user.roles = await this.rolesRepository.findByIds(roleIds);
    return this.usersRepository.save(user);
  }
}

src/users/users.controller.ts

import { Controller, Post, Body } from '@nestjs/common';
import { UsersService } from './users.service';

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Post('create')
  async createUser(@Body() createUserDto: { username: string, password: string, roleIds: number[] }) {
    return this.usersService.createUser(createUserDto.username, createUserDto.password, createUserDto.roleIds);
  }
}

3. 身份验证与权限控制

src/auth/jwt.strategy.ts

import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-jwt';
import { ExtractJwt } from 'passport-jwt';
import { AuthService } from './auth.service';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly authService: AuthService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: 'your-secret-key', // 请替换为环境变量
    });
  }

  async validate(payload: any) {
    return { userId: payload.sub, username: payload.username };
  }
}

src/auth/auth.service.ts

import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';

@Injectable()
export class AuthService {
  constructor(
    private readonly jwtService: JwtService,
    private readonly usersService: UsersService,
  ) {}

  async login(user: any) {
    const payload = { username: user.username, sub: user.userId };
    return {
      access_token: this.jwtService.sign(payload),
    };
  }

  async validateUser(username: string, pass: string): Promise<any> {
    const user = await this.usersService.findOne(username);
    if (user && user.password === pass) {
      return user;
    }
    return null;
  }
}

4. 日志与全局异常处理

src/common/logger/logger.service.ts

import { Injectable } from '@nestjs/common';
import * as winston from 'winston';

@Injectable()
export class LoggerService {
  private readonly logger: winston.Logger;

  constructor() {
    this.logger = winston.createLogger({
      level: 'info',
      transports: [
        new winston.transports.Console({ format: winston.format.simple() }),
        new winston.transports.File({ filename: 'logs/app.log' }),
      ],
    });
  }

  log(message: string) {
    this.logger.info(message);
  }

  error(message: string, trace: string) {
    this.logger.error(`${message} - ${trace}`);
  }

  warn(message: string) {
    this.logger.warn(message);
  }
}

src/common/filters/http-exception.filter.ts

import { ExceptionFilter, Catch, ArgumentsHost } from '@nestjs/common';
import { Response } from 'express';

@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: any, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const status = exception.status || 500;

    response.status(status).json({
      statusCode: status,
      message: exception.message || 'Internal server error',
    });
  }
}

5. 主模块配置

src/app.module.ts

import { Module } from '@nestjs/common';
import { AuthModule } from './auth/auth.module';
import { UsersModule } from './users/users.module';
import { RolesModule } from './roles/roles.module';
import { DatabaseModule } from './database/database.module';
import { LoggerService } from './common/logger/logger.service';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';

@Module({
  imports: [
    AuthModule,
    UsersModule,
    RolesModule,
    DatabaseModule,
  ],
  providers: [
    LoggerService,
    {
      provide: 'APP_FILTER',
      useClass: HttpExceptionFilter,
    },
  ],
})
export class AppModule {}

1. 数据库连接配置提取

为了实现更灵活的数据库配置,可以使用 @nestjs/config 模块将数据库连接信息提取到配置文件中。

首先,安装 @nestjs/config 依赖:

 npm install @nestjs/config

src/config/database.config.ts - 数据库配置

import { registerAs } from '@nestjs/config';

export default registerAs('database', () => ({
  type: 'mysql',
  host: process.env.DB_HOST || 'localhost',
  port: process.env.DB_PORT || 3306,
  username: process.env.DB_USERNAME || 'root',
  password: process.env.DB_PASSWORD || 'password',
  database: process.env.DB_NAME || 'enterprise_db',
  entities: [__dirname + '/../**/*.entity{.ts,.js}'],
  synchronize: process.env.NODE_ENV !== 'production',
}));

src/app.module.ts - 导入配置模块

然后,在 app.module.ts 中引入配置模块并加载数据库配置:

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AuthModule } from './auth/auth.module';
import { UsersModule } from './users/users.module';
import { RolesModule } from './roles/roles.module';
import { DatabaseModule } from './database/database.module';
import { LoggerService } from './common/logger/logger.service';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
import databaseConfig from './config/database.config';

@Module({
  imports: [
    ConfigModule.forRoot({
      load: [databaseConfig],
      isGlobal: true, // 配置为全局
    }),
    AuthModule,
    UsersModule,
    RolesModule,
    DatabaseModule,
  ],
  providers: [
    LoggerService,
    {
      provide: 'APP_FILTER',
      useClass: HttpExceptionFilter,
    },
  ],
})
export class AppModule {}

src/database/database.module.ts - 修改数据库模块以支持动态配置

database.module.ts 中,我们使用 ConfigService 来获取数据库连接信息。

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigService } from '@nestjs/config';
import { User } from './entities/user.entity';
import { Role } from './entities/role.entity';
import { Permission } from './entities/permission.entity';

@Module({
  imports: [
    TypeOrmModule.forRootAsync({
      useFactory: async (configService: ConfigService) => ({
        type: 'mysql',
        host: configService.get('database.host'),
        port: configService.get('database.port'),
        username: configService.get('database.username'),
        password: configService.get('database.password'),
        database: configService.get('database.database'),
        entities: [User, Role, Permission],
        synchronize: configService.get('database.synchronize'),
      }),
      inject: [ConfigService],
    }),
    TypeOrmModule.forFeature([User, Role, Permission]),
  ],
  exports: [TypeOrmModule],
})
export class DatabaseModule {}

2. 单元测试:补全 AuthService 的测试模块

在这个例子中,我们将为 AuthService 编写测试。我们会使用 Jest 模拟 UsersServiceJwtService,来验证 AuthService 是否按预期工作。

src/auth/auth.service.spec.ts

import { Test, TestingModule } from '@nestjs/testing';
import { AuthService } from './auth.service';
import { UsersService } from '../users/users.service';
import { JwtService } from '@nestjs/jwt';
import { User } from '../database/entities/user.entity';

describe('AuthService', () => {
  let authService: AuthService;
  let usersService: UsersService;
  let jwtService: JwtService;

  beforeEach(async () => {
    const mockUsersService = {
      findOne: jest.fn().mockResolvedValue({
        id: 1,
        username: 'test',
        password: 'test123',
        roles: [],
      } as User),
    };

    const mockJwtService = {
      sign: jest.fn().mockReturnValue('jwt-token'),
    };

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        AuthService,
        { provide: UsersService, useValue: mockUsersService },
        { provide: JwtService, useValue: mockJwtService },
      ],
    }).compile();

    authService = module.get<AuthService>(AuthService);
    usersService = module.get<UsersService>(UsersService);
    jwtService = module.get<JwtService>(JwtService);
  });

  it('should be defined', () => {
    expect(authService).toBeDefined();
  });

  it('should return a JWT token when login is successful', async () => {
    const result = await authService.login({ username: 'test', password: 'test123' });
    expect(result.access_token).toEqual('jwt-token');
  });

  it('should validate user and return user info', async () => {
    const user = await authService.validateUser('test', 'test123');
    expect(user).toBeDefined();
    expect(user.username).toBe('test');
  });

  it('should return null if user credentials are invalid', async () => {
    const user = await authService.validateUser('test', 'wrong-password');
    expect(user).toBeNull();
  });

  it('should call findOne method of UsersService', async () => {
    await authService.validateUser('test', 'test123');
    expect(usersService.findOne).toHaveBeenCalledWith('test');
  });

  it('should call sign method of JwtService', async () => {
    await authService.login({ username: 'test', password: 'test123' });
    expect(jwtService.sign).toHaveBeenCalledWith({ username: 'test', sub: 1 });
  });
});

3. 环境变量配置

为了避免将敏感信息硬编码到代码中,您可以在项目根目录下创建 .env 文件,存储数据库配置信息,并确保这些值在项目中能够访问到。

.env 示例:

DB_HOST=localhost
DB_PORT=3306
DB_USERNAME=root
DB_PASSWORD=password
DB_NAME=enterprise_db
NODE_ENV=development

使 ConfigModule 加载 .env 文件

NestJS 的 ConfigModule 默认会自动加载 .env 文件。确保在 app.module.ts 中已启用 ConfigModule.forRoot()

ConfigModule.forRoot({
  isGlobal: true,  // 确保配置文件全局可用
  envFilePath: '.env', // 加载 .env 文件
})