494 lines
10 KiB
Markdown
494 lines
10 KiB
Markdown
# 开发指南
|
||
|
||
## 1. 环境要求
|
||
|
||
### 1.1 必需软件
|
||
|
||
| 软件 | 版本 | 用途 |
|
||
|------|------|------|
|
||
| Node.js | 20.x LTS | 运行时环境 |
|
||
| npm | 10.x | 包管理器 |
|
||
| Docker | 24.x+ | 容器化 |
|
||
| Docker Compose | 2.x | 容器编排 |
|
||
| PostgreSQL | 15.x | 数据库 |
|
||
| Redis | 7.x | 缓存 |
|
||
|
||
### 1.2 推荐 IDE
|
||
|
||
- **VS Code** (推荐)
|
||
- 插件: ESLint, Prettier, Prisma, TypeScript
|
||
- **WebStorm**
|
||
- **Cursor**
|
||
|
||
## 2. 项目设置
|
||
|
||
### 2.1 克隆项目
|
||
|
||
```bash
|
||
cd backend/services
|
||
git clone <repository-url> reporting-service
|
||
cd reporting-service
|
||
```
|
||
|
||
### 2.2 安装依赖
|
||
|
||
```bash
|
||
npm install
|
||
```
|
||
|
||
### 2.3 环境配置
|
||
|
||
创建 `.env.development` 文件:
|
||
|
||
```bash
|
||
cp .env.example .env.development
|
||
```
|
||
|
||
编辑 `.env.development`:
|
||
|
||
```env
|
||
# Application
|
||
NODE_ENV=development
|
||
PORT=3000
|
||
|
||
# Database
|
||
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/rwadurian_reporting?schema=public
|
||
|
||
# Redis
|
||
REDIS_HOST=localhost
|
||
REDIS_PORT=6379
|
||
|
||
# JWT
|
||
JWT_SECRET=your-development-secret-key
|
||
|
||
# External Services (可选)
|
||
LEADERBOARD_SERVICE_URL=http://localhost:3001
|
||
PLANTING_SERVICE_URL=http://localhost:3002
|
||
```
|
||
|
||
### 2.4 数据库设置
|
||
|
||
启动 PostgreSQL (使用 Docker):
|
||
|
||
```bash
|
||
docker run -d \
|
||
--name reporting-postgres \
|
||
-e POSTGRES_USER=postgres \
|
||
-e POSTGRES_PASSWORD=postgres \
|
||
-e POSTGRES_DB=rwadurian_reporting \
|
||
-p 5432:5432 \
|
||
postgres:15-alpine
|
||
```
|
||
|
||
生成 Prisma Client:
|
||
|
||
```bash
|
||
npx prisma generate
|
||
```
|
||
|
||
同步数据库 Schema:
|
||
|
||
```bash
|
||
npx prisma db push
|
||
```
|
||
|
||
(可选) 填充种子数据:
|
||
|
||
```bash
|
||
npm run prisma:seed
|
||
```
|
||
|
||
### 2.5 启动 Redis
|
||
|
||
```bash
|
||
docker run -d \
|
||
--name reporting-redis \
|
||
-p 6379:6379 \
|
||
redis:7-alpine
|
||
```
|
||
|
||
## 3. 开发工作流
|
||
|
||
### 3.1 启动开发服务器
|
||
|
||
```bash
|
||
# 热重载模式
|
||
npm run start:dev
|
||
|
||
# 调试模式
|
||
npm run start:debug
|
||
```
|
||
|
||
服务启动后访问:
|
||
- API: http://localhost:3000/api/v1
|
||
- Swagger: http://localhost:3000/api (如已启用)
|
||
|
||
### 3.2 常用命令
|
||
|
||
```bash
|
||
# 构建项目
|
||
npm run build
|
||
|
||
# 运行 Lint
|
||
npm run lint
|
||
|
||
# 格式化代码
|
||
npm run format
|
||
|
||
# 运行测试
|
||
npm test
|
||
|
||
# 运行测试 (watch 模式)
|
||
npm run test:watch
|
||
|
||
# 生成测试覆盖率
|
||
npm run test:cov
|
||
|
||
# Prisma Studio (数据库可视化)
|
||
npm run prisma:studio
|
||
```
|
||
|
||
### 3.3 使用 Makefile
|
||
|
||
```bash
|
||
# 查看所有可用命令
|
||
make help
|
||
|
||
# 安装依赖
|
||
make install
|
||
|
||
# 构建项目
|
||
make build
|
||
|
||
# 运行所有测试
|
||
make test
|
||
|
||
# 运行单元测试
|
||
make test-unit
|
||
|
||
# 运行集成测试
|
||
make test-integration
|
||
|
||
# 运行 E2E 测试
|
||
make test-e2e
|
||
|
||
# Docker 测试
|
||
make test-docker-all
|
||
```
|
||
|
||
## 4. 代码规范
|
||
|
||
### 4.1 目录结构规范
|
||
|
||
```
|
||
src/
|
||
├── api/ # API层
|
||
│ ├── controllers/ # 每个资源一个控制器文件
|
||
│ └── dto/
|
||
│ ├── request/ # 请求 DTO
|
||
│ └── response/ # 响应 DTO
|
||
├── application/ # 应用层
|
||
│ ├── commands/ # 每个命令独立目录
|
||
│ │ └── xxx/
|
||
│ │ ├── xxx.command.ts
|
||
│ │ └── xxx.handler.ts
|
||
│ └── queries/ # 每个查询独立目录
|
||
├── domain/ # 领域层
|
||
│ ├── aggregates/ # 每个聚合独立目录
|
||
│ │ └── xxx/
|
||
│ │ ├── index.ts
|
||
│ │ ├── xxx.aggregate.ts
|
||
│ │ └── xxx.spec.ts # 单元测试
|
||
│ ├── value-objects/ # 值对象
|
||
│ │ ├── xxx.vo.ts
|
||
│ │ └── xxx.spec.ts
|
||
│ └── repositories/ # 仅接口定义
|
||
└── infrastructure/ # 基础设施层
|
||
```
|
||
|
||
### 4.2 命名规范
|
||
|
||
**文件命名**:
|
||
- 使用 kebab-case: `report-definition.aggregate.ts`
|
||
- 后缀约定:
|
||
- `.aggregate.ts` - 聚合根
|
||
- `.entity.ts` - 实体
|
||
- `.vo.ts` - 值对象
|
||
- `.service.ts` - 服务
|
||
- `.controller.ts` - 控制器
|
||
- `.dto.ts` - DTO
|
||
- `.spec.ts` - 单元测试
|
||
- `.e2e-spec.ts` - E2E 测试
|
||
- `.integration.spec.ts` - 集成测试
|
||
|
||
**类命名**:
|
||
- 使用 PascalCase
|
||
- 聚合根: `ReportSnapshot`
|
||
- 值对象: `DateRange`
|
||
- 服务: `ReportingApplicationService`
|
||
- 控制器: `ReportController`
|
||
|
||
**接口命名**:
|
||
- 以 `I` 开头: `IReportSnapshotRepository`
|
||
|
||
### 4.3 代码风格
|
||
|
||
**TypeScript 配置** (`tsconfig.json`):
|
||
|
||
```json
|
||
{
|
||
"compilerOptions": {
|
||
"strict": true,
|
||
"strictNullChecks": true,
|
||
"noImplicitAny": true,
|
||
"noUnusedLocals": true,
|
||
"noUnusedParameters": true
|
||
}
|
||
}
|
||
```
|
||
|
||
**ESLint 规则** (关键规则):
|
||
|
||
```javascript
|
||
{
|
||
rules: {
|
||
'@typescript-eslint/explicit-function-return-type': 'warn',
|
||
'@typescript-eslint/no-explicit-any': 'warn',
|
||
'@typescript-eslint/no-unused-vars': 'error',
|
||
'no-console': 'warn'
|
||
}
|
||
}
|
||
```
|
||
|
||
### 4.4 领域驱动设计规范
|
||
|
||
**聚合根规则**:
|
||
|
||
```typescript
|
||
// 1. 使用私有属性 + getter
|
||
export class ReportSnapshot {
|
||
private readonly _id: bigint;
|
||
private _snapshotData: SnapshotData;
|
||
|
||
get id(): bigint { return this._id; }
|
||
get snapshotData(): SnapshotData { return this._snapshotData; }
|
||
|
||
// 2. 使用工厂方法创建
|
||
public static create(props: CreateProps): ReportSnapshot {
|
||
const snapshot = new ReportSnapshot(props);
|
||
snapshot.addDomainEvent(new SnapshotCreatedEvent(snapshot));
|
||
return snapshot;
|
||
}
|
||
|
||
// 3. 使用 reconstitute 重建 (不触发事件)
|
||
public static reconstitute(props: ReconstitutionProps): ReportSnapshot {
|
||
return new ReportSnapshot(props);
|
||
}
|
||
|
||
// 4. 业务方法封装状态变更
|
||
public updateData(newData: SnapshotData): void {
|
||
this.validateDataUpdate(newData);
|
||
this._snapshotData = newData;
|
||
this._updatedAt = new Date();
|
||
}
|
||
|
||
// 5. 私有构造函数
|
||
private constructor(props: Props) { ... }
|
||
}
|
||
```
|
||
|
||
**值对象规则**:
|
||
|
||
```typescript
|
||
// 1. 不可变
|
||
export class DateRange {
|
||
private readonly _startDate: Date;
|
||
private readonly _endDate: Date;
|
||
|
||
private constructor(startDate: Date, endDate: Date) {
|
||
this._startDate = startDate;
|
||
this._endDate = endDate;
|
||
Object.freeze(this); // 冻结对象
|
||
}
|
||
|
||
// 2. 工厂方法 + 验证
|
||
public static create(startDate: Date, endDate: Date): DateRange {
|
||
if (startDate > endDate) {
|
||
throw new DomainException('Invalid date range');
|
||
}
|
||
return new DateRange(startDate, endDate);
|
||
}
|
||
|
||
// 3. 实现 equals 方法
|
||
public equals(other: DateRange): boolean {
|
||
return this._startDate.getTime() === other._startDate.getTime() &&
|
||
this._endDate.getTime() === other._endDate.getTime();
|
||
}
|
||
}
|
||
```
|
||
|
||
## 5. Git 工作流
|
||
|
||
### 5.1 分支策略
|
||
|
||
```
|
||
main # 生产分支
|
||
├── develop # 开发分支
|
||
│ ├── feature/xxx # 功能分支
|
||
│ ├── bugfix/xxx # 修复分支
|
||
│ └── refactor/xxx # 重构分支
|
||
└── release/x.x.x # 发布分支
|
||
```
|
||
|
||
### 5.2 提交规范
|
||
|
||
使用 Conventional Commits:
|
||
|
||
```bash
|
||
# 格式
|
||
<type>(<scope>): <subject>
|
||
|
||
# 示例
|
||
feat(report): add daily report generation
|
||
fix(export): resolve Excel formatting issue
|
||
refactor(domain): extract value object for date range
|
||
test(e2e): add report generation tests
|
||
docs(api): update API documentation
|
||
```
|
||
|
||
**Type 类型**:
|
||
- `feat`: 新功能
|
||
- `fix`: Bug 修复
|
||
- `refactor`: 代码重构
|
||
- `test`: 测试相关
|
||
- `docs`: 文档更新
|
||
- `chore`: 构建/配置变更
|
||
- `perf`: 性能优化
|
||
|
||
### 5.3 PR 规范
|
||
|
||
PR 标题遵循提交规范,PR 描述需包含:
|
||
|
||
```markdown
|
||
## Summary
|
||
简要说明本次变更内容
|
||
|
||
## Changes
|
||
- 变更点 1
|
||
- 变更点 2
|
||
|
||
## Test Plan
|
||
- [ ] 单元测试通过
|
||
- [ ] 集成测试通过
|
||
- [ ] 手动测试场景
|
||
```
|
||
|
||
## 6. 调试技巧
|
||
|
||
### 6.1 VS Code 调试配置
|
||
|
||
`.vscode/launch.json`:
|
||
|
||
```json
|
||
{
|
||
"version": "0.2.0",
|
||
"configurations": [
|
||
{
|
||
"type": "node",
|
||
"request": "launch",
|
||
"name": "Debug NestJS",
|
||
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
|
||
"args": ["${workspaceFolder}/src/main.ts"],
|
||
"sourceMaps": true,
|
||
"envFile": "${workspaceFolder}/.env.development"
|
||
},
|
||
{
|
||
"type": "node",
|
||
"request": "launch",
|
||
"name": "Debug Jest Tests",
|
||
"program": "${workspaceFolder}/node_modules/.bin/jest",
|
||
"args": ["--runInBand", "--watchAll=false"],
|
||
"console": "integratedTerminal",
|
||
"internalConsoleOptions": "neverOpen"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
### 6.2 日志调试
|
||
|
||
```typescript
|
||
import { Logger } from '@nestjs/common';
|
||
|
||
@Injectable()
|
||
export class ReportingApplicationService {
|
||
private readonly logger = new Logger(ReportingApplicationService.name);
|
||
|
||
async generateReport(dto: GenerateReportDto) {
|
||
this.logger.debug(`Generating report: ${dto.reportCode}`);
|
||
this.logger.log(`Report generated successfully`);
|
||
this.logger.warn(`Performance warning: slow query`);
|
||
this.logger.error(`Failed to generate report`, error.stack);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 6.3 Prisma 调试
|
||
|
||
启用查询日志:
|
||
|
||
```typescript
|
||
// prisma.service.ts
|
||
const prisma = new PrismaClient({
|
||
log: [
|
||
{ level: 'query', emit: 'event' },
|
||
{ level: 'error', emit: 'stdout' },
|
||
{ level: 'warn', emit: 'stdout' },
|
||
],
|
||
});
|
||
|
||
prisma.$on('query', (e) => {
|
||
console.log('Query: ' + e.query);
|
||
console.log('Params: ' + e.params);
|
||
console.log('Duration: ' + e.duration + 'ms');
|
||
});
|
||
```
|
||
|
||
## 7. 常见问题
|
||
|
||
### Q: Prisma Client 未生成
|
||
|
||
```bash
|
||
npx prisma generate
|
||
```
|
||
|
||
### Q: 数据库连接失败
|
||
|
||
1. 检查 PostgreSQL 是否运行: `docker ps`
|
||
2. 检查 DATABASE_URL 环境变量
|
||
3. 检查网络连接和端口
|
||
|
||
### Q: 测试失败 - 表不存在
|
||
|
||
```bash
|
||
# 推送 schema 到测试数据库
|
||
DATABASE_URL="postgresql://..." npx prisma db push
|
||
```
|
||
|
||
### Q: TypeScript 编译错误
|
||
|
||
```bash
|
||
# 清理并重新构建
|
||
rm -rf dist
|
||
npm run build
|
||
```
|
||
|
||
### Q: 端口被占用
|
||
|
||
```bash
|
||
# 查找占用端口的进程
|
||
lsof -i :3000
|
||
# 或在 Windows
|
||
netstat -ano | findstr :3000
|
||
```
|