Skip to content

套餐

套餐是 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 为套餐对象数组,元素字段:

字段类型说明
idnumber套餐 ID
namestring套餐名称,全局唯一
pricenumber套餐价格,浮点数,单位与账户余额一致
trafficAmountBytesnumber套餐流量额度,单位为字节(int64)
durationDaysnumber有效期天数;0 表示长期(到期时间加 100 年,见购买接口说明)
descriptionstring套餐描述,最长 500 字符,可为空串
isActiveboolean是否上架;本接口恒为 true
maxProxiesnumber套餐标称的隧道数量上限
bandwidthOutKbpsnumber套餐标称的出站带宽上限,单位 kbps
bandwidthInKbpsnumber套餐标称的入站带宽上限,单位 kbps
targetGroupIdnumber 或 null购买后切换到的用户组 ID;null 表示该套餐不切换用户组
targetGroupobject用户组详情对象;本章接口未预加载该关联,不会出现
createdAtstring套餐创建时间(RFC 3339)
updatedAtstring套餐最后更新时间(RFC 3339)

请求示例

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

res = requests.get(
    'https://api.hyperfrp.com/api/users/packages/available',
    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/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)
}

响应示例

json
{
  "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统一鉴权失败形态,见鉴权方式
账户被封禁403data 携带 isBannedban_reason
数据库查询失败500message 形如「获取可用套餐列表失败: …」

注意事项

  • 结果按 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 为用户套餐对象数组,元素字段:

字段类型说明
idnumber用户套餐记录 ID
userIdnumber所属用户 ID
packageIdnumber关联套餐 ID
packageobject关联套餐完整对象,字段见下表
purchaseDatestring购买时间(RFC 3339)
startDatestring生效开始时间(RFC 3339)
endDatestring到期时间(RFC 3339)
initialTrafficBytesnumber购买时记入的初始流量额度,单位为字节(int64)
remainingTrafficBytesnumber该条记录的剩余流量,单位为字节(int64)
statusstring记录状态:active / expired / exhausted / pending(见注意事项)
createdAtstring记录创建时间(RFC 3339)
updatedAtstring记录最后更新时间(RFC 3339)

内嵌 package 对象字段:

字段类型说明
idnumber套餐 ID
namestring套餐名称,全局唯一
pricenumber套餐价格,浮点数,单位与账户余额一致
trafficAmountBytesnumber套餐流量额度,单位为字节(int64)
durationDaysnumber有效期天数
descriptionstring套餐描述,最长 500 字符
isActiveboolean是否上架
maxProxiesnumber套餐标称的隧道数量上限
bandwidthOutKbpsnumber套餐标称的出站带宽上限,单位 kbps
bandwidthInKbpsnumber套餐标称的入站带宽上限,单位 kbps
targetGroupIdnumber 或 null购买后切换到的用户组 ID;null 表示不切换
createdAtstring套餐创建时间(RFC 3339)
updatedAtstring套餐最后更新时间(RFC 3339)

请求示例

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

res = requests.get(
    'https://api.hyperfrp.com/api/users/packages/my',
    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/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)
}

响应示例

json
{
  "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统一鉴权失败形态,见鉴权方式
账户被封禁403data 携带 isBannedban_reason
数据库查询失败500message 形如「获取用户套餐列表失败: …」

注意事项

  • 返回全部状态的记录,按创建时间倒序;只想看生效中的套餐用 /active
  • status 取值含义:
    • active:生效中;
    • expired:已到期(定时任务每小时将 endDate 已过且仍为 active 的记录批量改写为 expired);
    • exhausted:流量耗尽(remainingTrafficBytes ≤ 0);该状态没有定时任务维护,可能出现记录仍为 active 但流量已耗尽的窗口期,判断可用性时应以 remainingTrafficBytesendDate 为准;
    • 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 完全一致:

字段类型说明
idnumber用户套餐记录 ID
userIdnumber所属用户 ID
packageIdnumber关联套餐 ID
packageobject关联套餐完整对象,字段见下表
purchaseDatestring购买时间(RFC 3339)
startDatestring生效开始时间(RFC 3339)
endDatestring到期时间(RFC 3339)
initialTrafficBytesnumber购买时记入的初始流量额度,单位为字节(int64)
remainingTrafficBytesnumber该条记录的剩余流量,单位为字节(int64)
statusstring恒为 active(查询条件即 status = active)
createdAtstring记录创建时间(RFC 3339)
updatedAtstring记录最后更新时间(RFC 3339)

内嵌 package 对象字段:

字段类型说明
idnumber套餐 ID
namestring套餐名称,全局唯一
pricenumber套餐价格,浮点数,单位与账户余额一致
trafficAmountBytesnumber套餐流量额度,单位为字节(int64)
durationDaysnumber有效期天数
descriptionstring套餐描述,最长 500 字符
isActiveboolean是否上架
maxProxiesnumber套餐标称的隧道数量上限
bandwidthOutKbpsnumber套餐标称的出站带宽上限,单位 kbps
bandwidthInKbpsnumber套餐标称的入站带宽上限,单位 kbps
targetGroupIdnumber 或 null购买后切换到的用户组 ID;null 表示不切换
createdAtstring套餐创建时间(RFC 3339)
updatedAtstring套餐最后更新时间(RFC 3339)

请求示例

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

res = requests.get(
    'https://api.hyperfrp.com/api/users/packages/active',
    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/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)
}

响应示例

json
{
  "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统一鉴权失败形态,见鉴权方式
账户被封禁403data 携带 isBannedban_reason
数据库查询失败500message 形如「获取用户活跃套餐列表失败: …」

注意事项

  • 只按落库的 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)

请求体

字段类型必填说明
packageIdnumber要购买的套餐 ID,正整数;缺失或为 0 会被拒绝

请求示例

bash
curl -X POST 'https://api.hyperfrp.com/api/users/packages/purchase' \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{"packageId": 3}'
javascript
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);
python
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)
go
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 为对象:

字段类型说明
messagestring业务提示语,固定为「套餐购买成功!」
userPackageobject新建的用户套餐记录,字段见下表
newBalancenumber购买完成后的账户余额(浮点数,单位与购买前一致)
newRemainingTrafficnumber购买完成后的账户总剩余流量,单位为字节(int64,内部浮点值取整)

userPackage 对象字段:

字段类型说明
idnumber用户套餐记录 ID
userIdnumber所属用户 ID
packageIdnumber关联套餐 ID
packageobject关联套餐完整对象,字段见下表
purchaseDatestring购买时间,即生效开始时间(RFC 3339)
startDatestring生效开始时间(RFC 3339)
endDatestring到期时间(RFC 3339)
initialTrafficBytesnumber初始流量额度(等于套餐 trafficAmountBytes),单位为字节(int64)
remainingTrafficBytesnumber该条记录剩余流量,购买后等于 initialTrafficBytes,单位为字节(int64)
statusstring购买成功后恒为 active
createdAtstring记录创建时间(RFC 3339)
updatedAtstring记录最后更新时间(RFC 3339)

内嵌 package 对象字段:

字段类型说明
idnumber套餐 ID
namestring套餐名称
pricenumber套餐价格(即本次扣费金额),浮点数
trafficAmountBytesnumber套餐流量额度,单位为字节(int64)
durationDaysnumber有效期天数
descriptionstring套餐描述
isActiveboolean是否上架
maxProxiesnumber套餐标称的隧道数量上限
bandwidthOutKbpsnumber套餐标称的出站带宽上限,单位 kbps
bandwidthInKbpsnumber套餐标称的入站带宽上限,单位 kbps
targetGroupIdnumber 或 null购买后切换到的用户组 ID;null 表示不切换
createdAtstring套餐创建时间(RFC 3339)
updatedAtstring套餐最后更新时间(RFC 3339)

响应示例

HTTP 状态码为 201:

json
{
  "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 兜底提示「未认证」
账户被封禁403data 携带 isBannedban_reason
请求体不是合法 JSON400「无效的请求数据」
packageId 缺失或为 0400「请选择要购买的套餐」
套餐 ID 不存在400「套餐未找到」
套餐已下架(isActive 为 false)400「此套餐不可购买」
账户余额小于套餐价格400「余额不足,请充值」
用户记录不存在(异常场景)400「用户未找到」

注意事项

  • 扣费与流量叠加语义:成功后 newBalance = 购买前余额 − 套餐价格;newRemainingTraffic = 购买前账户总剩余流量 + 套餐流量。注意 newRemainingTraffic账户级累计值(叠加所有套餐后的总流量),不是新购套餐单条的剩余流量;套餐单条的剩余流量见 userPackage.remainingTrafficBytes
  • 有效期:startDatepurchaseDate 均为购买时刻;durationDays > 0endDate = 起始时间加对应天数,durationDays = 0endDate = 起始时间加 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 为历史记录对象数组,元素字段:

字段类型说明
idnumber用户套餐记录 ID
packageNamestring套餐名称;套餐已被删除时为空串
packagePricenumber套餐价格(查询时实时读取套餐表),浮点数;套餐已被删除时为 0
initialTrafficBytesnumber购买时记入的初始流量额度,单位为字节(int64)
remainingTrafficBytesnumber该记录当前剩余流量,单位为字节(int64)
startDatestring生效开始时间(RFC 3339)
endDatestring到期时间(RFC 3339)
statusstring记录状态:active / expired / exhausted / pending(取值含义见「我的全部套餐」注意事项)
purchaseDatestring购买时间(RFC 3339)
createdAtstring记录创建时间(RFC 3339)

请求示例

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

res = requests.get(
    'https://api.hyperfrp.com/api/users/packages/history',
    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/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)
}

响应示例

json
{
  "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 兜底提示「未认证」
账户被封禁403data 携带 isBannedban_reason
数据库查询失败500「查询套餐历史失败」

注意事项

  • /my 的区别:本接口是精简视图,不返回 userIdpackageId 与内嵌 package 对象;需要完整结构时用 /my
  • packageName / packagePrice 在每次查询时实时关联套餐表读取,不是购买时刻的快照;套餐被管理员删除后(硬删除),对应历史项的这两个字段分别落为空串与 0,其余字段不受影响。
  • 无购买记录时 data 为空数组 [],仍返回 code 0。

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