Files
invest-mind-store/packages/design-document/机生文档/NestJS与PostgreSQL集成方案.md
2026-02-11 16:01:42 +08:00

21 KiB
Raw Blame History

NestJS 与 PostgreSQL 集成方案

一、兼容性说明

PostgreSQL 完全支持 NestJS

简短回答PostgreSQL 与 NestJS 完全兼容,并且是 NestJS 官方推荐和广泛使用的数据库组合。

1.1 官方支持

NestJS 官方文档中明确支持 PostgreSQL主要通过以下 ORM 库:

  1. TypeORM - NestJS 官方推荐,最常用
  2. Prisma - 现代化 ORM类型安全
  3. Sequelize - 成熟的 ORM 库
  4. Knex.js - SQL 查询构建器
  5. 原生 pg 驱动 - 直接使用 PostgreSQL 驱动

1.2 为什么选择 NestJS + PostgreSQL

  1. 完美兼容NestJS 的依赖注入和模块化架构与 PostgreSQL 配合良好
  2. TypeScript 支持:两者都原生支持 TypeScript类型安全
  3. 生态成熟:有丰富的库和工具支持
  4. 性能优秀PostgreSQL 性能强大NestJS 框架高效
  5. 开发体验好:代码生成、迁移工具完善

二、推荐方案对比

2.1 TypeORM最推荐

优势:

  • NestJS 官方文档示例主要使用 TypeORM
  • 装饰器语法,与 NestJS 风格一致
  • 支持数据库迁移Migration
  • 支持实体关系映射ORM
  • 支持事务管理
  • 支持查询构建器

适用场景:

  • 需要完整的 ORM 功能
  • 团队熟悉装饰器语法
  • 需要数据库迁移管理

2.2 Prisma现代化选择

优势:

  • 类型安全TypeScript 支持最好
  • 代码生成,自动生成类型定义
  • 迁移工具强大
  • 性能优秀
  • 开发体验极佳

适用场景:

  • 重视类型安全
  • 需要快速开发
  • 团队喜欢现代化工具

2.3 Sequelize成熟稳定

优势:

  • 非常成熟,生态丰富
  • 文档完善
  • 支持多种数据库

劣势:

  • ⚠️ TypeScript 支持不如 Prisma
  • ⚠️ 语法相对传统

三、TypeORM 集成方案(推荐)

3.1 安装依赖

npm install @nestjs/typeorm typeorm pg
npm install --save-dev @types/pg

3.2 项目结构

src/
├── app.module.ts
├── database/
│   ├── database.module.ts
│   └── database.config.ts
├── users/
│   ├── entities/
│   │   └── user.entity.ts
│   ├── users.module.ts
│   ├── users.service.ts
│   └── users.controller.ts
├── accounts/
│   ├── entities/
│   │   └── account.entity.ts
│   └── ...
└── transactions/
    ├── entities/
    │   └── transaction.entity.ts
    └── ...

3.3 配置数据库连接

database/database.config.ts

import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { ConfigService } from '@nestjs/config';

export const getDatabaseConfig = (
  configService: ConfigService,
): TypeOrmModuleOptions => ({
  type: 'postgres',
  host: configService.get('DB_HOST', 'localhost'),
  port: configService.get('DB_PORT', 5432),
  username: configService.get('DB_USERNAME', 'postgres'),
  password: configService.get('DB_PASSWORD', 'password'),
  database: configService.get('DB_NAME', 'vestmind'),
  entities: [__dirname + '/../**/*.entity{.ts,.js}'],
  synchronize: configService.get('NODE_ENV') !== 'production', // 生产环境设为 false
  logging: configService.get('NODE_ENV') === 'development',
  migrations: [__dirname + '/../migrations/**/*{.ts,.js}'],
  migrationsRun: false,
  ssl: configService.get('DB_SSL', false),
});

database/database.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { getDatabaseConfig } from './database.config';

@Module({
  imports: [
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: getDatabaseConfig,
      inject: [ConfigService],
    }),
  ],
})
export class DatabaseModule {}

app.module.ts

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { DatabaseModule } from './database/database.module';
import { UsersModule } from './users/users.module';
import { AccountsModule } from './accounts/accounts.module';
import { TransactionsModule } from './transactions/transactions.module';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: '.env',
    }),
    DatabaseModule,
    UsersModule,
    AccountsModule,
    TransactionsModule,
  ],
})
export class AppModule {}

3.4 实体定义示例

users/entities/user.entity.ts

import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  CreateDateColumn,
  UpdateDateColumn,
  OneToMany,
} from 'typeorm';
import { Account } from '../../accounts/entities/account.entity';

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

  @Column({ type: 'varchar', length: 50, unique: true })
  username: string;

  @Column({ type: 'varchar', length: 100, unique: true, nullable: true })
  email: string;

  @Column({ type: 'varchar', length: 20, unique: true, nullable: true })
  phone: string;

  @Column({ type: 'varchar', length: 255 })
  passwordHash: string;

  @Column({ type: 'varchar', length: 50, nullable: true })
  nickname: string;

  @Column({ type: 'varchar', length: 255, nullable: true })
  avatarUrl: string;

  @Column({ type: 'varchar', length: 20, default: 'active' })
  status: string;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;

  @Column({ type: 'timestamp', nullable: true })
  lastLoginAt: Date;

  // 关系
  @OneToMany(() => Account, (account) => account.user)
  accounts: Account[];
}

accounts/entities/account.entity.ts

import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  CreateDateColumn,
  UpdateDateColumn,
  ManyToOne,
  OneToMany,
  JoinColumn,
} from 'typeorm';
import { User } from '../../users/entities/user.entity';
import { Position } from '../../positions/entities/position.entity';
import { Transaction } from '../../transactions/entities/transaction.entity';

@Entity('accounts')
export class Account {
  @PrimaryGeneratedColumn('increment')
  id: number;

  @Column({ type: 'int' })
  userId: number;

  @Column({ type: 'varchar', length: 100 })
  name: string;

  @Column({ type: 'varchar', length: 20 })
  type: string; // stock, fund, cash, mixed

  @Column({ type: 'varchar', length: 10, default: 'CNY' })
  currency: string;

  @Column({ type: 'text', nullable: true })
  description: string;

  @Column({ type: 'varchar', length: 20, default: 'active' })
  status: string;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;

  // 关系
  @ManyToOne(() => User, (user) => user.accounts)
  @JoinColumn({ name: 'userId' })
  user: User;

  @OneToMany(() => Position, (position) => position.account)
  positions: Position[];

  @OneToMany(() => Transaction, (transaction) => transaction.account)
  transactions: Transaction[];
}

transactions/entities/transaction.entity.ts

import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  CreateDateColumn,
  UpdateDateColumn,
  ManyToOne,
  JoinColumn,
  Index,
} from 'typeorm';
import { User } from '../../users/entities/user.entity';
import { Account } from '../../accounts/entities/account.entity';
import { Position } from '../../positions/entities/position.entity';
import { TradingPlan } from '../../trading-plans/entities/trading-plan.entity';

@Entity('transactions')
@Index(['userId', 'date', 'createdAt'])
@Index(['accountId'])
@Index(['positionId'])
export class Transaction {
  @PrimaryGeneratedColumn('increment')
  id: number;

  @Column({ type: 'int' })
  userId: number;

  @Column({ type: 'int' })
  accountId: number;

  @Column({ type: 'int', nullable: true })
  positionId: number;

  @Column({ type: 'int', nullable: true })
  tradingPlanId: number;

  @Column({ type: 'varchar', length: 20 })
  type: string; // buy, sell, dividend, split, bonus, rights, deposit, withdraw

  @Column({ type: 'date' })
  date: Date;

  @Column({ type: 'varchar', length: 20, nullable: true })
  symbol: string;

  @Column({ type: 'varchar', length: 100, nullable: true })
  name: string;

  @Column({ type: 'varchar', length: 20, nullable: true })
  market: string;

  @Column({ type: 'decimal', precision: 18, scale: 4, nullable: true })
  shares: number;

  @Column({ type: 'decimal', precision: 18, scale: 4, nullable: true })
  price: number;

  @Column({ type: 'decimal', precision: 18, scale: 2 })
  amount: number;

  @Column({ type: 'decimal', precision: 18, scale: 2, default: 0 })
  fee: number;

  @Column({ type: 'varchar', length: 10, default: 'CNY' })
  currency: string;

  @Column({ type: 'decimal', precision: 10, scale: 6, default: 1 })
  exchangeRate: number;

  @Column({ type: 'jsonb', nullable: true })
  metadata: Record<string, any>;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;

  // 关系
  @ManyToOne(() => User)
  @JoinColumn({ name: 'userId' })
  user: User;

  @ManyToOne(() => Account)
  @JoinColumn({ name: 'accountId' })
  account: Account;

  @ManyToOne(() => Position, { nullable: true })
  @JoinColumn({ name: 'positionId' })
  position: Position;

  @ManyToOne(() => TradingPlan, { nullable: true })
  @JoinColumn({ name: 'tradingPlanId' })
  tradingPlan: TradingPlan;
}

3.5 Service 使用示例

transactions/transactions.service.ts

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, Between } from 'typeorm';
import { Transaction } from './entities/transaction.entity';
import { CreateTransactionDto } from './dto/create-transaction.dto';

@Injectable()
export class TransactionsService {
  constructor(
    @InjectRepository(Transaction)
    private transactionRepository: Repository<Transaction>,
  ) {}

  async create(createTransactionDto: CreateTransactionDto): Promise<Transaction> {
    const transaction = this.transactionRepository.create(createTransactionDto);
    return await this.transactionRepository.save(transaction);
  }

  async findAll(userId: number, page: number = 1, limit: number = 20) {
    const [data, total] = await this.transactionRepository.findAndCount({
      where: { userId },
      relations: ['account', 'position', 'tradingPlan'],
      order: { date: 'DESC', createdAt: 'DESC' },
      skip: (page - 1) * limit,
      take: limit,
    });

    return {
      data,
      total,
      page,
      limit,
      totalPages: Math.ceil(total / limit),
    };
  }

  async findOne(id: number, userId: number): Promise<Transaction> {
    return await this.transactionRepository.findOne({
      where: { id, userId },
      relations: ['account', 'position', 'tradingPlan'],
    });
  }

  async findByDateRange(
    userId: number,
    startDate: Date,
    endDate: Date,
  ): Promise<Transaction[]> {
    return await this.transactionRepository.find({
      where: {
        userId,
        date: Between(startDate, endDate),
      },
      order: { date: 'DESC' },
    });
  }

  async update(id: number, userId: number, updateData: Partial<Transaction>) {
    await this.transactionRepository.update({ id, userId }, updateData);
    return this.findOne(id, userId);
  }

  async remove(id: number, userId: number): Promise<void> {
    await this.transactionRepository.delete({ id, userId });
  }
}

3.6 Module 配置

transactions/transactions.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TransactionsService } from './transactions.service';
import { TransactionsController } from './transactions.controller';
import { Transaction } from './entities/transaction.entity';

@Module({
  imports: [TypeOrmModule.forFeature([Transaction])],
  controllers: [TransactionsController],
  providers: [TransactionsService],
  exports: [TransactionsService],
})
export class TransactionsModule {}

3.7 事务处理示例

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import { Transaction } from './entities/transaction.entity';
import { Position } from '../positions/entities/position.entity';
import { CashAccount } from '../cash-accounts/entities/cash-account.entity';

@Injectable()
export class TradingService {
  constructor(
    private dataSource: DataSource,
    @InjectRepository(Transaction)
    private transactionRepository: Repository<Transaction>,
  ) {}

  async buyStock(buyDto: CreateBuyDto) {
    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();

    try {
      // 1. 创建交易记录
      const transaction = this.transactionRepository.create({
        ...buyDto,
        type: 'buy',
      });
      await queryRunner.manager.save(transaction);

      // 2. 更新持仓
      const position = await queryRunner.manager.findOne(Position, {
        where: { accountId: buyDto.accountId, symbol: buyDto.symbol },
      });

      if (position) {
        // 更新现有持仓
        const newShares = position.shares + buyDto.shares;
        const newCostPrice =
          (position.shares * position.costPrice +
            buyDto.shares * buyDto.price +
            buyDto.fee) /
          newShares;
        position.shares = newShares;
        position.costPrice = newCostPrice;
        await queryRunner.manager.save(position);
      } else {
        // 创建新持仓
        const newPosition = queryRunner.manager.create(Position, {
          accountId: buyDto.accountId,
          symbol: buyDto.symbol,
          name: buyDto.name,
          market: buyDto.market,
          shares: buyDto.shares,
          costPrice: buyDto.price,
          currentPrice: buyDto.price,
        });
        await queryRunner.manager.save(newPosition);
      }

      // 3. 更新现金账户
      const cashAccount = await queryRunner.manager.findOne(CashAccount, {
        where: { accountId: buyDto.accountId },
      });
      cashAccount.balance -= buyDto.amount + buyDto.fee;
      await queryRunner.manager.save(cashAccount);

      await queryRunner.commitTransaction();
      return transaction;
    } catch (error) {
      await queryRunner.rollbackTransaction();
      throw error;
    } finally {
      await queryRunner.release();
    }
  }
}

四、Prisma 集成方案(备选)

4.1 安装依赖

npm install @prisma/client
npm install -D prisma
npx prisma init

4.2 Prisma Schema 示例

prisma/schema.prisma

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id          BigInt    @id @default(autoincrement())
  username    String    @unique @db.VarChar(50)
  email       String?   @unique @db.VarChar(100)
  phone       String?   @unique @db.VarChar(20)
  passwordHash String   @db.VarChar(255)
  nickname    String?  @db.VarChar(50)
  avatarUrl   String?   @db.VarChar(255)
  status      String    @default("active") @db.VarChar(20)
  createdAt   DateTime  @default(now())
  updatedAt   DateTime  @updatedAt
  lastLoginAt DateTime?

  accounts    Account[]
  transactions Transaction[]

  @@map("users")
}

model Account {
  id          BigInt    @id @default(autoincrement())
  userId      BigInt
  name        String    @db.VarChar(100)
  type        String    @db.VarChar(20)
  currency    String    @default("CNY") @db.VarChar(10)
  description String?
  status      String    @default("active") @db.VarChar(20)
  createdAt   DateTime  @default(now())
  updatedAt   DateTime  @updatedAt

  user        User      @relation(fields: [userId], references: [id], onDelete: Cascade)
  positions   Position[]
  transactions Transaction[]

  @@map("accounts")
}

model Transaction {
  id            BigInt    @id @default(autoincrement())
  userId        BigInt
  accountId     BigInt
  positionId    BigInt?
  tradingPlanId BigInt?
  type          String    @db.VarChar(20)
  date          DateTime  @db.Date
  symbol        String?   @db.VarChar(20)
  name          String?   @db.VarChar(100)
  market        String?   @db.VarChar(20)
  shares        Decimal?  @db.Decimal(18, 4)
  price         Decimal?  @db.Decimal(18, 4)
  amount        Decimal   @db.Decimal(18, 2)
  fee           Decimal   @default(0) @db.Decimal(18, 2)
  currency      String    @default("CNY") @db.VarChar(10)
  exchangeRate  Decimal   @default(1) @db.Decimal(10, 6)
  metadata      Json?
  createdAt     DateTime  @default(now())
  updatedAt     DateTime  @updatedAt

  user          User      @relation(fields: [userId], references: [id], onDelete: Cascade)
  account       Account   @relation(fields: [accountId], references: [id], onDelete: Cascade)

  @@index([userId, date, createdAt])
  @@index([accountId])
  @@index([positionId])
  @@map("transactions")
}

4.3 Prisma Service

prisma/prisma.service.ts

import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService
  extends PrismaClient
  implements OnModuleInit, OnModuleDestroy
{
  async onModuleInit() {
    await this.$connect();
  }

  async onModuleDestroy() {
    await this.$disconnect();
  }
}

五、环境配置

5.1 .env 文件

# 数据库配置
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=postgres
DB_PASSWORD=your_password
DB_NAME=vestmind
DB_SSL=false

# 或者使用连接字符串Prisma
DATABASE_URL="postgresql://postgres:password@localhost:5432/vestmind?schema=public"

# 应用配置
NODE_ENV=development
PORT=3000

5.2 package.json 脚本

{
  "scripts": {
    "start": "nest start",
    "start:dev": "nest start --watch",
    "start:prod": "node dist/main",
    "typeorm": "typeorm-ts-node-commonjs",
    "migration:generate": "typeorm-ts-node-commonjs migration:generate",
    "migration:run": "typeorm-ts-node-commonjs migration:run",
    "migration:revert": "typeorm-ts-node-commonjs migration:revert",
    "prisma:generate": "prisma generate",
    "prisma:migrate": "prisma migrate dev",
    "prisma:studio": "prisma studio"
  }
}

六、数据库迁移

6.1 TypeORM 迁移

创建迁移

npm run typeorm migration:generate -- -n CreateUsersTable

迁移文件示例

import { MigrationInterface, QueryRunner } from 'typeorm';

export class CreateUsersTable1234567890 implements MigrationInterface {
  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`
      CREATE TABLE users (
        id BIGSERIAL PRIMARY KEY,
        username VARCHAR(50) UNIQUE NOT NULL,
        email VARCHAR(100) UNIQUE,
        password_hash VARCHAR(255) NOT NULL,
        created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
        updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
      );
    `);
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`DROP TABLE users;`);
  }
}

6.2 Prisma 迁移

# 创建迁移
npx prisma migrate dev --name init

# 应用迁移
npx prisma migrate deploy

# 查看迁移状态
npx prisma migrate status

七、性能优化建议

7.1 连接池配置

// TypeORM
{
  type: 'postgres',
  // ... 其他配置
  extra: {
    max: 20, // 最大连接数
    min: 5,  // 最小连接数
    idleTimeoutMillis: 30000,
    connectionTimeoutMillis: 2000,
  },
}

7.2 查询优化

// 使用 select 只查询需要的字段
const users = await userRepository.find({
  select: ['id', 'username', 'email'],
});

// 使用 relations 预加载关联数据
const transaction = await transactionRepository.findOne({
  where: { id },
  relations: ['account', 'position'],
});

// 使用 QueryBuilder 进行复杂查询
const result = await transactionRepository
  .createQueryBuilder('transaction')
  .leftJoinAndSelect('transaction.account', 'account')
  .where('transaction.userId = :userId', { userId })
  .andWhere('transaction.date BETWEEN :start AND :end', { start, end })
  .orderBy('transaction.date', 'DESC')
  .getMany();

八、总结

8.1 推荐方案

对于您的项目,推荐使用 TypeORM + PostgreSQL

  1. NestJS 官方推荐,文档完善
  2. 装饰器语法,与 NestJS 风格一致
  3. 支持复杂的实体关系
  4. 支持事务管理
  5. 迁移工具完善

8.2 快速开始

# 1. 创建 NestJS 项目
npm i -g @nestjs/cli
nest new vest-mind-backend

# 2. 安装依赖
cd vest-mind-backend
npm install @nestjs/typeorm typeorm pg
npm install --save-dev @types/pg

# 3. 配置数据库连接
# 参考上面的 database.module.ts

# 4. 创建实体
nest g module users
nest g service users
nest g controller users

# 5. 运行项目
npm run start:dev

8.3 参考资料


文档版本v1.0
创建日期2024年