# 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 { const data = await this.prisma.example.findUnique({ where: { id }, }); if (!data) return null; return this.toDomain(data); } async save(aggregate: ExampleAggregate): Promise { 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 { // 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/)