# 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/)
