Files
pangolin/design/server/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

656 lines
18 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) Control-Plane API
description: |
穿山甲网络加速 控制面 API v1。
**设计原则**
- 控制面仅管账户、套餐、节点目录、兑换;用户流量走加速节点(WireGuard),不经过本 API。
- App 内无支付表单;资金流全走外部(激活码渠道)。
- 最小数据:不记录目的地址;仅保留计费所需元数据。
**认证**:除 `/auth/*` 外,全部接口需 `Authorization: Bearer <access_token>`。
**错误体**`{ code: string, message_zh: string, message_en: string }`
version: 1.0.0
contact:
email: support@pangolin.app
servers:
- url: https://api.pangolin.app/v1
description: 生产环境
security:
- bearerAuth: []
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
schemas:
Error:
type: object
required: [code, message_zh, message_en]
properties:
code:
type: string
example: INVALID_CODE
message_zh:
type: string
example: 兑换码无效
message_en:
type: string
example: Invalid activation code
JWT:
type: object
required: [access_token, refresh_token]
properties:
access_token:
type: string
description: 有效期 15 分钟
refresh_token:
type: string
description: 有效期 30 天
UserProfile:
type: object
properties:
id:
type: string
format: uuid
email:
type: string
format: email
subscription:
$ref: '#/components/schemas/Subscription'
usage:
$ref: '#/components/schemas/UsageSummary'
Subscription:
type: object
properties:
plan:
type: string
enum: [free, pro, team]
expires_at:
type: string
format: date-time
nullable: true
description: null 表示无限期(免费版)
source:
type: string
enum: [trial, code, admin, free]
Device:
type: object
properties:
id:
type: string
format: uuid
name:
type: string
platform:
type: string
example: ios
pubkey:
type: string
description: WireGuard 公钥 (base64)
last_seen:
type: string
format: date-time
Plan:
type: object
properties:
id:
type: string
format: uuid
code:
type: string
enum: [free, pro, team]
description: free=免费版; pro=25¥/月; team=99¥/月(10席位)
max_devices:
type: integer
max_routes:
type: integer
daily_minutes:
type: integer
nullable: true
description: null 表示无限制;免费版为 10 分钟/天
ad_gate:
type: boolean
description: 是否需要每日激励视频解锁
Node:
type: object
properties:
id:
type: string
format: uuid
region:
type: string
description: 2 字母地区码,如 HK / JP / SG
example: HK
name_zh:
type: string
example: 香港 01
name_en:
type: string
example: Hong Kong 01
tier:
type: string
enum: [free, pro]
status:
type: string
enum: [up, down, draining]
weight:
type: integer
latency_ms:
type: integer
nullable: true
description: 由客户端测速后本地排序;此字段为服务端建议值
NodeList:
type: object
required: [version, nodes]
properties:
version:
type: integer
description: 节点目录版本号,递增;客户端带 if_version 实现 304
nodes:
type: array
items:
$ref: '#/components/schemas/Node'
WireGuardConfig:
type: object
required: [endpoint, server_pubkey, client_ip, dns]
properties:
endpoint:
type: string
description: 节点 WireGuard 端点,格式 host:port
example: hk1.pangolin.app:51820
server_pubkey:
type: string
description: 服务端 WireGuard 公钥 (base64)
client_ip:
type: string
description: 分配给客户端的隧道内网 IP
example: 10.66.0.2/32
dns:
type: string
example: 1.1.1.1
ttl_seconds:
type: integer
nullable: true
description: peer TTL;免费版 = 今日剩余秒数,付费版 = 86400
UsageSummary:
type: object
properties:
bytes_up:
type: integer
bytes_down:
type: integer
minutes_used:
type: integer
ad_unlocked_at:
type: string
format: date-time
nullable: true
description: 今日激励视频解锁时刻;null = 未解锁(免费版)
DailyUsage:
type: object
properties:
date:
type: string
format: date
bytes_up:
type: integer
bytes_down:
type: integer
minutes_used:
type: integer
paths:
# ── Auth ──────────────────────────────────────────────────────────────────
/auth/code:
post:
summary: 发送邮箱验证码
description: 触发邮件发送 6 位数字验证码;有效期 10 分钟,限 1 次/分钟(Redis 滑窗)。
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [email]
properties:
email:
type: string
format: email
responses:
'204':
description: 验证码已发送
'422':
description: 参数格式错误
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
description: 请求频率超限
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/auth/register:
post:
summary: 注册账户(自动开 7 天体验期)
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [email, code, password]
properties:
email:
type: string
format: email
code:
type: string
minLength: 6
maxLength: 6
pattern: '^\d{6}$'
password:
type: string
minLength: 8
responses:
'200':
description: 注册成功,返回 JWT
content:
application/json:
schema:
$ref: '#/components/schemas/JWT'
'400':
description: 验证码错误 / 已过期 / 邮箱已注册
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'422':
description: 参数格式错误
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/auth/login:
post:
summary: 登录
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [email, password]
properties:
email:
type: string
format: email
password:
type: string
responses:
'200':
description: 登录成功,返回 JWT
content:
application/json:
schema:
$ref: '#/components/schemas/JWT'
'401':
description: 邮箱或密码错误
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/auth/refresh:
post:
summary: 刷新 access_token
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [refresh_token]
properties:
refresh_token:
type: string
responses:
'200':
description: 新 JWT
content:
application/json:
schema:
$ref: '#/components/schemas/JWT'
'401':
description: refresh_token 无效或已过期
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
# ── Me ────────────────────────────────────────────────────────────────────
/me:
get:
summary: 获取当前用户信息(账户 + 订阅 + 用量摘要)
responses:
'200':
description: 用户信息
content:
application/json:
schema:
$ref: '#/components/schemas/UserProfile'
'401':
description: 未认证
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/me/devices:
get:
summary: 获取设备列表
responses:
'200':
description: 设备列表
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Device'
'401':
description: 未认证
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/me/devices/{id}:
delete:
summary: 移除设备(回收节点 peer
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
responses:
'204':
description: 已移除
'401':
description: 未认证
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: 设备不存在
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
# ── Plans & Subscriptions ─────────────────────────────────────────────────
/plans:
get:
summary: 获取套餐目录(供客户端展示价格页)
security: []
responses:
'200':
description: 套餐列表
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Plan'
/redeem:
post:
summary: 兑换激活码(幂等;同码重复提交返回首次结果)
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [code]
properties:
code:
type: string
description: Crockford Base3216 位,含校验位
example: 0A1B2C3D4E5F6G7H
responses:
'200':
description: 兑换成功,返回最新订阅信息
content:
application/json:
schema:
$ref: '#/components/schemas/Subscription'
'400':
description: 激活码无效、已使用或已作废
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: 未认证
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
description: 兑换失败频率超限(5次/小时后锁 1 小时)
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
# ── Ads ───────────────────────────────────────────────────────────────────
/ads/unlock:
post:
summary: 免费版:激励视频解锁当日时长
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [device_id, ad_token]
properties:
device_id:
type: string
format: uuid
ad_token:
type: string
description: 广告 SDK 服务端回执 token(用于服务端验证)
responses:
'200':
description: 当日时长已解锁
content:
application/json:
schema:
type: object
required: [ad_unlocked_at]
properties:
ad_unlocked_at:
type: string
format: date-time
'400':
description: ad_token 无效 / 今日已解锁
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: 未认证
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
# ── Nodes ─────────────────────────────────────────────────────────────────
/nodes:
get:
summary: 获取节点目录(按当前订阅过滤;支持 304)
parameters:
- name: if_version
in: query
required: false
schema:
type: integer
description: 客户端持有的版本号;服务端未变更时返回 304
responses:
'200':
description: 节点列表(含版本号)
content:
application/json:
schema:
$ref: '#/components/schemas/NodeList'
'304':
description: 节点目录未更新
'401':
description: 未认证
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/nodes/{id}/connect:
post:
summary: 连接节点,下发 WireGuard 配置
description: |
校验:订阅状态、设备数限制;免费版额外校验当日 ad_unlocked_at 与剩余分钟。
通过后经 gRPC 让节点 agent 注册 peer,返回 WireGuard 配置。
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [device_pubkey]
properties:
device_pubkey:
type: string
description: 客户端 WireGuard 公钥 (base64)
responses:
'200':
description: WireGuard 配置
content:
application/json:
schema:
$ref: '#/components/schemas/WireGuardConfig'
'401':
description: 未认证
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'402':
description: 订阅已到期 / 设备数超限 / 免费版未解锁当日时长
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: 节点不存在或已下线
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/nodes/{id}/disconnect:
post:
summary: 断开节点(回收 WireGuard peer
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
responses:
'204':
description: 已断开
'401':
description: 未认证
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: 节点不存在
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
# ── Usage ─────────────────────────────────────────────────────────────────
/usage:
get:
summary: 获取用量曲线(统计页)
parameters:
- name: days
in: query
required: false
schema:
type: integer
minimum: 1
maximum: 30
default: 7
description: 返回最近 N 天数据
responses:
'200':
description: 每日用量数组(含字节数与时长,不含目的地址)
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/DailyUsage'
'401':
description: 未认证
content:
application/json:
schema:
$ref: '#/components/schemas/Error'