Files
pangolin/server/api/openapi.yaml
T
wangjia d88c1ae647 merge: maestro/tsk_x7wrlA87orsY [devices + 订阅校验中间件] (tsk_VRzw-af__qWx)
手动合并 tsk_x7wrlA87orsY(设备管理 + 订阅校验中间件)到 main:

冲突解决:
- server/internal/apierr/apierr.go:保留 tsk_GXDoc3Cs07Rn 版本(New/StatusFor/
  Middleware/ErrConflict/改善文档),并入 tsk_x7wrlA87orsY 新增的 ErrAccountBanned
  及对应 StatusFor case(→ 403)。

新增文件(来自 tsk_x7wrlA87orsY):
- server/internal/devices/doc.go       package 文档(替换占位 stub)
- server/internal/devices/context.go   CtxKeyUserID / Plan / WithPlan / PlanFromCtx
- server/internal/devices/handler.go   GET /v1/me/devices · DELETE /v1/me/devices/{id}
- server/internal/devices/middleware.go SubscriptionMiddleware · CheckDeviceQuota · RequirePaidTier
- server/internal/devices/service.go   RegisterIfAbsent / DeleteDevice / ResolvePlan + 纯函数 resolveEffectivePlan
- server/internal/devices/store.go     MySQL 数据访问层
- server/internal/devices/service_test.go          15 个单测(全通过)
- server/internal/devices/devices_integration_test.go  testcontainers 集成测试

OpenAPI 更新(来自 tsk_x7wrlA87orsY):
- server/api/openapi.yaml:SubscriptionInfo.source 枚举补 free
- design/server/openapi.yaml:SubscriptionInfo.source 枚举补 admin, free

测试:go build ./... ✓;go test ./internal/apierr/... ✓(8 tests);
      go test ./internal/devices/... ✓(15 tests)。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 15:09:51 +08:00

1000 lines
33 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
openapi: 3.0.3
info:
title: Pangolin VPN — Control-Plane API
description: |
穿山甲 (Pangolin) 控制面 HTTP API,版本 v1。
数据面使用 sing-boxREALITY 主线 + Hysteria2 备线),connect 端点下发用户凭证。
**认证**:除 `auth` 四个端点外,全部操作需在 `Authorization` 头携带 `Bearer <access_token>`JWT RS256)。
**错误体**:所有 4xx/5xx 统一返回 `Error` schema,三字段均 required
`{code: string, message_zh: string, message_en: string}`。
**脱敏口径**:接口文案与日志不记录目的地址 / DNS 查询 / 流量内容;
仅保留字节数与分钟数(`usage_daily`)。
version: "1.0.0"
contact:
email: support@pangolin.vpn
servers:
- url: https://api.pangolin.vpn/v1
description: 生产环境
security:
- bearerAuth: []
paths:
# ── 认证(无需 JWT) ──────────────────────────────────────
/auth/code:
post:
operationId: sendVerificationCode
summary: 发送邮箱验证码
description: |
发送 6 位数字验证码到指定邮箱(Redis 存储,10 分钟有效)。
限流:同一 IP + 同一邮箱 1 次/分钟。防一次性邮箱域名黑名单。
tags: [Auth]
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [email]
properties:
email:
type: string
format: email
example: user@example.com
responses:
"204":
description: 验证码已发送(无响应体)
"400":
$ref: "#/components/responses/BadRequest"
"429":
$ref: "#/components/responses/TooManyRequests"
"500":
$ref: "#/components/responses/Internal"
/auth/register:
post:
operationId: register
summary: 注册账户
description: |
验证邮箱验证码 → 创建账户 → 自动写入 7 天 PRO 试用(`subscriptions(plan=pro, source='trial')`
一个邮箱仅一次)→ 返回 JWTaccess 15min + refresh 30d)。
tags: [Auth]
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [email, code, password]
properties:
email:
type: string
format: email
code:
type: string
description: 6 位数字验证码
minLength: 6
maxLength: 6
example: "123456"
password:
type: string
format: password
minLength: 8
description: 用户密码,argon2id 存储
responses:
"200":
description: 注册成功,返回 JWT 令牌对
content:
application/json:
schema:
$ref: "#/components/schemas/TokenPair"
"400":
$ref: "#/components/responses/BadRequest"
"409":
$ref: "#/components/responses/Conflict"
"429":
$ref: "#/components/responses/TooManyRequests"
"500":
$ref: "#/components/responses/Internal"
/auth/login:
post:
operationId: login
summary: 登录
description: |
邮箱 + 密码登录,返回 JWT 令牌对(access 15min + refresh 30d)。
登录失败次数过多触发锁定。
tags: [Auth]
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [email, password]
properties:
email:
type: string
format: email
password:
type: string
format: password
responses:
"200":
description: 登录成功,返回 JWT 令牌对
content:
application/json:
schema:
$ref: "#/components/schemas/TokenPair"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"429":
$ref: "#/components/responses/TooManyRequests"
"500":
$ref: "#/components/responses/Internal"
/auth/refresh:
post:
operationId: refreshToken
summary: 刷新 Access Token
description: 用有效的 refresh_token 换取新的令牌对。
tags: [Auth]
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [refresh_token]
properties:
refresh_token:
type: string
description: 有效的 refresh token30 天有效期)
responses:
"200":
description: 刷新成功,返回新令牌对
content:
application/json:
schema:
$ref: "#/components/schemas/TokenPair"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"429":
$ref: "#/components/responses/TooManyRequests"
"500":
$ref: "#/components/responses/Internal"
# ── 账户 ──────────────────────────────────────────────────
/me:
get:
operationId: getMe
summary: 获取当前账户信息
description: 返回账户基本信息 + 当前订阅状态 + 今日用量摘要。
tags: [Account]
responses:
"200":
description: 账户详情
content:
application/json:
schema:
$ref: "#/components/schemas/Me"
"401":
$ref: "#/components/responses/Unauthorized"
"500":
$ref: "#/components/responses/Internal"
/me/devices:
get:
operationId: listDevices
summary: 获取设备列表
description: 返回当前用户下所有已注册设备。
tags: [Account]
responses:
"200":
description: 设备列表
content:
application/json:
schema:
type: object
required: [devices]
properties:
devices:
type: array
items:
$ref: "#/components/schemas/Device"
"401":
$ref: "#/components/responses/Unauthorized"
"500":
$ref: "#/components/responses/Internal"
/me/devices/{id}:
delete:
operationId: deleteDevice
summary: 移除设备
description: |
从账户移除指定设备,同步通过 gRPC 回收该设备在所有节点侧的凭证(数据面 UUID 无效化)。
tags: [Account]
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: 设备 UUID
responses:
"204":
description: 设备已移除(无响应体)
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/Internal"
# ── 商业闭环 ─────────────────────────────────────────────
/redeem:
post:
operationId: redeemCode
summary: 兑换激活码
description: |
兑换激活码,订阅顺延(叠加非覆盖)。操作幂等:同一用户重复提交同一码返回首次结果。
失败风控:单用户兑换失败 5 次/小时后锁 1 小时。所有兑换操作写入 audit_log。
tags: [Commerce]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [code]
properties:
code:
type: string
description: Crockford Base32 激活码,16 位含校验位
example: "ABCD-EFGH-JKLM-NP00"
responses:
"200":
description: 兑换成功
content:
application/json:
schema:
$ref: "#/components/schemas/RedeemResult"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"
"409":
$ref: "#/components/responses/Conflict"
"429":
$ref: "#/components/responses/TooManyRequests"
"500":
$ref: "#/components/responses/Internal"
/ads/unlock:
post:
operationId: adsUnlock
summary: 激励视频广告解锁当日时长
description: |
免费版用户完成激励视频广告后调用。服务端向广告平台(AdMob/Unity)校验 ad_token 真伪,
通过后记录 `usage_daily.ad_unlocked_at`,当日 connect 接口方可放行。
tags: [Commerce]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [device_id, ad_token]
properties:
device_id:
type: string
format: uuid
description: 发起广告的设备 UUID
ad_token:
type: string
description: 广告 SDK 签发的服务端回执 token
responses:
"204":
description: 广告解锁成功(无响应体)
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"429":
$ref: "#/components/responses/TooManyRequests"
"500":
$ref: "#/components/responses/Internal"
/plans:
get:
operationId: listPlans
summary: 获取套餐目录
description: |
返回全部套餐定义。客户端用于展示套餐页与价格对比。
套餐口径:免费版(每日 10 分钟,须看广告,1 台设备);
PRO(不限时长,80+ 线路,5 台设备);团队版(10 席位)。
tags: [Commerce]
responses:
"200":
description: 套餐列表
content:
application/json:
schema:
type: object
required: [plans]
properties:
plans:
type: array
items:
$ref: "#/components/schemas/Plan"
"401":
$ref: "#/components/responses/Unauthorized"
"500":
$ref: "#/components/responses/Internal"
# ── 节点 ─────────────────────────────────────────────────
/nodes:
get:
operationId: listNodes
summary: 获取节点目录
description: |
返回当前用户套餐可用的节点列表。响应体带全局 `version` 整数字段。
客户端携带 `if_version=N` 时,若服务端版本未变则返回 304(无响应体)——
这是节点秒级灰度的基础:节点任何变更 bump 全局 version,被封节点即时从目录消失。
**注意:目录不含任何密钥参数**reality_pbk/sni 等),密钥仅通过 connect 端点下发。
tags: [Nodes]
parameters:
- name: if_version
in: query
required: false
schema:
type: integer
format: int64
description: 客户端已缓存的目录版本号;命中则返回 304
responses:
"200":
description: 节点目录(版本有变化或首次请求)
content:
application/json:
schema:
$ref: "#/components/schemas/NodeDirectory"
"304":
description: 节点目录未变化,客户端应使用缓存(无响应体)
"401":
$ref: "#/components/responses/Unauthorized"
"500":
$ref: "#/components/responses/Internal"
/nodes/{id}/connect:
post:
operationId: connectNode
summary: 连接节点 — 下发连接凭证
description: |
校验订阅有效性与设备数量上限(免费版额外校验 ad_unlocked_at + 剩余分钟)→
通过 gRPC 确保用户 UUID 已在节点 sing-box 用户表 →
返回 sing-box 连接凭证(REALITY 主线 + Hysteria2 备线)。
凭证模型:每用户一个数据面 UUID(与账号解耦,可独立轮换),节点侧只见 UUID。
免费版凭证 TTL = 当日剩余分钟数,到时 agent 自动回收;付费版 TTL 24h,在线自动续期。
**注意**`ConnectCredential` 的具体字段可能随数据面树(任务 #5)最终敲定后 bump,
以该任务产出为准。
tags: [Nodes]
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: 节点 UUID
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [device_id]
properties:
device_id:
type: string
format: uuid
description: 发起连接的设备 UUID
responses:
"200":
description: 连接凭证(含 sing-box 参数)
content:
application/json:
schema:
$ref: "#/components/schemas/ConnectCredential"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"403":
$ref: "#/components/responses/Forbidden"
"404":
$ref: "#/components/responses/NotFound"
"429":
$ref: "#/components/responses/TooManyRequests"
"500":
$ref: "#/components/responses/Internal"
/nodes/{id}/disconnect:
post:
operationId: disconnectNode
summary: 断开节点连接
description: |
通知服务端客户端主动断开。服务端可提前回收免费版凭证以释放配额,
但凭证 TTL 到期时也会被 agent 自动回收。
tags: [Nodes]
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
description: 节点 UUID
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [device_id]
properties:
device_id:
type: string
format: uuid
description: 发起断开的设备 UUID
responses:
"204":
description: 已断开(无响应体)
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"404":
$ref: "#/components/responses/NotFound"
"500":
$ref: "#/components/responses/Internal"
# ── 用量与公告 ────────────────────────────────────────────
/usage:
get:
operationId: getUsage
summary: 获取用量曲线
description: 返回最近 N 天的每日用量数据,用于统计页折线图 / 柱状图展示。
tags: [Usage]
parameters:
- name: days
in: query
required: false
schema:
type: integer
minimum: 1
maximum: 90
default: 7
description: 查询最近几天,默认 7,最大 90
responses:
"200":
description: 用量数据点列表
content:
application/json:
schema:
type: object
required: [points]
properties:
points:
type: array
items:
$ref: "#/components/schemas/UsagePoint"
"400":
$ref: "#/components/responses/BadRequest"
"401":
$ref: "#/components/responses/Unauthorized"
"500":
$ref: "#/components/responses/Internal"
/notices:
get:
operationId: listNotices
summary: 获取公告列表
description: |
返回当前有效公告。同一份内容亦以多镜像签名静态 JSON 发布,客户端优先拉静态副本,
失败时回落到本端点。
tags: [Notices]
responses:
"200":
description: 公告列表
content:
application/json:
schema:
type: object
required: [notices]
properties:
notices:
type: array
items:
$ref: "#/components/schemas/Notice"
"401":
$ref: "#/components/responses/Unauthorized"
"500":
$ref: "#/components/responses/Internal"
# ── 组件定义 ──────────────────────────────────────────────────
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: JWT RS256`Authorization: Bearer <access_token>`
# ── 统一错误响应 ────────────────────────────────────────────
responses:
BadRequest:
description: 请求参数非法或业务校验失败
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
Unauthorized:
description: 未提供有效的访问令牌,或令牌已过期
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
Forbidden:
description: 令牌有效但权限不足(如免费版访问付费节点、设备不属于当前用户)
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
NotFound:
description: 目标资源不存在
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
Conflict:
description: 资源冲突(如邮箱已注册、激活码已被兑换)
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
TooManyRequests:
description: 请求频率超过限制,稍后重试
headers:
Retry-After:
description: 客户端应等待的秒数后再重试
schema:
type: integer
example: 60
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
Internal:
description: 服务端内部错误
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
# ── Schema ────────────────────────────────────────────────
schemas:
# ── 通用错误体 ─────────────────────────────────────────
Error:
type: object
description: 统一错误响应体,三字段均 required。文案遵守脱敏口径(不含红线词)。
required: [code, message_zh, message_en]
properties:
code:
type: string
description: 机器可读错误码,例如 `auth.code_expired` / `redeem.already_redeemed`
example: auth.code_expired
message_zh:
type: string
description: 中文错误文案(面向用户展示)
example: 验证码已过期,请重新获取
message_en:
type: string
description: 英文错误文案(面向用户展示)
example: Verification code has expired. Please request a new one.
# ── 认证 ───────────────────────────────────────────────
TokenPair:
type: object
required: [access_token, refresh_token, expires_in]
properties:
access_token:
type: string
description: JWT access token,有效期 15 分钟
refresh_token:
type: string
description: Refresh token,有效期 30 天
expires_in:
type: integer
description: access_token 有效秒数(固定 900
example: 900
# ── 账户 ───────────────────────────────────────────────
Me:
type: object
required: [user, subscription, today_usage]
properties:
user:
$ref: "#/components/schemas/UserInfo"
subscription:
$ref: "#/components/schemas/SubscriptionInfo"
today_usage:
$ref: "#/components/schemas/TodayUsageSummary"
UserInfo:
type: object
required: [uuid, email, status]
properties:
uuid:
type: string
format: uuid
description: 用户全局唯一标识(对外口径,永不暴露自增 ID)
email:
type: string
format: email
status:
type: string
enum: [active, suspended]
description: 账户状态
SubscriptionInfo:
type: object
required: [plan_code, source]
properties:
plan_code:
type: string
enum: [free, pro, team]
description: 当前套餐代码
expires_at:
type: string
format: date-time
nullable: true
description: 订阅到期时间(UTC ISO-8601),免费版无到期时间时为 null
source:
type: string
enum: [trial, code, admin, free]
description: 订阅来源(试用 / 激活码 / 管理员 / 无订阅时的免费回落)
TodayUsageSummary:
type: object
required: [minutes_used, minutes_remaining]
properties:
minutes_used:
type: integer
description: 今日已使用分钟数
example: 3
minutes_remaining:
type: integer
nullable: true
description: 今日剩余分钟数,付费无限制时为 null
example: 7
ad_unlocked:
type: boolean
description: 免费版今日是否已完成激励广告解锁
example: false
Device:
type: object
required: [uuid, name, platform, last_seen]
properties:
uuid:
type: string
format: uuid
description: 设备 UUID
name:
type: string
description: 设备名称(客户端上报,如「iPhone 15 Pro」)
example: iPhone 15 Pro
platform:
type: string
enum: [ios, android, macos, windows, linux]
description: 设备平台
last_seen:
type: string
format: date-time
description: 最后活跃时间(UTC ISO-8601
# ── 商业 ───────────────────────────────────────────────
Plan:
type: object
required: [code, max_devices, ad_gate]
properties:
code:
type: string
enum: [free, pro, team]
description: 套餐唯一代码
max_devices:
type: integer
description: 最大同时在线设备数(free=1, pro=5, team=10
example: 5
daily_minutes:
type: integer
nullable: true
description: 每日可用分钟数上限;null 表示无限制(pro/team
example: 10
ad_gate:
type: boolean
description: 首次连接是否需要激励广告解锁(仅 free=true)
example: false
RedeemResult:
type: object
description: |
激活码兑换结果。幂等设计:同一用户重复提交同一码返回首次结果(非报错)。
required: [plan_code, extended_days, expires_at]
properties:
plan_code:
type: string
enum: [free, pro, team]
description: 本次兑换激活的套餐代码
extended_days:
type: integer
description: 本次顺延的天数(叠加而非覆盖)
example: 30
expires_at:
type: string
format: date-time
description: 兑换后的订阅到期时间(UTC ISO-8601
# ── 节点目录 ────────────────────────────────────────────
NodeDirectory:
type: object
required: [version, nodes]
properties:
version:
type: integer
format: int64
description: 目录全局版本号;任何节点变更 bump 此值,客户端据此决定是否刷新缓存
example: 42
nodes:
type: array
items:
$ref: "#/components/schemas/Node"
Node:
type: object
description: |
节点目录条目。**不含任何密钥或连接参数**reality_pbk/sni/password 等),
这些信息仅在 POST /nodes/{id}/connect 成功后下发。
required: [uuid, region, name_zh, name_en, tier, signal, load]
properties:
uuid:
type: string
format: uuid
description: 节点 UUIDconnect 时作为 path 参数)
region:
type: string
description: 地区代码,2 字母大写(HK/JP/SG/US …)
example: HK
name_zh:
type: string
description: 节点中文名称
example: 香港 01
name_en:
type: string
description: 节点英文名称
example: Hong Kong 01
tier:
type: string
enum: [free, pro]
description: 节点等级(free 节点免费版可用,pro 节点需付费订阅)
signal:
type: integer
minimum: 0
maximum: 4
description: 信号格数(0~4),由 agent 上报延迟换算,客户端展示信号条
example: 3
load:
type: integer
minimum: 0
maximum: 100
description: 节点当前负载百分比(0~100),由 agent 每 30s 上报到 Redis
example: 45
# ── 连接凭证 ────────────────────────────────────────────
ConnectCredential:
type: object
description: |
sing-box 连接凭证,支持 REALITY 主线 + Hysteria2 备线。
每用户一个数据面 UUID(dp_uuid),与账号 UUID 解耦、可独立轮换;
节点侧只见 dp_uuid,零账号信息,单节点被抄不泄露用户库。
**字段稳定性**:本 schema 可能随数据面树(任务 #5)最终敲定后 bump,
在正式实现前以该任务产出为权威依据。
required: [protocol, server, port, uuid, ttl_seconds]
properties:
protocol:
type: string
enum: [reality, hysteria2]
description: 优先协议(客户端按网络状况自动选;服务端返回当前最优)
server:
type: string
description: 节点服务器地址(IP 或域名)
example: hk01.node.pangolin.vpn
port:
type: integer
minimum: 1
maximum: 65535
description: 主协议端口(REALITY 用 TCP 443Hysteria2 用 UDP 443
example: 443
uuid:
type: string
format: uuid
description: |
用户数据面 UUIDdp_uuid),即 VLESS uuid 字段。
Hysteria2 auth password 由同一 UUID 派生(客户端无需单独字段)。
flow:
type: string
description: VLESS flow 参数,REALITY 协议时使用(通常为 `xtls-rprx-vision`
example: xtls-rprx-vision
reality:
$ref: "#/components/schemas/RealityParams"
hysteria2:
$ref: "#/components/schemas/Hysteria2Params"
ttl_seconds:
type: integer
description: |
凭证有效秒数。免费版 = 当日剩余分钟数 × 60;付费版 = 86400(24h,在线自动续期)。
example: 86400
RealityParams:
type: object
required: [public_key, short_id, sni]
description: REALITY 协议参数(仅 protocol=reality 时有意义)
properties:
public_key:
type: string
description: REALITY 服务端公钥(x25519
example: ABC123...
short_id:
type: string
description: REALITY short ID(十六进制字符串)
example: "a1b2c3d4"
sni:
type: string
description: TLS SNI 伪装域名
example: www.cloudflare.com
Hysteria2Params:
type: object
required: [port, password]
description: Hysteria2 备线参数(仅 protocol=hysteria2 或客户端协议降级时使用)
properties:
port:
type: integer
minimum: 1
maximum: 65535
description: Hysteria2 UDP 端口(通常 443
example: 443
password:
type: string
description: Hysteria2 auth password(由 dp_uuid 派生,服务端同源生成)
obfs:
type: string
nullable: true
description: 混淆模式(可选,如 `salamander`);null 表示不启用混淆
example: null
# ── 用量 ────────────────────────────────────────────────
UsagePoint:
type: object
required: [date, bytes_up, bytes_down, minutes_used]
description: 单日用量数据点(无目的地/DNS/流量内容,仅字节数与分钟数,符合无日志承诺)
properties:
date:
type: string
format: date
description: 日期(UTCYYYY-MM-DD
example: "2026-06-13"
bytes_up:
type: integer
format: int64
description: 当日上行字节数
example: 10485760
bytes_down:
type: integer
format: int64
description: 当日下行字节数
example: 104857600
minutes_used:
type: integer
description: 当日已用分钟数
example: 7
# ── 公告 ────────────────────────────────────────────────
Notice:
type: object
required: [id, title_zh, title_en, body_zh, body_en, published_at]
properties:
id:
type: string
format: uuid
description: 公告 UUID
title_zh:
type: string
description: 公告中文标题
example: 线路维护通知
title_en:
type: string
description: 公告英文标题
example: Line Maintenance Notice
body_zh:
type: string
description: 公告中文正文(Markdown
body_en:
type: string
description: 公告英文正文(Markdown
published_at:
type: string
format: date-time
description: 发布时间(UTC ISO-8601
tags:
- name: Auth
description: 认证相关(发验证码、注册、登录、刷新 Token)——无需 JWT
- name: Account
description: 账户与设备管理
- name: Commerce
description: 商业闭环(激活码兑换、广告解锁、套餐目录)
- name: Nodes
description: 节点目录与连接凭证下发(数据面入口)
- name: Usage
description: 用量统计
- name: Notices
description: 公告