484 lines
20 KiB
Markdown
484 lines
20 KiB
Markdown
# 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<string, number>; // 按省份统计
|
||
dauByCity: Map<string, number>; // 按城市统计
|
||
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<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 (事件日志表)
|
||
|
||
```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 <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 仓储注入
|
||
|
||
```typescript
|
||
// 接口定义 (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 领域异常
|
||
|
||
```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"
|
||
}
|
||
```
|