# Presence Service API 文档

## 概述

Base URL: `/api/v1`

所有需要认证的接口都需要在请求头中携带 JWT Token：
```
Authorization: Bearer <token>
```

---

## 1. 在线状态 API

### 1.1 记录心跳

记录用户心跳，更新在线状态。

**请求**

```
POST /api/v1/presence/heartbeat
Authorization: Bearer <token>
Content-Type: application/json
```

**请求体**

| 字段 | 类型 | 必填 | 描述 |
|-----|------|-----|------|
| installId | string | 是 | 安装ID (8-64字符) |
| appVersion | string | 是 | 应用版本号 |
| clientTs | number | 是 | 客户端时间戳 (Unix秒) |

**请求示例**

```json
{
  "installId": "uuid-xxxx-xxxx-xxxx",
  "appVersion": "1.0.0",
  "clientTs": 1732685100
}
```

**响应**

```json
{
  "ok": true,
  "serverTs": 1732685100123
}
```

| 字段 | 类型 | 描述 |
|-----|------|------|
| ok | boolean | 是否成功 |
| serverTs | number | 服务器时间戳 (Unix毫秒) |

**错误码**

| HTTP 状态码 | 错误码 | 描述 |
|------------|-------|------|
| 400 | VALIDATION_ERROR | 参数校验失败 |
| 401 | UNAUTHORIZED | 未授权 |

---

### 1.2 查询在线人数

查询当前在线用户数量。

**请求**

```
GET /api/v1/presence/online-count
Authorization: Bearer <token>
```

**查询参数**

| 参数 | 类型 | 必填 | 默认值 | 描述 |
|-----|------|-----|-------|------|
| windowSeconds | number | 否 | 180 | 时间窗口 (秒) |

**响应**

```json
{
  "count": 12345,
  "windowSeconds": 180,
  "queriedAt": "2025-01-01T12:00:00.000Z"
}
```

| 字段 | 类型 | 描述 |
|-----|------|------|
| count | number | 在线用户数 |
| windowSeconds | number | 统计时间窗口 |
| queriedAt | string | 查询时间 (ISO 8601) |

---

### 1.3 查询在线人数历史

查询指定时间范围内的在线人数历史快照。

**请求**

```
GET /api/v1/presence/online-history
Authorization: Bearer <token>
```

**查询参数**

| 参数 | 类型 | 必填 | 默认值 | 描述 |
|-----|------|-----|-------|------|
| startTime | string | 是 | - | 开始时间 (ISO 8601) |
| endTime | string | 是 | - | 结束时间 (ISO 8601) |
| interval | string | 否 | 5m | 聚合间隔: 1m, 5m, 15m, 30m, 1h |

**响应**

```json
{
  "data": [
    {
      "ts": "2025-01-01T12:00:00.000Z",
      "count": 12345
    },
    {
      "ts": "2025-01-01T12:05:00.000Z",
      "count": 12400
    }
  ],
  "interval": "5m",
  "startTime": "2025-01-01T12:00:00.000Z",
  "endTime": "2025-01-01T13:00:00.000Z",
  "total": 12,
  "summary": {
    "max": 12500,
    "min": 12000,
    "avg": 12250
  }
}
```

| 字段 | 类型 | 描述 |
|-----|------|------|
| data | array | 快照数据列表 |
| data[].ts | string | 快照时间 |
| data[].count | number | 在线人数 |
| interval | string | 聚合间隔 |
| startTime | string | 实际开始时间 |
| endTime | string | 实际结束时间 |
| total | number | 数据点总数 |
| summary.max | number | 最大在线人数 |
| summary.min | number | 最小在线人数 |
| summary.avg | number | 平均在线人数 |

**错误码**

| HTTP 状态码 | 错误码 | 描述 |
|------------|-------|------|
| 400 | INVALID_TIME_RANGE | 无效的时间范围 |
| 400 | INVALID_INTERVAL | 无效的聚合间隔 |

---

## 2. 分析事件 API

### 2.1 批量上报事件

批量上报客户端分析事件。

**请求**

```
POST /api/v1/analytics/events
Content-Type: application/json
```

**请求体**

| 字段 | 类型 | 必填 | 描述 |
|-----|------|-----|------|
| events | array | 是 | 事件列表 |
| events[].eventName | string | 是 | 事件名称 (字母开头，1-64字符) |
| events[].installId | string | 是 | 安装ID |
| events[].userId | string | 否 | 用户ID (已登录用户) |
| events[].clientTs | number | 是 | 客户端时间戳 (Unix秒) |
| events[].properties | object | 否 | 事件属性 |

**请求示例**

```json
{
  "events": [
    {
      "eventName": "app_session_start",
      "installId": "uuid-xxxx-xxxx-xxxx",
      "userId": "12345",
      "clientTs": 1732685100,
      "properties": {
        "os": "iOS",
        "osVersion": "17.0",
        "appVersion": "1.0.0",
        "deviceModel": "iPhone 15 Pro",
        "province": "广东省",
        "city": "深圳市"
      }
    },
    {
      "eventName": "presence_heartbeat",
      "installId": "uuid-xxxx-xxxx-xxxx",
      "clientTs": 1732685160
    }
  ]
}
```

**响应**

```json
{
  "accepted": 2,
  "failed": 0,
  "errors": []
}
```

| 字段 | 类型 | 描述 |
|-----|------|------|
| accepted | number | 成功接收的事件数 |
| failed | number | 失败的事件数 |
| errors | array | 错误详情列表 |

**预定义事件名称**

| 事件名称 | 描述 |
|---------|------|
| app_session_start | 应用会话开始 |
| app_session_end | 应用会话结束 |
| presence_heartbeat | 心跳事件 |
| user_login | 用户登录 |
| user_logout | 用户登出 |

---

### 2.2 查询日活统计

查询指定日期范围的日活统计数据。

**请求**

```
GET /api/v1/analytics/dau
Authorization: Bearer <token>
```

**查询参数**

| 参数 | 类型 | 必填 | 描述 |
|-----|------|-----|------|
| startDate | string | 是 | 开始日期 (YYYY-MM-DD) |
| endDate | string | 是 | 结束日期 (YYYY-MM-DD) |

**响应**

```json
{
  "data": [
    {
      "day": "2025-01-01",
      "dauCount": 50000,
      "dauByProvince": {
        "广东省": 15000,
        "北京市": 8000,
        "上海市": 7000
      },
      "dauByCity": {
        "深圳市": 10000,
        "北京市": 8000,
        "上海市": 7000
      }
    }
  ],
  "total": 1,
  "summary": {
    "totalDau": 50000,
    "avgDau": 50000,
    "maxDau": 50000,
    "minDau": 50000
  }
}
```

| 字段 | 类型 | 描述 |
|-----|------|------|
| data | array | 日活数据列表 |
| data[].day | string | 统计日期 |
| data[].dauCount | number | 日活人数 |
| data[].dauByProvince | object | 按省份统计 |
| data[].dauByCity | object | 按城市统计 |
| total | number | 数据天数 |
| summary | object | 汇总统计 |

**错误码**

| HTTP 状态码 | 错误码 | 描述 |
|------------|-------|------|
| 400 | INVALID_DATE_FORMAT | 无效的日期格式 |
| 400 | DATE_RANGE_TOO_LARGE | 日期范围过大 (最多90天) |

---

## 3. 健康检查 API

### 3.1 健康检查

检查服务健康状态。

**请求**

```
GET /api/v1/health
```

**响应**

```json
{
  "status": "ok",
  "service": "presence-service",
  "timestamp": "2025-01-01T12:00:00.000Z"
}
```

---

## 4. 错误响应格式

所有错误响应遵循统一格式：

```json
{
  "statusCode": 400,
  "path": "/api/v1/presence/heartbeat",
  "method": "POST",
  "message": "installId must be a string",
  "timestamp": "2025-01-01T12:00:00.000Z"
}
```

| 字段 | 类型 | 描述 |
|-----|------|------|
| statusCode | number | HTTP 状态码 |
| path | string | 请求路径 |
| method | string | 请求方法 |
| message | string | 错误信息 |
| timestamp | string | 错误时间 |

**ValidationPipe 错误格式**

当请求体校验失败时，message 字段可能为数组：

```json
{
  "statusCode": 400,
  "path": "/api/v1/presence/heartbeat",
  "method": "POST",
  "message": [
    "installId must be a string",
    "clientTs must be a number"
  ],
  "timestamp": "2025-01-01T12:00:00.000Z"
}
```

---

## 5. 通用 HTTP 状态码

| 状态码 | 描述 |
|-------|------|
| 200 | 请求成功 |
| 201 | 资源创建成功 |
| 400 | 请求参数错误 |
| 401 | 未授权 |
| 403 | 禁止访问 |
| 404 | 资源不存在 |
| 422 | 业务逻辑错误 |
| 500 | 服务器内部错误 |

---

## 6. 限流策略

| 接口 | 限制 | 窗口 |
|-----|------|-----|
| POST /heartbeat | 60 次/分钟/用户 | 滑动窗口 |
| POST /events | 100 次/分钟/IP | 滑动窗口 |
| GET /online-count | 120 次/分钟/用户 | 滑动窗口 |
| GET /online-history | 30 次/分钟/用户 | 滑动窗口 |
| GET /dau | 30 次/分钟/用户 | 滑动窗口 |

超出限制返回 `429 Too Many Requests`。

---

## 7. SDK 示例

### JavaScript/TypeScript

```typescript
class PresenceClient {
  private baseUrl: string;
  private token: string;

  constructor(baseUrl: string, token: string) {
    this.baseUrl = baseUrl;
    this.token = token;
  }

  async sendHeartbeat(installId: string, appVersion: string): Promise<void> {
    const response = await fetch(`${this.baseUrl}/api/v1/presence/heartbeat`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${this.token}`,
      },
      body: JSON.stringify({
        installId,
        appVersion,
        clientTs: Math.floor(Date.now() / 1000),
      }),
    });

    if (!response.ok) {
      throw new Error(`Heartbeat failed: ${response.status}`);
    }
  }

  async getOnlineCount(windowSeconds = 180): Promise<number> {
    const response = await fetch(
      `${this.baseUrl}/api/v1/presence/online-count?windowSeconds=${windowSeconds}`,
      {
        headers: {
          'Authorization': `Bearer ${this.token}`,
        },
      }
    );

    const data = await response.json();
    return data.count;
  }
}
```

### cURL 示例

```bash
# 发送心跳
curl -X POST https://api.example.com/api/v1/presence/heartbeat \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <token>" \
  -d '{
    "installId": "uuid-xxxx-xxxx-xxxx",
    "appVersion": "1.0.0",
    "clientTs": 1732685100
  }'

# 查询在线人数
curl https://api.example.com/api/v1/presence/online-count \
  -H "Authorization: Bearer <token>"

# 查询在线历史
curl "https://api.example.com/api/v1/presence/online-history?startTime=2025-01-01T00:00:00Z&endTime=2025-01-01T12:00:00Z&interval=5m" \
  -H "Authorization: Bearer <token>"

# 批量上报事件
curl -X POST https://api.example.com/api/v1/analytics/events \
  -H "Content-Type: application/json" \
  -d '{
    "events": [
      {
        "eventName": "app_session_start",
        "installId": "uuid-xxxx-xxxx-xxxx",
        "clientTs": 1732685100,
        "properties": {"os": "iOS"}
      }
    ]
  }'
```
