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

12 KiB
Raw Blame History

Presence Service 开发指南

1. 环境要求

1.1 必需软件

软件 版本 说明
Node.js 20.x LTS 运行时环境
npm 10.x 包管理器
Docker 24.x+ 容器运行时
Docker Compose 2.x 容器编排
PostgreSQL 15.x 关系数据库
Redis 7.x 缓存数据库

1.2 推荐工具

工具 用途
VS Code IDE
Prisma Extension Prisma 语法高亮
ESLint Extension 代码检查
Prettier Extension 代码格式化
REST Client API 测试

2. 项目设置

2.1 克隆项目

git clone <repository-url>
cd backend/services/presence-service

2.2 安装依赖

npm install

2.3 环境配置

复制环境变量模板:

cp .env.example .env.development

编辑 .env.development

# 应用配置
NODE_ENV=development
PORT=3000

# 数据库配置
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/presence_dev?schema=public

# Redis 配置
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DB=0

# Kafka 配置
KAFKA_BROKERS=localhost:9092
KAFKA_CLIENT_ID=presence-service-dev

# JWT 配置 (从 identity-service 获取)
JWT_SECRET=your-jwt-secret-key

2.4 启动基础设施

使用 Docker Compose 启动 PostgreSQL 和 Redis

# 启动开发环境依赖
docker compose -f docker-compose.dev.yml up -d

# 查看服务状态
docker compose -f docker-compose.dev.yml ps

docker-compose.dev.yml 示例:

services:
  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: presence_dev
    ports:
      - "5432:5432"
    volumes:
      - postgres-data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  postgres-data:

2.5 数据库迁移

# 生成 Prisma Client
npx prisma generate

# 同步数据库 Schema (开发环境)
npx prisma db push

# 或使用迁移 (生产环境)
npx prisma migrate dev --name init

2.6 启动服务

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

# 调试模式
npm run start:debug

# 生产模式
npm run start:prod

3. 项目结构约定

3.1 命名规范

类型 命名规范 示例
文件名 kebab-case event-log.entity.ts
类名 PascalCase EventLogEntity
接口名 PascalCase + I 前缀 IEventLogRepository
方法名 camelCase findByTimeRange
常量 UPPER_SNAKE_CASE EVENT_LOG_REPOSITORY
枚举值 UPPER_SNAKE_CASE EventType.APP_SESSION_START

3.2 目录约定

src/
├── domain/           # 领域层 - 纯业务逻辑,无框架依赖
├── application/      # 应用层 - 用例编排CQRS 处理器
├── infrastructure/   # 基础设施层 - 技术实现
├── api/              # API 层 - HTTP 控制器
└── shared/           # 共享模块 - 工具、过滤器、守卫

3.3 模块组织

每个功能模块遵循以下结构:

feature/
├── feature.module.ts       # 模块定义
├── feature.controller.ts   # 控制器 (可选)
├── feature.service.ts      # 服务 (可选)
├── dto/                    # 数据传输对象
│   ├── request/
│   └── response/
└── __tests__/              # 单元测试 (可选)

4. 编码规范

4.1 DDD 原则

领域层规则

  1. 不依赖任何框架 - 领域层只使用纯 TypeScript
  2. 聚合根保护内部状态 - 通过方法修改状态,不直接暴露属性
  3. 值对象不可变 - 创建后不能修改
  4. 领域服务无状态 - 不持有任何状态
// ✅ 好的做法 - 值对象不可变
export class InstallId {
  private constructor(private readonly _value: string) {}

  static fromString(value: string): InstallId {
    // 校验逻辑
    return new InstallId(value);
  }

  get value(): string {
    return this._value;
  }
}

// ❌ 不好的做法 - 值对象可变
export class InstallId {
  public value: string;  // 可以被外部修改
}

聚合根设计

// ✅ 好的做法 - 聚合根保护内部状态
export class DailyActiveStats {
  private _dauCount: number;

  updateStats(count: number): void {
    if (count < 0) {
      throw new InvalidDauCountException();
    }
    this._dauCount = count;
    this.incrementVersion();
  }

  get dauCount(): number {
    return this._dauCount;
  }
}

// ❌ 不好的做法 - 直接暴露内部状态
export class DailyActiveStats {
  public dauCount: number;  // 外部可以直接修改
}

4.2 CQRS 规范

Command 设计

// Command 只包含执行操作所需的数据
export class RecordHeartbeatCommand {
  constructor(
    public readonly userId: bigint,
    public readonly installId: string,
    public readonly appVersion: string,
    public readonly clientTs: number,
  ) {}
}

// Handler 负责业务编排
@CommandHandler(RecordHeartbeatCommand)
export class RecordHeartbeatHandler implements ICommandHandler<RecordHeartbeatCommand> {
  async execute(command: RecordHeartbeatCommand): Promise<void> {
    // 1. 验证
    // 2. 执行业务逻辑
    // 3. 持久化
    // 4. 发布事件
  }
}

Query 设计

// Query 只包含查询条件
export class GetOnlineCountQuery {
  constructor(
    public readonly windowSeconds: number = 180,
  ) {}
}

// Handler 返回查询结果
@QueryHandler(GetOnlineCountQuery)
export class GetOnlineCountHandler implements IQueryHandler<GetOnlineCountQuery, OnlineCountResult> {
  async execute(query: GetOnlineCountQuery): Promise<OnlineCountResult> {
    // 直接读取数据,不修改状态
  }
}

4.3 依赖注入

// 1. 定义接口和 Token (domain/repositories/)
export const EVENT_LOG_REPOSITORY = Symbol('EVENT_LOG_REPOSITORY');
export interface IEventLogRepository {
  insert(log: EventLog): Promise<EventLog>;
}

// 2. 实现接口 (infrastructure/persistence/repositories/)
@Injectable()
export class EventLogRepositoryImpl implements IEventLogRepository {
  async insert(log: EventLog): Promise<EventLog> {
    // 具体实现
  }
}

// 3. 配置绑定 (infrastructure/infrastructure.module.ts)
@Module({
  providers: [
    {
      provide: EVENT_LOG_REPOSITORY,
      useClass: EventLogRepositoryImpl,
    },
  ],
})

// 4. 使用 (application/commands/)
@CommandHandler(SomeCommand)
export class SomeHandler {
  constructor(
    @Inject(EVENT_LOG_REPOSITORY)
    private readonly repo: IEventLogRepository,
  ) {}
}

4.4 错误处理

// 1. 定义领域异常 (domain/exceptions/)
export class InvalidInstallIdException extends DomainException {
  constructor(value: string) {
    super(`Invalid install ID: ${value}`);
  }

  get code(): string {
    return 'INVALID_INSTALL_ID';
  }
}

// 2. 在值对象中使用
export class InstallId {
  static fromString(value: string): InstallId {
    if (!this.isValid(value)) {
      throw new InvalidInstallIdException(value);
    }
    return new InstallId(value);
  }
}

// 3. GlobalExceptionFilter 自动处理
// DomainException -> 400 Bad Request

5. 常用命令

5.1 开发命令

# 启动开发服务器
npm run start:dev

# 代码检查
npm run lint

# 代码格式化
npm run format

# 类型检查
npm run type-check

5.2 数据库命令

# 生成 Prisma Client
npx prisma generate

# 同步 Schema (开发)
npx prisma db push

# 创建迁移
npx prisma migrate dev --name <migration-name>

# 应用迁移 (生产)
npx prisma migrate deploy

# 打开 Prisma Studio
npx prisma studio

# 重置数据库
npx prisma migrate reset

5.3 测试命令

# 运行所有测试
npm test

# 运行单元测试
npm run test:unit

# 运行集成测试
npm run test:integration

# 运行 E2E 测试
npm run test:e2e

# 生成覆盖率报告
npm run test:cov

# 监视模式
npm run test:watch

5.4 构建命令

# 构建生产版本
npm run build

# 清理构建产物
rm -rf dist/

6. 开发流程

6.1 新增功能流程

  1. 领域建模

    • 识别聚合、实体、值对象
    • 定义领域服务
    • 设计仓储接口
  2. 编写领域层代码

    • 创建值对象 (domain/value-objects/)
    • 创建实体/聚合 (domain/entities/, domain/aggregates/)
    • 创建领域服务 (domain/services/)
    • 定义仓储接口 (domain/repositories/)
  3. 编写应用层代码

    • 创建 Command/Query (application/commands/, application/queries/)
    • 创建 Handler
  4. 编写基础设施层代码

    • 创建 Mapper (infrastructure/persistence/mappers/)
    • 实现仓储 (infrastructure/persistence/repositories/)
  5. 编写 API 层代码

    • 创建 DTO (api/dto/)
    • 创建 Controller (api/controllers/)
  6. 编写测试

    • 单元测试 (领域层)
    • 集成测试 (应用层)
    • E2E 测试 (API 层)

6.2 代码审查清单

  • 领域层是否无框架依赖?
  • 值对象是否不可变?
  • 聚合根是否保护了内部状态?
  • 仓储接口是否定义在领域层?
  • Command/Query 是否职责单一?
  • 是否有充分的测试覆盖?
  • 异常处理是否完善?
  • 是否遵循命名规范?

7. 调试技巧

7.1 VS Code 调试配置

.vscode/launch.json:

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Debug NestJS",
      "runtimeExecutable": "npm",
      "runtimeArgs": ["run", "start:debug"],
      "console": "integratedTerminal",
      "restart": true,
      "autoAttachChildProcesses": true
    },
    {
      "type": "node",
      "request": "launch",
      "name": "Debug Jest Tests",
      "runtimeExecutable": "npm",
      "runtimeArgs": ["run", "test:debug"],
      "console": "integratedTerminal"
    }
  ]
}

7.2 日志调试

import { Logger } from '@nestjs/common';

const logger = new Logger('MyService');

logger.log('Info message');
logger.warn('Warning message');
logger.error('Error message', error.stack);
logger.debug('Debug message');  // 需要设置 LOG_LEVEL=debug

7.3 Prisma 调试

# 启用查询日志
DEBUG="prisma:query" npm run start:dev

# 或在 schema.prisma 中配置
generator client {
  provider = "prisma-client-js"
  previewFeatures = ["tracing"]
}

8. Git 工作流

8.1 分支命名

类型 格式 示例
功能 feature/<description> feature/add-online-history-api
修复 fix/<description> fix/heartbeat-validation
重构 refactor/<description> refactor/redis-repository
文档 docs/<description> docs/api-documentation

8.2 提交信息格式

<type>(<scope>): <description>

[optional body]

[optional footer]

类型:

  • feat: 新功能
  • fix: 修复 bug
  • refactor: 重构
  • docs: 文档
  • test: 测试
  • chore: 构建/工具

示例:

feat(presence): add online history query API

- Add GetOnlineHistoryQuery and handler
- Add OnlineHistoryResponseDto
- Add endpoint GET /api/v1/presence/online-history

Closes #123

9. 常见问题

Q: Prisma Client 未生成

npx prisma generate

Q: 数据库连接失败

  1. 检查 Docker 容器是否运行
  2. 检查 DATABASE_URL 是否正确
  3. 检查端口是否被占用

Q: Redis 连接失败

  1. 检查 REDIS_HOST 和 REDIS_PORT
  2. 检查 Redis 容器状态
  3. 如有密码,检查 REDIS_PASSWORD

Q: 测试失败

  1. 确保测试数据库已启动
  2. 检查环境变量配置
  3. 运行 npx prisma db push 同步 schema