用户资料与账户
本章覆盖用户侧「个人资料与账户」全部 18 个接口,前缀均为 /api/users,全部要求 JWT 鉴权(Authorization: Bearer <token>,校验链与失败形态见鉴权方式)。统一响应包络、错误码、字节单位等全局约定见通用约定。
鉴权链路要点:每个请求依次校验 Bearer 令牌签名 → Redis 会话有效性 → 用户存在与状态(active)→ authVersion 匹配。修改密码会使 authVersion 递增,该账户所有旧 JWT 立即失效(下一次请求即返回 401,需重新登录)。被封禁账户返回 403 并附带 data.isBanned=true 与封禁原因。
各接口没有更细粒度的专用限流中间件,全站 WAF 防护照常生效。缓存策略按路由分两级:no-store, no-cache, must-revalidate, private(下文简称 no-store)与 public, max-age=60, s-maxage=60(下文简称 short-cache 60s)。
| 方法 | 路径 | 说明 | 缓存策略 |
|---|---|---|---|
| GET | /api/users/profile | 当前用户资料 | no-store |
| PUT | /api/users/profile | 更新用户名/QQ/头像 URL | no-store |
| PUT | /api/users/profile/avatar | 上传头像文件 | no-store |
| PUT | /api/users/profile/password | 修改登录密码 | no-store |
| POST | /api/users/profile/wallpaper | 上传控制台壁纸 | no-store |
| PUT | /api/users/profile/preferences | 更新偏好设置(浅合并) | no-store |
| POST | /api/users/profile/speed-boost/check | 极速模式资格检查 | no-store |
| POST | /api/users/reset-access-key | 轮换访问密钥并下线全部隧道 | no-store |
| POST | /api/users/checkin | 每日签到领奖励 | no-store |
| POST | /api/users/force-unregister-all | 强制下线本人全部隧道 | no-store |
| GET | /api/users/audit-logs | 查询本人操作审计日志 | no-store |
| GET | /api/users/audit-logs/export | 导出审计日志(CSV/JSON) | no-store |
| GET | /api/users/traffic-history | 用户维度流量历史 | no-store |
| GET | /api/users/traffic-history/:period | 同上(路径参数被忽略的别名路由) | no-store |
| GET | /api/users/nodes | 可见节点列表 | short-cache 60s |
| GET | /api/users/nodes/:id | 单节点详情 | short-cache 60s |
| GET | /api/users/nodes/:id/traffic-history | 节点维度流量历史 | no-store |
| GET | /api/users/announcements/:id | 已发布公告详情 | short-cache 60s |
个人资料
GET /api/users/profile
获取当前登录用户的完整资料,含用户组、配额、流量余额与偏好设置。
鉴权:JWT Bearer · 限流:无专用限流(全站 WAF 防护照常生效) · 缓存:no-store
响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
| id | number | 用户 ID |
| username | string | 用户名 |
| string | 注册邮箱 | |
| role | string | 角色:user / admin / superadmin |
| status | string | 账户状态:active / banned / inactive |
| groupId | number | 所属用户组 ID;0 表示未分组 |
| group | object | 用户组对象,恒返回;未分组时为零值对象(见下表) |
| accessKey | string | FRP 客户端访问密钥,32 位十六进制字符串;历史空密钥账户首次调用时自动补发并落库 |
| balance | number | 账户余额(积分),浮点数 |
| traffic | number | 恒为 0(遗留占位字段,库中无此列) |
| usedTraffic | number | 恒为 0(遗留占位字段,实际用量见 remainingTrafficBytes 口径) |
| remainingTrafficBytes | number | 剩余流量,单位字节(float64) |
| realName | string | 实名姓名;为空时该键省略(omitempty) |
| isVerified | boolean | 是否已通过实名认证 |
| avatar | string | 头像 URL;默认为占位图地址 |
| lastLoginDate | string 或缺省 | 最近登录时间(RFC 3339);从未登录时省略(omitempty) |
| lastLoginIp | string | 最近登录 IP |
| maxProxies | number | 有效隧道数量上限(用户与用户组取大者);-1 表示不限制 |
| proxyCount | number | 当前隧道数量(实时统计 proxies 表,非冗余列) |
| preferences | object | 偏好设置键值对,至少为 {} |
| remark | string | 恒为空串(handler 固定写死,不回传管理员备注) |
| createdAt | string | 注册时间(RFC 3339) |
| lastSignInDate | string 或 null | 最近签到时间;从未签到时为 null |
| string | 恒为空串(handler 固定写死) | |
| bandwidthInKbps | number | 有效入站带宽上限,单位 kbps;仅取自用户组,未分组时为 0 |
| bandwidthOutKbps | number | 有效出站带宽上限,单位 kbps;仅取自用户组,未分组时为 0 |
| realNameVerificationStatus | string | 恒为空串(handler 固定写死) |
| twoFactorEnabled | boolean | 是否已开启两步验证 |
group 对象字段(用户组模型原样序列化):
| 字段 | 类型 | 说明 |
|---|---|---|
| id | number | 用户组 ID;未分组时为 0 |
| name | string | 组名 |
| description | string | 组描述 |
| badgeColor | string | 徽章颜色,默认 #808080 |
| isDefault | boolean | 是否默认组 |
| maxProxies | number | 组隧道数上限 |
| bandwidthOutKbps | number | 组出站带宽,单位 kbps |
| bandwidthInKbps | number | 组入站带宽,单位 kbps |
| addedTrafficBytes | number | 组附加流量,单位字节 |
| addedBalance | number | 组附加余额 |
| isVisible | boolean | 是否在组列表可见 |
| isJoinable | boolean | 是否可自行加入 |
| linkedPackageId | number 或 null | 关联套餐 ID |
| createdAt | string | 创建时间 |
| updatedAt | string | 更新时间 |
请求示例
curl -H 'Authorization: Bearer <token>' \
'https://api.hyperfrp.com/api/users/profile'const res = await fetch('https://api.hyperfrp.com/api/users/profile', {
headers: {
'Authorization': 'Bearer <token>',
},
});
const data = await res.json();
console.log(data);import requests
res = requests.get(
'https://api.hyperfrp.com/api/users/profile',
headers={'Authorization': 'Bearer <token>'},
)
data = res.json()
print(data)package main
import (
"fmt"
"net/http"
)
func main() {
req, _ := http.NewRequest("GET", "https://api.hyperfrp.com/api/users/profile", nil)
req.Header.Set("Authorization", "Bearer <token>")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var result map[string]any
json.NewDecoder(res.Body).Decode(&result)
fmt.Println(result)
}响应示例
{
"code": 0,
"message": "成功",
"data": {
"id": 1024,
"username": "alice",
"email": "alice@example.com",
"role": "user",
"status": "active",
"groupId": 2,
"group": {
"id": 2,
"name": "会员组",
"description": "付费会员",
"badgeColor": "#f0ad4e",
"isDefault": false,
"maxProxies": 10,
"bandwidthOutKbps": 20480,
"bandwidthInKbps": 20480,
"addedTrafficBytes": 10737418240,
"addedBalance": 0,
"isVisible": true,
"isJoinable": false,
"linkedPackageId": 3,
"createdAt": "2026-01-10T09:00:00+08:00",
"updatedAt": "2026-08-01T12:00:00+08:00"
},
"accessKey": "5f8a9c0d1e2b3a4f6c7d8e9a0b1c2d3e",
"balance": 320,
"traffic": 0,
"usedTraffic": 0,
"remainingTrafficBytes": 53687091200,
"isVerified": true,
"avatar": "/uploads/avatars/1024-1726723200000.png",
"lastLoginIp": "203.0.113.7",
"maxProxies": 10,
"proxyCount": 4,
"preferences": { "theme": "dark", "sidebarCollapsed": true },
"remark": "",
"createdAt": "2026-03-15T20:11:32+08:00",
"lastSignInDate": "2026-09-19T08:02:11+08:00",
"qq": "",
"bandwidthInKbps": 20480,
"bandwidthOutKbps": 20480,
"realNameVerificationStatus": "",
"twoFactorEnabled": false
}
}错误场景
| 场景 | HTTP | 说明 |
|---|---|---|
| 上下文中无用户 | 401 | 返回「未认证」(正常携带有效令牌不会出现) |
| 用户记录不存在 | 404 | 返回「用户不存在」 |
| 令牌/会话/版本失效 | 401 | 见章首鉴权链路说明 |
注意事项
traffic/usedTraffic为遗留占位键,恒为 0;真实剩余流量以remainingTrafficBytes为准。maxProxies是「用户与用户组取大者」的有效值,可能与用户组标称值不同;任一侧为 -1 时结果为 -1(不限制)。bandwidthInKbps/bandwidthOutKbps完全来自用户组,未分组用户恒为 0,不代表节点实际限速。
PUT /api/users/profile
更新当前用户的基本资料,支持修改用户名、QQ 号与头像 URL。
鉴权:JWT Bearer · 限流:无专用限流(全站 WAF 防护照常生效) · 缓存:no-store
请求体
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| username | string | 否 | 新用户名;空串或与当前相同则跳过;修改时做全局唯一性校验 |
| string | 否 | QQ 号;仅非空时更新(传空串不会清空已有值) | |
| avatar | string | 否 | 新头像 URL;仅非空且以 http 开头时生效 |
请求示例
curl -X PUT -H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{"username":"alice2","qq":"12345678"}' \
'https://api.hyperfrp.com/api/users/profile'const res = await fetch('https://api.hyperfrp.com/api/users/profile', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer <token>',
},
body: JSON.stringify({"username":"alice2","qq":"12345678"})
});
const data = await res.json();
console.log(data);import requests
res = requests.put(
'https://api.hyperfrp.com/api/users/profile',
headers={'Authorization': 'Bearer <token>'},
json={
'username': 'alice2',
'qq': '12345678'
},
)
data = res.json()
print(data)package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func main() {
var payload bytes.Buffer
json.NewEncoder(&payload).Encode(map[string]any{
"username": "alice2",
"qq": "12345678",
})
req, _ := http.NewRequest("PUT", "https://api.hyperfrp.com/api/users/profile", &payload)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer <token>")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var result map[string]any
json.NewDecoder(res.Body).Decode(&result)
fmt.Println(result)
}响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
| id | number | 用户 ID |
| username | string | 更新后的用户名 |
| string | 注册邮箱(不可经本接口修改) | |
| avatar | string | 更新后的头像 URL |
响应示例
{
"code": 0,
"message": "成功",
"data": {
"id": 1024,
"username": "alice2",
"email": "alice@example.com",
"avatar": "/uploads/avatars/1024-1726723200000.png"
}
}错误场景
| 场景 | HTTP | 说明 |
|---|---|---|
| 请求体非法 JSON | 400 | 返回「无效的请求数据」 |
| 用户名已被占用 | 400 | 返回「用户名已被使用」 |
| 用户记录不存在 | 404 | 返回「用户不存在」 |
注意事项
- 用户名修改写入审计日志(actionType 为
update_profile,details 含changedUsername)。 avatar传本站上传路径(非http开头)会被忽略;把原头像从本站上传文件换成 http URL 时,旧头像文件会从磁盘删除。qq无法通过本接口清空(空串跳过)。
PUT /api/users/profile/avatar
上传头像文件,替换旧头像并落库新路径。
鉴权:JWT Bearer · 限流:无专用限流(全站 WAF 防护照常生效) · 缓存:no-store
请求体(multipart/form-data)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| avatar | file | 是 | 图片文件;扩展名限 jpg/jpeg/png/gif/webp,Part 的 Content-Type 须与扩展名匹配,且做文件头魔数校验;≤ 2MiB |
请求示例
curl -X PUT -H 'Authorization: Bearer <token>' \
-F 'avatar=@/path/to/avatar.png;type=image/png' \
'https://api.hyperfrp.com/api/users/profile/avatar'const form = new FormData();
form.append('avatar', fileInput.files[0]); // 待上传的文件:/path/to/avatar.png
const res = await fetch('https://api.hyperfrp.com/api/users/profile/avatar', {
method: 'PUT',
headers: { 'Authorization': 'Bearer <token>' }, // 不手动设 Content-Type,由浏览器自动写入 boundary
body: form
});
const data = await res.json();
console.log(data);import requests
res = requests.put(
'https://api.hyperfrp.com/api/users/profile/avatar',
headers={'Authorization': 'Bearer <token>'},
files={'avatar': open('/path/to/avatar.png', 'rb')},
)
data = res.json()
print(data)package main
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
)
func main() {
var buf bytes.Buffer
writer := multipart.NewWriter(&buf)
file, _ := os.Open('/path/to/avatar.png')
defer file.Close()
part, _ := writer.CreateFormFile('avatar', filepath.Base('/path/to/avatar.png'))
io.Copy(part, file)
writer.Close()
req, _ := http.NewRequest("PUT", "https://api.hyperfrp.com/api/users/profile/avatar", &buf)
req.Header.Set("Content-Type", writer.FormDataContentType())
req.Header.Set("Authorization", "Bearer <token>")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var result map[string]any
json.NewDecoder(res.Body).Decode(&result)
fmt.Println(result)
}响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
| message | string | 固定为「头像上传成功」 |
| avatar | string | 新头像访问路径,形如 /uploads/avatars/<用户ID>-<毫秒时间戳><扩展名> |
响应示例
{
"code": 0,
"message": "成功",
"data": {
"message": "头像上传成功",
"avatar": "/uploads/avatars/1024-1726723200000.png"
}
}错误场景
| 场景 | HTTP | 说明 |
|---|---|---|
| 缺少 avatar 文件 | 400 | 返回「未检测到头像文件」 |
| 扩展名/Content-Type/魔数不匹配 | 400 | 返回「只允许上传有效的图片文件」 |
| 文件超过 2MiB | 413 | 返回「文件大小不能超过2MiB」 |
| 请求体超过服务端动态上限 | 413 | 返回「请求体不能超过 xxMiB」 |
| 读取请求体超时 | 408 | 返回「读取上传请求超时」 |
| 创建目录/保存文件失败 | 500 | 返回「创建上传目录失败」/「保存文件失败」 |
| 用户记录不存在 | 404 | 返回「用户不存在」,已落盘文件会删除 |
注意事项
- 上传成功后删除旧的本站上传头像(http 外链头像不删文件);旧 URL 立即失效。
PUT /api/users/profile/password
修改登录密码;成功后账户 authVersion 递增,该用户所有已签发的 JWT 立即失效,全部设备需重新登录。
鉴权:JWT Bearer · 限流:无专用限流(全站 WAF 防护照常生效) · 缓存:no-store
请求体
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| currentPassword | string | 见说明 | 当前密码;与 oldPassword 二选一,同时提供时优先 currentPassword |
| oldPassword | string | 见说明 | 当前密码的兼容字段名;currentPassword 为空时回退使用 |
| newPassword | string | 见说明 | 新密码;与 password 二选一,同时提供时优先 newPassword |
| password | string | 见说明 | 新密码的兼容字段名;newPassword 为空时回退使用 |
两组字段解析后均会去除首尾空白;任一组解析结果为空即报错。
请求示例
curl -X PUT -H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{"currentPassword":"OldPass123","newPassword":"NewPass456"}' \
'https://api.hyperfrp.com/api/users/profile/password'const res = await fetch('https://api.hyperfrp.com/api/users/profile/password', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer <token>',
},
body: JSON.stringify({"currentPassword":"OldPass123","newPassword":"NewPass456"})
});
const data = await res.json();
console.log(data);import requests
res = requests.put(
'https://api.hyperfrp.com/api/users/profile/password',
headers={'Authorization': 'Bearer <token>'},
json={
'currentPassword': 'OldPass123',
'newPassword': 'NewPass456'
},
)
data = res.json()
print(data)package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func main() {
var payload bytes.Buffer
json.NewEncoder(&payload).Encode(map[string]any{
"currentPassword": "OldPass123",
"newPassword": "NewPass456",
})
req, _ := http.NewRequest("PUT", "https://api.hyperfrp.com/api/users/profile/password", &payload)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer <token>")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var result map[string]any
json.NewDecoder(res.Body).Decode(&result)
fmt.Println(result)
}响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
| message | string | 固定为「密码更新成功」 |
响应示例
{
"code": 0,
"message": "成功",
"data": {
"message": "密码更新成功"
}
}错误场景
| 场景 | HTTP | 说明 |
|---|---|---|
| 请求体非法或两组字段解析后均为空 | 400 | 返回「请提供当前密码和新密码」 |
| 当前密码错误 | 401 | 返回「当前密码不正确」 |
| 用户记录不存在 | 404 | 返回「用户不存在」 |
| 落库失败 | 500 | 返回「密码更新失败」 |
注意事项
- 失效语义是「版本失效」而非「删除会话」:Redis 会话记录不主动删除,但鉴权中间件逐请求比对 JWT 载荷中的
authVersion与用户当前值,不一致即拒绝(401「账户状态或认证版本已变更,请重新登录」),等效于全部会话立即失效。 - 本操作写入审计日志(actionType 为
update_password)。
POST /api/users/profile/wallpaper
上传控制台壁纸,替换该用户此前上传的全部旧壁纸,返回新壁纸 URL。
鉴权:JWT Bearer · 限流:无专用限流(全站 WAF 防护照常生效) · 缓存:no-store
请求体(multipart/form-data)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| wallpaper | file | 是 | 图片文件;扩展名限 jpg/jpeg/png/gif/webp,Content-Type 须匹配,做魔数校验;≤ 10MiB |
请求示例
curl -X POST -H 'Authorization: Bearer <token>' \
-F 'wallpaper=@/path/to/wallpaper.jpg;type=image/jpeg' \
'https://api.hyperfrp.com/api/users/profile/wallpaper'const form = new FormData();
form.append('wallpaper', fileInput.files[0]); // 待上传的文件:/path/to/wallpaper.jpg
const res = await fetch('https://api.hyperfrp.com/api/users/profile/wallpaper', {
method: 'POST',
headers: { 'Authorization': 'Bearer <token>' }, // 不手动设 Content-Type,由浏览器自动写入 boundary
body: form
});
const data = await res.json();
console.log(data);import requests
res = requests.post(
'https://api.hyperfrp.com/api/users/profile/wallpaper',
headers={'Authorization': 'Bearer <token>'},
files={'wallpaper': open('/path/to/wallpaper.jpg', 'rb')},
)
data = res.json()
print(data)package main
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
)
func main() {
var buf bytes.Buffer
writer := multipart.NewWriter(&buf)
file, _ := os.Open('/path/to/wallpaper.jpg')
defer file.Close()
part, _ := writer.CreateFormFile('wallpaper', filepath.Base('/path/to/wallpaper.jpg'))
io.Copy(part, file)
writer.Close()
req, _ := http.NewRequest("POST", "https://api.hyperfrp.com/api/users/profile/wallpaper", &buf)
req.Header.Set("Content-Type", writer.FormDataContentType())
req.Header.Set("Authorization", "Bearer <token>")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var result map[string]any
json.NewDecoder(res.Body).Decode(&result)
fmt.Println(result)
}响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
| message | string | 固定为「壁纸上传成功」 |
| url | string | 新壁纸访问路径,形如 /uploads/wallpapers/wp-<用户ID>-<毫秒时间戳><扩展名> |
响应示例
{
"code": 0,
"message": "成功",
"data": {
"message": "壁纸上传成功",
"url": "/uploads/wallpapers/wp-1024-1726723200000.jpg"
}
}错误场景
| 场景 | HTTP | 说明 |
|---|---|---|
| 缺少 wallpaper 文件 | 400 | 返回「未检测到壁纸文件」 |
| 扩展名/Content-Type/魔数不匹配 | 400 | 返回「只允许上传有效的图片文件」 |
| 文件超过 10MiB | 413 | 返回「文件大小不能超过10MiB」 |
| 请求体超过服务端动态上限 | 413 | 返回「请求体不能超过 xxMiB」 |
| 读取请求体超时 | 408 | 返回「读取上传请求超时」 |
| 创建目录/保存文件失败 | 500 | 返回「创建上传目录失败」/「保存文件失败」 |
注意事项
- 上传成功时先删除该用户所有旧壁纸文件(
wp-<用户ID>-前缀),一个用户只保留最新一张。 - 壁纸 URL 不写入用户资料,接口仅返回 URL,由前端自行保存使用。
PUT /api/users/profile/preferences
更新偏好设置。合并语义:与既有偏好做顶层键浅合并——请求体中出现的键逐个覆盖,未出现的键保留原值;不做嵌套对象深合并,顶层键的整个值(含嵌套结构)整体替换。
鉴权:JWT Bearer · 限流:无专用限流(全站 WAF 防护照常生效) · 缓存:no-store
请求体
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| preferences | object | 是 | 任意键值对;显式传 null 或缺省视为非法 |
请求示例
curl -X PUT -H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{"preferences":{"theme":"dark","language":"zh-CN"}}' \
'https://api.hyperfrp.com/api/users/profile/preferences'const res = await fetch('https://api.hyperfrp.com/api/users/profile/preferences', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer <token>',
},
body: JSON.stringify({"preferences":{"theme":"dark","language":"zh-CN"}})
});
const data = await res.json();
console.log(data);import requests
res = requests.put(
'https://api.hyperfrp.com/api/users/profile/preferences',
headers={'Authorization': 'Bearer <token>'},
json={
'preferences': {
'theme': 'dark',
'language': 'zh-CN'
}
},
)
data = res.json()
print(data)package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func main() {
var payload bytes.Buffer
json.NewEncoder(&payload).Encode(map[string]any{
"preferences": map[string]any{
"theme": "dark",
"language": "zh-CN",
},
})
req, _ := http.NewRequest("PUT", "https://api.hyperfrp.com/api/users/profile/preferences", &payload)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer <token>")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var result map[string]any
json.NewDecoder(res.Body).Decode(&result)
fmt.Println(result)
}响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
| message | string | 固定为「偏好设置已更新」 |
| preferences | object | 合并后的完整偏好设置 |
响应示例
{
"code": 0,
"message": "成功",
"data": {
"message": "偏好设置已更新",
"preferences": {
"theme": "dark",
"language": "zh-CN",
"sidebarCollapsed": true
}
}
}错误场景
| 场景 | HTTP | 说明 |
|---|---|---|
| 请求体非法 JSON | 400 | 返回「无效的请求数据」 |
| preferences 缺失或为 null | 400 | 返回「偏好设置数据不能为空」 |
| 用户记录不存在 | 404 | 返回「用户不存在」 |
| 落库失败 | 500 | 返回「保存偏好设置失败」 |
POST /api/users/profile/speed-boost/check
检查当前用户全部隧道所涉节点是否满足「极速模式」条件,供开启极速模式前预检。
鉴权:JWT Bearer · 限流:无专用限流(全站 WAF 防护照常生效) · 缓存:no-store
判定条件(对用户隧道关联的每个去重节点):节点带宽 bandwidthLimitKbps > 102400(即 > 100 Mbps)、CPU 负载 cpuUsage < 50(百分比)、节点状态为 online 或 api_only。任一不满足即列入 ineligibleNodes 并给出原因。
响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
| eligible | boolean | 全部节点是否符合条件 |
| message | string | 结论描述;全部符合为「所有节点都满足极速模式条件」,否则「有 N 个节点不满足极速模式条件」 |
| totalNodes | number | 去重后涉及的节点总数 |
| eligibleNodes | number | 符合条件的节点数(计数,非数组) |
| ineligibleNodes | array 或 null | 不符合条件的节点明细;全部符合时为 null,用户没有隧道时为 [] |
ineligibleNodes 元素字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| nodeId | number | 节点 ID |
| nodeName | string | 节点名称 |
| bandwidth | number | 节点带宽上限,单位 kbps |
| load | number | 节点 CPU 负载,百分比数值 |
| status | string | 节点状态 |
| reasons | string[] | 不达标原因列表(带宽不足 / 负载过高 / 节点状态异常) |
请求示例
curl -X POST -H 'Authorization: Bearer <token>' \
'https://api.hyperfrp.com/api/users/profile/speed-boost/check'const res = await fetch('https://api.hyperfrp.com/api/users/profile/speed-boost/check', {
method: 'POST',
headers: {
'Authorization': 'Bearer <token>',
},
});
const data = await res.json();
console.log(data);import requests
res = requests.post(
'https://api.hyperfrp.com/api/users/profile/speed-boost/check',
headers={'Authorization': 'Bearer <token>'},
)
data = res.json()
print(data)package main
import (
"fmt"
"net/http"
)
func main() {
req, _ := http.NewRequest("POST", "https://api.hyperfrp.com/api/users/profile/speed-boost/check", nil)
req.Header.Set("Authorization", "Bearer <token>")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var result map[string]any
json.NewDecoder(res.Body).Decode(&result)
fmt.Println(result)
}响应示例
{
"code": 0,
"message": "成功",
"data": {
"eligible": false,
"message": "有 1 个节点不满足极速模式条件",
"totalNodes": 2,
"eligibleNodes": 1,
"ineligibleNodes": [
{
"nodeId": 7,
"nodeName": "华东-1",
"bandwidth": 102400,
"load": 63.5,
"status": "online",
"reasons": ["带宽不足 (102400 Kbps <= 100Mbps)", "负载过高 (63.5% >= 50%)"]
}
]
}
}错误场景
| 场景 | HTTP | 说明 |
|---|---|---|
| 隧道查询失败 | 500 | 返回「查询隧道失败」 |
注意事项
- 用户当前没有隧道时返回
eligible: true、totalNodes: 0、eligibleNodes: 0、ineligibleNodes: []。 totalNodes/eligibleNodes是计数;不合条件明细只出现在ineligibleNodes中。
账户操作
POST /api/users/reset-access-key
轮换 FRP 客户端访问密钥,并强制下线该用户全部隧道(含非运行中,用于清理历史不一致状态)。
鉴权:JWT Bearer · 限流:无专用限流(全站 WAF 防护照常生效) · 缓存:no-store
处理语义:在单事务内生成新密钥(32 位十六进制,唯一性校验)并落库,同时把该用户全部隧道原子递增持久强制下线代次、状态置为 stopped;随后用旧密钥向 Redis 推送强制下线信号(键 force-offline:<旧accessKey>.<隧道名>,TTL 90 秒)作为低延迟通知——信号失败不阻断轮换,持久代次会在节点下次状态检查时兜底关闭实例。
响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
| message | string | 按真实推送结果拼接的提示,如「访问密钥已成功重置,该用户原有隧道已被强制下线,请用新密钥重新配置客户端」 |
| accessKey | string | 新访问密钥,32 位十六进制字符串 |
| proxyCount | number | 该用户隧道总数(含非运行中) |
| runningCount | number | 轮换时处于 running 状态的隧道数 |
| signaledCount | number | 成功写入 Redis 强制下线信号的隧道数 |
| forceOfflineFailed | boolean | 即时信号推送是否失败(Redis 未初始化或写入失败);true 时由持久代次兜底 |
请求示例
curl -X POST -H 'Authorization: Bearer <token>' \
'https://api.hyperfrp.com/api/users/reset-access-key'const res = await fetch('https://api.hyperfrp.com/api/users/reset-access-key', {
method: 'POST',
headers: {
'Authorization': 'Bearer <token>',
},
});
const data = await res.json();
console.log(data);import requests
res = requests.post(
'https://api.hyperfrp.com/api/users/reset-access-key',
headers={'Authorization': 'Bearer <token>'},
)
data = res.json()
print(data)package main
import (
"fmt"
"net/http"
)
func main() {
req, _ := http.NewRequest("POST", "https://api.hyperfrp.com/api/users/reset-access-key", nil)
req.Header.Set("Authorization", "Bearer <token>")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var result map[string]any
json.NewDecoder(res.Body).Decode(&result)
fmt.Println(result)
}响应示例
{
"code": 0,
"message": "成功",
"data": {
"message": "访问密钥已成功重置,该用户原有隧道已被强制下线,请用新密钥重新配置客户端",
"accessKey": "3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b",
"proxyCount": 4,
"runningCount": 2,
"signaledCount": 4,
"forceOfflineFailed": false
}
}错误场景
| 场景 | HTTP | 说明 |
|---|---|---|
| 用户记录不存在 | 404 | 返回「用户不存在」 |
| 密钥生成重试耗尽或落库失败 | 500 | 返回「重置访问密钥失败」 |
注意事项
- 强制下线信号以旧密钥构造:运行中的客户端仍持旧密钥,用新密钥构造的键永远匹配不上。
- 原访问密钥为空的历史账户跳过信号推送,不视为失败(
signaledCount为 0,forceOfflineFailed为 false)。 - 轮换成功后必须用新密钥重新配置全部 FRP 客户端,否则无法重连。
- 本操作写入审计日志(actionType 为
reset_access_key,details 含proxyCount/runningCount)。
POST /api/users/checkin
每日签到,随机发放积分与流量奖励;同一自然日(服务器本地时区)只能签到一次。
鉴权:JWT Bearer 或访问密钥(X-Access-Key 头,二选一,见下) · 限流:无专用限流(全站 WAF 防护照常生效) · 缓存:no-store
请求体
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| captchaTicket | string | 否 | 预留的验证码票据字段(结构体已定义);当前 handler 未读取、未校验,可省略 |
访问密钥鉴权(可选)
本接口是全站唯一支持用访问密钥替代 JWT 的接口,供脚本/定时任务免登录自动签到:
curl -X POST -H 'X-Access-Key: <访问密钥>' \
'https://api.hyperfrp.com/api/users/checkin'- 访问密钥即「安全设置 → 密码与访问密钥」展示的 32 位小写 hex 字符串,与 frpc 配置内嵌凭证同源(
users.access_key);只允许走请求头,不接受 URL Query / 请求体传递; - 请求同时携带
Authorization: Bearer时一律按 JWT 校验,失败不回退访问密钥——避免会话过期被静默换道掩盖; - 无效或格式非法的密钥统一 401「访问密钥无效」(不区分密钥不存在与格式非法);封禁账户 403(含
isBanned);非active账户 401「账户状态异常,无法使用访问密钥」; - 重置访问密钥后旧密钥立即失效——签到脚本与 frpc 配置同时失效,需同步更新;
- 该替代通道仅对签到开放:
/api/users/*其余接口仍只认 JWT,访问密钥不因此获得改密码、关 2FA 等会话权限。
请求示例
curl -X POST -H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{}' \
'https://api.hyperfrp.com/api/users/checkin'const res = await fetch('https://api.hyperfrp.com/api/users/checkin', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer <token>',
},
body: JSON.stringify({})
});
const data = await res.json();
console.log(data);import requests
res = requests.post(
'https://api.hyperfrp.com/api/users/checkin',
headers={'Authorization': 'Bearer <token>'},
json={},
)
data = res.json()
print(data)package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func main() {
var payload bytes.Buffer
json.NewEncoder(&payload).Encode(map[string]any{})
req, _ := http.NewRequest("POST", "https://api.hyperfrp.com/api/users/checkin", &payload)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer <token>")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var result map[string]any
json.NewDecoder(res.Body).Decode(&result)
fmt.Println(result)
}响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
| message | string | 如「签到成功!您获得了 128 积分和 3 GB 流量。」 |
| rewards | object | 奖励明细,见下表 |
rewards 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| points | number | 积分奖励,随机 50~200(含两端),直接累加进 balance |
| trafficGB | number | 流量奖励(GB),随机 1~5(含两端) |
| trafficBytes | number | 流量奖励的字节数,即 trafficGB × 1073741824,累加进 remainingTrafficBytes |
响应示例
{
"code": 0,
"message": "成功",
"data": {
"message": "签到成功!您获得了 128 积分和 3 GB 流量。",
"rewards": {
"points": 128,
"trafficGB": 3,
"trafficBytes": 3221225472
}
}
}错误场景
| 场景 | HTTP | 说明 |
|---|---|---|
| 未携带任何凭证 | 401 | 返回「未提供认证令牌或访问密钥」;JWT 与访问密钥二选一 |
| 访问密钥无效或格式非法 | 401 | 仅使用 X-Access-Key 鉴权时;统一返回「访问密钥无效」,不区分密钥不存在与格式非法 |
| 访问密钥鉴权但账户非 active | 401 | 返回「账户状态异常,无法使用访问密钥」 |
| 访问密钥鉴权但账户被封禁 | 403 | 响应体 data 含 isBanned: true 与 ban_reason,与 JWT 封禁响应同构 |
| JWT 鉴权但账户被封禁 | 403 | 响应体 data 含 isBanned: true 与 ban_reason |
| 当日已签到 | 400 | 返回「您今天已经签到过了」;判定依据为 lastSignInDate 是否落在今天(服务器本地时区) |
| 事务执行失败 | 500 | 返回「签到失败,请稍后重试」 |
注意事项
- 防重复依赖数据库事务 +
SELECT ... FOR UPDATE行锁,并发重复签到只有一个请求成功,其余落入「当日已签到」分支;本路由无更细粒度的专用限流中间件。 - 随机数使用 crypto/rand,奖励在请求处理前预生成,与落库同事务生效。
- 本操作写入审计日志(actionType 为
checkin,details 含points/trafficGB/authMethod,后者为jwt或access_key,便于管理员区分签到来源)。
POST /api/users/force-unregister-all
强制下线本人全部隧道:数据库层原子递增该用户全部隧道的持久强制下线代次并置为 stopped,再经 Redis 推送即时信号。
鉴权:JWT Bearer · 限流:无专用限流(全站 WAF 防护照常生效) · 缓存:no-store
响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
| success | boolean | 恒为 true(到达此响应即已受理) |
| message | string | 如「操作完成。已向 4 个隧道发送强制下线指令,其中 2 个记录为在线。」 |
| forceOfflineFailed | boolean | Redis 即时通知是否失败;true 时追加提示「即时通知失败,已由持久下线记录兜底,将在节点下次状态检查时生效」 |
| summary | object | 汇总,见下表 |
summary 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| success | number | 成功写入即时信号的隧道数 |
| failed | number | 恒为 0(信号失败归入 forceOfflineFailed 兜底语义,不计失败) |
请求示例
curl -X POST -H 'Authorization: Bearer <token>' \
'https://api.hyperfrp.com/api/users/force-unregister-all'const res = await fetch('https://api.hyperfrp.com/api/users/force-unregister-all', {
method: 'POST',
headers: {
'Authorization': 'Bearer <token>',
},
});
const data = await res.json();
console.log(data);import requests
res = requests.post(
'https://api.hyperfrp.com/api/users/force-unregister-all',
headers={'Authorization': 'Bearer <token>'},
)
data = res.json()
print(data)package main
import (
"fmt"
"net/http"
)
func main() {
req, _ := http.NewRequest("POST", "https://api.hyperfrp.com/api/users/force-unregister-all", nil)
req.Header.Set("Authorization", "Bearer <token>")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var result map[string]any
json.NewDecoder(res.Body).Decode(&result)
fmt.Println(result)
}响应示例
{
"code": 0,
"message": "成功",
"data": {
"success": true,
"message": "操作完成。已向 4 个隧道发送强制下线指令,其中 2 个记录为在线。",
"forceOfflineFailed": false,
"summary": {
"success": 4,
"failed": 0
}
}
}错误场景
| 场景 | HTTP | 说明 |
|---|---|---|
| 隧道查询失败 | 500 | 返回「查询隧道失败」 |
| 递增持久下线代次失败 | 500 | 返回「强制下线失败,请稍后重试」 |
注意事项
- 没有任何隧道时返回
success: true、message「当前账户没有可下线的隧道。」、summary: {"success": 0, "failed": 0}。 - 持久代次先行:即使 Redis 不可用(
forceOfflineFailed: true),FRPS 也会在下次状态检查时依据代次变化关闭运行实例。 - 本操作写入审计日志(actionType 为
force_unregister_all_proxies)。
审计日志
用户操作审计日志来自按用户分文件的 JSONL 日志(user_<用户ID>.log),查询时按时间倒序分页返回。
GET /api/users/audit-logs
分页查询本人操作审计日志,支持按操作类型、状态、IP 与时间范围过滤。
鉴权:JWT Bearer · 限流:无专用限流(全站 WAF 防护照常生效) · 缓存:no-store
查询参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| page | number | 否 | 页码,默认 1;小于 1 时按 1 处理 |
| limit | number | 否 | 每页条数,默认 15;解析失败、小于 1 或大于 100 时回退为 20 |
| pageSize | number | 否 | 每页条数的别名;仅在未提供 limit 时生效,回退规则同上 |
| actionType | string | 否 | 操作类型过滤,不区分大小写的子串匹配,如 checkin、update_password |
| status | string | 否 | 状态精确匹配:success / failure |
| ipAddress | string | 否 | IP 子串匹配 |
| startDate | string | 否 | 起始时间,RFC 3339 格式;解析失败时忽略该条件 |
| endDate | string | 否 | 结束时间,RFC 3339 格式;解析失败时忽略该条件 |
请求示例
curl -H 'Authorization: Bearer <token>' \
'https://api.hyperfrp.com/api/users/audit-logs?page=1&limit=20&actionType=checkin&status=success'const res = await fetch('https://api.hyperfrp.com/api/users/audit-logs?page=1&limit=20&actionType=checkin&status=success', {
headers: {
'Authorization': 'Bearer <token>',
},
});
const data = await res.json();
console.log(data);import requests
res = requests.get(
'https://api.hyperfrp.com/api/users/audit-logs?page=1&limit=20&actionType=checkin&status=success',
headers={'Authorization': 'Bearer <token>'},
)
data = res.json()
print(data)package main
import (
"fmt"
"net/http"
)
func main() {
req, _ := http.NewRequest("GET", "https://api.hyperfrp.com/api/users/audit-logs?page=1&limit=20&actionType=checkin&status=success", nil)
req.Header.Set("Authorization", "Bearer <token>")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var result map[string]any
json.NewDecoder(res.Body).Decode(&result)
fmt.Println(result)
}响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
| logs | array | 当前页日志条目,字段见下表 |
| totalLogs | number | 满足过滤条件的日志总数 |
| page | number | 当前页码(回填后的实际值) |
| pageSize | number | 当前每页条数(回填后的实际值) |
logs 元素字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | number | 恒为 0(文件日志条目无自增 ID) |
| userId | number | 用户 ID |
| username | string | 操作时的用户名 |
| timestamp | string | 操作时间(RFC 3339) |
| ipAddress | string | 操作来源 IP |
| location | string | IP 归属地(ip2region 查询;库未加载时为空串) |
| ipLocation | string | 同 location(冗余字段) |
| actionType | string | 操作类型,如 update_profile / update_avatar / upload_wallpaper / update_preferences / checkin / reset_access_key / force_unregister_all_proxies / update_password |
| actionTypeZh | string | 恒为空串(本接口不回填中文类型) |
| resourceTypeZh | string | 恒为空串 |
| targetResource | string | 恒为空串 |
| targetId | number | 恒为 0 |
| details | string | 操作详情的 JSON 字符串(如 "{\"points\": 128}"),无详情时为 "{}" |
| status | string | success / failure;条目缺失状态时按 success 回填 |
| statusDetail | string | 恒为空串 |
| userAgent | string | 操作时的 User-Agent |
| actionDetailZh | string | 详情的中文拼接串(形如 points: 128, trafficGB: 3),无详情时为空串 |
响应示例
{
"code": 0,
"message": "成功",
"data": {
"logs": [
{
"id": 0,
"userId": 1024,
"username": "alice",
"timestamp": "2026-09-19T08:02:11.532+08:00",
"ipAddress": "203.0.113.7",
"location": "中国 上海",
"ipLocation": "中国 上海",
"actionType": "checkin",
"actionTypeZh": "",
"resourceTypeZh": "",
"targetResource": "",
"targetId": 0,
"details": "{\"points\": 128, \"trafficGB\": 3}",
"status": "success",
"statusDetail": "",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
"actionDetailZh": "points: 128, trafficGB: 3"
}
],
"totalLogs": 42,
"page": 1,
"pageSize": 20
}
}错误场景
| 场景 | HTTP | 说明 |
|---|---|---|
| 日志文件读取失败 | 500 | 返回「查询日志失败」 |
注意事项
- 日志文件不存在时视为空集,返回空
logs与totalLogs: 0,不报错。 details是字符串而非对象,前端需二次JSON.parse。
GET /api/users/audit-logs/export
导出本人审计日志文件。该端点不走统一响应包络,直接以附件形式返回文件流。
鉴权:JWT Bearer · 限流:无专用限流(全站 WAF 防护照常生效) · 缓存:no-store
查询参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| actionType | string | 否 | 同列表接口(子串匹配,不区分大小写) |
| status | string | 否 | success / failure 精确匹配 |
| ipAddress | string | 否 | IP 子串匹配 |
| startDate | string | 否 | 起始时间,RFC 3339 |
| endDate | string | 否 | 结束时间,RFC 3339 |
| format | string | 否 | json 导出 JSON;缺省或其它值导出 CSV |
导出条数固定为按过滤条件匹配的前 1000 条(时间倒序),无分页参数。
请求示例
# 默认导出 CSV
curl -H 'Authorization: Bearer <token>' \
'https://api.hyperfrp.com/api/users/audit-logs/export?status=success' \
-o audit-logs.csv
# 导出 JSON
curl -H 'Authorization: Bearer <token>' \
'https://api.hyperfrp.com/api/users/audit-logs/export?format=json' \
-o audit-logs.json// 导出 JSON:GET /api/users/audit-logs/export?format=json
const res = await fetch('https://api.hyperfrp.com/api/users/audit-logs/export?status=success', {
headers: {
'Authorization': 'Bearer <token>',
},
});
const blob = await res.blob(); // 浏览器可用 URL.createObjectURL(blob) 触发保存import requests
# 导出 JSON:GET /api/users/audit-logs/export?format=json
res = requests.get(
'https://api.hyperfrp.com/api/users/audit-logs/export?status=success',
headers={'Authorization': 'Bearer <token>'},
)
with open('audit-logs.csv', 'wb') as f:
f.write(res.content)// 导出 JSON:GET /api/users/audit-logs/export?format=json
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
req, _ := http.NewRequest("GET", "https://api.hyperfrp.com/api/users/audit-logs/export?status=success", nil)
req.Header.Set("Authorization", "Bearer <token>")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, _ := os.Create('audit-logs.csv')
defer out.Close()
io.Copy(out, res.Body)
fmt.Println("saved audit-logs.csv")
}响应说明(文件下载)
CSV 格式(默认):
- 响应头:
Content-Type: text/csv; charset=utf-8、Content-Disposition: attachment; filename=audit-logs.csv - 正文以 UTF-8 BOM(
EF BB BF)开头,表头为时间,操作类型,操作详情,IP地址,状态;时间格式2006-01-02 15:04:05,操作详情列含逗号/引号/换行时按 CSV 规则转义加引号
JSON 格式(format=json):
- 响应头:
Content-Type: application/json; charset=utf-8、Content-Disposition: attachment; filename=audit-logs.json - 正文为 AuditLog 对象的裸数组(元素字段与列表接口
logs元素一致),无code/message/data包络
两种格式均附带导出上限响应头:
| 响应头 | 说明 |
|---|---|
| X-Audit-Logs-Limit | 导出条数上限,固定 1000 |
| X-Audit-Logs-Truncated | true 表示匹配总数超过 1000 条已被截断;false 表示完整导出 |
错误场景
| 场景 | HTTP | 说明 |
|---|---|---|
| 日志文件读取失败 | 500 | 返回「导出日志失败」(此时仍为统一包络 JSON) |
流量历史
流量历史按 MySQL traffic_histories 表聚合,进出流量单位均为字节(int64),负值在聚合时按 0 处理;结果按 period 倒序返回。
GET /api/users/traffic-history
按时间粒度聚合当前用户的流量历史。
鉴权:JWT Bearer · 限流:无专用限流(全站 WAF 防护照常生效) · 缓存:no-store
查询参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| interval | string | 否 | 聚合粒度:day(默认)/ hour / month;其它值按 day 处理 |
| limit | number | 否 | 返回的周期数,默认 30;超过 365 时按 365 处理 |
请求示例
curl -H 'Authorization: Bearer <token>' \
'https://api.hyperfrp.com/api/users/traffic-history?interval=day&limit=30'const res = await fetch('https://api.hyperfrp.com/api/users/traffic-history?interval=day&limit=30', {
headers: {
'Authorization': 'Bearer <token>',
},
});
const data = await res.json();
console.log(data);import requests
res = requests.get(
'https://api.hyperfrp.com/api/users/traffic-history?interval=day&limit=30',
headers={'Authorization': 'Bearer <token>'},
)
data = res.json()
print(data)package main
import (
"fmt"
"net/http"
)
func main() {
req, _ := http.NewRequest("GET", "https://api.hyperfrp.com/api/users/traffic-history?interval=day&limit=30", nil)
req.Header.Set("Authorization", "Bearer <token>")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var result map[string]any
json.NewDecoder(res.Body).Decode(&result)
fmt.Println(result)
}响应字段
data 为对象数组,元素字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| period | string | 周期标识:day 为 2026-09-19,hour 为 2026-09-19 08:00:00,month 为 2026-09 |
| totalTrafficIn | number | 该周期入站流量合计,单位字节 |
| totalTrafficOut | number | 该周期出站流量合计,单位字节 |
| totalTraffic | number | 入站 + 出站合计,单位字节 |
响应示例
{
"code": 0,
"message": "成功",
"data": [
{
"period": "2026-09-19",
"totalTrafficIn": 1073741824,
"totalTrafficOut": 2147483648,
"totalTraffic": 3221225472
},
{
"period": "2026-09-18",
"totalTrafficIn": 536870912,
"totalTrafficOut": 1073741824,
"totalTraffic": 1610612736
}
]
}错误场景
| 场景 | HTTP | 说明 |
|---|---|---|
| — | — | 无业务错误分支;查询异常时返回空数组 |
GET /api/users/traffic-history/:period
上一接口的别名路由:与 GET /api/users/traffic-history 共用同一 handler,查询参数、响应结构完全一致。
鉴权:JWT Bearer · 限流:无专用限流(全站 WAF 防护照常生效) · 缓存:no-store
行为说明
- 路径参数
:period被忽略,不影响查询结果。 - 聚合粒度仍由查询参数
interval决定(未提供时默认day),即 query 优先。 limit参数与上限规则同上。
请求示例
curl -H 'Authorization: Bearer <token>' \
'https://api.hyperfrp.com/api/users/traffic-history/2026-09?interval=month&limit=12'const res = await fetch('https://api.hyperfrp.com/api/users/traffic-history/2026-09?interval=month&limit=12', {
headers: {
'Authorization': 'Bearer <token>',
},
});
const data = await res.json();
console.log(data);import requests
res = requests.get(
'https://api.hyperfrp.com/api/users/traffic-history/2026-09?interval=month&limit=12',
headers={'Authorization': 'Bearer <token>'},
)
data = res.json()
print(data)package main
import (
"fmt"
"net/http"
)
func main() {
req, _ := http.NewRequest("GET", "https://api.hyperfrp.com/api/users/traffic-history/2026-09?interval=month&limit=12", nil)
req.Header.Set("Authorization", "Bearer <token>")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var result map[string]any
json.NewDecoder(res.Body).Decode(&result)
fmt.Println(result)
}响应示例
与 GET /api/users/traffic-history 完全一致(见上)。
错误场景
| 场景 | HTTP | 说明 |
|---|---|---|
| — | — | 无业务错误分支;查询异常时返回空数组 |
用户视角节点与公告
GET /api/users/nodes
获取对用户可见的节点列表(is_visible 为 true),按创建时间倒序;返回节点模型的原样数组。
鉴权:JWT Bearer · 限流:无专用限流(全站 WAF 防护照常生效) · 缓存:short-cache 60s(响应头 Cache-Control: public, max-age=60, s-maxage=60)
响应字段
data 为节点对象数组,元素字段(节点模型全部 JSON 序列化字段):
| 字段 | 类型 | 说明 |
|---|---|---|
| id | number | 节点 ID |
| name | string | 节点名称,全局唯一 |
| host | string | 节点主机地址(IP 或域名) |
| domain | string | 节点域名 |
| port | number | FRP 连接端口 |
| bindAddress | string | FRPS 监听地址 |
| kcpBindPort | number | KCP 绑定端口,0 表示未启用 |
| quicEnabled | boolean | 是否启用 QUIC |
| quicBindPort | number | QUIC 绑定端口 |
| vhostHTTPPort | number | HTTP 虚拟主机端口 |
| vhostHTTPSPort | number | HTTPS 虚拟主机端口 |
| speedTestPort | number | 测速 HTTP 端口 |
| probeEnabled | boolean | 是否启用 WebRTC 延迟探针 |
| probePort | number | 探针端口(TCP 信令与 UDP 媒体同号) |
| allowedGroupIds | number[] | 允许访问的用户组 ID 列表;空数组表示不限 |
| isGroupExclusive | boolean | 是否专属组节点 |
| permissionMessage | string | 无权限提示文案 |
| exclusiveTagText | string | 专属标签文案 |
| exclusiveTagColor | string | 专属标签颜色 |
| exclusiveTagIcon | string | 专属标签图标 |
| allowedPortsStart | number | 允许端口范围起点 |
| allowedPortsEnd | number | 允许端口范围终点 |
| allowPortRanges | array | 允许端口段列表,元素为 {start, end} |
| location | string | 节点位置描述 |
| description | string | 节点描述 |
| status | string | 节点状态:online / api_only / unreachable / frps_stopped / offline / maintenance |
| cpuInfo | string | CPU 型号信息 |
| cpuTemperature | number | CPU 温度(°C) |
| isVisible | boolean | 是否可见;本接口恒为 true |
| maxProxies | number | 节点隧道数上限 |
| bandwidthLimitKbps | number | 节点带宽上限,单位 kbps |
| totalTrafficInBytes | number | 节点累计入站流量,单位字节 |
| totalTrafficOutBytes | number | 节点累计出站流量,单位字节 |
| networkPacketsSent | number | 累计发送数据包数 |
| networkPacketsRecv | number | 累计接收数据包数 |
| clientsOnline | number | 在线客户端数 |
| proxiesCount | number | 节点上隧道数 |
| lastHeartbeat | string 或 null | 最近心跳时间 |
| onlineSince | string 或 null | 本次上线起始时间 |
| bootTime | number 或 null | 节点系统启动时间(Unix 秒) |
| cpuUsage | number | CPU 占用率(百分比) |
| loadAverage | string | 负载均值(字符串) |
| systemLoad | string | 系统负载 JSON 串 |
| memoryUsage | number | 内存占用率(百分比) |
| memoryTotal | number | 恒为 0(模型计算字段,本接口未填充) |
| memoryUsed | number | 恒为 0(同上) |
| diskUsage | number | 磁盘占用率(百分比) |
| diskTotal | number | 恒为 0(同上) |
| diskUsed | number | 恒为 0(同上) |
| networkSpeedIn | number | 实时入站速率,单位 B/s |
| networkSpeedOut | number | 实时出站速率,单位 B/s |
| frpsReportedInBytes | number | FRPS 上报累计入站流量,单位字节 |
| frpsReportedOutBytes | number | FRPS 上报累计出站流量,单位字节 |
| logPath | string | 恒为空串(模型计算字段,本接口未填充) |
| frpsPath | string | 恒为空串(同上) |
| configPath | string | 恒为空串(同上) |
| dashboardPort | number | FRPS 仪表盘端口 |
| webServerAddr | string | FRPS web 服务监听地址 |
| tcpMux | boolean | 是否启用 TCP 多路复用 |
| tcpMuxHTTPConnectPort | number | TCP Mux HTTP Connect 端口 |
| tcpKeepAlive | number | TCP keepalive,单位秒 |
| heartbeatTimeout | number | 心跳超时,单位秒 |
| maxPoolCount | number | 最大连接池数 |
| tlsForce | boolean | 是否强制 TLS |
| logLevel | string | FRPS 日志级别 |
| logMaxDays | number | FRPS 日志保留天数 |
| disableLogColor | boolean | 是否禁用日志颜色 |
| region | string | 地区(如 Overseas) |
| allowedProtocols | string[] | 允许的隧道协议列表 |
| bandwidthValue | number | 带宽数值(与 bandwidthUnit 配合展示) |
| bandwidthUnit | string | 带宽单位:Mbps / Gbps |
| hyperzonefrpApiURL | string | 节点侧面板 API 基址 |
| hyperzonefrpHeartbeatInterval | number | 心跳上报间隔,单位秒 |
| hyperzonefrpResourceInterval | number | 资源上报间隔,单位秒 |
| hyperzonefrpTrafficFlushInterval | number | 流量落盘间隔,单位秒 |
| hyperzonefrpTunnelCheckInterval | number | 隧道检查间隔,单位秒 |
| trafficQueuePath | string | 流量 WAL 路径 |
| trafficQueueHighWatermark | number | 流量队列高水位(条数) |
| upgradeEnabled | boolean | 节点自动升级开关 |
| panelFailureMode | string | 面板不可用降级策略:fail-closed / fail-open |
| upgradeServiceName | string | 升级服务名 |
| upgradeInstallDir | string | 升级安装目录 |
| upgradeSelfCheckWindow | number | 升级自检窗口,单位秒 |
| createdAt | string | 创建时间(RFC 3339) |
| updatedAt | string | 更新时间(RFC 3339) |
请求示例
curl -H 'Authorization: Bearer <token>' \
'https://api.hyperfrp.com/api/users/nodes'const res = await fetch('https://api.hyperfrp.com/api/users/nodes', {
headers: {
'Authorization': 'Bearer <token>',
},
});
const data = await res.json();
console.log(data);import requests
res = requests.get(
'https://api.hyperfrp.com/api/users/nodes',
headers={'Authorization': 'Bearer <token>'},
)
data = res.json()
print(data)package main
import (
"fmt"
"net/http"
)
func main() {
req, _ := http.NewRequest("GET", "https://api.hyperfrp.com/api/users/nodes", nil)
req.Header.Set("Authorization", "Bearer <token>")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var result map[string]any
json.NewDecoder(res.Body).Decode(&result)
fmt.Println(result)
}响应示例
{
"code": 0,
"message": "成功",
"data": [
{
"id": 2,
"name": "华东-1",
"host": "cn-east-1.example.net",
"domain": "cn-east-1.example.net",
"port": 7000,
"bindAddress": "0.0.0.0",
"kcpBindPort": 0,
"quicEnabled": false,
"quicBindPort": 0,
"vhostHTTPPort": 8080,
"vhostHTTPSPort": 8443,
"speedTestPort": 9001,
"probeEnabled": true,
"probePort": 27100,
"allowedGroupIds": [],
"isGroupExclusive": false,
"permissionMessage": "",
"exclusiveTagText": "",
"exclusiveTagColor": "#ff69b4",
"exclusiveTagIcon": "",
"allowedPortsStart": 10000,
"allowedPortsEnd": 60000,
"allowPortRanges": [],
"location": "上海",
"description": "华东电信节点",
"status": "online",
"cpuInfo": "Intel Xeon Platinum 8269CY",
"cpuTemperature": 45.5,
"isVisible": true,
"maxProxies": 10,
"bandwidthLimitKbps": 102400,
"totalTrafficInBytes": 1099511627776,
"totalTrafficOutBytes": 2199023255552,
"networkPacketsSent": 123456789,
"networkPacketsRecv": 987654321,
"clientsOnline": 42,
"proxiesCount": 128,
"lastHeartbeat": "2026-09-19T09:00:02+08:00",
"onlineSince": "2026-09-01T00:00:00+08:00",
"bootTime": 1758600000,
"cpuUsage": 23.4,
"loadAverage": "0.52 0.48 0.45",
"systemLoad": "{}",
"memoryUsage": 61.2,
"memoryTotal": 0,
"memoryUsed": 0,
"diskUsage": 55.0,
"diskTotal": 0,
"diskUsed": 0,
"networkSpeedIn": 1048576,
"networkSpeedOut": 2097152,
"frpsReportedInBytes": 107374182400,
"frpsReportedOutBytes": 214748364800,
"logPath": "",
"frpsPath": "",
"configPath": "",
"dashboardPort": 7500,
"webServerAddr": "127.0.0.1",
"tcpMux": true,
"tcpMuxHTTPConnectPort": 0,
"tcpKeepAlive": 7200,
"heartbeatTimeout": 90,
"maxPoolCount": 5,
"tlsForce": false,
"logLevel": "info",
"logMaxDays": 7,
"disableLogColor": false,
"region": "Overseas",
"allowedProtocols": ["tcp", "udp", "http", "https"],
"bandwidthValue": 100,
"bandwidthUnit": "Mbps",
"hyperzonefrpApiURL": "https://api.hyperfrp.com/api",
"hyperzonefrpHeartbeatInterval": 30,
"hyperzonefrpResourceInterval": 15,
"hyperzonefrpTrafficFlushInterval": 10,
"hyperzonefrpTunnelCheckInterval": 30,
"trafficQueuePath": "",
"trafficQueueHighWatermark": 10000,
"upgradeEnabled": false,
"panelFailureMode": "fail-closed",
"upgradeServiceName": "",
"upgradeInstallDir": "",
"upgradeSelfCheckWindow": 60,
"createdAt": "2026-01-01T00:00:00+08:00",
"updatedAt": "2026-09-19T09:00:05+08:00"
}
]
}错误场景
| 场景 | HTTP | 说明 |
|---|---|---|
| — | — | 无业务错误分支;无可见节点时返回空数组 |
注意事项
- 节点敏感凭证字段(
token、hyperzonefrpApiToken、hyperzonefrpNodeToken、dashboardUser、dashboardPassword)在模型上标记为不序列化,永远不会出现在响应中。 - 列表只按
is_visible过滤,不按用户组做访问过滤;组权限判断由创建隧道等接口负责。
GET /api/users/nodes/:id
获取单个节点详情;返回 handler 手工构造的字段子集(与列表接口不同,非整模型序列化)。
鉴权:JWT Bearer · 限流:无专用限流(全站 WAF 防护照常生效) · 缓存:short-cache 60s
响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
| id | number | 节点 ID |
| name | string | 节点名称 |
| address | string | 节点地址(取自节点 host 字段) |
| port | number | FRP 连接端口 |
| location | string | 节点位置描述 |
| description | string | 节点描述 |
| status | string | 节点状态 |
| cpuUsage | number | CPU 占用率(百分比) |
| memoryUsage | number | 内存占用率(百分比) |
| diskUsage | number | 磁盘占用率(百分比) |
| networkSpeedIn | number | 实时入站速率,单位 B/s |
| networkSpeedOut | number | 实时出站速率,单位 B/s |
| clientsOnline | number | 在线客户端数 |
| proxiesCount | number | 节点上隧道数 |
| lastHeartbeat | string 或 null | 最近心跳时间 |
| createdAt | string | 创建时间(RFC 3339) |
| updatedAt | string | 更新时间(RFC 3339) |
请求示例
curl -H 'Authorization: Bearer <token>' \
'https://api.hyperfrp.com/api/users/nodes/2'const res = await fetch('https://api.hyperfrp.com/api/users/nodes/2', {
headers: {
'Authorization': 'Bearer <token>',
},
});
const data = await res.json();
console.log(data);import requests
res = requests.get(
'https://api.hyperfrp.com/api/users/nodes/2',
headers={'Authorization': 'Bearer <token>'},
)
data = res.json()
print(data)package main
import (
"fmt"
"net/http"
)
func main() {
req, _ := http.NewRequest("GET", "https://api.hyperfrp.com/api/users/nodes/2", nil)
req.Header.Set("Authorization", "Bearer <token>")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var result map[string]any
json.NewDecoder(res.Body).Decode(&result)
fmt.Println(result)
}响应示例
{
"code": 0,
"message": "成功",
"data": {
"id": 2,
"name": "华东-1",
"address": "cn-east-1.example.net",
"port": 7000,
"location": "上海",
"description": "华东电信节点",
"status": "online",
"cpuUsage": 23.4,
"memoryUsage": 61.2,
"diskUsage": 55.0,
"networkSpeedIn": 1048576,
"networkSpeedOut": 2097152,
"clientsOnline": 42,
"proxiesCount": 128,
"lastHeartbeat": "2026-09-19T09:00:02+08:00",
"createdAt": "2026-01-01T00:00:00+08:00",
"updatedAt": "2026-09-19T09:00:05+08:00"
}
}错误场景
| 场景 | HTTP | 说明 |
|---|---|---|
| 节点 ID 非数字 | 400 | 返回「无效的节点ID」 |
| 节点不存在 | 404 | 返回「节点未找到」 |
注意事项
- 本接口按 ID 直查,不校验
is_visible,即隐藏节点同样可按 ID 查到详情。
GET /api/users/nodes/:id/traffic-history
按时间粒度聚合指定节点的流量历史,参数与响应结构同用户维度流量历史,聚合范围按 node_id 过滤。
鉴权:JWT Bearer · 限流:无专用限流(全站 WAF 防护照常生效) · 缓存:no-store
查询参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| interval | string | 否 | 聚合粒度:day(默认)/ hour / month;其它值按 day 处理 |
| limit | number | 否 | 返回的周期数,默认 30;超过 365 时按 365 处理 |
请求示例
curl -H 'Authorization: Bearer <token>' \
'https://api.hyperfrp.com/api/users/nodes/2/traffic-history?interval=hour&limit=24'const res = await fetch('https://api.hyperfrp.com/api/users/nodes/2/traffic-history?interval=hour&limit=24', {
headers: {
'Authorization': 'Bearer <token>',
},
});
const data = await res.json();
console.log(data);import requests
res = requests.get(
'https://api.hyperfrp.com/api/users/nodes/2/traffic-history?interval=hour&limit=24',
headers={'Authorization': 'Bearer <token>'},
)
data = res.json()
print(data)package main
import (
"fmt"
"net/http"
)
func main() {
req, _ := http.NewRequest("GET", "https://api.hyperfrp.com/api/users/nodes/2/traffic-history?interval=hour&limit=24", nil)
req.Header.Set("Authorization", "Bearer <token>")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var result map[string]any
json.NewDecoder(res.Body).Decode(&result)
fmt.Println(result)
}响应字段
data 为对象数组,元素字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| period | string | 周期标识,格式随 interval:2026-09-19 / 2026-09-19 08:00:00 / 2026-09 |
| totalTrafficIn | number | 该周期该节点入站流量合计,单位字节 |
| totalTrafficOut | number | 该周期该节点出站流量合计,单位字节 |
| totalTraffic | number | 入站 + 出站合计,单位字节 |
响应示例
{
"code": 0,
"message": "成功",
"data": [
{
"period": "2026-09-19 08:00:00",
"totalTrafficIn": 104857600,
"totalTrafficOut": 209715200,
"totalTraffic": 314572800
},
{
"period": "2026-09-19 07:00:00",
"totalTrafficIn": 52428800,
"totalTrafficOut": 104857600,
"totalTraffic": 157286400
}
]
}错误场景
| 场景 | HTTP | 说明 |
|---|---|---|
| 节点 ID 非数字 | 400 | 返回「无效的节点ID」 |
| 节点不存在 | 404 | 返回「节点未找到」 |
注意事项
- 聚合按
node_id过滤,反映的是节点整体流量(含全部用户),并非当前用户个人的流量。
GET /api/users/announcements/:id
获取单个已发布公告的详情;未发布或不存在的公告一律 404。
鉴权:JWT Bearer · 限流:无专用限流(全站 WAF 防护照常生效) · 缓存:short-cache 60s
响应字段
| 字段 | 类型 | 说明 |
|---|---|---|
| id | number | 公告 ID |
| title | string | 公告标题 |
| content | string | 公告正文 |
| contentType | string | 内容类型,如 text / markdown |
| authorId | number | 作者用户 ID |
| visibleGroupIds | string | 可见用户组 ID 的 JSON 串(如 "[1,2]";空数组通常表示全部可见) |
| isPublished | boolean | 是否发布;本接口恒为 true |
| publishDate | string | 发布时间(RFC 3339) |
| createdAt | string | 创建时间(RFC 3339) |
| updatedAt | string | 更新时间(RFC 3339) |
请求示例
curl -H 'Authorization: Bearer <token>' \
'https://api.hyperfrp.com/api/users/announcements/5'const res = await fetch('https://api.hyperfrp.com/api/users/announcements/5', {
headers: {
'Authorization': 'Bearer <token>',
},
});
const data = await res.json();
console.log(data);import requests
res = requests.get(
'https://api.hyperfrp.com/api/users/announcements/5',
headers={'Authorization': 'Bearer <token>'},
)
data = res.json()
print(data)package main
import (
"fmt"
"net/http"
)
func main() {
req, _ := http.NewRequest("GET", "https://api.hyperfrp.com/api/users/announcements/5", nil)
req.Header.Set("Authorization", "Bearer <token>")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
var result map[string]any
json.NewDecoder(res.Body).Decode(&result)
fmt.Println(result)
}响应示例
{
"code": 0,
"message": "成功",
"data": {
"id": 5,
"title": "9 月华东节点扩容公告",
"content": "华东-1 节点已完成带宽扩容至 100Mbps。",
"contentType": "markdown",
"authorId": 1,
"visibleGroupIds": "[]",
"isPublished": true,
"publishDate": "2026-09-10T10:00:00+08:00",
"createdAt": "2026-09-10T09:55:00+08:00",
"updatedAt": "2026-09-10T10:00:00+08:00"
}
}错误场景
| 场景 | HTTP | 说明 |
|---|---|---|
| 公告 ID 非数字 | 400 | 返回「无效的公告ID」 |
| 公告不存在或未发布 | 404 | 返回「公告未找到」 |
注意事项
- 响应不含
author对象(模型关联未预加载,omitempty生效),作者信息仅有authorId。 - 本接口不按
visibleGroupIds做用户组过滤,仅校验is_published。