套餐
套餐是 HyperFRP Panel 的流量售卖单元:用户以账户余额购买,获得一定额度的流量与有效期,部分套餐还会附赠目标用户组身份。本章覆盖用户侧「套餐」全部 5 个接口,前缀均为 /api/users/packages,全部要求 JWT 鉴权(Authorization: Bearer <token>,校验链与失败形态见鉴权方式);统一响应包络、错误码、字节单位等全局约定见通用约定。
购买的核心语义:从账户余额扣除套餐价格,把套餐流量叠加到账户总剩余流量,并新增一条独立的用户套餐记录;套餐配置了目标用户组时,购买成功后账户切换到该用户组。同一套餐允许重复购买,每次购买独立扣费、独立计时、独立计量。本组接口没有专用限流中间件,但与其他接口一样经过全站 WAF 防护(见通用约定)。
| 方法 | 路径 | 说明 | 缓存策略 |
|---|---|---|---|
| GET | /api/users/packages/available | 在售套餐列表 | public, max-age=60 |
| GET | /api/users/packages/my | 我的全部套餐 | no-store |
| GET | /api/users/packages/active | 生效中套餐 | no-store |
| POST | /api/users/packages/purchase | 余额购买套餐(201) | no-store |
| GET | /api/users/packages/history | 购买历史 | no-store |
套餐浏览
GET /api/users/packages/available
获取当前在售(isActive 为 true)的套餐列表,按价格从低到高排序,供购买前浏览与选型。
鉴权:JWT Bearer(见鉴权方式) · 缓存:public, max-age=60(响应头 Cache-Control: public, max-age=60, s-maxage=60)
响应字段
data 为套餐对象数组,元素字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | number | 套餐 ID |
| name | string | 套餐名称,全局唯一 |
| price | number | 套餐价格,浮点数,单位与账户余额一致 |
| trafficAmountBytes | number | 套餐流量额度,单位为字节(int64) |
| durationDays | number | 有效期天数;0 表示长期(到期时间加 100 年,见购买接口说明) |
| description | string | 套餐描述,最长 500 字符,可为空串 |
| isActive | boolean | 是否上架;本接口恒为 true |
| maxProxies | number | 套餐标称的隧道数量上限 |
| bandwidthOutKbps | number | 套餐标称的出站带宽上限,单位 kbps |
| bandwidthInKbps | number | 套餐标称的入站带宽上限,单位 kbps |
| targetGroupId | number 或 null | 购买后切换到的用户组 ID;null 表示该套餐不切换用户组 |
| targetGroup | object | 用户组详情对象;本章接口未预加载该关联,不会出现 |
| createdAt | string | 套餐创建时间(RFC 3339) |
| updatedAt | string | 套餐最后更新时间(RFC 3339) |
请求示例
curl -H 'Authorization: Bearer <token>' \
'https://api.hyperfrp.com/api/users/packages/available'const res = await fetch('https://api.hyperfrp.com/api/users/packages/available', {
headers: {
'Authorization': 'Bearer <token>',
},
});
const data = await res.json();
console.log(data);import requests
res = requests.get(
'https://api.hyperfrp.com/api/users/packages/available',
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/packages/available", 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": 1,
"name": "体验套餐",
"price": 5,
"trafficAmountBytes": 10737418240,
"durationDays": 30,
"description": "10GB 流量 / 30 天,适合轻度使用",
"isActive": true,
"maxProxies": 5,
"bandwidthOutKbps": 10240,
"bandwidthInKbps": 10240,
"targetGroupId": null,
"createdAt": "2026-08-01T10:00:00+08:00",
"updatedAt": "2026-08-20T15:30:00+08:00"
},
{
"id": 3,
"name": "专业套餐",
"price": 30,
"trafficAmountBytes": 107374182400,
"durationDays": 30,
"description": "100GB 流量 / 30 天,附赠会员用户组",
"isActive": true,
"maxProxies": 20,
"bandwidthOutKbps": 51200,
"bandwidthInKbps": 51200,
"targetGroupId": 2,
"createdAt": "2026-08-01T10:05:00+08:00",
"updatedAt": "2026-09-01T09:00:00+08:00"
}
]
}错误场景
| 场景 | HTTP | 说明 |
|---|---|---|
| 未携带令牌 / JWT 无效或过期 / 会话失效 | 401 | 统一鉴权失败形态,见鉴权方式 |
| 账户被封禁 | 403 | data 携带 isBanned 与 ban_reason |
| 数据库查询失败 | 500 | message 形如「获取可用套餐列表失败: …」 |
注意事项
- 结果按
price升序排列,包含价格为 0 的套餐。 - 响应允许浏览器与共享缓存保存 60 秒,刚下架的套餐最长可能仍返回 60 秒;需要强一致的场合不要依赖本接口。
targetGroupId非 null 的套餐,购买后会把账户切换到该用户组;是否切换以购买接口的实际行为为准。
GET /api/users/packages/my
获取当前用户的全部套餐记录(含所有状态),按创建时间倒序,每个元素内嵌完整的套餐对象。
鉴权:JWT Bearer(见鉴权方式) · 缓存:no-store(响应头 Cache-Control: no-store, no-cache, must-revalidate, private)
响应字段
data 为用户套餐对象数组,元素字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | number | 用户套餐记录 ID |
| userId | number | 所属用户 ID |
| packageId | number | 关联套餐 ID |
| package | object | 关联套餐完整对象,字段见下表 |
| purchaseDate | string | 购买时间(RFC 3339) |
| startDate | string | 生效开始时间(RFC 3339) |
| endDate | string | 到期时间(RFC 3339) |
| initialTrafficBytes | number | 购买时记入的初始流量额度,单位为字节(int64) |
| remainingTrafficBytes | number | 该条记录的剩余流量,单位为字节(int64) |
| status | string | 记录状态:active / expired / exhausted / pending(见注意事项) |
| createdAt | string | 记录创建时间(RFC 3339) |
| updatedAt | string | 记录最后更新时间(RFC 3339) |
内嵌 package 对象字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | number | 套餐 ID |
| name | string | 套餐名称,全局唯一 |
| price | number | 套餐价格,浮点数,单位与账户余额一致 |
| trafficAmountBytes | number | 套餐流量额度,单位为字节(int64) |
| durationDays | number | 有效期天数 |
| description | string | 套餐描述,最长 500 字符 |
| isActive | boolean | 是否上架 |
| maxProxies | number | 套餐标称的隧道数量上限 |
| bandwidthOutKbps | number | 套餐标称的出站带宽上限,单位 kbps |
| bandwidthInKbps | number | 套餐标称的入站带宽上限,单位 kbps |
| targetGroupId | number 或 null | 购买后切换到的用户组 ID;null 表示不切换 |
| createdAt | string | 套餐创建时间(RFC 3339) |
| updatedAt | string | 套餐最后更新时间(RFC 3339) |
请求示例
curl -H 'Authorization: Bearer <token>' \
'https://api.hyperfrp.com/api/users/packages/my'const res = await fetch('https://api.hyperfrp.com/api/users/packages/my', {
headers: {
'Authorization': 'Bearer <token>',
},
});
const data = await res.json();
console.log(data);import requests
res = requests.get(
'https://api.hyperfrp.com/api/users/packages/my',
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/packages/my", 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": 102,
"userId": 8,
"packageId": 3,
"package": {
"id": 3,
"name": "专业套餐",
"price": 30,
"trafficAmountBytes": 107374182400,
"durationDays": 30,
"description": "100GB 流量 / 30 天,附赠会员用户组",
"isActive": true,
"maxProxies": 20,
"bandwidthOutKbps": 51200,
"bandwidthInKbps": 51200,
"targetGroupId": 2,
"createdAt": "2026-08-01T10:05:00+08:00",
"updatedAt": "2026-09-01T09:00:00+08:00"
},
"purchaseDate": "2026-09-19T14:20:00+08:00",
"startDate": "2026-09-19T14:20:00+08:00",
"endDate": "2026-10-19T14:20:00+08:00",
"initialTrafficBytes": 107374182400,
"remainingTrafficBytes": 107374182400,
"status": "active",
"createdAt": "2026-09-19T14:20:00+08:00",
"updatedAt": "2026-09-19T14:20:00+08:00"
},
{
"id": 87,
"userId": 8,
"packageId": 1,
"package": {
"id": 1,
"name": "体验套餐",
"price": 5,
"trafficAmountBytes": 10737418240,
"durationDays": 30,
"description": "10GB 流量 / 30 天,适合轻度使用",
"isActive": true,
"maxProxies": 5,
"bandwidthOutKbps": 10240,
"bandwidthInKbps": 10240,
"targetGroupId": null,
"createdAt": "2026-08-01T10:00:00+08:00",
"updatedAt": "2026-08-20T15:30:00+08:00"
},
"purchaseDate": "2026-08-01T09:30:00+08:00",
"startDate": "2026-08-01T09:30:00+08:00",
"endDate": "2026-08-31T09:30:00+08:00",
"initialTrafficBytes": 10737418240,
"remainingTrafficBytes": 3221225472,
"status": "expired",
"createdAt": "2026-08-01T09:30:00+08:00",
"updatedAt": "2026-08-31T10:00:00+08:00"
}
]
}错误场景
| 场景 | HTTP | 说明 |
|---|---|---|
| 未携带令牌 / JWT 无效或过期 / 会话失效 | 401 | 统一鉴权失败形态,见鉴权方式 |
| 账户被封禁 | 403 | data 携带 isBanned 与 ban_reason |
| 数据库查询失败 | 500 | message 形如「获取用户套餐列表失败: …」 |
注意事项
- 返回全部状态的记录,按创建时间倒序;只想看生效中的套餐用
/active。 - status 取值含义:
active:生效中;expired:已到期(定时任务每小时将endDate已过且仍为 active 的记录批量改写为 expired);exhausted:流量耗尽(remainingTrafficBytes≤ 0);该状态没有定时任务维护,可能出现记录仍为 active 但流量已耗尽的窗口期,判断可用性时应以remainingTrafficBytes与endDate为准;pending:预留的初始状态,正常购买流程直接创建为 active。
- 关联的用户对象(
user)与逻辑删除字段(deletedAt)不会出现在响应中;user仅在预加载时返回,本接口未预加载。
GET /api/users/packages/active
获取当前用户 status 为 active 的套餐记录,按到期时间升序(最先到期的在前)。
鉴权:JWT Bearer(见鉴权方式) · 缓存:no-store(响应头 Cache-Control: no-store, no-cache, must-revalidate, private)
响应字段
data 为用户套餐对象数组,元素字段与 /my 完全一致:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | number | 用户套餐记录 ID |
| userId | number | 所属用户 ID |
| packageId | number | 关联套餐 ID |
| package | object | 关联套餐完整对象,字段见下表 |
| purchaseDate | string | 购买时间(RFC 3339) |
| startDate | string | 生效开始时间(RFC 3339) |
| endDate | string | 到期时间(RFC 3339) |
| initialTrafficBytes | number | 购买时记入的初始流量额度,单位为字节(int64) |
| remainingTrafficBytes | number | 该条记录的剩余流量,单位为字节(int64) |
| status | string | 恒为 active(查询条件即 status = active) |
| createdAt | string | 记录创建时间(RFC 3339) |
| updatedAt | string | 记录最后更新时间(RFC 3339) |
内嵌 package 对象字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | number | 套餐 ID |
| name | string | 套餐名称,全局唯一 |
| price | number | 套餐价格,浮点数,单位与账户余额一致 |
| trafficAmountBytes | number | 套餐流量额度,单位为字节(int64) |
| durationDays | number | 有效期天数 |
| description | string | 套餐描述,最长 500 字符 |
| isActive | boolean | 是否上架 |
| maxProxies | number | 套餐标称的隧道数量上限 |
| bandwidthOutKbps | number | 套餐标称的出站带宽上限,单位 kbps |
| bandwidthInKbps | number | 套餐标称的入站带宽上限,单位 kbps |
| targetGroupId | number 或 null | 购买后切换到的用户组 ID;null 表示不切换 |
| createdAt | string | 套餐创建时间(RFC 3339) |
| updatedAt | string | 套餐最后更新时间(RFC 3339) |
请求示例
curl -H 'Authorization: Bearer <token>' \
'https://api.hyperfrp.com/api/users/packages/active'const res = await fetch('https://api.hyperfrp.com/api/users/packages/active', {
headers: {
'Authorization': 'Bearer <token>',
},
});
const data = await res.json();
console.log(data);import requests
res = requests.get(
'https://api.hyperfrp.com/api/users/packages/active',
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/packages/active", 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": 102,
"userId": 8,
"packageId": 3,
"package": {
"id": 3,
"name": "专业套餐",
"price": 30,
"trafficAmountBytes": 107374182400,
"durationDays": 30,
"description": "100GB 流量 / 30 天,附赠会员用户组",
"isActive": true,
"maxProxies": 20,
"bandwidthOutKbps": 51200,
"bandwidthInKbps": 51200,
"targetGroupId": 2,
"createdAt": "2026-08-01T10:05:00+08:00",
"updatedAt": "2026-09-01T09:00:00+08:00"
},
"purchaseDate": "2026-09-19T14:20:00+08:00",
"startDate": "2026-09-19T14:20:00+08:00",
"endDate": "2026-10-19T14:20:00+08:00",
"initialTrafficBytes": 107374182400,
"remainingTrafficBytes": 107374182400,
"status": "active",
"createdAt": "2026-09-19T14:20:00+08:00",
"updatedAt": "2026-09-19T14:20:00+08:00"
}
]
}错误场景
| 场景 | HTTP | 说明 |
|---|---|---|
| 未携带令牌 / JWT 无效或过期 / 会话失效 | 401 | 统一鉴权失败形态,见鉴权方式 |
| 账户被封禁 | 403 | data 携带 isBanned 与 ban_reason |
| 数据库查询失败 | 500 | message 形如「获取用户活跃套餐列表失败: …」 |
注意事项
- 只按落库的 status 字段过滤,不实时校验
endDate与剩余流量:定时任务标记存在小时级延迟,endDate已过但尚未被改写为 expired 的记录仍会出现在结果里,客户端应自行以endDate兜底判断。 - 无生效套餐时 data 为空数组
[],仍返回 code 0。
购买
POST /api/users/packages/purchase
用账户余额购买指定套餐:扣费、叠加账户总流量、创建一条生效中的用户套餐记录,并在套餐配置了目标用户组时切换用户组。
鉴权:JWT Bearer(见鉴权方式) · 缓存:no-store(响应头 Cache-Control: no-store, no-cache, must-revalidate, private)
请求体
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| packageId | number | 是 | 要购买的套餐 ID,正整数;缺失或为 0 会被拒绝 |
请求示例
curl -X POST 'https://api.hyperfrp.com/api/users/packages/purchase' \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{"packageId": 3}'const res = await fetch('https://api.hyperfrp.com/api/users/packages/purchase', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer <token>',
},
body: JSON.stringify({"packageId": 3})
});
const data = await res.json();
console.log(data);import requests
res = requests.post(
'https://api.hyperfrp.com/api/users/packages/purchase',
headers={'Authorization': 'Bearer <token>'},
json={
'packageId': 3
},
)
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{
"packageId": 3,
})
req, _ := http.NewRequest("POST", "https://api.hyperfrp.com/api/users/packages/purchase", &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)
}响应字段
data 为对象:
| 字段 | 类型 | 说明 |
|---|---|---|
| message | string | 业务提示语,固定为「套餐购买成功!」 |
| userPackage | object | 新建的用户套餐记录,字段见下表 |
| newBalance | number | 购买完成后的账户余额(浮点数,单位与购买前一致) |
| newRemainingTraffic | number | 购买完成后的账户总剩余流量,单位为字节(int64,内部浮点值取整) |
userPackage 对象字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | number | 用户套餐记录 ID |
| userId | number | 所属用户 ID |
| packageId | number | 关联套餐 ID |
| package | object | 关联套餐完整对象,字段见下表 |
| purchaseDate | string | 购买时间,即生效开始时间(RFC 3339) |
| startDate | string | 生效开始时间(RFC 3339) |
| endDate | string | 到期时间(RFC 3339) |
| initialTrafficBytes | number | 初始流量额度(等于套餐 trafficAmountBytes),单位为字节(int64) |
| remainingTrafficBytes | number | 该条记录剩余流量,购买后等于 initialTrafficBytes,单位为字节(int64) |
| status | string | 购买成功后恒为 active |
| createdAt | string | 记录创建时间(RFC 3339) |
| updatedAt | string | 记录最后更新时间(RFC 3339) |
内嵌 package 对象字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | number | 套餐 ID |
| name | string | 套餐名称 |
| price | number | 套餐价格(即本次扣费金额),浮点数 |
| trafficAmountBytes | number | 套餐流量额度,单位为字节(int64) |
| durationDays | number | 有效期天数 |
| description | string | 套餐描述 |
| isActive | boolean | 是否上架 |
| maxProxies | number | 套餐标称的隧道数量上限 |
| bandwidthOutKbps | number | 套餐标称的出站带宽上限,单位 kbps |
| bandwidthInKbps | number | 套餐标称的入站带宽上限,单位 kbps |
| targetGroupId | number 或 null | 购买后切换到的用户组 ID;null 表示不切换 |
| createdAt | string | 套餐创建时间(RFC 3339) |
| updatedAt | string | 套餐最后更新时间(RFC 3339) |
响应示例
HTTP 状态码为 201:
{
"code": 0,
"message": "创建成功",
"data": {
"message": "套餐购买成功!",
"userPackage": {
"id": 102,
"userId": 8,
"packageId": 3,
"package": {
"id": 3,
"name": "专业套餐",
"price": 30,
"trafficAmountBytes": 107374182400,
"durationDays": 30,
"description": "100GB 流量 / 30 天,附赠会员用户组",
"isActive": true,
"maxProxies": 20,
"bandwidthOutKbps": 51200,
"bandwidthInKbps": 51200,
"targetGroupId": 2,
"createdAt": "2026-08-01T10:05:00+08:00",
"updatedAt": "2026-09-01T09:00:00+08:00"
},
"purchaseDate": "2026-09-19T14:20:00+08:00",
"startDate": "2026-09-19T14:20:00+08:00",
"endDate": "2026-10-19T14:20:00+08:00",
"initialTrafficBytes": 107374182400,
"remainingTrafficBytes": 107374182400,
"status": "active",
"createdAt": "2026-09-19T14:20:00+08:00",
"updatedAt": "2026-09-19T14:20:00+08:00"
},
"newBalance": 95,
"newRemainingTraffic": 109521666048
}
}错误场景
| 场景 | HTTP | 说明 |
|---|---|---|
| 未携带令牌 / JWT 无效或过期 / 会话失效 | 401 | 统一鉴权失败形态,见鉴权方式;handler 兜底提示「未认证」 |
| 账户被封禁 | 403 | data 携带 isBanned 与 ban_reason |
| 请求体不是合法 JSON | 400 | 「无效的请求数据」 |
| packageId 缺失或为 0 | 400 | 「请选择要购买的套餐」 |
| 套餐 ID 不存在 | 400 | 「套餐未找到」 |
| 套餐已下架(isActive 为 false) | 400 | 「此套餐不可购买」 |
| 账户余额小于套餐价格 | 400 | 「余额不足,请充值」 |
| 用户记录不存在(异常场景) | 400 | 「用户未找到」 |
注意事项
- 扣费与流量叠加语义:成功后
newBalance= 购买前余额 − 套餐价格;newRemainingTraffic= 购买前账户总剩余流量 + 套餐流量。注意newRemainingTraffic是账户级累计值(叠加所有套餐后的总流量),不是新购套餐单条的剩余流量;套餐单条的剩余流量见userPackage.remainingTrafficBytes。 - 有效期:
startDate与purchaseDate均为购买时刻;durationDays > 0时endDate= 起始时间加对应天数,durationDays = 0时endDate= 起始时间加 100 年(视作长期套餐)。 - 用户组切换:套餐配置了
targetGroupId(大于 0)时,购买成功后账户用户组切换为该组,组过期时间设为本套餐endDate;未配置则用户组不变。切换发生在同一事务内,与扣费原子生效。 - 事务与并发:余额校验执行两次——事务外先做一次快照校验,事务内再以
SELECT ... FOR UPDATE行锁重读用户并复查余额,防止并发购买把余额扣成负数。 - 重复购买:接口不限制重复购买同一套餐。每次成功调用都会全额扣费、新增一条独立的用户套餐记录并把套餐流量再次叠加到账户总流量。
- 配额边界:购买事务只更新余额、账户总流量与用户组三项,套餐上的
maxProxies/bandwidthInKbps/bandwidthOutKbps不在购买时写入用户配额,隧道配额与带宽以用户组体系计算。 - 事务内部的数据库异常(创建记录、更新用户、提交事务失败)同样以 400 返回,message 为具体错误描述;扣费不会生效(事务已回滚)。
- 成功包络的
message固定为「创建成功」(201 Created 语义),业务提示语在data.message。
历史
GET /api/users/packages/history
获取当前用户的套餐购买历史(全部状态),按创建时间倒序,返回精简字段视图。
鉴权:JWT Bearer(见鉴权方式) · 缓存:no-store(响应头 Cache-Control: no-store, no-cache, must-revalidate, private)
响应字段
data 为历史记录对象数组,元素字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | number | 用户套餐记录 ID |
| packageName | string | 套餐名称;套餐已被删除时为空串 |
| packagePrice | number | 套餐价格(查询时实时读取套餐表),浮点数;套餐已被删除时为 0 |
| initialTrafficBytes | number | 购买时记入的初始流量额度,单位为字节(int64) |
| remainingTrafficBytes | number | 该记录当前剩余流量,单位为字节(int64) |
| startDate | string | 生效开始时间(RFC 3339) |
| endDate | string | 到期时间(RFC 3339) |
| status | string | 记录状态:active / expired / exhausted / pending(取值含义见「我的全部套餐」注意事项) |
| purchaseDate | string | 购买时间(RFC 3339) |
| createdAt | string | 记录创建时间(RFC 3339) |
请求示例
curl -H 'Authorization: Bearer <token>' \
'https://api.hyperfrp.com/api/users/packages/history'const res = await fetch('https://api.hyperfrp.com/api/users/packages/history', {
headers: {
'Authorization': 'Bearer <token>',
},
});
const data = await res.json();
console.log(data);import requests
res = requests.get(
'https://api.hyperfrp.com/api/users/packages/history',
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/packages/history", 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": 102,
"packageName": "专业套餐",
"packagePrice": 30,
"initialTrafficBytes": 107374182400,
"remainingTrafficBytes": 107374182400,
"startDate": "2026-09-19T14:20:00+08:00",
"endDate": "2026-10-19T14:20:00+08:00",
"status": "active",
"purchaseDate": "2026-09-19T14:20:00+08:00",
"createdAt": "2026-09-19T14:20:00+08:00"
},
{
"id": 87,
"packageName": "体验套餐",
"packagePrice": 5,
"initialTrafficBytes": 10737418240,
"remainingTrafficBytes": 0,
"startDate": "2026-08-01T09:30:00+08:00",
"endDate": "2026-08-31T09:30:00+08:00",
"status": "exhausted",
"purchaseDate": "2026-08-01T09:30:00+08:00",
"createdAt": "2026-08-01T09:30:00+08:00"
}
]
}错误场景
| 场景 | HTTP | 说明 |
|---|---|---|
| 未携带令牌 / JWT 无效或过期 / 会话失效 | 401 | 统一鉴权失败形态,见鉴权方式;handler 兜底提示「未认证」 |
| 账户被封禁 | 403 | data 携带 isBanned 与 ban_reason |
| 数据库查询失败 | 500 | 「查询套餐历史失败」 |
注意事项
- 与
/my的区别:本接口是精简视图,不返回userId、packageId与内嵌 package 对象;需要完整结构时用/my。 packageName/packagePrice在每次查询时实时关联套餐表读取,不是购买时刻的快照;套餐被管理员删除后(硬删除),对应历史项的这两个字段分别落为空串与 0,其余字段不受影响。- 无购买记录时 data 为空数组
[],仍返回 code 0。