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

20 KiB
Raw Blame History

Presence Service 架构文档

1. 概述

Presence Service 是一个基于 DDD (领域驱动设计) + 六边形架构 (Hexagonal Architecture) + CQRS 模式构建的微服务,负责用户在线状态检测和活跃度分析。

1.1 核心职责

  • 实时在线状态检测: 基于心跳机制检测用户在线状态
  • 日活统计 (DAU): 按日统计活跃用户数,支持地域维度
  • 在线人数快照: 定期记录在线人数历史
  • 事件日志收集: 收集客户端上报的分析事件

1.2 技术栈

组件 技术选型
运行时 Node.js 20+
框架 NestJS 10.x
ORM Prisma 5.x
数据库 PostgreSQL 15
缓存 Redis 7
消息队列 Kafka
语言 TypeScript 5.x

2. 架构模式

2.1 六边形架构 (Ports and Adapters)

┌─────────────────────────────────────────────────────────────────┐
│                        Driving Adapters                          │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐              │
│  │  REST API   │  │   Kafka     │  │   Cron      │              │
│  │ Controllers │  │  Consumer   │  │   Jobs      │              │
│  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘              │
│         │                │                │                      │
│         └────────────────┼────────────────┘                      │
│                          ▼                                       │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │                    Application Layer                       │  │
│  │  ┌─────────────────┐      ┌─────────────────┐             │  │
│  │  │    Commands     │      │     Queries     │             │  │
│  │  │  (Write Side)   │      │   (Read Side)   │             │  │
│  │  └────────┬────────┘      └────────┬────────┘             │  │
│  │           │                        │                       │  │
│  │           └────────────┬───────────┘                       │  │
│  │                        ▼                                   │  │
│  │  ┌─────────────────────────────────────────────────────┐  │  │
│  │  │                   Domain Layer                       │  │  │
│  │  │  ┌───────────┐ ┌───────────┐ ┌───────────────────┐  │  │  │
│  │  │  │Aggregates │ │ Entities  │ │  Value Objects    │  │  │  │
│  │  │  └───────────┘ └───────────┘ └───────────────────┘  │  │  │
│  │  │  ┌───────────────────┐ ┌─────────────────────────┐  │  │  │
│  │  │  │  Domain Services  │ │   Repository Ports      │  │  │  │
│  │  │  └───────────────────┘ └───────────────────────┘  │  │  │
│  │  └─────────────────────────────────────────────────────┘  │  │
│  └───────────────────────────────────────────────────────────┘  │
│                          │                                       │
│                          ▼                                       │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │                   Driven Adapters                          │  │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐        │  │
│  │  │   Prisma    │  │    Redis    │  │    Kafka    │        │  │
│  │  │ Repository  │  │  Repository │  │  Publisher  │        │  │
│  │  └─────────────┘  └─────────────┘  └─────────────┘        │  │
│  └───────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘

2.2 CQRS 模式

服务采用 CQRS (Command Query Responsibility Segregation) 模式分离读写操作:

Commands (写操作)

  • RecordHeartbeatCommand - 记录用户心跳
  • BatchEventsCommand - 批量写入事件日志
  • SnapshotOnlineCountCommand - 快照在线人数
  • CalculateDauCommand - 计算日活统计

Queries (读操作)

  • GetOnlineCountQuery - 查询当前在线人数
  • GetOnlineHistoryQuery - 查询在线人数历史
  • GetDauQuery - 查询日活统计

3. 目录结构

src/
├── api/                          # API 层 (Driving Adapter)
│   ├── controllers/              # REST 控制器
│   │   ├── presence.controller.ts
│   │   └── analytics.controller.ts
│   └── dto/                      # 数据传输对象
│       ├── request/
│       │   ├── heartbeat.dto.ts
│       │   ├── batch-events.dto.ts
│       │   ├── query-dau.dto.ts
│       │   └── query-online-history.dto.ts
│       └── response/
│           ├── online-count.dto.ts
│           ├── dau-stats.dto.ts
│           └── online-history.dto.ts
│
├── application/                  # 应用层
│   ├── commands/                 # 命令处理器
│   │   ├── record-heartbeat/
│   │   │   ├── record-heartbeat.command.ts
│   │   │   └── record-heartbeat.handler.ts
│   │   ├── batch-events/
│   │   ├── snapshot-online-count/
│   │   └── calculate-dau/
│   └── queries/                  # 查询处理器
│       ├── get-online-count/
│       │   ├── get-online-count.query.ts
│       │   └── get-online-count.handler.ts
│       ├── get-online-history/
│       └── get-dau/
│
├── domain/                       # 领域层 (核心)
│   ├── aggregates/               # 聚合根
│   │   └── daily-active-stats/
│   │       └── daily-active-stats.aggregate.ts
│   ├── entities/                 # 实体
│   │   ├── event-log.entity.ts
│   │   └── online-snapshot.entity.ts
│   ├── value-objects/            # 值对象
│   │   ├── install-id.vo.ts
│   │   ├── event-name.vo.ts
│   │   ├── event-properties.vo.ts
│   │   └── time-window.vo.ts
│   ├── services/                 # 领域服务
│   │   ├── online-detection.service.ts
│   │   └── dau-calculation.service.ts
│   ├── repositories/             # 仓储接口 (Ports)
│   │   ├── event-log.repository.interface.ts
│   │   ├── daily-active-stats.repository.interface.ts
│   │   └── online-snapshot.repository.interface.ts
│   └── events/                   # 领域事件
│       └── heartbeat-received.event.ts
│
├── infrastructure/               # 基础设施层 (Driven Adapters)
│   ├── persistence/              # 持久化
│   │   ├── prisma/
│   │   │   └── prisma.service.ts
│   │   ├── mappers/              # 对象映射器
│   │   │   ├── event-log.mapper.ts
│   │   │   ├── daily-active-stats.mapper.ts
│   │   │   └── online-snapshot.mapper.ts
│   │   └── repositories/         # 仓储实现
│   │       ├── event-log.repository.impl.ts
│   │       ├── daily-active-stats.repository.impl.ts
│   │       └── online-snapshot.repository.impl.ts
│   ├── redis/                    # Redis 适配器
│   │   ├── redis.module.ts
│   │   ├── redis.service.ts
│   │   └── presence-redis.repository.ts
│   └── kafka/                    # Kafka 适配器
│       ├── kafka.module.ts
│       └── kafka-event.publisher.ts
│
├── shared/                       # 共享模块
│   ├── filters/
│   │   └── global-exception.filter.ts
│   ├── interceptors/
│   │   └── logging.interceptor.ts
│   ├── guards/
│   │   └── jwt-auth.guard.ts
│   └── utils/
│       └── timezone.util.ts
│
├── app.module.ts                 # 根模块
└── main.ts                       # 入口文件

4. 领域模型

4.1 聚合根

DailyActiveStats (日活统计聚合)

class DailyActiveStats {
  // 属性
  day: Date;                           // 统计日期
  dauCount: number;                    // 日活人数
  dauByProvince: Map<string, number>;  // 按省份统计
  dauByCity: Map<string, number>;      // 按城市统计
  calculatedAt: Date;                  // 计算时间
  version: number;                     // 乐观锁版本

  // 行为
  updateStats(total, byProvince, byCity): void
  incrementVersion(): void
}

4.2 实体

EventLog (事件日志)

class EventLog {
  id: bigint;
  userId?: bigint;
  installId: InstallId;
  eventName: EventName;
  eventTime: Date;
  properties: EventProperties;
  createdAt: Date;
}

OnlineSnapshot (在线快照)

class OnlineSnapshot {
  id: bigint;
  ts: Date;
  onlineCount: number;
  windowSeconds: number;
}

4.3 值对象

值对象 描述 校验规则
InstallId 安装ID 8-64字符字母数字下划线连字符
EventName 事件名称 字母开头字母数字下划线1-64字符
EventProperties 事件属性 JSON 对象
TimeWindow 时间窗口 正整数秒数

4.4 领域服务

OnlineDetectionService

class OnlineDetectionService {
  // 判断用户是否在线 (3分钟内有心跳)
  isOnline(lastHeartbeat: Date, windowSeconds: number): boolean

  // 计算在线阈值时间
  calculateThresholdTime(windowSeconds: number): Date
}

DauCalculationService

class DauCalculationService {
  // 计算日活统计
  calculateDau(events: EventLog[]): DauResult

  // 去重用户 (优先 userId其次 installId)
  deduplicateUsers(events: EventLog[]): Set<string>
}

5. 数据流

5.1 心跳记录流程

Client                API                  Application           Domain              Infrastructure
  │                    │                       │                    │                      │
  │  POST /heartbeat   │                       │                    │                      │
  │───────────────────>│                       │                    │                      │
  │                    │  RecordHeartbeatCmd   │                    │                      │
  │                    │──────────────────────>│                    │                      │
  │                    │                       │  validate()        │                      │
  │                    │                       │───────────────────>│                      │
  │                    │                       │                    │                      │
  │                    │                       │  updatePresence()  │                      │
  │                    │                       │────────────────────────────────────────-->│
  │                    │                       │                    │                 (Redis ZADD)
  │                    │                       │  publishEvent()    │                      │
  │                    │                       │────────────────────────────────────────-->│
  │                    │                       │                    │                (Kafka Publish)
  │                    │  { ok: true }         │                    │                      │
  │<───────────────────│<──────────────────────│                    │                      │

5.2 在线人数查询流程

Client                API                  Application           Infrastructure
  │                    │                       │                      │
  │  GET /online-count │                       │                      │
  │───────────────────>│                       │                      │
  │                    │  GetOnlineCountQuery  │                      │
  │                    │──────────────────────>│                      │
  │                    │                       │  ZCOUNT online_users │
  │                    │                       │─────────────────────>│
  │                    │                       │      count: 1234     │
  │                    │                       │<─────────────────────│
  │                    │  { count: 1234 }      │                      │
  │<───────────────────│<──────────────────────│                      │

6. 存储设计

6.1 PostgreSQL 表结构

analytics_event_log (事件日志表)

CREATE TABLE analytics_event_log (
  id            BIGSERIAL PRIMARY KEY,
  user_id       BIGINT,
  install_id    VARCHAR(64) NOT NULL,
  event_name    VARCHAR(64) NOT NULL,
  event_time    TIMESTAMPTZ NOT NULL,
  properties    JSONB,
  created_at    TIMESTAMPTZ DEFAULT NOW()
);

-- 索引
CREATE INDEX idx_event_log_event_time ON analytics_event_log(event_time);
CREATE INDEX idx_event_log_event_name ON analytics_event_log(event_name);
CREATE INDEX idx_event_log_event_name_time ON analytics_event_log(event_name, event_time);

analytics_daily_active_users (日活统计表)

CREATE TABLE analytics_daily_active_users (
  day             DATE PRIMARY KEY,
  dau_count       INT NOT NULL,
  dau_by_province JSONB,
  dau_by_city     JSONB,
  calculated_at   TIMESTAMPTZ NOT NULL,
  version         INT DEFAULT 1
);

analytics_online_snapshots (在线快照表)

CREATE TABLE analytics_online_snapshots (
  id              BIGSERIAL PRIMARY KEY,
  ts              TIMESTAMPTZ UNIQUE NOT NULL,
  online_count    INT NOT NULL,
  window_seconds  INT DEFAULT 180
);

CREATE INDEX idx_online_snapshots_ts ON analytics_online_snapshots(ts DESC);

6.2 Redis 数据结构

在线用户 Sorted Set

Key:    presence:online_users
Type:   Sorted Set
Score:  Unix timestamp (毫秒)
Member: userId 或 installId

Commands:
  ZADD   presence:online_users <timestamp> <userId>    # 更新心跳
  ZCOUNT presence:online_users <threshold> +inf        # 统计在线人数
  ZRANGEBYSCORE presence:online_users <threshold> +inf # 获取在线用户列表
  ZREMRANGEBYSCORE presence:online_users -inf <threshold> # 清理过期用户

6.3 Kafka Topics

Topic 用途 消费者
presence.heartbeat 心跳事件 内部处理
presence.events 分析事件 数据平台
presence.dau 日活统计结果 报表服务

7. 依赖注入

7.1 仓储注入

// 接口定义 (domain/repositories/)
export const EVENT_LOG_REPOSITORY = Symbol('EVENT_LOG_REPOSITORY');
export interface IEventLogRepository {
  batchInsert(logs: EventLog[]): Promise<void>;
  insert(log: EventLog): Promise<EventLog>;
  queryDau(eventName: EventName, start: Date, end: Date): Promise<DauQueryResult>;
}

// 模块配置 (infrastructure/infrastructure.module.ts)
@Module({
  providers: [
    {
      provide: EVENT_LOG_REPOSITORY,
      useClass: EventLogRepositoryImpl,
    },
  ],
  exports: [EVENT_LOG_REPOSITORY],
})
export class InfrastructureModule {}

// 使用 (application/commands/)
@CommandHandler(RecordHeartbeatCommand)
export class RecordHeartbeatHandler {
  constructor(
    @Inject(EVENT_LOG_REPOSITORY)
    private readonly eventLogRepo: IEventLogRepository,
  ) {}
}

8. 错误处理

8.1 领域异常

// 基础领域异常
export abstract class DomainException extends Error {
  abstract readonly code: string;
}

// 具体异常
export class InvalidInstallIdException extends DomainException {
  code = 'INVALID_INSTALL_ID';
}

export class InvalidEventNameException extends DomainException {
  code = 'INVALID_EVENT_NAME';
}

8.2 全局异常过滤器

@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    // HttpException -> 原样返回状态码
    // DomainException -> 400 Bad Request
    // Unknown -> 500 Internal Server Error (生产环境隐藏详情)
  }
}

9. 跨切关注点

9.1 日志

  • 使用 NestJS 内置 Logger
  • LoggingInterceptor 记录请求/响应
  • 结构化日志格式

9.2 监控指标

  • 请求延迟 (P50/P95/P99)
  • 在线用户数
  • 心跳 QPS
  • 错误率

9.3 健康检查

GET /api/v1/health
{
  "status": "ok",
  "service": "presence-service",
  "timestamp": "2025-01-01T00:00:00Z"
}