13 KiB
13 KiB
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/* # 紧急修复分支
开发流程
-
创建功能分支
git checkout develop git pull origin develop git checkout -b feature/new-feature -
开发功能
- 编写代码
- 编写测试
- 运行测试确保通过
-
提交代码
git add . git commit -m "feat: add new feature" -
推送并创建 PR
git push origin feature/new-feature # 在 GitHub 创建 Pull Request
提交规范
<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: 数据库连接失败
检查:
- PostgreSQL 服务是否启动
.env中 DATABASE_URL 是否正确- 数据库是否存在
# 创建数据库
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
}
}