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