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

651 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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/)