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

13 KiB
Raw Blame History

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 项目克隆与安装

# 进入项目目录
cd backend/services/leaderboard-service

# 安装依赖
npm install

# 生成 Prisma Client
npm run prisma:generate

# 复制环境配置
cp .env.example .env.development

2. 项目配置

2.1 环境变量

创建 .env.development 文件:

# 应用配置
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 数据库初始化

# 运行数据库迁移
npm run prisma:migrate

# 或直接推送 schema开发环境
npx prisma db push

# 填充初始数据
npm run prisma:seed

# 打开 Prisma Studio 查看数据
npm run prisma:studio

3. 开发流程

3.1 启动服务

# 开发模式(热重载)
npm run start:dev

# 调试模式
npm run start:debug

# 生产模式
npm run start:prod

3.2 代码规范

# 代码格式化
npm run format

# 代码检查
npm run lint

# 自动修复
npm run lint -- --fix

3.3 Git 工作流

# 创建功能分支
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 规范:

类型 说明
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 领域层开发

创建值对象

// 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;
  }
}

创建聚合根

// 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 = [];
  }
}

创建领域服务

// src/domain/services/example.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class ExampleDomainService {
  /**
   * 复杂的业务逻辑,不属于单个聚合根
   */
  calculateComplexBusinessLogic(
    aggregate1: Aggregate1,
    aggregate2: Aggregate2,
  ): Result {
    // 跨聚合的业务逻辑
  }
}

4.2 基础设施层开发

实现仓储

// 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 应用层开发

创建应用服务

// 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 层开发

创建控制器

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

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

{
  "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 日志调试

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 中启用:

const prisma = new PrismaClient({
  log: ['query', 'info', 'warn', 'error'],
});

6. 常见问题

6.1 Prisma Client 生成失败

# 删除现有 client
rm -rf node_modules/.prisma

# 重新生成
npx prisma generate

6.2 数据库连接失败

检查:

  1. PostgreSQL 服务是否运行
  2. DATABASE_URL 配置是否正确
  3. 数据库用户权限

6.3 Redis 连接失败

检查:

  1. Redis 服务是否运行
  2. REDIS_HOSTREDIS_PORT 配置
  3. 防火墙设置

6.4 热重载不生效

# 清理缓存
rm -rf dist

# 重新启动
npm run start:dev

6.5 BigInt 序列化问题

main.ts 中添加:

// BigInt 序列化支持
(BigInt.prototype as any).toJSON = function () {
  return this.toString();
};

7. 性能优化建议

7.1 数据库查询

// 使用 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 缓存使用

// 缓存查询结果
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 异步处理

// 使用事件驱动
@OnEvent('ranking.updated')
async handleRankingUpdated(event: RankingUpdatedEvent) {
  // 异步处理,不阻塞主流程
  await this.notificationService.notifyUser(event.userId);
}

8. 安全注意事项

8.1 输入验证

// 使用 class-validator
@IsString()
@IsNotEmpty()
@MaxLength(100)
name: string;

@IsInt()
@Min(1)
@Max(100)
limit: number;

8.2 SQL 注入防护

// 使用参数化查询Prisma 自动处理)
await prisma.user.findFirst({
  where: { email: userInput }, // 安全
});

// 避免原始查询
// await prisma.$queryRaw`SELECT * FROM users WHERE email = ${userInput}`

8.3 敏感数据处理

// 响应时过滤敏感字段
return {
  id: user.id,
  nickname: user.nickname,
  // 不返回 password, email 等敏感信息
};

9. 参考资源