13 KiB
13 KiB
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 数据库连接失败
检查:
- PostgreSQL 服务是否运行
DATABASE_URL配置是否正确- 数据库用户权限
6.3 Redis 连接失败
检查:
- Redis 服务是否运行
REDIS_HOST和REDIS_PORT配置- 防火墙设置
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 等敏感信息
};