- FileUploadResponse / FileUploadCompleteResponse 精简为只返回 url - S3Store.BuildObjectURL 拼接完整访问地址 - 提现 method=0 时 content 不再参与必填校验 - 新增用户端 API 接口文档
13 KiB
提现 & 文件上传 & 日志上报 — 用户端 API 接口文档
基于 ppanel-server 源码整理,所有时间戳均为秒级 Unix。
目录
一、提现接口
认证方式: 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 示例
{
"amount": 5000,
"content": "提现到支付宝",
"method": 1,
"account": "user@example.com",
"qr_code_url": "https://cdn.example.com/qrcode/alipay.png"
}
Response: WithdrawalLog
1.2 取消提现
用户取消自己的待审核提现申请,佣金退回账户。
POST /v1/public/user/withdrawal_cancel
Request Body
| 字段 | 类型 | 必填 | 校验 | 说明 |
|---|---|---|---|---|
withdrawal_id |
int64 | 是 | required,gt=0 |
提现记录 ID |
Request 示例
{
"withdrawal_id": 123
}
Response: 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
{
"list": [WithdrawalLog, ...],
"total": 25
}
二、枚举值与状态流转
提现状态 (status)
| 值 | 说明 |
|---|---|
| 0 | 待审核 |
| 1 | 已通过 |
| 2 | 已拒绝 |
| 3 | 已取消 |
收款方式 (method)
| 值 | 说明 |
|---|---|
| 0 | 其他 |
| 1 | 支付宝 |
| 2 | 微信 |
| 3 | 银行卡 |
状态流转
┌── 管理员通过 ──▶ 已通过 (1)
│
待审核 (0) ──────┼── 管理员拒绝 ──▶ 已拒绝 (2)
│
└── 用户取消 ───▶ 已取消 (3)
WithdrawalLog 对象
所有提现接口共用的响应结构:
{
"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 示例
curl -X POST /v1/public/file/upload \
-H "Authorization: Bearer <token>" \
-F "biz_type=withdrawal_qrcode" \
-F "file=@/path/to/alipay_qr.png"
Response
{
"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 示例
{
"biz_type": "withdrawal_qrcode",
"file_name": "wechat_qr.png",
"content_type": "image/png",
"size": 102400,
"sha256": "e3b0c44298fc1c149afbf4c8996fb924..."
}
Response
{
"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 示例
{
"file_id": "b2c3d4e5f6789012345a"
}
Response
{
"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
{
"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
{
"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
{
"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 | 内容摘要(用于去重) |