20 KiB
20 KiB
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"
}