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