Skip to content

用户资料与账户

本章覆盖用户侧「个人资料与账户」全部 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/头像 URLno-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

响应字段

字段类型说明
idnumber用户 ID
usernamestring用户名
emailstring注册邮箱
rolestring角色:user / admin / superadmin
statusstring账户状态:active / banned / inactive
groupIdnumber所属用户组 ID;0 表示未分组
groupobject用户组对象,恒返回;未分组时为零值对象(见下表)
accessKeystringFRP 客户端访问密钥,32 位十六进制字符串;历史空密钥账户首次调用时自动补发并落库
balancenumber账户余额(积分),浮点数
trafficnumber恒为 0(遗留占位字段,库中无此列)
usedTrafficnumber恒为 0(遗留占位字段,实际用量见 remainingTrafficBytes 口径)
remainingTrafficBytesnumber剩余流量,单位字节(float64)
realNamestring实名姓名;为空时该键省略(omitempty)
isVerifiedboolean是否已通过实名认证
avatarstring头像 URL;默认为占位图地址
lastLoginDatestring 或缺省最近登录时间(RFC 3339);从未登录时省略(omitempty)
lastLoginIpstring最近登录 IP
maxProxiesnumber有效隧道数量上限(用户与用户组取大者);-1 表示不限制
proxyCountnumber当前隧道数量(实时统计 proxies 表,非冗余列)
preferencesobject偏好设置键值对,至少为 {}
remarkstring恒为空串(handler 固定写死,不回传管理员备注)
createdAtstring注册时间(RFC 3339)
lastSignInDatestring 或 null最近签到时间;从未签到时为 null
qqstring恒为空串(handler 固定写死)
bandwidthInKbpsnumber有效入站带宽上限,单位 kbps;仅取自用户组,未分组时为 0
bandwidthOutKbpsnumber有效出站带宽上限,单位 kbps;仅取自用户组,未分组时为 0
realNameVerificationStatusstring恒为空串(handler 固定写死)
twoFactorEnabledboolean是否已开启两步验证

group 对象字段(用户组模型原样序列化):

字段类型说明
idnumber用户组 ID;未分组时为 0
namestring组名
descriptionstring组描述
badgeColorstring徽章颜色,默认 #808080
isDefaultboolean是否默认组
maxProxiesnumber组隧道数上限
bandwidthOutKbpsnumber组出站带宽,单位 kbps
bandwidthInKbpsnumber组入站带宽,单位 kbps
addedTrafficBytesnumber组附加流量,单位字节
addedBalancenumber组附加余额
isVisibleboolean是否在组列表可见
isJoinableboolean是否可自行加入
linkedPackageIdnumber 或 null关联套餐 ID
createdAtstring创建时间
updatedAtstring更新时间

请求示例

bash
curl -H 'Authorization: Bearer <token>' \
  'https://api.hyperfrp.com/api/users/profile'
javascript
const res = await fetch('https://api.hyperfrp.com/api/users/profile', {
  headers: {
    'Authorization': 'Bearer <token>',
  },
});
const data = await res.json();
console.log(data);
python
import requests

res = requests.get(
    'https://api.hyperfrp.com/api/users/profile',
    headers={'Authorization': 'Bearer <token>'},
)
data = res.json()
print(data)
go
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)
}

响应示例

json
{
  "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

请求体

字段类型必填说明
usernamestring新用户名;空串或与当前相同则跳过;修改时做全局唯一性校验
qqstringQQ 号;仅非空时更新(传空串不会清空已有值)
avatarstring新头像 URL;仅非空且以 http 开头时生效

请求示例

bash
curl -X PUT -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{"username":"alice2","qq":"12345678"}' \
  'https://api.hyperfrp.com/api/users/profile'
javascript
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);
python
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)
go
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)
}

响应字段

字段类型说明
idnumber用户 ID
usernamestring更新后的用户名
emailstring注册邮箱(不可经本接口修改)
avatarstring更新后的头像 URL

响应示例

json
{
  "code": 0,
  "message": "成功",
  "data": {
    "id": 1024,
    "username": "alice2",
    "email": "alice@example.com",
    "avatar": "/uploads/avatars/1024-1726723200000.png"
  }
}

错误场景

场景HTTP说明
请求体非法 JSON400返回「无效的请求数据」
用户名已被占用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)

字段类型必填说明
avatarfile图片文件;扩展名限 jpg/jpeg/png/gif/webp,Part 的 Content-Type 须与扩展名匹配,且做文件头魔数校验;≤ 2MiB

请求示例

bash
curl -X PUT -H 'Authorization: Bearer <token>' \
  -F 'avatar=@/path/to/avatar.png;type=image/png' \
  'https://api.hyperfrp.com/api/users/profile/avatar'
javascript
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);
python
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)
go
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)
}

响应字段

字段类型说明
messagestring固定为「头像上传成功」
avatarstring新头像访问路径,形如 /uploads/avatars/<用户ID>-<毫秒时间戳><扩展名>

响应示例

json
{
  "code": 0,
  "message": "成功",
  "data": {
    "message": "头像上传成功",
    "avatar": "/uploads/avatars/1024-1726723200000.png"
  }
}

错误场景

场景HTTP说明
缺少 avatar 文件400返回「未检测到头像文件」
扩展名/Content-Type/魔数不匹配400返回「只允许上传有效的图片文件」
文件超过 2MiB413返回「文件大小不能超过2MiB」
请求体超过服务端动态上限413返回「请求体不能超过 xxMiB」
读取请求体超时408返回「读取上传请求超时」
创建目录/保存文件失败500返回「创建上传目录失败」/「保存文件失败」
用户记录不存在404返回「用户不存在」,已落盘文件会删除

注意事项

  • 上传成功后删除旧的本站上传头像(http 外链头像不删文件);旧 URL 立即失效。

PUT /api/users/profile/password

修改登录密码;成功后账户 authVersion 递增,该用户所有已签发的 JWT 立即失效,全部设备需重新登录。

鉴权:JWT Bearer · 限流:无专用限流(全站 WAF 防护照常生效) · 缓存:no-store

请求体

字段类型必填说明
currentPasswordstring见说明当前密码;与 oldPassword 二选一,同时提供时优先 currentPassword
oldPasswordstring见说明当前密码的兼容字段名;currentPassword 为空时回退使用
newPasswordstring见说明新密码;与 password 二选一,同时提供时优先 newPassword
passwordstring见说明新密码的兼容字段名;newPassword 为空时回退使用

两组字段解析后均会去除首尾空白;任一组解析结果为空即报错。

请求示例

bash
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'
javascript
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);
python
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)
go
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)
}

响应字段

字段类型说明
messagestring固定为「密码更新成功」

响应示例

json
{
  "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)

字段类型必填说明
wallpaperfile图片文件;扩展名限 jpg/jpeg/png/gif/webp,Content-Type 须匹配,做魔数校验;≤ 10MiB

请求示例

bash
curl -X POST -H 'Authorization: Bearer <token>' \
  -F 'wallpaper=@/path/to/wallpaper.jpg;type=image/jpeg' \
  'https://api.hyperfrp.com/api/users/profile/wallpaper'
javascript
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);
python
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)
go
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)
}

响应字段

字段类型说明
messagestring固定为「壁纸上传成功」
urlstring新壁纸访问路径,形如 /uploads/wallpapers/wp-<用户ID>-<毫秒时间戳><扩展名>

响应示例

json
{
  "code": 0,
  "message": "成功",
  "data": {
    "message": "壁纸上传成功",
    "url": "/uploads/wallpapers/wp-1024-1726723200000.jpg"
  }
}

错误场景

场景HTTP说明
缺少 wallpaper 文件400返回「未检测到壁纸文件」
扩展名/Content-Type/魔数不匹配400返回「只允许上传有效的图片文件」
文件超过 10MiB413返回「文件大小不能超过10MiB」
请求体超过服务端动态上限413返回「请求体不能超过 xxMiB」
读取请求体超时408返回「读取上传请求超时」
创建目录/保存文件失败500返回「创建上传目录失败」/「保存文件失败」

注意事项

  • 上传成功时先删除该用户所有旧壁纸文件(wp-<用户ID>- 前缀),一个用户只保留最新一张。
  • 壁纸 URL 不写入用户资料,接口仅返回 URL,由前端自行保存使用。

PUT /api/users/profile/preferences

更新偏好设置。合并语义:与既有偏好做顶层键浅合并——请求体中出现的键逐个覆盖,未出现的键保留原值;不做嵌套对象深合并,顶层键的整个值(含嵌套结构)整体替换。

鉴权:JWT Bearer · 限流:无专用限流(全站 WAF 防护照常生效) · 缓存:no-store

请求体

字段类型必填说明
preferencesobject任意键值对;显式传 null 或缺省视为非法

请求示例

bash
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'
javascript
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);
python
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)
go
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)
}

响应字段

字段类型说明
messagestring固定为「偏好设置已更新」
preferencesobject合并后的完整偏好设置

响应示例

json
{
  "code": 0,
  "message": "成功",
  "data": {
    "message": "偏好设置已更新",
    "preferences": {
      "theme": "dark",
      "language": "zh-CN",
      "sidebarCollapsed": true
    }
  }
}

错误场景

场景HTTP说明
请求体非法 JSON400返回「无效的请求数据」
preferences 缺失或为 null400返回「偏好设置数据不能为空」
用户记录不存在404返回「用户不存在」
落库失败500返回「保存偏好设置失败」

POST /api/users/profile/speed-boost/check

检查当前用户全部隧道所涉节点是否满足「极速模式」条件,供开启极速模式前预检。

鉴权:JWT Bearer · 限流:无专用限流(全站 WAF 防护照常生效) · 缓存:no-store

判定条件(对用户隧道关联的每个去重节点):节点带宽 bandwidthLimitKbps > 102400(即 > 100 Mbps)、CPU 负载 cpuUsage < 50(百分比)、节点状态为 onlineapi_only。任一不满足即列入 ineligibleNodes 并给出原因。

响应字段

字段类型说明
eligibleboolean全部节点是否符合条件
messagestring结论描述;全部符合为「所有节点都满足极速模式条件」,否则「有 N 个节点不满足极速模式条件」
totalNodesnumber去重后涉及的节点总数
eligibleNodesnumber符合条件的节点数(计数,非数组)
ineligibleNodesarray 或 null不符合条件的节点明细;全部符合时为 null,用户没有隧道时为 []

ineligibleNodes 元素字段:

字段类型说明
nodeIdnumber节点 ID
nodeNamestring节点名称
bandwidthnumber节点带宽上限,单位 kbps
loadnumber节点 CPU 负载,百分比数值
statusstring节点状态
reasonsstring[]不达标原因列表(带宽不足 / 负载过高 / 节点状态异常)

请求示例

bash
curl -X POST -H 'Authorization: Bearer <token>' \
  'https://api.hyperfrp.com/api/users/profile/speed-boost/check'
javascript
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);
python
import requests

res = requests.post(
    'https://api.hyperfrp.com/api/users/profile/speed-boost/check',
    headers={'Authorization': 'Bearer <token>'},
)
data = res.json()
print(data)
go
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)
}

响应示例

json
{
  "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: truetotalNodes: 0eligibleNodes: 0ineligibleNodes: []
  • totalNodes / eligibleNodes 是计数;不合条件明细只出现在 ineligibleNodes 中。

账户操作

POST /api/users/reset-access-key

轮换 FRP 客户端访问密钥,并强制下线该用户全部隧道(含非运行中,用于清理历史不一致状态)。

鉴权:JWT Bearer · 限流:无专用限流(全站 WAF 防护照常生效) · 缓存:no-store

处理语义:在单事务内生成新密钥(32 位十六进制,唯一性校验)并落库,同时把该用户全部隧道原子递增持久强制下线代次、状态置为 stopped;随后用旧密钥向 Redis 推送强制下线信号(键 force-offline:<旧accessKey>.<隧道名>,TTL 90 秒)作为低延迟通知——信号失败不阻断轮换,持久代次会在节点下次状态检查时兜底关闭实例。

响应字段

字段类型说明
messagestring按真实推送结果拼接的提示,如「访问密钥已成功重置,该用户原有隧道已被强制下线,请用新密钥重新配置客户端」
accessKeystring新访问密钥,32 位十六进制字符串
proxyCountnumber该用户隧道总数(含非运行中)
runningCountnumber轮换时处于 running 状态的隧道数
signaledCountnumber成功写入 Redis 强制下线信号的隧道数
forceOfflineFailedboolean即时信号推送是否失败(Redis 未初始化或写入失败);true 时由持久代次兜底

请求示例

bash
curl -X POST -H 'Authorization: Bearer <token>' \
  'https://api.hyperfrp.com/api/users/reset-access-key'
javascript
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);
python
import requests

res = requests.post(
    'https://api.hyperfrp.com/api/users/reset-access-key',
    headers={'Authorization': 'Bearer <token>'},
)
data = res.json()
print(data)
go
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)
}

响应示例

json
{
  "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

请求体

字段类型必填说明
captchaTicketstring预留的验证码票据字段(结构体已定义);当前 handler 未读取、未校验,可省略

访问密钥鉴权(可选)

本接口是全站唯一支持用访问密钥替代 JWT 的接口,供脚本/定时任务免登录自动签到:

bash
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 等会话权限。

请求示例

bash
curl -X POST -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{}' \
  'https://api.hyperfrp.com/api/users/checkin'
javascript
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);
python
import requests

res = requests.post(
    'https://api.hyperfrp.com/api/users/checkin',
    headers={'Authorization': 'Bearer <token>'},
    json={},
)
data = res.json()
print(data)
go
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)
}

响应字段

字段类型说明
messagestring如「签到成功!您获得了 128 积分和 3 GB 流量。」
rewardsobject奖励明细,见下表

rewards 字段:

字段类型说明
pointsnumber积分奖励,随机 50~200(含两端),直接累加进 balance
trafficGBnumber流量奖励(GB),随机 1~5(含两端)
trafficBytesnumber流量奖励的字节数,即 trafficGB × 1073741824,累加进 remainingTrafficBytes

响应示例

json
{
  "code": 0,
  "message": "成功",
  "data": {
    "message": "签到成功!您获得了 128 积分和 3 GB 流量。",
    "rewards": {
      "points": 128,
      "trafficGB": 3,
      "trafficBytes": 3221225472
    }
  }
}

错误场景

场景HTTP说明
未携带任何凭证401返回「未提供认证令牌或访问密钥」;JWT 与访问密钥二选一
访问密钥无效或格式非法401仅使用 X-Access-Key 鉴权时;统一返回「访问密钥无效」,不区分密钥不存在与格式非法
访问密钥鉴权但账户非 active401返回「账户状态异常,无法使用访问密钥」
访问密钥鉴权但账户被封禁403响应体 dataisBanned: trueban_reason,与 JWT 封禁响应同构
JWT 鉴权但账户被封禁403响应体 dataisBanned: trueban_reason
当日已签到400返回「您今天已经签到过了」;判定依据为 lastSignInDate 是否落在今天(服务器本地时区)
事务执行失败500返回「签到失败,请稍后重试」

注意事项

  • 防重复依赖数据库事务 + SELECT ... FOR UPDATE 行锁,并发重复签到只有一个请求成功,其余落入「当日已签到」分支;本路由无更细粒度的专用限流中间件。
  • 随机数使用 crypto/rand,奖励在请求处理前预生成,与落库同事务生效。
  • 本操作写入审计日志(actionType 为 checkin,details 含 points / trafficGB / authMethod,后者为 jwtaccess_key,便于管理员区分签到来源)。

POST /api/users/force-unregister-all

强制下线本人全部隧道:数据库层原子递增该用户全部隧道的持久强制下线代次并置为 stopped,再经 Redis 推送即时信号。

鉴权:JWT Bearer · 限流:无专用限流(全站 WAF 防护照常生效) · 缓存:no-store

响应字段

字段类型说明
successboolean恒为 true(到达此响应即已受理)
messagestring如「操作完成。已向 4 个隧道发送强制下线指令,其中 2 个记录为在线。」
forceOfflineFailedbooleanRedis 即时通知是否失败;true 时追加提示「即时通知失败,已由持久下线记录兜底,将在节点下次状态检查时生效」
summaryobject汇总,见下表

summary 字段:

字段类型说明
successnumber成功写入即时信号的隧道数
failednumber恒为 0(信号失败归入 forceOfflineFailed 兜底语义,不计失败)

请求示例

bash
curl -X POST -H 'Authorization: Bearer <token>' \
  'https://api.hyperfrp.com/api/users/force-unregister-all'
javascript
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);
python
import requests

res = requests.post(
    'https://api.hyperfrp.com/api/users/force-unregister-all',
    headers={'Authorization': 'Bearer <token>'},
)
data = res.json()
print(data)
go
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)
}

响应示例

json
{
  "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

查询参数

参数类型必填说明
pagenumber页码,默认 1;小于 1 时按 1 处理
limitnumber每页条数,默认 15;解析失败、小于 1 或大于 100 时回退为 20
pageSizenumber每页条数的别名;仅在未提供 limit 时生效,回退规则同上
actionTypestring操作类型过滤,不区分大小写的子串匹配,如 checkinupdate_password
statusstring状态精确匹配:success / failure
ipAddressstringIP 子串匹配
startDatestring起始时间,RFC 3339 格式;解析失败时忽略该条件
endDatestring结束时间,RFC 3339 格式;解析失败时忽略该条件

请求示例

bash
curl -H 'Authorization: Bearer <token>' \
  'https://api.hyperfrp.com/api/users/audit-logs?page=1&limit=20&actionType=checkin&status=success'
javascript
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);
python
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)
go
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)
}

响应字段

字段类型说明
logsarray当前页日志条目,字段见下表
totalLogsnumber满足过滤条件的日志总数
pagenumber当前页码(回填后的实际值)
pageSizenumber当前每页条数(回填后的实际值)

logs 元素字段:

字段类型说明
idnumber恒为 0(文件日志条目无自增 ID)
userIdnumber用户 ID
usernamestring操作时的用户名
timestampstring操作时间(RFC 3339)
ipAddressstring操作来源 IP
locationstringIP 归属地(ip2region 查询;库未加载时为空串)
ipLocationstring同 location(冗余字段)
actionTypestring操作类型,如 update_profile / update_avatar / upload_wallpaper / update_preferences / checkin / reset_access_key / force_unregister_all_proxies / update_password
actionTypeZhstring恒为空串(本接口不回填中文类型)
resourceTypeZhstring恒为空串
targetResourcestring恒为空串
targetIdnumber恒为 0
detailsstring操作详情的 JSON 字符串(如 "{\"points\": 128}"),无详情时为 "{}"
statusstringsuccess / failure;条目缺失状态时按 success 回填
statusDetailstring恒为空串
userAgentstring操作时的 User-Agent
actionDetailZhstring详情的中文拼接串(形如 points: 128, trafficGB: 3),无详情时为空串

响应示例

json
{
  "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返回「查询日志失败」

注意事项

  • 日志文件不存在时视为空集,返回空 logstotalLogs: 0,不报错。
  • details 是字符串而非对象,前端需二次 JSON.parse

GET /api/users/audit-logs/export

导出本人审计日志文件。该端点不走统一响应包络,直接以附件形式返回文件流。

鉴权:JWT Bearer · 限流:无专用限流(全站 WAF 防护照常生效) · 缓存:no-store

查询参数

参数类型必填说明
actionTypestring同列表接口(子串匹配,不区分大小写)
statusstringsuccess / failure 精确匹配
ipAddressstringIP 子串匹配
startDatestring起始时间,RFC 3339
endDatestring结束时间,RFC 3339
formatstringjson 导出 JSON;缺省或其它值导出 CSV

导出条数固定为按过滤条件匹配的前 1000 条(时间倒序),无分页参数。

请求示例

bash
# 默认导出 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
javascript
// 导出 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) 触发保存
python
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)
go
// 导出 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-8Content-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-8Content-Disposition: attachment; filename=audit-logs.json
  • 正文为 AuditLog 对象的裸数组(元素字段与列表接口 logs 元素一致),无 code/message/data 包络

两种格式均附带导出上限响应头:

响应头说明
X-Audit-Logs-Limit导出条数上限,固定 1000
X-Audit-Logs-Truncatedtrue 表示匹配总数超过 1000 条已被截断;false 表示完整导出

错误场景

场景HTTP说明
日志文件读取失败500返回「导出日志失败」(此时仍为统一包络 JSON)

流量历史

流量历史按 MySQL traffic_histories 表聚合,进出流量单位均为字节(int64),负值在聚合时按 0 处理;结果按 period 倒序返回。

GET /api/users/traffic-history

按时间粒度聚合当前用户的流量历史。

鉴权:JWT Bearer · 限流:无专用限流(全站 WAF 防护照常生效) · 缓存:no-store

查询参数

参数类型必填说明
intervalstring聚合粒度:day(默认)/ hour / month;其它值按 day 处理
limitnumber返回的周期数,默认 30;超过 365 时按 365 处理

请求示例

bash
curl -H 'Authorization: Bearer <token>' \
  'https://api.hyperfrp.com/api/users/traffic-history?interval=day&limit=30'
javascript
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);
python
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)
go
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 为对象数组,元素字段:

字段类型说明
periodstring周期标识:day2026-09-19,hour2026-09-19 08:00:00,month2026-09
totalTrafficInnumber该周期入站流量合计,单位字节
totalTrafficOutnumber该周期出站流量合计,单位字节
totalTrafficnumber入站 + 出站合计,单位字节

响应示例

json
{
  "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 参数与上限规则同上。

请求示例

bash
curl -H 'Authorization: Bearer <token>' \
  'https://api.hyperfrp.com/api/users/traffic-history/2026-09?interval=month&limit=12'
javascript
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);
python
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)
go
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 序列化字段):

字段类型说明
idnumber节点 ID
namestring节点名称,全局唯一
hoststring节点主机地址(IP 或域名)
domainstring节点域名
portnumberFRP 连接端口
bindAddressstringFRPS 监听地址
kcpBindPortnumberKCP 绑定端口,0 表示未启用
quicEnabledboolean是否启用 QUIC
quicBindPortnumberQUIC 绑定端口
vhostHTTPPortnumberHTTP 虚拟主机端口
vhostHTTPSPortnumberHTTPS 虚拟主机端口
speedTestPortnumber测速 HTTP 端口
probeEnabledboolean是否启用 WebRTC 延迟探针
probePortnumber探针端口(TCP 信令与 UDP 媒体同号)
allowedGroupIdsnumber[]允许访问的用户组 ID 列表;空数组表示不限
isGroupExclusiveboolean是否专属组节点
permissionMessagestring无权限提示文案
exclusiveTagTextstring专属标签文案
exclusiveTagColorstring专属标签颜色
exclusiveTagIconstring专属标签图标
allowedPortsStartnumber允许端口范围起点
allowedPortsEndnumber允许端口范围终点
allowPortRangesarray允许端口段列表,元素为 {start, end}
locationstring节点位置描述
descriptionstring节点描述
statusstring节点状态:online / api_only / unreachable / frps_stopped / offline / maintenance
cpuInfostringCPU 型号信息
cpuTemperaturenumberCPU 温度(°C)
isVisibleboolean是否可见;本接口恒为 true
maxProxiesnumber节点隧道数上限
bandwidthLimitKbpsnumber节点带宽上限,单位 kbps
totalTrafficInBytesnumber节点累计入站流量,单位字节
totalTrafficOutBytesnumber节点累计出站流量,单位字节
networkPacketsSentnumber累计发送数据包数
networkPacketsRecvnumber累计接收数据包数
clientsOnlinenumber在线客户端数
proxiesCountnumber节点上隧道数
lastHeartbeatstring 或 null最近心跳时间
onlineSincestring 或 null本次上线起始时间
bootTimenumber 或 null节点系统启动时间(Unix 秒)
cpuUsagenumberCPU 占用率(百分比)
loadAveragestring负载均值(字符串)
systemLoadstring系统负载 JSON 串
memoryUsagenumber内存占用率(百分比)
memoryTotalnumber恒为 0(模型计算字段,本接口未填充)
memoryUsednumber恒为 0(同上)
diskUsagenumber磁盘占用率(百分比)
diskTotalnumber恒为 0(同上)
diskUsednumber恒为 0(同上)
networkSpeedInnumber实时入站速率,单位 B/s
networkSpeedOutnumber实时出站速率,单位 B/s
frpsReportedInBytesnumberFRPS 上报累计入站流量,单位字节
frpsReportedOutBytesnumberFRPS 上报累计出站流量,单位字节
logPathstring恒为空串(模型计算字段,本接口未填充)
frpsPathstring恒为空串(同上)
configPathstring恒为空串(同上)
dashboardPortnumberFRPS 仪表盘端口
webServerAddrstringFRPS web 服务监听地址
tcpMuxboolean是否启用 TCP 多路复用
tcpMuxHTTPConnectPortnumberTCP Mux HTTP Connect 端口
tcpKeepAlivenumberTCP keepalive,单位秒
heartbeatTimeoutnumber心跳超时,单位秒
maxPoolCountnumber最大连接池数
tlsForceboolean是否强制 TLS
logLevelstringFRPS 日志级别
logMaxDaysnumberFRPS 日志保留天数
disableLogColorboolean是否禁用日志颜色
regionstring地区(如 Overseas)
allowedProtocolsstring[]允许的隧道协议列表
bandwidthValuenumber带宽数值(与 bandwidthUnit 配合展示)
bandwidthUnitstring带宽单位:Mbps / Gbps
hyperzonefrpApiURLstring节点侧面板 API 基址
hyperzonefrpHeartbeatIntervalnumber心跳上报间隔,单位秒
hyperzonefrpResourceIntervalnumber资源上报间隔,单位秒
hyperzonefrpTrafficFlushIntervalnumber流量落盘间隔,单位秒
hyperzonefrpTunnelCheckIntervalnumber隧道检查间隔,单位秒
trafficQueuePathstring流量 WAL 路径
trafficQueueHighWatermarknumber流量队列高水位(条数)
upgradeEnabledboolean节点自动升级开关
panelFailureModestring面板不可用降级策略:fail-closed / fail-open
upgradeServiceNamestring升级服务名
upgradeInstallDirstring升级安装目录
upgradeSelfCheckWindownumber升级自检窗口,单位秒
createdAtstring创建时间(RFC 3339)
updatedAtstring更新时间(RFC 3339)

请求示例

bash
curl -H 'Authorization: Bearer <token>' \
  'https://api.hyperfrp.com/api/users/nodes'
javascript
const res = await fetch('https://api.hyperfrp.com/api/users/nodes', {
  headers: {
    'Authorization': 'Bearer <token>',
  },
});
const data = await res.json();
console.log(data);
python
import requests

res = requests.get(
    'https://api.hyperfrp.com/api/users/nodes',
    headers={'Authorization': 'Bearer <token>'},
)
data = res.json()
print(data)
go
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)
}

响应示例

json
{
  "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说明
无业务错误分支;无可见节点时返回空数组

注意事项

  • 节点敏感凭证字段(tokenhyperzonefrpApiTokenhyperzonefrpNodeTokendashboardUserdashboardPassword)在模型上标记为不序列化,永远不会出现在响应中
  • 列表只按 is_visible 过滤,不按用户组做访问过滤;组权限判断由创建隧道等接口负责。

GET /api/users/nodes/:id

获取单个节点详情;返回 handler 手工构造的字段子集(与列表接口不同,非整模型序列化)。

鉴权:JWT Bearer · 限流:无专用限流(全站 WAF 防护照常生效) · 缓存:short-cache 60s

响应字段

字段类型说明
idnumber节点 ID
namestring节点名称
addressstring节点地址(取自节点 host 字段)
portnumberFRP 连接端口
locationstring节点位置描述
descriptionstring节点描述
statusstring节点状态
cpuUsagenumberCPU 占用率(百分比)
memoryUsagenumber内存占用率(百分比)
diskUsagenumber磁盘占用率(百分比)
networkSpeedInnumber实时入站速率,单位 B/s
networkSpeedOutnumber实时出站速率,单位 B/s
clientsOnlinenumber在线客户端数
proxiesCountnumber节点上隧道数
lastHeartbeatstring 或 null最近心跳时间
createdAtstring创建时间(RFC 3339)
updatedAtstring更新时间(RFC 3339)

请求示例

bash
curl -H 'Authorization: Bearer <token>' \
  'https://api.hyperfrp.com/api/users/nodes/2'
javascript
const res = await fetch('https://api.hyperfrp.com/api/users/nodes/2', {
  headers: {
    'Authorization': 'Bearer <token>',
  },
});
const data = await res.json();
console.log(data);
python
import requests

res = requests.get(
    'https://api.hyperfrp.com/api/users/nodes/2',
    headers={'Authorization': 'Bearer <token>'},
)
data = res.json()
print(data)
go
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)
}

响应示例

json
{
  "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

查询参数

参数类型必填说明
intervalstring聚合粒度:day(默认)/ hour / month;其它值按 day 处理
limitnumber返回的周期数,默认 30;超过 365 时按 365 处理

请求示例

bash
curl -H 'Authorization: Bearer <token>' \
  'https://api.hyperfrp.com/api/users/nodes/2/traffic-history?interval=hour&limit=24'
javascript
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);
python
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)
go
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 为对象数组,元素字段:

字段类型说明
periodstring周期标识,格式随 interval:2026-09-19 / 2026-09-19 08:00:00 / 2026-09
totalTrafficInnumber该周期该节点入站流量合计,单位字节
totalTrafficOutnumber该周期该节点出站流量合计,单位字节
totalTrafficnumber入站 + 出站合计,单位字节

响应示例

json
{
  "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

响应字段

字段类型说明
idnumber公告 ID
titlestring公告标题
contentstring公告正文
contentTypestring内容类型,如 text / markdown
authorIdnumber作者用户 ID
visibleGroupIdsstring可见用户组 ID 的 JSON 串(如 "[1,2]";空数组通常表示全部可见)
isPublishedboolean是否发布;本接口恒为 true
publishDatestring发布时间(RFC 3339)
createdAtstring创建时间(RFC 3339)
updatedAtstring更新时间(RFC 3339)

请求示例

bash
curl -H 'Authorization: Bearer <token>' \
  'https://api.hyperfrp.com/api/users/announcements/5'
javascript
const res = await fetch('https://api.hyperfrp.com/api/users/announcements/5', {
  headers: {
    'Authorization': 'Bearer <token>',
  },
});
const data = await res.json();
console.log(data);
python
import requests

res = requests.get(
    'https://api.hyperfrp.com/api/users/announcements/5',
    headers={'Authorization': 'Bearer <token>'},
)
data = res.json()
print(data)
go
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)
}

响应示例

json
{
  "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

本站基于 VitePress 构建,文档内容以 backend-Go handler 源码为权威依据