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

484 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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"
}
```