# 提现 & 文件上传 & 日志上报 — 用户端 API 接口文档

> 基于 ppanel-server 源码整理，所有时间戳均为**秒级 Unix**。

---

## 目录

- [一、提现接口](#一提现接口)
  - [1.1 申请提现](#11-申请提现)
  - [1.2 取消提现](#12-取消提现)
  - [1.3 查询提现记录](#13-查询提现记录)
- [二、枚举值与状态流转](#二枚举值与状态流转)
- [三、文件上传接口](#三文件上传接口)
  - [3.1 直传文件（小文件）](#31-直传文件小文件)
  - [3.2 初始化上传（大文件 — 预签名）](#32-初始化上传大文件--预签名)
  - [3.3 确认上传完成](#33-确认上传完成)
- [四、日志查询接口 (Admin)](#四日志查询接口-admin)
  - [4.1 错误日志列表](#41-错误日志列表)
  - [4.2 错误日志详情](#42-错误日志详情)
  - [4.3 日志消息原始详情](#43-日志消息原始详情)

---

## 一、提现接口

> 认证方式: JWT（用户登录态）
>
> 路由前缀: `/v1/public/user`

### 1.1 申请提现

提交佣金提现申请，创建一条待审核的提现记录。

```
POST /v1/public/user/commission_withdraw
```

**Request Body**

| 字段 | 类型 | 必填 | 校验 | 说明 |
|------|------|------|------|------|
| `amount` | int64 | 是 | — | 提现金额（分） |
| `method` | uint8 | 是 | `oneof=0 1 2 3` | 收款方式（见枚举表） |
| `content` | string | 否 | — | 提现备注 |
| `account` | string | 条件必填 | — | 收款账号 |
| `qr_code_url` | string | 条件必填 | — | 收款码图片 URL |

**各收款方式的必填字段**

| method | 收款方式 | 必填字段 |
|--------|---------|---------|
| `1` 支付宝 | `qr_code_url` | 收款码图片 |
| `2` 微信 | `qr_code_url` | 收款码图片 |
| `3` 银行卡 | `account` | 收款账号 |
| `0` 其他 | `account` 必填 |

**Request 示例**

```json
{
  "amount": 5000,
  "content": "提现到支付宝",
  "method": 1,
  "account": "user@example.com",
  "qr_code_url": "https://cdn.example.com/qrcode/alipay.png"
}
```

**Response**: [`WithdrawalLog`](#withdrawallog-对象)

---

### 1.2 取消提现

用户取消自己的待审核提现申请，佣金退回账户。

```
POST /v1/public/user/withdrawal_cancel
```

**Request Body**

| 字段 | 类型 | 必填 | 校验 | 说明 |
|------|------|------|------|------|
| `withdrawal_id` | int64 | 是 | `required,gt=0` | 提现记录 ID |

**Request 示例**

```json
{
  "withdrawal_id": 123
}
```

**Response**: [`WithdrawalLog`](#withdrawallog-对象)（状态已变为 `3=已取消`）

---

### 1.3 查询提现记录

分页查询当前用户的提现记录（自动按 JWT 中的 userId 过滤）。

```
GET /v1/public/user/withdrawal_log
```

**Query 参数**

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `page` | int | 否 | 页码，默认 1 |
| `size` | int | 否 | 每页条数，默认 10 |

**Request 示例**

```
GET /v1/public/user/withdrawal_log?page=1&size=10
```

**Response**

```json
{
  "list": [WithdrawalLog, ...],
  "total": 25
}
```

---

## 二、枚举值与状态流转

### 提现状态 (`status`)

| 值 | 说明 |
|----|------|
| 0 | 待审核 |
| 1 | 已通过 |
| 2 | 已拒绝 |
| 3 | 已取消 |

### 收款方式 (`method`)

| 值 | 说明 |
|----|------|
| 0 | 其他 |
| 1 | 支付宝 |
| 2 | 微信 |
| 3 | 银行卡 |

### 状态流转

```
                 ┌── 管理员通过 ──▶ 已通过 (1)
                 │
待审核 (0) ──────┼── 管理员拒绝 ──▶ 已拒绝 (2)
                 │
                 └── 用户取消 ───▶ 已取消 (3)
```

### WithdrawalLog 对象

所有提现接口共用的响应结构：

```json
{
  "id": 1,
  "user_id": 100,
  "amount": 5000,
  "content": "提现备注",
  "status": 0,
  "reason": "",
  "method": 1,
  "account": "user@example.com",
  "qr_code_url": "https://cdn.example.com/qrcode/alipay.png",
  "created_at": 1716700000,
  "updated_at": 1716700000
}
```

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | int64 | 提现记录 ID |
| `user_id` | int64 | 用户 ID |
| `amount` | int64 | 提现金额（分） |
| `content` | string | 提现备注 |
| `status` | uint8 | 状态（见枚举表） |
| `reason` | string | 拒绝原因（仅 status=2 时有值，其余 omitempty） |
| `method` | uint8 | 收款方式（见枚举表） |
| `account` | string | 收款账号 |
| `qr_code_url` | string | 收款码图片 URL |
| `created_at` | int64 | 创建时间（秒级 Unix） |
| `updated_at` | int64 | 更新时间（秒级 Unix） |

---

## 三、文件上传接口

> 认证方式: JWT + DeviceMiddleware（用户登录态 + 设备认证）
>
> 路由前缀: `/v1/public/file`
>
> 存储后端: S3 兼容（RustFS）

提供两种上传方式：

| 方式 | 适用场景 | 流程 |
|------|---------|------|
| **直传** | 小文件（收款码等） | 1 次请求，`multipart/form-data` 直接上传 |
| **预签名** | 大文件 / 客户端直传 S3 | init → 客户端 PUT 到预签名 URL → complete 确认 |

---

### 3.1 直传文件（小文件）

通过 `multipart/form-data` 直接上传文件到服务端，服务端转存至 S3。

```
POST /v1/public/file/upload
Content-Type: multipart/form-data
```

**Form 参数**

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `biz_type` | string | 是 | 业务类型（如 `withdrawal_qrcode`、`avatar` 等） |
| `file` | file | 是 | 上传的文件（multipart） |

**cURL 示例**

```bash
curl -X POST /v1/public/file/upload \
  -H "Authorization: Bearer <token>" \
  -F "biz_type=withdrawal_qrcode" \
  -F "file=@/path/to/alipay_qr.png"
```

**Response**

```json
{
  "file_id": "a1b2c3d4e5f678901234",
  "file_name": "alipay_qr.png",
  "object_key": "app-upload/2026/05/27/100/alipay_qr.png__a1b2c3d4e5f678901234",
  "size": 52480,
  "content_type": "image/png",
  "etag": "\"d41d8cd98f00b204e9800998ecf8427e\"",
  "status": "completed"
}
```

**FileUploadResponse 字段说明**

| 字段 | 类型 | 说明 |
|------|------|------|
| `file_id` | string | 文件唯一 ID（24 字符 hex） |
| `file_name` | string | 原始文件名 |
| `object_key` | string | S3 对象路径 |
| `size` | int64 | 文件大小（字节） |
| `content_type` | string | MIME 类型 |
| `etag` | string | S3 ETag |
| `status` | string | 状态，直传成功即 `completed` |

---

### 3.2 初始化上传（大文件 — 预签名）

获取 S3 预签名 URL，客户端直接 PUT 到 S3，避免文件经过服务端。

```
POST /v1/public/file/upload/init
```

**Request Body**

| 字段 | 类型 | 必填 | 校验 | 说明 |
|------|------|------|------|------|
| `biz_type` | string | 是 | `required` | 业务类型 |
| `file_name` | string | 是 | `required` | 文件名 |
| `content_type` | string | 是 | `required` | MIME 类型（如 `image/png`） |
| `size` | int64 | 是 | `required` | 文件大小（字节） |
| `sha256` | string | 否 | — | 文件 SHA256（可选校验） |

**Request 示例**

```json
{
  "biz_type": "withdrawal_qrcode",
  "file_name": "wechat_qr.png",
  "content_type": "image/png",
  "size": 102400,
  "sha256": "e3b0c44298fc1c149afbf4c8996fb924..."
}
```

**Response**

```json
{
  "file_id": "b2c3d4e5f6789012345a",
  "object_key": "app-upload/2026/05/27/100/wechat_qr.png__b2c3d4e5f6789012345a",
  "upload_url": "https://s3.example.com/bucket/app-upload/...?X-Amz-Signature=...",
  "method": "PUT",
  "headers": {
    "Content-Type": "image/png"
  },
  "expired_at": 1716700300
}
```

**FileUploadInitResponse 字段说明**

| 字段 | 类型 | 说明 |
|------|------|------|
| `file_id` | string | 文件唯一 ID |
| `object_key` | string | S3 对象路径 |
| `upload_url` | string | 预签名上传 URL |
| `method` | string | HTTP 方法（`PUT`） |
| `headers` | map | 上传时需携带的请求头 |
| `expired_at` | int64 | 预签名过期时间（秒级 Unix，默认 300 秒） |

**客户端上传流程**

```
1. 调用 /upload/init 获取 upload_url
2. 用返回的 method + headers 直接上传文件到 upload_url
3. 上传成功后调用 /upload/complete 确认
```

---

### 3.3 确认上传完成

客户端通过预签名 URL 上传完成后，调用此接口确认文件状态。

```
POST /v1/public/file/upload/complete
```

**Request Body**

| 字段 | 类型 | 必填 | 校验 | 说明 |
|------|------|------|------|------|
| `file_id` | string | 是 | `required` | init 返回的 file_id |

**Request 示例**

```json
{
  "file_id": "b2c3d4e5f6789012345a"
}
```

**Response**

```json
{
  "file_id": "b2c3d4e5f6789012345a",
  "object_key": "app-upload/2026/05/27/100/wechat_qr.png__b2c3d4e5f6789012345a",
  "size": 102400,
  "content_type": "image/png",
  "etag": "\"d41d8cd98f00b204e9800998ecf8427e\"",
  "status": "completed"
}
```

**FileUploadCompleteResponse 字段说明**

| 字段 | 类型 | 说明 |
|------|------|------|
| `file_id` | string | 文件唯一 ID |
| `object_key` | string | S3 对象路径 |
| `size` | int64 | 实际文件大小（S3 HeadObject 获取） |
| `content_type` | string | MIME 类型 |
| `etag` | string | S3 ETag |
| `status` | string | `completed` |

**校验规则**

- 文件大小不能超过配置的 `S3.MaxUploadSize`
- Content-Type 必须在配置的 `S3.AllowedContentTypes` 白名单内（若配置了）
- complete 时会校验 S3 上的实际文件大小是否与 init 声明的一致
- 只能确认自己发起的上传（userId 校验）

---

## 四、日志查询接口 (Admin)

> 认证方式: AuthMiddleware（管理员权限）
>
> 路由前缀: `/v1/admin/log`
>
> 数据来源: `log_message` 表（客户端上报的错误/崩溃日志）

---

### 4.1 错误日志列表

分页查询客户端上报的错误日志，支持多维度筛选。

```
GET /v1/admin/log/error_message/list
```

**Query 参数**

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `page` | int | 是 | 页码 |
| `size` | int | 是 | 每页条数 |
| `platform` | string | 否 | 平台筛选（ios / android / windows / mac / harmony） |
| `level` | uint8 | 否 | 日志级别 |
| `user_id` | int64 | 否 | 用户 ID |
| `device_id` | string | 否 | 设备 ID |
| `error_code` | string | 否 | 错误码 |
| `keyword` | string | 否 | 关键字搜索（匹配 message） |
| `start` | int64 | 否 | 开始时间（秒级 Unix） |
| `end` | int64 | 否 | 结束时间（秒级 Unix） |

**Request 示例**

```
GET /v1/admin/log/error_message/list?page=1&size=20&platform=ios&start=1716600000&end=1716700000
```

**Response**

```json
{
  "total": 50,
  "list": [
    {
      "id": 1,
      "platform": "ios",
      "app_version": "2.1.0",
      "os_name": "iOS",
      "os_version": "17.5",
      "device_id": "A1B2C3D4",
      "user_id": 100,
      "session_id": "sess_xxx",
      "level": 3,
      "error_code": "VPN_CONNECT_FAIL",
      "message": "Failed to establish VPN tunnel",
      "created_at": 1716700000
    }
  ]
}
```

**ErrorLogMessage 字段说明**

| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | int64 | 日志 ID |
| `platform` | string | 平台 |
| `app_version` | string | 客户端版本 |
| `os_name` | string | 操作系统名称 |
| `os_version` | string | 操作系统版本 |
| `device_id` | string | 设备 ID |
| `user_id` | int64 | 用户 ID |
| `session_id` | string | 会话 ID |
| `level` | uint8 | 日志级别 |
| `error_code` | string | 错误码 |
| `message` | string | 错误消息 |
| `created_at` | int64 | 创建时间（秒级 Unix） |

---

### 4.2 错误日志详情

获取单条错误日志的完整详情（列表字段 + 堆栈/IP/UA 等扩展信息）。

```
GET /v1/admin/log/error_message/detail
```

**Response**

```json
{
  "id": 1,
  "platform": "ios",
  "app_version": "2.1.0",
  "os_name": "iOS",
  "os_version": "17.5",
  "device_id": "A1B2C3D4",
  "user_id": 100,
  "session_id": "sess_xxx",
  "level": 3,
  "error_code": "VPN_CONNECT_FAIL",
  "message": "Failed to establish VPN tunnel",
  "stack": "at VPNManager.connect() line 42\nat ...",
  "client_ip": "1.2.3.4",
  "user_agent": "PPanel/2.1.0 iOS/17.5",
  "locale": "zh-CN",
  "occurred_at": 1716700000,
  "created_at": 1716700000
}
```

**相比列表额外返回的字段**

| 字段 | 类型 | 说明 |
|------|------|------|
| `stack` | string | 堆栈信息 |
| `client_ip` | string | 客户端 IP |
| `user_agent` | string | User-Agent |
| `locale` | string | 客户端语言/地区 |
| `occurred_at` | int64 | 错误发生时间（秒级 Unix） |

---

### 4.3 日志消息原始详情

获取单条 `log_message` 的完整原始数据（含 context、digest 等全量字段）。

```
GET /v1/admin/log/message/detail
```

**Query 参数**

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `id` | int64 | 是 | 日志消息 ID |

**Response**

```json
{
  "id": 1,
  "platform": "ios",
  "app_version": "2.1.0",
  "os_name": "iOS",
  "os_version": "17.5",
  "device_id": "A1B2C3D4",
  "user_id": 100,
  "session_id": "sess_xxx",
  "level": 3,
  "error_code": "VPN_CONNECT_FAIL",
  "message": "Failed to establish VPN tunnel",
  "stack": "at VPNManager.connect() line 42\nat ...",
  "context": { "server_id": 5, "protocol": "vmess" },
  "client_ip": "1.2.3.4",
  "user_agent": "PPanel/2.1.0 iOS/17.5",
  "locale": "zh-CN",
  "digest": "sha256_abc123...",
  "occurred_at": 1716700000,
  "created_at": 1716700000
}
```

**相比详情额外返回的字段**

| 字段 | 类型 | 说明 |
|------|------|------|
| `context` | any | 附加上下文（原始 JSON） |
| `digest` | string | 内容摘要（用于去重） |
