# 07 - 遥测 (Telemetry) 与移动端版本管理开发指南

> 参考项目: rwadurian/backend/services/presence-service + admin-service
> 目标: 为 Genex 券金融平台增加 **用户遥测分析** 和 **移动端 OTA 版本管理** 能力

---

## 一、功能概览

### 1.1 遥测系统 (Telemetry)

| 能力 | 说明 |
|------|------|
| 心跳检测 | 客户端定时上报心跳，服务端通过 Redis Sorted Set 实时计算在线用户数 |
| 事件采集 | 批量上报客户端事件（会话开始、页面浏览、操作行为等），写入 PostgreSQL |
| DAU 统计 | 基于 `app_session_start` 事件按 userId/installId 去重，支持省市维度 |
| 在线快照 | 每分钟记录在线用户数快照，支持 1m/5m/1h 区间聚合查询 |
| Prometheus | 暴露 `/metrics` 端点供 Grafana 抓取（心跳数、事件数、在线人数、DAU） |

### 1.2 版本管理 (App Version / OTA Update)

| 能力 | 说明 |
|------|------|
| 版本 CRUD | 管理员创建/编辑/删除/启禁用版本记录 |
| APK/IPA 上传 | 上传安装包，自动解析 versionCode/versionName/minSdkVersion |
| 强制更新 | `isForceUpdate` 标志，客户端据此决定是否阻断使用 |
| 检查更新 API | 移动端调用，返回是否有更新、下载地址、SHA256 校验 |
| 断点续传下载 | HTTP Range 206 支持，大文件友好 |
| 文件完整性 | SHA256 哈希校验，防篡改 |

---

## 二、Genex 项目适配方案

### 2.1 架构决策

rwadurian 项目将遥测放在独立的 `presence-service`，版本管理放在 `admin-service`。
Genex 项目的适配方案：

| 功能 | 归属服务 | 理由 |
|------|---------|------|
| **遥测 (Telemetry)** | **user-service (扩展)** | 遥测与用户强关联，共享用户表和 Redis 连接，减少跨服务调用 |
| **版本管理 (App Version)** | **user-service (扩展)** | 版本管理与用户设备信息紧密耦合，统一管理降低运维复杂度 |

### 2.2 与参考项目的差异

| 维度 | rwadurian | Genex 适配 |
|------|-----------|-----------|
| ORM | Prisma | TypeORM (与现有一致) |
| 架构 | 独立服务 + CQRS | 扩展 user-service，DDD 四层架构 |
| 文件存储 | 本地 `./uploads` | **MinIO** (已有基础设施) |
| 事件总线 | Kafka | **Kafka** (已有 @genex/kafka-client) |
| 缓存 | Redis | **Redis** (已有) |
| APK 解析 | adbkit-apkreader | 同方案 |
| IPA 解析 | unzipper + bplist-parser | 同方案 |
| Prometheus | prom-client | 同方案 |
| 在线检测窗口 | 180s (3 min) | 180s |
| DAU 计算 | 每天凌晨 1:00 + 每小时滚动 | 同方案 |
| 下载端点 | Express 流式 | NestJS StreamableFile + Range |

### 2.3 数据库表 (新增 4 张)

#### `telemetry_events` — 事件日志 (append-only)

```sql
CREATE TABLE IF NOT EXISTS telemetry_events (
    id BIGSERIAL PRIMARY KEY,
    user_id UUID REFERENCES users(id),
    install_id VARCHAR(128) NOT NULL,
    event_name VARCHAR(64) NOT NULL,
    event_time TIMESTAMPTZ NOT NULL,
    properties JSONB DEFAULT '{}',
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_telemetry_events_time ON telemetry_events(event_time);
CREATE INDEX idx_telemetry_events_name_time ON telemetry_events(event_name, event_time);
CREATE INDEX idx_telemetry_events_user ON telemetry_events(user_id);
```

#### `daily_active_stats` — DAU 日统计

```sql
CREATE TABLE IF NOT EXISTS daily_active_stats (
    day DATE PRIMARY KEY,
    dau_count INT NOT NULL DEFAULT 0,
    dau_by_platform JSONB DEFAULT '{}',
    dau_by_region JSONB DEFAULT '{}',
    calculated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    version INT NOT NULL DEFAULT 1
);
```

#### `online_snapshots` — 在线快照

```sql
CREATE TABLE IF NOT EXISTS online_snapshots (
    id BIGSERIAL PRIMARY KEY,
    ts TIMESTAMPTZ NOT NULL UNIQUE,
    online_count INT NOT NULL DEFAULT 0,
    window_seconds INT NOT NULL DEFAULT 180
);
CREATE INDEX idx_online_snapshots_ts ON online_snapshots(ts DESC);
```

#### `app_versions` — 应用版本

```sql
CREATE TABLE IF NOT EXISTS app_versions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    platform VARCHAR(10) NOT NULL CHECK (platform IN ('ANDROID', 'IOS')),
    version_code INT NOT NULL,
    version_name VARCHAR(32) NOT NULL,
    build_number VARCHAR(64) NOT NULL,
    download_url TEXT NOT NULL,
    file_size BIGINT NOT NULL DEFAULT 0,
    file_sha256 VARCHAR(64) NOT NULL,
    min_os_version VARCHAR(16),
    changelog TEXT NOT NULL DEFAULT '',
    is_force_update BOOLEAN NOT NULL DEFAULT FALSE,
    is_enabled BOOLEAN NOT NULL DEFAULT TRUE,
    release_date TIMESTAMPTZ,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    created_by UUID,
    updated_by UUID
);
CREATE INDEX idx_app_versions_platform ON app_versions(platform, is_enabled);
CREATE INDEX idx_app_versions_code ON app_versions(platform, version_code DESC);
```

### 2.4 Redis 数据结构

| Key | 类型 | 用途 |
|-----|------|------|
| `genex:presence:online` | Sorted Set | member=userId, score=heartbeat Unix时间戳 |
| `genex:dau:{YYYY-MM-DD}` | HyperLogLog | 实时近似 DAU（userId/installId） |
| `genex:dau:{YYYY-MM-DD}:ttl` | - | 自动过期 48h |

### 2.5 Kafka Topics (新增)

| Topic | 生产者 | 消费者 | 用途 |
|-------|--------|--------|------|
| `telemetry.session.started` | user-service | notification-service | 会话开始事件 |
| `telemetry.heartbeat` | user-service | - | 心跳事件（可选） |

### 2.6 MinIO Bucket

| Bucket | 用途 |
|--------|------|
| `app-releases` | 存放 APK/IPA 安装包 |

---

## 三、API 设计

### 3.1 遥测 API

```
POST /api/v1/telemetry/events          — 批量上报事件 (无需认证，支持匿名)
POST /api/v1/telemetry/heartbeat       — 心跳上报 (需认证)
GET  /api/v1/telemetry/online-count    — 当前在线人数 (需认证)
GET  /api/v1/telemetry/online-history  — 在线历史趋势 (需认证)

# Admin (遥测分析 — 独立路径避免与 issuer 的 analytics 冲突)
GET  /api/v1/admin/telemetry/dau       — DAU 统计查询
GET  /api/v1/admin/telemetry/events    — 事件列表查询
GET  /api/v1/admin/telemetry/realtime  — 实时数据面板
```

### 3.2 版本管理 API

```
# 移动端 (无需/轻认证)
GET  /api/v1/app/version/check         — 检查更新
GET  /api/v1/app/version/download/:id  — 下载安装包 (支持断点续传)

# Admin
GET    /api/v1/admin/versions          — 版本列表
GET    /api/v1/admin/versions/:id      — 版本详情
POST   /api/v1/admin/versions          — 创建版本 (手动填写)
POST   /api/v1/admin/versions/upload   — 上传 APK/IPA 自动创建
POST   /api/v1/admin/versions/parse    — 解析安装包 (不保存)
PUT    /api/v1/admin/versions/:id      — 更新版本信息
PATCH  /api/v1/admin/versions/:id/toggle — 启用/禁用
DELETE /api/v1/admin/versions/:id      — 删除版本
```

### 3.3 检查更新响应示例

```json
{
  "code": 0,
  "data": {
    "needUpdate": true,
    "forceUpdate": true,
    "version": "2.1.0",
    "versionCode": 210,
    "downloadUrl": "https://minio.gogenex.com/app-releases/ANDROID-2.1.0.apk",
    "fileSize": 52428800,
    "fileSizeFriendly": "50.0 MB",
    "sha256": "a1b2c3d4e5f6...",
    "updateLog": "1. 新增券转让功能\n2. 修复钱包余额显示\n3. 性能优化",
    "releaseDate": "2026-02-12T10:00:00Z"
  }
}
```

### 3.4 强制更新决策流程

```
客户端 → GET /app/version/check?platform=android&current_version_code=200
                    ↓
服务端: 查找该平台最新 enabled 版本
                    ↓
          ┌── 无更新版本 → { needUpdate: false }
          │
          └── 有更新版本 (latestCode > currentCode)
                    ↓
              isForceUpdate && isEnabled?
              ├── YES → { needUpdate: true, forceUpdate: true, ... }
              └── NO  → { needUpdate: true, forceUpdate: false, ... }
                    ↓
客户端:
  forceUpdate=true  → 弹窗阻断，只能更新
  forceUpdate=false → 提示可选更新
```

---

## 四、实现文件清单

### 4.1 遥测模块 (在 user-service 中扩展)

```
services/user-service/src/
├── domain/entities/
│   ├── telemetry-event.entity.ts      # 事件日志实体
│   ├── online-snapshot.entity.ts      # 在线快照实体
│   └── daily-active-stats.entity.ts   # DAU 统计实体
├── application/services/
│   ├── telemetry.service.ts           # 事件采集 + 心跳 + DAU
│   └── telemetry-scheduler.service.ts # 定时任务 (快照/DAU/清理)
├── infrastructure/
│   └── redis/
│       └── presence-redis.service.ts  # Redis 在线检测操作
└── interface/http/
    ├── controllers/
    │   ├── telemetry.controller.ts    # 遥测 API
    │   └── admin-telemetry.controller.ts # Admin 遥测分析 API
    └── dto/
        ├── batch-events.dto.ts
        ├── heartbeat.dto.ts
        └── query-dau.dto.ts
```

### 4.2 版本管理模块 (在 user-service 中扩展)

```
services/user-service/src/
├── domain/entities/
│   └── app-version.entity.ts          # 版本实体
├── application/services/
│   ├── app-version.service.ts         # 版本 CRUD + 检查更新
│   └── file-storage.service.ts        # MinIO 文件上传/下载
├── infrastructure/
│   └── parsers/
│       └── package-parser.service.ts  # APK/IPA 解析
└── interface/http/
    ├── controllers/
    │   ├── app-version.controller.ts  # 移动端检查更新 + 下载
    │   └── admin-version.controller.ts # Admin 版本管理
    └── dto/
        ├── check-update.dto.ts
        ├── create-version.dto.ts
        └── upload-version.dto.ts
```

### 4.3 数据库迁移

```
migrations/
├── 032_create_telemetry_events.sql
├── 033_create_daily_active_stats.sql
├── 034_create_online_snapshots.sql
└── 035_create_app_versions.sql
```

### 4.4 Kong 路由 (新增)

```yaml
# user-service 新增路由
- name: telemetry-routes
  paths:
    - /api/v1/telemetry
  strip_path: false
- name: app-version-routes
  paths:
    - /api/v1/app/version
  strip_path: false
- name: admin-version-routes
  paths:
    - /api/v1/admin/versions
  strip_path: false
```

---

## 五、关键实现要点

### 5.1 心跳在线检测

- 客户端每 60s 发送心跳
- 服务端用 Redis `ZADD genex:presence:online userId timestamp`
- 在线判定窗口: 180s (3 分钟内有心跳即在线)
- 在线人数: `ZCOUNT genex:presence:online (now-180) +inf`
- 每小时清理 24 小时前的过期数据: `ZREMRANGEBYSCORE`

### 5.2 DAU 计算策略

- **实时近似**: Redis HyperLogLog `PFADD genex:dau:2026-02-12 userId`
- **精确计算**: 每天凌晨 1:00 从 `telemetry_events` 查询 `app_session_start`
- **去重优先级**: userId > installId
- **地理维度**: 从 event properties 中提取 province/city

### 5.3 文件存储 (MinIO)

- Bucket: `app-releases`
- 对象命名: `{platform}/{versionName}/{timestamp}-{random}.{ext}`
- 预签名 URL: 下载时生成 24h 有效的签名链接
- SHA256: 上传时计算，存入 `app_versions.file_sha256`

### 5.4 APK/IPA 解析

- **Android**: `adbkit-apkreader` 读取 AndroidManifest.xml
  - 提取: packageName, versionCode, versionName, minSdkVersion
- **iOS**: `unzipper` 解压 → `bplist-parser` 读取 Info.plist
  - 提取: CFBundleIdentifier, CFBundleVersion, CFBundleShortVersionString, MinimumOSVersion

### 5.5 断点续传

- 响应头: `Accept-Ranges: bytes`
- 请求头: `Range: bytes=1048576-`
- 状态码: 206 Partial Content
- 流式读取: `fs.createReadStream(path, { start, end })`

---

## 六、与现有系统集成

### 6.1 Admin-web 管理面板

admin-web 已有 Dashboard 页面，新增:
- **遥测面板**: DAU 趋势图、在线用户数、事件分析
- **版本管理页**: 版本列表、上传、强制更新开关

### 6.2 genex-mobile / miniapp

Flutter/Taro 客户端需要:
- 启动时发送 `app_session_start` 事件
- 定时心跳 (60s)
- 启动时调用检查更新 API
- 根据 `forceUpdate` 决定是否阻断

### 6.3 notification-service

消费 `telemetry.session.started` 事件:
- 新用户首次登录 → 发送欢迎通知
- 用户回归 (>7 天未登录) → 发送召回通知

---

## 七、Prometheus 指标

| 指标名 | 类型 | 标签 | 说明 |
|--------|------|------|------|
| `genex_online_users` | Gauge | - | 当前在线用户数 |
| `genex_dau` | Gauge | date | 每日活跃用户数 |
| `genex_heartbeat_total` | Counter | app_version | 心跳总数 |
| `genex_events_total` | Counter | event_name | 事件总数 |
| `genex_event_batch_duration` | Histogram | - | 批量事件处理耗时 |
