# 提现 & 文件上传 & 日志上报 — 用户端 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 " \ -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 | 内容摘要(用于去重) |