651 lines
13 KiB
Markdown
651 lines
13 KiB
Markdown
# 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/)
|