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

621 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.

# Leaderboard Service 开发指南
## 1. 环境准备
### 1.1 系统要求
| 软件 | 版本 | 说明 |
|------|------|------|
| Node.js | >= 20.x | 推荐使用 LTS 版本 |
| npm | >= 10.x | 随 Node.js 安装 |
| PostgreSQL | >= 15.x | 数据库 |
| Redis | >= 7.x | 缓存 |
| Docker | >= 24.x | 容器化(可选)|
| Git | >= 2.x | 版本控制 |
### 1.2 开发工具推荐
- **IDE**: VS Code / WebStorm
- **VS Code 扩展**:
- ESLint
- Prettier
- Prisma
- REST Client
- GitLens
### 1.3 项目克隆与安装
```bash
# 进入项目目录
cd backend/services/leaderboard-service
# 安装依赖
npm install
# 生成 Prisma Client
npm run prisma:generate
# 复制环境配置
cp .env.example .env.development
```
## 2. 项目配置
### 2.1 环境变量
创建 `.env.development` 文件:
```env
# 应用配置
NODE_ENV=development
PORT=3000
# 数据库配置
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/leaderboard_db
# Redis 配置
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
# Kafka 配置
KAFKA_BROKERS=localhost:9092
KAFKA_GROUP_ID=leaderboard-service-group
KAFKA_CLIENT_ID=leaderboard-service
# JWT 配置
JWT_SECRET=your-development-secret-key
JWT_EXPIRES_IN=7d
# 外部服务
REFERRAL_SERVICE_URL=http://localhost:3001
IDENTITY_SERVICE_URL=http://localhost:3002
# 日志级别
LOG_LEVEL=debug
```
### 2.2 数据库初始化
```bash
# 运行数据库迁移
npm run prisma:migrate
# 或直接推送 schema开发环境
npx prisma db push
# 填充初始数据
npm run prisma:seed
# 打开 Prisma Studio 查看数据
npm run prisma:studio
```
## 3. 开发流程
### 3.1 启动服务
```bash
# 开发模式(热重载)
npm run start:dev
# 调试模式
npm run start:debug
# 生产模式
npm run start:prod
```
### 3.2 代码规范
```bash
# 代码格式化
npm run format
# 代码检查
npm run lint
# 自动修复
npm run lint -- --fix
```
### 3.3 Git 工作流
```bash
# 创建功能分支
git checkout -b feature/add-new-ranking-type
# 提交代码
git add .
git commit -m "feat(domain): add new ranking type support"
# 推送分支
git push origin feature/add-new-ranking-type
```
#### 提交规范
使用 [Conventional Commits](https://www.conventionalcommits.org/) 规范:
| 类型 | 说明 |
|------|------|
| feat | 新功能 |
| fix | Bug 修复 |
| docs | 文档更新 |
| style | 代码格式 |
| refactor | 重构 |
| test | 测试相关 |
| chore | 构建/工具 |
示例:
```
feat(api): add monthly leaderboard endpoint
fix(domain): correct score calculation formula
docs(readme): update installation guide
```
## 4. 代码结构指南
### 4.1 领域层开发
#### 创建值对象
```typescript
// src/domain/value-objects/example.vo.ts
export class ExampleValueObject {
private constructor(
public readonly value: string,
) {}
static create(value: string): ExampleValueObject {
// 验证逻辑
if (!value || value.length === 0) {
throw new Error('Value cannot be empty');
}
return new ExampleValueObject(value);
}
equals(other: ExampleValueObject): boolean {
return this.value === other.value;
}
}
```
#### 创建聚合根
```typescript
// src/domain/aggregates/example/example.aggregate.ts
import { DomainEvent } from '../../events/domain-event.base';
export class ExampleAggregate {
private _domainEvents: DomainEvent[] = [];
private constructor(
public readonly id: bigint,
private _name: string,
) {}
// 工厂方法
static create(props: CreateExampleProps): ExampleAggregate {
const aggregate = new ExampleAggregate(
props.id,
props.name,
);
aggregate.addDomainEvent(new ExampleCreatedEvent(aggregate));
return aggregate;
}
// 业务方法
updateName(newName: string, operator: string): void {
if (this._name === newName) return;
const oldName = this._name;
this._name = newName;
this.addDomainEvent(new NameUpdatedEvent(this.id, oldName, newName));
}
// 领域事件
get domainEvents(): DomainEvent[] {
return [...this._domainEvents];
}
private addDomainEvent(event: DomainEvent): void {
this._domainEvents.push(event);
}
clearDomainEvents(): void {
this._domainEvents = [];
}
}
```
#### 创建领域服务
```typescript
// src/domain/services/example.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class ExampleDomainService {
/**
* 复杂的业务逻辑,不属于单个聚合根
*/
calculateComplexBusinessLogic(
aggregate1: Aggregate1,
aggregate2: Aggregate2,
): Result {
// 跨聚合的业务逻辑
}
}
```
### 4.2 基础设施层开发
#### 实现仓储
```typescript
// src/infrastructure/repositories/example.repository.impl.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../database/prisma.service';
import { IExampleRepository } from '../../domain/repositories/example.repository.interface';
@Injectable()
export class ExampleRepositoryImpl implements IExampleRepository {
constructor(private readonly prisma: PrismaService) {}
async findById(id: bigint): Promise<ExampleAggregate | null> {
const data = await this.prisma.example.findUnique({
where: { id },
});
if (!data) return null;
return this.toDomain(data);
}
async save(aggregate: ExampleAggregate): Promise<void> {
const data = this.toPersistence(aggregate);
await this.prisma.example.upsert({
where: { id: aggregate.id },
create: data,
update: data,
});
}
// 映射方法
private toDomain(data: PrismaExample): ExampleAggregate {
// Prisma 模型 -> 领域模型
}
private toPersistence(aggregate: ExampleAggregate): PrismaExampleInput {
// 领域模型 -> Prisma 模型
}
}
```
### 4.3 应用层开发
#### 创建应用服务
```typescript
// src/application/services/example-application.service.ts
import { Injectable, Inject } from '@nestjs/common';
import { IExampleRepository } from '../../domain/repositories/example.repository.interface';
@Injectable()
export class ExampleApplicationService {
constructor(
@Inject('IExampleRepository')
private readonly exampleRepository: IExampleRepository,
private readonly eventPublisher: EventPublisherService,
) {}
async executeUseCase(command: ExampleCommand): Promise<ExampleResult> {
// 1. 加载聚合
const aggregate = await this.exampleRepository.findById(command.id);
// 2. 执行业务逻辑
aggregate.doSomething(command.data);
// 3. 持久化
await this.exampleRepository.save(aggregate);
// 4. 发布领域事件
await this.eventPublisher.publishAll(aggregate.domainEvents);
aggregate.clearDomainEvents();
// 5. 返回结果
return new ExampleResult(aggregate);
}
}
```
### 4.4 API 层开发
#### 创建控制器
```typescript
// src/api/controllers/example.controller.ts
import {
Controller,
Get,
Post,
Body,
Param,
UseGuards,
} from '@nestjs/common';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
@ApiTags('Example')
@Controller('example')
export class ExampleController {
constructor(
private readonly exampleService: ExampleApplicationService,
) {}
@Get(':id')
@ApiOperation({ summary: '获取示例' })
async getById(@Param('id') id: string) {
return this.exampleService.findById(BigInt(id));
}
@Post()
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOperation({ summary: '创建示例' })
async create(@Body() dto: CreateExampleDto) {
return this.exampleService.create(dto);
}
}
```
#### 创建 DTO
```typescript
// src/api/dto/example.dto.ts
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsNotEmpty, IsOptional, Min, Max } from 'class-validator';
export class CreateExampleDto {
@ApiProperty({ description: '名称', example: '示例名称' })
@IsString()
@IsNotEmpty()
name: string;
@ApiProperty({ description: '描述', required: false })
@IsString()
@IsOptional()
description?: string;
@ApiProperty({ description: '数量', minimum: 1, maximum: 100 })
@Min(1)
@Max(100)
count: number;
}
```
## 5. 调试指南
### 5.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",
"-r",
"tsconfig-paths/register"
],
"args": ["${workspaceFolder}/src/main.ts"],
"sourceMaps": true,
"cwd": "${workspaceFolder}",
"protocol": "inspector"
},
{
"type": "node",
"request": "launch",
"name": "Debug Tests",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": ["--runInBand", "--config", "jest.config.js"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
}
]
}
```
### 5.2 日志调试
```typescript
import { Logger } from '@nestjs/common';
@Injectable()
export class ExampleService {
private readonly logger = new Logger(ExampleService.name);
async doSomething() {
this.logger.debug('开始处理...');
this.logger.log('处理完成');
this.logger.warn('警告信息');
this.logger.error('错误信息', error.stack);
}
}
```
### 5.3 Prisma 查询日志
`prisma.service.ts` 中启用:
```typescript
const prisma = new PrismaClient({
log: ['query', 'info', 'warn', 'error'],
});
```
## 6. 常见问题
### 6.1 Prisma Client 生成失败
```bash
# 删除现有 client
rm -rf node_modules/.prisma
# 重新生成
npx prisma generate
```
### 6.2 数据库连接失败
检查:
1. PostgreSQL 服务是否运行
2. `DATABASE_URL` 配置是否正确
3. 数据库用户权限
### 6.3 Redis 连接失败
检查:
1. Redis 服务是否运行
2. `REDIS_HOST``REDIS_PORT` 配置
3. 防火墙设置
### 6.4 热重载不生效
```bash
# 清理缓存
rm -rf dist
# 重新启动
npm run start:dev
```
### 6.5 BigInt 序列化问题
`main.ts` 中添加:
```typescript
// BigInt 序列化支持
(BigInt.prototype as any).toJSON = function () {
return this.toString();
};
```
## 7. 性能优化建议
### 7.1 数据库查询
```typescript
// 使用 select 限制返回字段
await prisma.user.findMany({
select: {
id: true,
name: true,
},
});
// 使用分页
await prisma.ranking.findMany({
skip: (page - 1) * limit,
take: limit,
orderBy: { score: 'desc' },
});
// 使用事务
await prisma.$transaction([
prisma.ranking.deleteMany({ where: { periodKey } }),
prisma.ranking.createMany({ data: rankings }),
]);
```
### 7.2 缓存使用
```typescript
// 缓存查询结果
async getLeaderboard(type: string, period: string) {
const cacheKey = `leaderboard:${type}:${period}`;
// 尝试从缓存读取
const cached = await this.redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// 从数据库查询
const data = await this.repository.findByPeriod(type, period);
// 写入缓存
await this.redis.setex(cacheKey, 600, JSON.stringify(data));
return data;
}
```
### 7.3 异步处理
```typescript
// 使用事件驱动
@OnEvent('ranking.updated')
async handleRankingUpdated(event: RankingUpdatedEvent) {
// 异步处理,不阻塞主流程
await this.notificationService.notifyUser(event.userId);
}
```
## 8. 安全注意事项
### 8.1 输入验证
```typescript
// 使用 class-validator
@IsString()
@IsNotEmpty()
@MaxLength(100)
name: string;
@IsInt()
@Min(1)
@Max(100)
limit: number;
```
### 8.2 SQL 注入防护
```typescript
// 使用参数化查询Prisma 自动处理)
await prisma.user.findFirst({
where: { email: userInput }, // 安全
});
// 避免原始查询
// await prisma.$queryRaw`SELECT * FROM users WHERE email = ${userInput}`
```
### 8.3 敏感数据处理
```typescript
// 响应时过滤敏感字段
return {
id: user.id,
nickname: user.nickname,
// 不返回 password, email 等敏感信息
};
```
## 9. 参考资源
- [NestJS 官方文档](https://docs.nestjs.com/)
- [Prisma 官方文档](https://www.prisma.io/docs/)
- [TypeScript 手册](https://www.typescriptlang.org/docs/)
- [领域驱动设计](https://domainlanguage.com/ddd/)