# 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 (日活统计聚合) ```typescript class DailyActiveStats { // 属性 day: Date; // 统计日期 dauCount: number; // 日活人数 dauByProvince: Map; // 按省份统计 dauByCity: Map; // 按城市统计 calculatedAt: Date; // 计算时间 version: number; // 乐观锁版本 // 行为 updateStats(total, byProvince, byCity): void incrementVersion(): void } ``` ### 4.2 实体 #### EventLog (事件日志) ```typescript class EventLog { id: bigint; userId?: bigint; installId: InstallId; eventName: EventName; eventTime: Date; properties: EventProperties; createdAt: Date; } ``` #### OnlineSnapshot (在线快照) ```typescript 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 ```typescript class OnlineDetectionService { // 判断用户是否在线 (3分钟内有心跳) isOnline(lastHeartbeat: Date, windowSeconds: number): boolean // 计算在线阈值时间 calculateThresholdTime(windowSeconds: number): Date } ``` #### DauCalculationService ```typescript class DauCalculationService { // 计算日活统计 calculateDau(events: EventLog[]): DauResult // 去重用户 (优先 userId,其次 installId) deduplicateUsers(events: EventLog[]): Set } ``` --- ## 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 (事件日志表) ```sql 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 (日活统计表) ```sql 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 (在线快照表) ```sql 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 # 更新心跳 ZCOUNT presence:online_users +inf # 统计在线人数 ZRANGEBYSCORE presence:online_users +inf # 获取在线用户列表 ZREMRANGEBYSCORE presence:online_users -inf # 清理过期用户 ``` ### 6.3 Kafka Topics | Topic | 用途 | 消费者 | |-------|------|-------| | `presence.heartbeat` | 心跳事件 | 内部处理 | | `presence.events` | 分析事件 | 数据平台 | | `presence.dau` | 日活统计结果 | 报表服务 | --- ## 7. 依赖注入 ### 7.1 仓储注入 ```typescript // 接口定义 (domain/repositories/) export const EVENT_LOG_REPOSITORY = Symbol('EVENT_LOG_REPOSITORY'); export interface IEventLogRepository { batchInsert(logs: EventLog[]): Promise; insert(log: EventLog): Promise; queryDau(eventName: EventName, start: Date, end: Date): Promise; } // 模块配置 (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 领域异常 ```typescript // 基础领域异常 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 全局异常过滤器 ```typescript @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" } ```