# 后端 API 开发指南

> 本文档为 Flutter 前端已实现功能提供后端 API 接口规范，供后端开发参考。

---

## 目录

1. [APK 在线升级 API](#1-apk-在线升级-api)
2. [遥测系统 API](#2-遥测系统-api)
3. [数据库设计](#3-数据库设计)
4. [Redis 数据结构](#4-redis-数据结构)
5. [定时任务](#5-定时任务)

---

## 1. APK 在线升级 API

### 1.1 版本检测接口

**请求**

```
GET /api/app/version/check
```

**Query 参数**

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| platform | string | 是 | 平台类型: `android` / `ios` |
| currentVersion | string | 是 | 当前版本号，如 `1.0.0` |
| currentVersionCode | int | 是 | 当前版本代码，如 `1` |

**响应示例 - 有新版本**

```json
{
  "code": 0,
  "message": "success",
  "data": {
    "hasUpdate": true,
    "version": "1.1.0",
    "versionCode": 2,
    "downloadUrl": "https://cdn.rwadurian.com/releases/app-v1.1.0.apk",
    "fileSize": 52428800,
    "fileSizeFriendly": "50.0 MB",
    "sha256": "a1b2c3d4e5f6...完整64字符哈希",
    "forceUpdate": false,
    "updateLog": "1. 新增挖矿动画效果\n2. 修复已知BUG\n3. 性能优化",
    "releaseDate": "2024-01-15T10:00:00Z"
  }
}
```

**响应示例 - 已是最新版本**

```json
{
  "code": 0,
  "message": "success",
  "data": {
    "hasUpdate": false
  }
}
```

**前端对应代码**

```dart
// lib/core/updater/version_checker.dart
Future<VersionInfo?> checkForUpdate() async {
  final response = await _dio.get(
    '/api/app/version/check',
    queryParameters: {
      'platform': 'android',
      'currentVersion': packageInfo.version,
      'currentVersionCode': int.parse(packageInfo.buildNumber),
    },
  );
  // ...
}
```

### 1.2 APK 下载

**要求**

- 下载链接必须使用 HTTPS
- 支持断点续传 (Range 请求头)
- 提供正确的 `Content-Length` 响应头
- 提供 SHA-256 校验值供前端验证文件完整性

**前端下载流程**

```
1. 请求版本检测接口获取下载信息
2. 使用 Dio 下载 APK 到应用私有目录
3. 计算下载文件 SHA-256 与服务器返回值比对
4. 校验通过后调用系统安装器安装
```

### 1.3 版本管理后台 (建议)

建议实现后台管理界面支持：

- 上传新版本 APK
- 自动计算文件大小和 SHA-256
- 设置强制更新标志
- 填写更新日志
- 查看各版本下载统计

---

## 2. 遥测系统 API

### 2.1 事件上报接口

**请求**

```
POST /api/v1/analytics/events
```

**请求头**

```
Content-Type: application/json
Authorization: Bearer <token>  // 可选，用户已登录时携带
```

**请求体**

```json
{
  "events": [
    {
      "eventId": "550e8400-e29b-41d4-a716-446655440000",
      "name": "page_view",
      "type": "page_view",
      "level": "info",
      "installId": "device-unique-install-id",
      "deviceContextId": "ctx-abc123",
      "sessionId": "session-xyz789",
      "userId": "user-123",
      "timestamp": "2024-01-15T10:30:00.000Z",
      "properties": {
        "page": "/ranking",
        "referrer": "/splash"
      }
    },
    {
      "eventId": "550e8400-e29b-41d4-a716-446655440001",
      "name": "button_click",
      "type": "user_action",
      "level": "info",
      "installId": "device-unique-install-id",
      "deviceContextId": "ctx-abc123",
      "sessionId": "session-xyz789",
      "userId": "user-123",
      "timestamp": "2024-01-15T10:30:05.000Z",
      "properties": {
        "button": "start_mining",
        "page": "/mining"
      }
    }
  ]
}
```

**事件类型枚举 (EventType)**

| 值 | 说明 |
|---|------|
| `page_view` | 页面浏览 |
| `user_action` | 用户操作 |
| `api_call` | API 调用 |
| `performance` | 性能指标 |
| `error` | 错误 |
| `crash` | 崩溃 |
| `session` | 会话事件 |
| `presence` | 在线状态 |

**事件级别枚举 (EventLevel)**

| 值 | 说明 |
|---|------|
| `debug` | 调试 |
| `info` | 信息 |
| `warning` | 警告 |
| `error` | 错误 |

**响应**

```json
{
  "code": 0,
  "message": "success"
}
```

**前端对应代码**

```dart
// lib/core/telemetry/uploader/telemetry_uploader.dart
Future<bool> uploadBatch({int batchSize = 20}) async {
  final events = storage.dequeueEvents(batchSize);
  final response = await _dio.post(
    '/api/v1/analytics/events',
    data: {
      'events': events.map((e) => e.toJson()).toList(),
    },
  );
  // ...
}
```

### 2.2 心跳接口 (在线状态)

**请求**

```
POST /api/v1/presence/heartbeat
```

**请求头**

```
Content-Type: application/json
Authorization: Bearer <token>  // 可选
```

**请求体**

```json
{
  "installId": "device-unique-install-id",
  "sessionId": "session-xyz789",
  "userId": "user-123",
  "timestamp": "2024-01-15T10:30:00.000Z"
}
```

**响应**

```json
{
  "code": 0,
  "message": "success"
}
```

**前端行为**

- 应用在前台时，每 **60 秒** 发送一次心跳
- 应用进入后台时暂停心跳
- 应用恢复前台时立即发送心跳并恢复定时器

**前端对应代码**

```dart
// lib/core/telemetry/presence/heartbeat_service.dart
void _sendHeartbeat() {
  _dio.post(
    '/api/v1/presence/heartbeat',
    data: {
      'installId': _installId,
      'sessionId': _sessionId,
      'userId': _userId,
      'timestamp': DateTime.now().toUtc().toIso8601String(),
    },
  );
}
```

### 2.3 遥测配置接口

**请求**

```
GET /api/telemetry/config
```

**响应**

```json
{
  "code": 0,
  "message": "success",
  "data": {
    "enabled": true,
    "samplingRate": 1.0,
    "enabledEventTypes": ["page_view", "user_action", "error", "crash", "session", "presence"],
    "maxQueueSize": 1000,
    "uploadBatchSize": 20,
    "uploadIntervalSeconds": 30,
    "heartbeatIntervalSeconds": 60,
    "sessionTimeoutMinutes": 30
  }
}
```

**字段说明**

| 字段 | 类型 | 说明 |
|------|------|------|
| enabled | bool | 全局开关，false 时前端停止所有遥测 |
| samplingRate | double | 采样率 0.0~1.0，用于控制上报比例 |
| enabledEventTypes | string[] | 启用的事件类型列表 |
| maxQueueSize | int | 本地队列最大容量 |
| uploadBatchSize | int | 每批上传事件数量 |
| uploadIntervalSeconds | int | 上传间隔秒数 |
| heartbeatIntervalSeconds | int | 心跳间隔秒数 |
| sessionTimeoutMinutes | int | 会话超时分钟数 |

**前端对应代码**

```dart
// lib/core/telemetry/models/telemetry_config.dart
class TelemetryConfig {
  final bool enabled;
  final double samplingRate;
  final List<String> enabledEventTypes;
  final int maxQueueSize;
  final int uploadBatchSize;
  final int uploadIntervalSeconds;
  final int heartbeatIntervalSeconds;
  final int sessionTimeoutMinutes;
}
```

---

## 3. 数据库设计

### 3.1 应用版本表 (app_versions)

```sql
CREATE TABLE app_versions (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  platform ENUM('android', 'ios') NOT NULL,
  version VARCHAR(20) NOT NULL COMMENT '版本号 如 1.0.0',
  version_code INT NOT NULL COMMENT '版本代码 如 1',
  download_url VARCHAR(500) NOT NULL COMMENT 'APK/IPA 下载地址',
  file_size BIGINT NOT NULL COMMENT '文件大小(字节)',
  sha256 CHAR(64) NOT NULL COMMENT 'SHA-256 哈希值',
  force_update TINYINT(1) DEFAULT 0 COMMENT '是否强制更新',
  update_log TEXT COMMENT '更新日志',
  release_date DATETIME NOT NULL COMMENT '发布时间',
  is_active TINYINT(1) DEFAULT 1 COMMENT '是否激活',
  download_count INT DEFAULT 0 COMMENT '下载次数',
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

  UNIQUE KEY uk_platform_version (platform, version_code),
  INDEX idx_platform_active (platform, is_active)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='应用版本管理';
```

### 3.2 遥测事件表 (analytics_events)

```sql
CREATE TABLE analytics_events (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  event_id VARCHAR(36) NOT NULL COMMENT '事件唯一ID (UUID)',
  name VARCHAR(100) NOT NULL COMMENT '事件名称',
  type ENUM('page_view', 'user_action', 'api_call', 'performance', 'error', 'crash', 'session', 'presence') NOT NULL,
  level ENUM('debug', 'info', 'warning', 'error') DEFAULT 'info',
  install_id VARCHAR(100) NOT NULL COMMENT '设备安装ID',
  device_context_id VARCHAR(100) COMMENT '设备上下文ID',
  session_id VARCHAR(100) COMMENT '会话ID',
  user_id VARCHAR(100) COMMENT '用户ID',
  properties JSON COMMENT '事件属性',
  event_time DATETIME(3) NOT NULL COMMENT '事件发生时间',
  received_at DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3) COMMENT '服务器接收时间',

  UNIQUE KEY uk_event_id (event_id),
  INDEX idx_install_id (install_id),
  INDEX idx_user_id (user_id),
  INDEX idx_session_id (session_id),
  INDEX idx_type_time (type, event_time),
  INDEX idx_event_time (event_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='遥测事件记录'
PARTITION BY RANGE (TO_DAYS(event_time)) (
  PARTITION p_default VALUES LESS THAN MAXVALUE
);
```

> 建议按天分区，便于数据清理和查询优化

### 3.3 设备上下文表 (device_contexts)

```sql
CREATE TABLE device_contexts (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  context_id VARCHAR(100) NOT NULL COMMENT '上下文ID',
  install_id VARCHAR(100) NOT NULL COMMENT '设备安装ID',
  platform VARCHAR(20) NOT NULL COMMENT '平台 android/ios',
  brand VARCHAR(50) COMMENT '品牌',
  model VARCHAR(100) COMMENT '型号',
  manufacturer VARCHAR(100) COMMENT '制造商',
  is_physical_device TINYINT(1) COMMENT '是否真机',
  os_version VARCHAR(50) COMMENT '系统版本',
  sdk_int INT COMMENT 'SDK版本号',
  android_id VARCHAR(100) COMMENT 'Android ID',
  screen_width INT COMMENT '屏幕宽度',
  screen_height INT COMMENT '屏幕高度',
  screen_density DECIMAL(4,2) COMMENT '屏幕密度',
  app_name VARCHAR(100) COMMENT '应用名称',
  package_name VARCHAR(200) COMMENT '包名',
  app_version VARCHAR(20) COMMENT '应用版本',
  build_number VARCHAR(20) COMMENT '构建号',
  build_mode VARCHAR(20) COMMENT '构建模式 debug/release',
  locale VARCHAR(20) COMMENT '语言区域',
  timezone VARCHAR(50) COMMENT '时区',
  network_type VARCHAR(20) COMMENT '网络类型',
  is_dark_mode TINYINT(1) COMMENT '是否深色模式',
  collected_at DATETIME(3) NOT NULL COMMENT '采集时间',
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,

  UNIQUE KEY uk_context_id (context_id),
  INDEX idx_install_id (install_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='设备上下文信息';
```

### 3.4 日活统计表 (analytics_daily_active)

```sql
CREATE TABLE analytics_daily_active (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  stat_date DATE NOT NULL COMMENT '统计日期',
  total_dau INT DEFAULT 0 COMMENT '总DAU',
  new_users INT DEFAULT 0 COMMENT '新用户数',
  returning_users INT DEFAULT 0 COMMENT '回访用户数',
  total_sessions INT DEFAULT 0 COMMENT '总会话数',
  avg_session_duration INT DEFAULT 0 COMMENT '平均会话时长(秒)',
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

  UNIQUE KEY uk_stat_date (stat_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='日活统计汇总';
```

### 3.5 在线快照表 (analytics_online_snapshots)

```sql
CREATE TABLE analytics_online_snapshots (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  snapshot_time DATETIME NOT NULL COMMENT '快照时间',
  online_count INT NOT NULL COMMENT '在线人数',
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,

  INDEX idx_snapshot_time (snapshot_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='在线人数快照';
```

---

## 4. Redis 数据结构

### 4.1 在线用户集合 (实时在线统计)

**Key**: `presence:online_users`
**Type**: Sorted Set (ZSET)
**Score**: 最后心跳时间戳 (Unix timestamp)
**Member**: `installId` 或 `userId` (取决于是否登录)

```redis
# 用户发送心跳时
ZADD presence:online_users <timestamp> <installId>

# 获取在线人数 (2分钟内有心跳的用户)
ZCOUNT presence:online_users <now - 120> <now>

# 清理过期用户 (可选，定时任务执行)
ZREMRANGEBYSCORE presence:online_users -inf <now - 120>
```

**在线判定规则**

用户最后心跳时间在 **2 分钟** 内视为在线（心跳间隔 60 秒，允许 1 次丢失）

### 4.2 日活去重集合

**Key**: `analytics:dau:<date>` (如 `analytics:dau:2024-01-15`)
**Type**: Set
**Member**: `installId`
**TTL**: 48 小时

```redis
# 记录日活
SADD analytics:dau:2024-01-15 <installId>
EXPIRE analytics:dau:2024-01-15 172800

# 获取当日DAU
SCARD analytics:dau:2024-01-15
```

### 4.3 会话缓存

**Key**: `session:<sessionId>`
**Type**: Hash
**TTL**: 30 分钟 (随活动更新)

```redis
HSET session:xyz789
  installId "device-xxx"
  userId "user-123"
  startTime "2024-01-15T10:00:00Z"
  lastActiveTime "2024-01-15T10:30:00Z"

EXPIRE session:xyz789 1800
```

---

## 5. 定时任务

### 5.1 DAU 统计任务

**执行频率**: 每天凌晨 00:05

```python
# 伪代码
def calculate_daily_dau():
    yesterday = date.today() - timedelta(days=1)
    date_str = yesterday.strftime('%Y-%m-%d')

    # 从 Redis 获取昨日 DAU
    dau_key = f'analytics:dau:{date_str}'
    total_dau = redis.scard(dau_key)

    # 或从数据库统计 (session_start 事件去重)
    total_dau = db.query("""
        SELECT COUNT(DISTINCT install_id)
        FROM analytics_events
        WHERE type = 'session'
          AND name = 'session_start'
          AND DATE(event_time) = %s
    """, [date_str])

    # 统计新用户 (首次出现的 install_id)
    new_users = db.query("""
        SELECT COUNT(*) FROM (
            SELECT install_id
            FROM analytics_events
            WHERE type = 'session' AND name = 'session_start'
            GROUP BY install_id
            HAVING MIN(DATE(event_time)) = %s
        ) t
    """, [date_str])

    # 写入统计表
    db.upsert('analytics_daily_active', {
        'stat_date': date_str,
        'total_dau': total_dau,
        'new_users': new_users,
        'returning_users': total_dau - new_users
    })
```

### 5.2 在线人数快照任务

**执行频率**: 每 5 分钟

```python
# 伪代码
def snapshot_online_count():
    now = datetime.now()
    threshold = now - timedelta(minutes=2)

    # 从 Redis 获取在线人数
    online_count = redis.zcount(
        'presence:online_users',
        threshold.timestamp(),
        now.timestamp()
    )

    # 写入快照表
    db.insert('analytics_online_snapshots', {
        'snapshot_time': now,
        'online_count': online_count
    })
```

### 5.3 过期数据清理任务

**执行频率**: 每天凌晨 03:00

```python
# 伪代码
def cleanup_expired_data():
    # 清理 30 天前的事件数据
    retention_days = 30
    cutoff_date = date.today() - timedelta(days=retention_days)

    db.execute("""
        DELETE FROM analytics_events
        WHERE event_time < %s
        LIMIT 100000
    """, [cutoff_date])

    # 清理 Redis 过期在线状态
    redis.zremrangebyscore(
        'presence:online_users',
        '-inf',
        (datetime.now() - timedelta(minutes=5)).timestamp()
    )
```

---

## 6. 前端关键行为说明

### 6.1 会话管理

**会话开始条件**:
- 应用冷启动
- 应用从后台恢复且距上次活动超过 30 分钟

**会话结束条件**:
- 应用进入后台

**前端发送事件**:

```dart
// 会话开始
TelemetryService().trackEvent(
  name: SessionEvents.sessionStart,
  type: EventType.session,
  properties: {'trigger': 'app_launch'},
);

// 会话结束
TelemetryService().trackEvent(
  name: SessionEvents.sessionEnd,
  type: EventType.session,
  properties: {
    'duration_seconds': duration,
    'trigger': 'app_pause',
  },
);
```

### 6.2 事件上报策略

- **队列阈值触发**: 本地队列 ≥10 条时触发上传
- **定时触发**: 每 30 秒检查一次
- **应用退出触发**: 强制上传全部队列
- **批量大小**: 每批最多 20 条

### 6.3 设备上下文

前端采集设备信息后生成唯一 `deviceContextId`，同一设备配置不变时复用相同 ID。设备信息变化（如系统升级）时生成新 ID。

---

## 7. API 错误码规范

| code | 说明 |
|------|------|
| 0 | 成功 |
| 1001 | 参数错误 |
| 1002 | 认证失败 |
| 2001 | 服务器内部错误 |
| 2002 | 数据库错误 |
| 3001 | 版本不存在 |
| 3002 | 下载链接已过期 |

---

## 8. 安全建议

1. **APK 下载**
   - 使用 HTTPS
   - 提供 SHA-256 校验
   - 下载链接可设置有效期

2. **遥测数据**
   - 不采集敏感个人信息 (如通讯录、短信)
   - installId 使用 UUID 生成，不关联设备硬件标识
   - 支持用户关闭遥测 (通过远程配置)

3. **心跳接口**
   - 防刷限流 (每个 installId 每分钟最多 2 次)
   - 异常流量监控

---

## 附录: 前端代码目录结构

```
lib/core/
├── updater/                        # APK升级模块
│   ├── models/
│   │   ├── version_info.dart       # 版本信息模型
│   │   └── update_config.dart      # 更新配置模型
│   ├── channels/
│   │   ├── google_play_updater.dart # Google Play更新器
│   │   └── self_hosted_updater.dart # 自建服务器更新器
│   ├── version_checker.dart        # 版本检测器
│   ├── download_manager.dart       # 下载管理器
│   ├── apk_installer.dart          # APK安装器
│   ├── app_market_detector.dart    # 应用市场检测
│   └── update_service.dart         # 统一更新服务
│
└── telemetry/                      # 遥测模块
    ├── models/
    │   ├── device_context.dart     # 设备上下文模型
    │   ├── telemetry_event.dart    # 遥测事件模型
    │   └── telemetry_config.dart   # 遥测配置模型
    ├── collectors/
    │   └── device_info_collector.dart # 设备信息采集器
    ├── storage/
    │   └── telemetry_storage.dart  # 本地事件存储
    ├── uploader/
    │   └── telemetry_uploader.dart # 事件上传器
    ├── session/
    │   ├── session_events.dart     # 会话事件常量
    │   └── session_manager.dart    # 会话管理器
    ├── presence/
    │   ├── presence_config.dart    # 心跳配置
    │   └── heartbeat_service.dart  # 心跳服务
    └── telemetry_service.dart      # 遥测服务入口
```
