# 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. 克隆项目

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

### 2. 安装依赖

```bash
npm install
# 或使用 make
make install
```

### 3. 环境配置

复制环境配置文件:

```bash
cp .env.example .env
```

编辑 `.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. 数据库设置

```bash
# 生成 Prisma Client
npx prisma generate

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

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

### 5. 启动开发服务器

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

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

---

## 开发流程

### Git 分支策略

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

### 开发流程

1. **创建功能分支**
   ```bash
   git checkout develop
   git pull origin develop
   git checkout -b feature/new-feature
   ```

2. **开发功能**
   - 编写代码
   - 编写测试
   - 运行测试确保通过

3. **提交代码**
   ```bash
   git add .
   git commit -m "feat: add new feature"
   ```

4. **推送并创建 PR**
   ```bash
   git push origin feature/new-feature
   # 在 GitHub 创建 Pull Request
   ```

### 提交规范

使用 [Conventional Commits](https://www.conventionalcommits.org/):

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

[optional body]

[optional footer]
```

**类型 (type)**:
- `feat`: 新功能
- `fix`: Bug 修复
- `docs`: 文档更新
- `style`: 代码格式
- `refactor`: 重构
- `test`: 测试
- `chore`: 构建/工具

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

---

## 代码规范

### TypeScript 规范

```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:

```bash
# 运行 lint
npm run lint

# 格式化代码
npm run format
```

---

## 领域开发指南

### 创建新的聚合根

```typescript
// 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 = [];
  }
}
```

### 创建值对象

```typescript
// 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;
  }
}
```

### 创建领域服务

```typescript
// 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. 定义接口 (领域层)**

```typescript
// 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. 实现仓储 (基础设施层)**

```typescript
// 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. 注册到模块**

```typescript
// src/infrastructure/infrastructure.module.ts

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

---

## 常用命令

### Makefile 命令

```bash
# 开发
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 命令

```bash
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`:

```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` 中启用:

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

在 `prisma.service.ts` 中配置:

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

### 请求日志

使用 NestJS Logger:

```typescript
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 未生成

```bash
# 解决方案
npx prisma generate
```

### Q: 数据库连接失败

检查:
1. PostgreSQL 服务是否启动
2. `.env` 中 DATABASE_URL 是否正确
3. 数据库是否存在

```bash
# 创建数据库
createdb rwadurian_planting
```

### Q: BigInt 序列化错误

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

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

### Q: 测试时数据库冲突

使用独立的测试数据库:

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

### Q: 循环依赖错误

使用 `forwardRef`:

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

### Q: 热重载不工作

检查 `nest-cli.json`:

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

---

## 参考资料

- [NestJS 官方文档](https://docs.nestjs.com/)
- [Prisma 官方文档](https://www.prisma.io/docs/)
- [TypeScript 手册](https://www.typescriptlang.org/docs/)
- [领域驱动设计精粹](https://www.domainlanguage.com/ddd/)
