43c76fb978
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Successful in 25s
ci-pangolin / Lint — shellcheck (push) Successful in 29s
ci-pangolin / Cleartext Scan — Android 禁明文 (push) Successful in 22s
ci-pangolin / OpenAPI Sync Check (push) Successful in 40s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (push) Successful in 19s
ci-pangolin / Flutter — analyze + test (push) Failing after 4m59s
ci-pangolin / Codegen Drift — token 生成物未漂移 (push) Successful in 1m51s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (push) Successful in 5s
ci-pangolin / Go — build + test (push) Failing after 1m33s
ci-pangolin / E2E Smoke — L4 进程级端到端 (push) Failing after 14s
ci-pangolin / Go — integration (mysql/redis testcontainers) (push) Failing after 4m59s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (push) Failing after 4s
# Conflicts: # docs/index.html # server/cmd/server/main.go
1497 lines
51 KiB
YAML
1497 lines
51 KiB
YAML
openapi: 3.0.3
|
||
info:
|
||
title: Pangolin VPN — Control-Plane API
|
||
description: |
|
||
穿山甲 (Pangolin) 控制面 HTTP API,版本 v1。
|
||
数据面使用 sing-box(REALITY 主线 + 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')`,
|
||
一个邮箱仅一次)→ 返回 JWT(access 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 token(30 天有效期)
|
||
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_bonus_minutes` 累加固定分钟(每日封顶),当日免费额度随之上浮。
|
||
额度为**全账户共享**(非每设备)。响应返回本次实际加时与最新剩余分钟。
|
||
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:
|
||
"200":
|
||
description: 广告加时成功
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: object
|
||
required: [granted_minutes, minutes_remaining]
|
||
properties:
|
||
granted_minutes:
|
||
type: integer
|
||
description: 本次广告实际加时分钟(已达每日封顶时为 0)
|
||
minutes_remaining:
|
||
type: integer
|
||
description: 加时后账户当日剩余分钟(全账户共享)
|
||
"400":
|
||
$ref: "#/components/responses/BadRequest"
|
||
"401":
|
||
$ref: "#/components/responses/Unauthorized"
|
||
"403":
|
||
$ref: "#/components/responses/Forbidden"
|
||
"409":
|
||
$ref: "#/components/responses/Conflict"
|
||
"429":
|
||
$ref: "#/components/responses/TooManyRequests"
|
||
"500":
|
||
$ref: "#/components/responses/Internal"
|
||
|
||
# ── Pay(pay v2 统一支付网关代理;PAY_BASE_URL 未配置时整组不挂载)───
|
||
|
||
/pay/catalog:
|
||
get:
|
||
operationId: payCatalog
|
||
summary: 可购档位目录
|
||
description: |
|
||
三档可购套餐(月/季/年),仅展示用(`price_minor` 为 CNY 展示价,单位分)。
|
||
实际扣款金额以 pay 侧 `product_prices` / `products.price` 为准,两处人工对齐
|
||
(联调 checklist 有价格一致性核对项)。
|
||
tags: [Pay]
|
||
responses:
|
||
"200":
|
||
description: 目录列表
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: object
|
||
required: [items]
|
||
properties:
|
||
items:
|
||
type: array
|
||
items:
|
||
$ref: "#/components/schemas/PayCatalogItem"
|
||
"401":
|
||
$ref: "#/components/responses/Unauthorized"
|
||
"500":
|
||
$ref: "#/components/responses/Internal"
|
||
|
||
/pay/orders:
|
||
post:
|
||
operationId: payCreateOrder
|
||
summary: 下单(代理 pay v2 create)
|
||
description: |
|
||
创建支付订单。**客户端只传 sku + method + 端型 metadata,永远不传金额**——
|
||
金额由服务端按 sku 查 pay 侧目录后代下单,`biz_ref` 由服务端从当前 JWT 用户
|
||
映射(`users.uuid`),客户端无感知。
|
||
|
||
下单成功后台账(`pay_purchases`)写入失败不影响本次下单结果(webhook 按
|
||
`biz_ref` 兜底补建),仅记服务端日志。
|
||
tags: [Pay]
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: object
|
||
required: [sku, method]
|
||
properties:
|
||
sku:
|
||
type: string
|
||
description: 档位代码,取自 `/pay/catalog`(如 `pro_month`)
|
||
example: pro_month
|
||
method:
|
||
type: string
|
||
description: 支付方式,取值以 pay 侧支持渠道为准(如 `crypto` / `alipay`)
|
||
example: crypto
|
||
metadata:
|
||
type: object
|
||
additionalProperties:
|
||
type: string
|
||
description: |
|
||
透传给 pay 的端型元数据,白名单仅 `is_mobile` / `render`(其余字段
|
||
服务端静默丢弃,不透传)。
|
||
example:
|
||
is_mobile: "true"
|
||
responses:
|
||
"200":
|
||
description: 下单成功,返回渲染会话
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: "#/components/schemas/PayOrderSessionResult"
|
||
"400":
|
||
description: 参数非法(未知 sku / method 为空 / 请求体解析失败)
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: "#/components/schemas/Error"
|
||
"401":
|
||
$ref: "#/components/responses/Unauthorized"
|
||
"404":
|
||
description: 当前用户不存在或账户非 active(NOT_FOUND)
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: "#/components/schemas/Error"
|
||
"409":
|
||
$ref: "#/components/responses/PayConflict"
|
||
"429":
|
||
$ref: "#/components/responses/TooManyRequests"
|
||
"500":
|
||
$ref: "#/components/responses/Internal"
|
||
"502":
|
||
$ref: "#/components/responses/PayUpstream"
|
||
|
||
/pay/orders/{orderNo}:
|
||
get:
|
||
operationId: payGetOrder
|
||
summary: 查单(代理 pay v2 查单 + 本地台账开通状态)
|
||
description: |
|
||
每次调用都会回源 pay 查一次单(无本地缓存;量大后可加 server 短缓存,见交付
|
||
说明「取舍」条目)。`activated` 是客户端轮询判定成功开通的唯一依据——本地台账
|
||
`pay_purchases.status = 'paid'`,与 `pay_status`(pay 侧状态词汇原样透传,
|
||
仅展示、不做分支判断)是两条独立信息。
|
||
tags: [Pay]
|
||
parameters:
|
||
- name: orderNo
|
||
in: path
|
||
required: true
|
||
schema:
|
||
type: string
|
||
description: pay 侧订单号(下单响应 `order_no`)
|
||
example: "PAY202607100001"
|
||
responses:
|
||
"200":
|
||
description: 订单状态
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: "#/components/schemas/PayOrderStatus"
|
||
"401":
|
||
$ref: "#/components/responses/Unauthorized"
|
||
"404":
|
||
description: 订单不属于当前用户,或订单不存在(NOT_FOUND)
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: "#/components/schemas/Error"
|
||
"500":
|
||
$ref: "#/components/responses/Internal"
|
||
"502":
|
||
$ref: "#/components/responses/PayUpstream"
|
||
|
||
/pay/orders/{orderNo}/retry:
|
||
post:
|
||
operationId: payRetryOrder
|
||
summary: 换渠道重试(同一订单换 method)
|
||
description: |
|
||
同一订单更换支付方式(如 crypto → alipay)。若目标渠道结算币种与订单原币种
|
||
不符,pay 侧拒绝并返回 409 `CURRENCY_MISMATCH`——客户端应据此自动取消旧单、
|
||
用新 method 重新走 `/pay/orders` 下单(见联调 checklist「换渠道」条)。
|
||
tags: [Pay]
|
||
parameters:
|
||
- name: orderNo
|
||
in: path
|
||
required: true
|
||
schema:
|
||
type: string
|
||
description: pay 侧订单号(下单响应 `order_no`)
|
||
example: "PAY202607100001"
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: object
|
||
required: [method]
|
||
properties:
|
||
method:
|
||
type: string
|
||
example: alipay
|
||
metadata:
|
||
type: object
|
||
additionalProperties:
|
||
type: string
|
||
description: 同 `/pay/orders`,仅 `is_mobile` / `render` 白名单透传
|
||
responses:
|
||
"200":
|
||
description: 重试成功,返回新的渲染会话
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: "#/components/schemas/PayOrderSessionResult"
|
||
"400":
|
||
description: method 为空或请求体解析失败(BAD_REQUEST)
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: "#/components/schemas/Error"
|
||
"401":
|
||
$ref: "#/components/responses/Unauthorized"
|
||
"404":
|
||
description: 订单不属于当前用户,或订单不存在(NOT_FOUND)
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: "#/components/schemas/Error"
|
||
"409":
|
||
$ref: "#/components/responses/PayConflict"
|
||
"429":
|
||
$ref: "#/components/responses/TooManyRequests"
|
||
"500":
|
||
$ref: "#/components/responses/Internal"
|
||
"502":
|
||
$ref: "#/components/responses/PayUpstream"
|
||
|
||
/pay/orders/{orderNo}/cancel:
|
||
post:
|
||
operationId: payCancelOrder
|
||
summary: 取消订单
|
||
description: |
|
||
取消一个仍处于待支付状态的订单。仅当用户显式点击「取消订单」时调用——
|
||
等待支付页面被系统返回键/手势关闭**不**触发取消(订单仍在 pay 侧 pending,
|
||
可从头再进),这是桌面壳的既定行为,见风险清单第 6 条。
|
||
tags: [Pay]
|
||
parameters:
|
||
- name: orderNo
|
||
in: path
|
||
required: true
|
||
schema:
|
||
type: string
|
||
description: pay 侧订单号(下单响应 `order_no`)
|
||
example: "PAY202607100001"
|
||
responses:
|
||
"200":
|
||
description: 取消结果(`canceled=false` 表示订单已不在待支付状态,未被取消)
|
||
content:
|
||
application/json:
|
||
schema:
|
||
type: object
|
||
required: [canceled]
|
||
properties:
|
||
canceled:
|
||
type: boolean
|
||
"401":
|
||
$ref: "#/components/responses/Unauthorized"
|
||
"404":
|
||
description: 订单不属于当前用户,或订单不存在(NOT_FOUND)
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: "#/components/schemas/Error"
|
||
"500":
|
||
$ref: "#/components/responses/Internal"
|
||
"502":
|
||
$ref: "#/components/responses/PayUpstream"
|
||
|
||
/webhook/pay:
|
||
post:
|
||
operationId: payWebhook
|
||
summary: pay v2 出站 webhook 接收(payment.succeeded)
|
||
description: |
|
||
pay 侧支付成功后的服务端到服务端回调,**不经过 JWT**(本接口不在
|
||
`bearerAuth` 保护范围内),改用 HMAC 请求头验签:与 pay
|
||
`util/sign.go` 逐字节同构,`parts = [X-Pay-System, X-Pay-Timestamp,
|
||
X-Pay-Nonce, rawBody]`,`\n` join 后 HMAC-SHA256 + 标准 base64,
|
||
时间窗容忍 ±5 分钟。
|
||
|
||
幂等三层:`X-Pay-Nonce` SETNX(传输层重放,Redis 不可用时跳过此层)→
|
||
`out_trade_no` 行锁 CAS(业务幂等,重投唯一可靠键,已 `paid` 直接
|
||
返回 SUCCESS)→ `biz_ref` 兜底(台账缺行按用户 uuid 自修复)。
|
||
|
||
**注意**:本接口的成功/失败响应体是**纯文本**,不是 `Error` JSON
|
||
schema——ACK 判据是 HTTP 200 且 body 字面包含大写 `SUCCESS`;验签/
|
||
载荷失败时用 `http.Error`(纯文本)返回 400/401;开通失败返回 500
|
||
纯文本(pay 侧据此退避重投,事务已回滚、不产生半态)。
|
||
tags: [Pay]
|
||
security: []
|
||
parameters:
|
||
- name: X-Pay-System
|
||
in: header
|
||
required: true
|
||
schema:
|
||
type: string
|
||
description: 业务系统标识,须等于服务端配置的 `PAY_BIZ_SYSTEM`(默认 `pangolin`)
|
||
- name: X-Pay-Timestamp
|
||
in: header
|
||
required: true
|
||
schema:
|
||
type: string
|
||
description: Unix 秒级时间戳,±300s 容忍窗口
|
||
- name: X-Pay-Nonce
|
||
in: header
|
||
required: true
|
||
schema:
|
||
type: string
|
||
description: 一次性随机串,用于传输层防重放(SETNX + TTL)
|
||
- name: X-Pay-Sign
|
||
in: header
|
||
required: true
|
||
schema:
|
||
type: string
|
||
description: HMAC-SHA256(secret, join("\n", system, ts, nonce, rawBody)) 的标准 base64
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: "#/components/schemas/PayWebhookEvent"
|
||
responses:
|
||
"200":
|
||
description: 已确认处理(幂等;重复投递也回 200 SUCCESS)
|
||
content:
|
||
text/plain:
|
||
schema:
|
||
type: string
|
||
example: SUCCESS
|
||
"400":
|
||
description: 请求体读取失败或载荷非法 JSON(纯文本响应,非 Error schema)
|
||
content:
|
||
text/plain:
|
||
schema:
|
||
type: string
|
||
"401":
|
||
description: 验签失败(system 不符 / 缺头 / 时间窗超限 / 签名不匹配,纯文本响应)
|
||
content:
|
||
text/plain:
|
||
schema:
|
||
type: string
|
||
"500":
|
||
description: 开通事务失败,pay 将按退避策略重投(纯文本响应,事务已回滚不产生半态)
|
||
content:
|
||
text/plain:
|
||
schema:
|
||
type: string
|
||
|
||
/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"
|
||
|
||
PayConflict:
|
||
description: |
|
||
订单状态冲突。`code` 可能为 `CURRENCY_MISMATCH`(换渠道结算币种与订单不符,
|
||
客户端应取消旧单重新下单)或 `ORDER_NOT_PENDING`(订单状态已变化,需刷新)。
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: "#/components/schemas/Error"
|
||
|
||
PayUpstream:
|
||
description: |
|
||
pay 网关暂不可用(`code = PAY_UPSTREAM`)——上游 pay-server 不可达、超时,
|
||
或返回了未分类的业务错误(`no_account` / `no_settle_currency` /
|
||
`create_failed` / `upstream_error` 等),客户端应提示稍后重试。
|
||
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_cap:
|
||
type: integer
|
||
nullable: true
|
||
description: 今日额度 = 套餐每日分钟 + 看广告累加分钟;付费无限制时为 null
|
||
example: 20
|
||
minutes_remaining:
|
||
type: integer
|
||
nullable: true
|
||
description: 今日剩余分钟数(全账户共享),付费无限制时为 null
|
||
example: 7
|
||
ad_bonus_minutes:
|
||
type: integer
|
||
description: 今日已通过看广告累加的分钟数
|
||
example: 10
|
||
ad_unlocked:
|
||
type: boolean
|
||
description: 免费版今日是否已通过看广告加过时(ad_bonus_minutes > 0,历史字段)
|
||
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: 节点 UUID(connect 时作为 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 443;Hysteria2 用 UDP 443)
|
||
example: 443
|
||
uuid:
|
||
type: string
|
||
format: uuid
|
||
description: |
|
||
用户数据面 UUID(dp_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: 日期(UTC,YYYY-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)
|
||
|
||
# ── Pay(pay v2 统一支付网关代理)──────────────────────
|
||
|
||
PayCatalogItem:
|
||
type: object
|
||
description: 可购档位(三档单源,见 `internal/pay/catalog.go`)
|
||
required: [sku, plan, days, price_minor, currency]
|
||
properties:
|
||
sku:
|
||
type: string
|
||
description: 档位代码,与 pay 侧 `products.biz_code` 一一对应
|
||
example: pro_month
|
||
plan:
|
||
type: string
|
||
enum: [free, pro, team]
|
||
description: 兑换后生效的套餐代码
|
||
example: pro
|
||
days:
|
||
type: integer
|
||
description: 时长天数(宽松口径:31/92/366,覆盖大月与最长季)
|
||
example: 31
|
||
price_minor:
|
||
type: integer
|
||
format: int64
|
||
description: |
|
||
展示价,CNY 分。**仅展示**,实际扣款以 pay 侧
|
||
`product_prices`(USDT 微单位)/ `products.price`(CNY 元)为准,
|
||
两处人工对齐(见交付说明「取舍」条目 4)。
|
||
example: 2999
|
||
currency:
|
||
type: string
|
||
example: CNY
|
||
|
||
PaySession:
|
||
type: object
|
||
description: |
|
||
pay 下单/retry 返回的渲染会话。`render_type` 多态,`payload` 结构随之变化,
|
||
原样透传给客户端:
|
||
- `crypto_address`:`{address, amount, amount_minor, currency, network}`(如 USDT/TRC20)
|
||
- `redirect`:`{url}`(如支付宝拉起链接)
|
||
- `qr`:`{data}`(当前仅复制兜底,未渲染二维码——见交付说明「取舍」条目 3)
|
||
required: [render_type, payload]
|
||
properties:
|
||
render_type:
|
||
type: string
|
||
enum: [crypto_address, redirect, qr]
|
||
payload:
|
||
type: object
|
||
description: 结构随 render_type 变化,原样透传,服务端不解析
|
||
additionalProperties: true
|
||
expires_at:
|
||
type: string
|
||
format: date-time
|
||
nullable: true
|
||
description: 会话/收款地址有效期(UTC ISO-8601),无限期时为 null
|
||
|
||
PayOrderSessionResult:
|
||
type: object
|
||
description: "`/pay/orders`(下单)与 `/pay/orders/{orderNo}/retry`(换渠道)的共同响应体"
|
||
required: [order_no, session]
|
||
properties:
|
||
order_no:
|
||
type: string
|
||
description: pay 侧订单号
|
||
example: "PAY202607100001"
|
||
session:
|
||
$ref: "#/components/schemas/PaySession"
|
||
|
||
PayOrderStatus:
|
||
type: object
|
||
required: [order_no, pay_status, activated]
|
||
properties:
|
||
order_no:
|
||
type: string
|
||
pay_status:
|
||
type: string
|
||
description: |
|
||
pay 侧状态词汇原样透传(`pending` / `succeeded` / `canceled` 等,具体取值
|
||
以 pay 实现为准)。**仅展示,不做分支判断**——本地成功判据是 `activated`。
|
||
example: succeeded
|
||
activated:
|
||
type: boolean
|
||
description: |
|
||
本地台账(`pay_purchases.status = 'paid'`)是否已消费权益开通。
|
||
客户端轮询以此字段为唯一成功判据。
|
||
example: true
|
||
expires_at:
|
||
type: string
|
||
format: date-time
|
||
nullable: true
|
||
description: 开通/延长后的订阅到期时间(UTC ISO-8601);未开通或查询失败时缺省
|
||
|
||
PayWebhookEvent:
|
||
type: object
|
||
description: |
|
||
pay `settle.go::enqueuePaymentSucceeded` 出站 payload(payment.succeeded 事件,
|
||
无 `refund_id` 字段)。
|
||
required: [event_type, out_trade_no, biz_system, biz_ref, product_biz_code,
|
||
amount_minor, currency, channel, paid_at]
|
||
properties:
|
||
event_type:
|
||
type: string
|
||
enum: [payment.succeeded]
|
||
description: 事件类型;白名单外的事件本接口直接确认(200 SUCCESS)不处理
|
||
out_trade_no:
|
||
type: string
|
||
description: pay 订单号,业务幂等唯一键
|
||
biz_system:
|
||
type: string
|
||
example: pangolin
|
||
biz_ref:
|
||
type: string
|
||
format: uuid
|
||
description: 下单时传入的业务方用户标识(`users.uuid`)
|
||
product_biz_code:
|
||
type: string
|
||
description: 对应 `/pay/catalog` 的 `sku`
|
||
example: pro_month
|
||
amount_minor:
|
||
type: integer
|
||
format: int64
|
||
description: 实付金额(最小单位)
|
||
currency:
|
||
type: string
|
||
example: USDT
|
||
channel:
|
||
type: string
|
||
description: 实际支付渠道
|
||
example: crypto
|
||
paid_at:
|
||
type: string
|
||
format: date-time
|
||
description: 支付完成时间(RFC3339);解析失败时服务端回落为处理时刻
|
||
|
||
tags:
|
||
- name: Auth
|
||
description: 认证相关(发验证码、注册、登录、刷新 Token)——无需 JWT
|
||
- name: Account
|
||
description: 账户与设备管理
|
||
- name: Commerce
|
||
description: 商业闭环(激活码兑换、广告解锁、套餐目录)
|
||
- name: Pay
|
||
description: |
|
||
pay v2 统一支付网关代理(下单/查单/换渠道/取消 + webhook 接收)。
|
||
`PAY_BASE_URL` 未配置时整组端点不挂载(服务端启动日志告警,无 404 以外的降级)。
|
||
- name: Nodes
|
||
description: 节点目录与连接凭证下发(数据面入口)
|
||
- name: Usage
|
||
description: 用量统计
|
||
- name: Notices
|
||
description: 公告
|