feat: 添加设计文件packages

This commit is contained in:
R524809
2026-02-11 16:01:42 +08:00
parent 161781cbbd
commit 571465cfbb
26 changed files with 15808 additions and 5 deletions

View File

@@ -0,0 +1,865 @@
# 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 安装依赖
```bash
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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 事务处理示例
```typescript
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 安装依赖
```bash
npm install @prisma/client
npm install -D prisma
npx prisma init
```
### 4.2 Prisma Schema 示例
#### prisma/schema.prisma
```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
```typescript
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 文件
```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 脚本
```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 迁移
#### 创建迁移
```bash
npm run typeorm migration:generate -- -n CreateUsersTable
```
#### 迁移文件示例
```typescript
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 迁移
```bash
# 创建迁移
npx prisma migrate dev --name init
# 应用迁移
npx prisma migrate deploy
# 查看迁移状态
npx prisma migrate status
```
---
## 七、性能优化建议
### 7.1 连接池配置
```typescript
// TypeORM
{
type: 'postgres',
// ... 其他配置
extra: {
max: 20, // 最大连接数
min: 5, // 最小连接数
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
},
}
```
### 7.2 查询优化
```typescript
// 使用 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 快速开始
```bash
# 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 参考资料
- [NestJS 官方文档 - TypeORM](https://docs.nestjs.com/techniques/database)
- [TypeORM 官方文档](https://typeorm.io/)
- [PostgreSQL 官方文档](https://www.postgresql.org/docs/)
---
**文档版本**v1.0
**创建日期**2024年