rwadurian/backend/services/planting-service/docs/DEVELOPMENT.md

13 KiB
Raw Blame History

Planting Service 开发指南

目录


环境要求

必需工具

工具 版本 用途
Node.js ≥ 20.x 运行时
npm ≥ 10.x 包管理
PostgreSQL ≥ 16.x 数据库
Docker ≥ 24.x 容器化 (可选)
Git ≥ 2.x 版本控制

推荐 IDE

  • VS Code + 推荐插件:
    • ESLint
    • Prettier
    • Prisma
    • GitLens
    • REST Client

项目设置

1. 克隆项目

git clone <repository-url>
cd backend/services/planting-service

2. 安装依赖

npm install
# 或使用 make
make install

3. 环境配置

复制环境配置文件:

cp .env.example .env

编辑 .env 文件:

# 数据库
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/rwadurian_planting?schema=public"

# JWT
JWT_SECRET="your-secret-key"

# 服务端口
PORT=3003

# 外部服务
WALLET_SERVICE_URL=http://localhost:3002
IDENTITY_SERVICE_URL=http://localhost:3001
REFERRAL_SERVICE_URL=http://localhost:3004

4. 数据库设置

# 生成 Prisma Client
npx prisma generate

# 运行数据库迁移
npx prisma migrate dev

# (可选) 打开 Prisma Studio
npx prisma studio

5. 启动开发服务器

npm run start:dev
# 或使用 make
make dev

服务将在 http://localhost:3003 启动。


开发流程

Git 分支策略

main                 # 生产分支
├── develop          # 开发分支
    ├── feature/*    # 功能分支
    ├── bugfix/*     # 修复分支
    └── hotfix/*     # 紧急修复分支

开发流程

  1. 创建功能分支

    git checkout develop
    git pull origin develop
    git checkout -b feature/new-feature
    
  2. 开发功能

    • 编写代码
    • 编写测试
    • 运行测试确保通过
  3. 提交代码

    git add .
    git commit -m "feat: add new feature"
    
  4. 推送并创建 PR

    git push origin feature/new-feature
    # 在 GitHub 创建 Pull Request
    

提交规范

使用 Conventional Commits:

<type>(<scope>): <description>

[optional body]

[optional footer]

类型 (type):

  • feat: 新功能
  • fix: Bug 修复
  • docs: 文档更新
  • style: 代码格式
  • refactor: 重构
  • test: 测试
  • chore: 构建/工具

示例:

feat(order): add province-city selection
fix(payment): fix balance check logic
docs(api): update API documentation
test(order): add integration tests

代码规范

TypeScript 规范

// 1. 使用明确的类型声明
function createOrder(userId: bigint, treeCount: number): PlantingOrder {
  // ...
}

// 2. 使用 readonly 保护不可变数据
class TreeCount {
  constructor(private readonly _value: number) {}
}

// 3. 使用接口定义契约
interface IPlantingOrderRepository {
  save(order: PlantingOrder): Promise<void>;
  findById(id: bigint): Promise<PlantingOrder | null>;
}

// 4. 使用枚举定义常量
enum PlantingOrderStatus {
  CREATED = 'CREATED',
  PAID = 'PAID',
}

命名规范

类型 规范 示例
类名 PascalCase PlantingOrder
接口 I + PascalCase IPlantingOrderRepository
方法 camelCase createOrder
常量 UPPER_SNAKE_CASE MAX_TREE_COUNT
文件 kebab-case planting-order.aggregate.ts
目录 kebab-case value-objects

文件命名约定

*.aggregate.ts       # 聚合根
*.entity.ts          # 实体
*.vo.ts              # 值对象
*.service.ts         # 服务
*.repository.*.ts    # 仓储
*.controller.ts      # 控制器
*.dto.ts             # 数据传输对象
*.spec.ts            # 单元测试
*.integration.spec.ts # 集成测试
*.e2e-spec.ts        # E2E 测试
*.mapper.ts          # 映射器
*.guard.ts           # 守卫
*.filter.ts          # 过滤器

ESLint + Prettier

项目已配置 ESLint 和 Prettier:

# 运行 lint
npm run lint

# 格式化代码
npm run format

领域开发指南

创建新的聚合根

// src/domain/aggregates/new-aggregate.aggregate.ts

export interface NewAggregateData {
  id?: bigint;
  name: string;
  // ... 其他字段
}

export class NewAggregate {
  private _id?: bigint;
  private _name: string;
  private _domainEvents: DomainEvent[] = [];

  private constructor(data: NewAggregateData) {
    this._id = data.id;
    this._name = data.name;
  }

  // 工厂方法 - 创建新实例
  static create(name: string): NewAggregate {
    const aggregate = new NewAggregate({ name });
    aggregate.addDomainEvent(new NewAggregateCreatedEvent(aggregate));
    return aggregate;
  }

  // 工厂方法 - 从持久化重建
  static reconstitute(data: NewAggregateData): NewAggregate {
    return new NewAggregate(data);
  }

  // 业务方法
  doSomething(): void {
    // 业务逻辑
    this.addDomainEvent(new SomethingHappenedEvent(this));
  }

  // Getters
  get id(): bigint | undefined { return this._id; }
  get name(): string { return this._name; }

  // 领域事件
  private addDomainEvent(event: DomainEvent): void {
    this._domainEvents.push(event);
  }

  getDomainEvents(): DomainEvent[] {
    return [...this._domainEvents];
  }

  clearDomainEvents(): void {
    this._domainEvents = [];
  }
}

创建值对象

// src/domain/value-objects/email.vo.ts

export class Email {
  private readonly _value: string;

  private constructor(value: string) {
    this._value = value;
  }

  static create(value: string): Email {
    if (!this.isValid(value)) {
      throw new Error('Invalid email format');
    }
    return new Email(value);
  }

  private static isValid(value: string): boolean {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(value);
  }

  get value(): string {
    return this._value;
  }

  equals(other: Email): boolean {
    return this._value === other._value;
  }

  toString(): string {
    return this._value;
  }
}

创建领域服务

// src/domain/services/pricing.service.ts

import { Injectable } from '@nestjs/common';

@Injectable()
export class PricingDomainService {
  private readonly PRICE_PER_TREE = 2199;

  calculateTotalPrice(treeCount: number): number {
    return treeCount * this.PRICE_PER_TREE;
  }

  calculateDiscount(treeCount: number): number {
    if (treeCount >= 100) return 0.05;
    if (treeCount >= 50) return 0.03;
    if (treeCount >= 10) return 0.01;
    return 0;
  }
}

创建仓储

1. 定义接口 (领域层)

// src/domain/repositories/new-aggregate.repository.interface.ts

export const NEW_AGGREGATE_REPOSITORY = Symbol('INewAggregateRepository');

export interface INewAggregateRepository {
  save(aggregate: NewAggregate): Promise<void>;
  findById(id: bigint): Promise<NewAggregate | null>;
  findByName(name: string): Promise<NewAggregate[]>;
}

2. 实现仓储 (基础设施层)

// src/infrastructure/persistence/repositories/new-aggregate.repository.impl.ts

import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { INewAggregateRepository } from '../../../domain/repositories/new-aggregate.repository.interface';

@Injectable()
export class NewAggregateRepositoryImpl implements INewAggregateRepository {
  constructor(private readonly prisma: PrismaService) {}

  async save(aggregate: NewAggregate): Promise<void> {
    const data = NewAggregateMapper.toPersistence(aggregate);

    if (aggregate.id) {
      await this.prisma.newAggregate.update({
        where: { id: aggregate.id },
        data,
      });
    } else {
      const created = await this.prisma.newAggregate.create({ data });
      aggregate.setId(created.id);
    }
  }

  async findById(id: bigint): Promise<NewAggregate | null> {
    const record = await this.prisma.newAggregate.findUnique({
      where: { id },
    });
    return record ? NewAggregateMapper.toDomain(record) : null;
  }

  async findByName(name: string): Promise<NewAggregate[]> {
    const records = await this.prisma.newAggregate.findMany({
      where: { name: { contains: name } },
    });
    return records.map(NewAggregateMapper.toDomain);
  }
}

3. 注册到模块

// src/infrastructure/infrastructure.module.ts

@Module({
  providers: [
    {
      provide: NEW_AGGREGATE_REPOSITORY,
      useClass: NewAggregateRepositoryImpl,
    },
  ],
  exports: [NEW_AGGREGATE_REPOSITORY],
})
export class InfrastructureModule {}

常用命令

Makefile 命令

# 开发
make install          # 安装依赖
make dev              # 启动开发服务器
make build            # 构建项目

# 数据库
make prisma-generate  # 生成 Prisma Client
make prisma-migrate   # 运行迁移
make prisma-studio    # 打开 Prisma Studio
make prisma-reset     # 重置数据库

# 测试
make test-unit        # 单元测试
make test-integration # 集成测试
make test-e2e         # E2E 测试
make test-cov         # 测试覆盖率
make test-all         # 运行所有测试

# Docker
make docker-build     # 构建 Docker 镜像
make docker-up        # 启动容器
make docker-down      # 停止容器
make test-docker-all  # Docker 中运行测试

# 代码质量
make lint             # 运行 ESLint
make format           # 格式化代码

npm 命令

npm run start         # 启动服务
npm run start:dev     # 开发模式
npm run start:debug   # 调试模式
npm run build         # 构建
npm run test          # 运行测试
npm run test:watch    # 监听模式测试
npm run test:cov      # 覆盖率测试
npm run test:e2e      # E2E 测试
npm run lint          # 代码检查
npm run format        # 代码格式化

调试技巧

VS Code 调试配置

创建 .vscode/launch.json:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Debug NestJS",
      "type": "node",
      "request": "launch",
      "runtimeArgs": [
        "--inspect-brk",
        "-r",
        "tsconfig-paths/register",
        "-r",
        "ts-node/register"
      ],
      "args": ["${workspaceFolder}/src/main.ts"],
      "cwd": "${workspaceFolder}",
      "console": "integratedTerminal",
      "protocol": "inspector"
    },
    {
      "name": "Debug Jest Tests",
      "type": "node",
      "request": "launch",
      "runtimeArgs": [
        "--inspect-brk",
        "${workspaceRoot}/node_modules/.bin/jest",
        "--runInBand"
      ],
      "console": "integratedTerminal"
    }
  ]
}

Prisma 查询日志

.env 中启用:

DATABASE_URL="postgresql://...?connection_limit=5"

prisma.service.ts 中配置:

super({
  log: ['query', 'info', 'warn', 'error'],
});

请求日志

使用 NestJS Logger:

import { Logger } from '@nestjs/common';

@Injectable()
export class PlantingApplicationService {
  private readonly logger = new Logger(PlantingApplicationService.name);

  async createOrder(userId: bigint, treeCount: number) {
    this.logger.log(`Creating order for user ${userId}, trees: ${treeCount}`);
    // ...
    this.logger.debug('Order created successfully', { orderNo });
  }
}

常见问题

Q: Prisma Client 未生成

# 解决方案
npx prisma generate

Q: 数据库连接失败

检查:

  1. PostgreSQL 服务是否启动
  2. .env 中 DATABASE_URL 是否正确
  3. 数据库是否存在
# 创建数据库
createdb rwadurian_planting

Q: BigInt 序列化错误

在返回 JSON 时BigInt 需要转换为字符串:

// 在响应 DTO 中
class OrderResponse {
  @Transform(({ value }) => value.toString())
  userId: string;
}

Q: 测试时数据库冲突

使用独立的测试数据库:

# .env.test
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/rwadurian_planting_test"

Q: 循环依赖错误

使用 forwardRef:

@Module({
  imports: [forwardRef(() => OtherModule)],
})
export class MyModule {}

Q: 热重载不工作

检查 nest-cli.json:

{
  "compilerOptions": {
    "deleteOutDir": true,
    "webpack": true,
    "watchAssets": true
  }
}

参考资料