docs+ui: add bilingual payment integration doc and rename purchase entry to recharge/subscription

This commit is contained in:
erio
2026-03-01 01:53:14 +08:00
parent 2e88e23002
commit 997cd1e332
4 changed files with 116 additions and 168 deletions

View File

@@ -1,41 +1,40 @@
# Sub2API Admin API: Payment Integration / 支付集成接口文档 # ADMIN_PAYMENT_INTEGRATION_API
> 单文件中英双语文档 / Single-file bilingual documentation (Chinese + English)
---
## 中文 ## 中文
### 概述 ### 目标
本文档用于对接外部支付系统(如 `sub2apipay`)与 Sub2API 的 Admin API覆盖
本文档描述外部支付系统(例如 sub2apipay对接 Sub2API 时的最小 Admin API 集合,用于完成充值发放与对账。 - 支付成功后充值
- 用户查询
- 人工余额修正
- 前端购买页参数透传
### 基础地址 ### 基础地址
- 生产:`https://<your-domain>`
- Beta`http://<your-server-ip>:8084`
- 生产环境:`https://<your-domain>` ### 认证
- Beta 环境:`http://<your-server-ip>:8084` 推荐使用:
- `x-api-key: admin-<64hex>`
- `Content-Type: application/json`
- 幂等接口额外传:`Idempotency-Key`
### 认证方式 说明:管理员 JWT 也可访问 admin 路由,但服务间调用建议使用 Admin API Key。
以下接口均建议使用:
- 请求头:`x-api-key: admin-<64hex>`(服务间调用推荐)
- 请求头:`Content-Type: application/json`
说明:管理员 JWT 也可访问 admin 路由,但机器对机器调用建议使用 Admin API Key。
### 1) 一步完成:创建兑换码并兑换
### 1) 一步完成创建并兑换
`POST /api/v1/admin/redeem-codes/create-and-redeem` `POST /api/v1/admin/redeem-codes/create-and-redeem`
用途: 用途:原子完成“创建兑换码 + 兑换到指定用户”。
- 原子化完成“创建固定兑换码 + 兑换给指定用户”。
- 常用于支付回调成功后的自动充值。
必需请求头:
请求头:
- `x-api-key` - `x-api-key`
- `Idempotency-Key` - `Idempotency-Key`
请求体: 请求体示例
```json ```json
{ {
"code": "s2p_cm1234567890", "code": "s2p_cm1234567890",
@@ -46,21 +45,12 @@
} }
``` ```
规则:
- `code`:外部订单映射的确定性兑换码。
- `type`:当前推荐使用 `balance`
- `value`:必须大于 0。
- `user_id`:目标用户 ID。
幂等语义: 幂等语义:
-`code``used_by` 一致:`200`
-`code``used_by` 不一致:`409`
- 缺少 `Idempotency-Key``400``IDEMPOTENCY_KEY_REQUIRED`
- 同一 `code``used_by` 一致:返回 `200`(幂等回放)。 curl 示例:
- 同一 `code``used_by` 不一致:返回 `409`(冲突)。
- 缺少 `Idempotency-Key`:返回 `400``IDEMPOTENCY_KEY_REQUIRED`)。
示例:
```bash ```bash
curl -X POST "${BASE}/api/v1/admin/redeem-codes/create-and-redeem" \ curl -X POST "${BASE}/api/v1/admin/redeem-codes/create-and-redeem" \
-H "x-api-key: ${KEY}" \ -H "x-api-key: ${KEY}" \
@@ -75,32 +65,20 @@ curl -X POST "${BASE}/api/v1/admin/redeem-codes/create-and-redeem" \
}' }'
``` ```
### 2) 查询用户(可选前置检查 ### 2) 查询用户(可选前置校验
`GET /api/v1/admin/users/:id` `GET /api/v1/admin/users/:id`
用途:
- 支付成功后充值前,确认目标用户是否存在。
示例:
```bash ```bash
curl -s "${BASE}/api/v1/admin/users/123" \ curl -s "${BASE}/api/v1/admin/users/123" \
-H "x-api-key: ${KEY}" -H "x-api-key: ${KEY}"
``` ```
### 3) 余额调整(已存在接口) ### 3) 余额调整(已接口)
`POST /api/v1/admin/users/:id/balance` `POST /api/v1/admin/users/:id/balance`
用途: 用途:人工补偿 / 扣减,支持 `set` / `add` / `subtract`
- 复用现有管理员接口做人工纠偏。
- 支持 `set``add``subtract`
示例(扣减):
请求体示例(扣减):
```json ```json
{ {
"balance": 100.0, "balance": 100.0,
@@ -121,36 +99,25 @@ curl -X POST "${BASE}/api/v1/admin/users/123/balance" \
}' }'
``` ```
### 4) 购买页跳转 URL Query 参数iframe 新窗口一) ### 4) 购买页 URL Query 透传iframe / 新窗口一
当 Sub2API 打开 `purchase_subscription_url` 时,会统一追加:
Sub2API 前端在打开 `purchase_subscription_url` 时,会给 iframe 和“新窗口打开”统一追加 query 参数,确保外部支付页拿到一致上下文。 - `user_id`
- `token`
追加参数: - `theme``light` / `dark`
- `ui_mode`(固定 `embedded`
- `user_id`:当前登录用户 ID
- `token`:当前登录 JWT token
- `theme`:当前主题(`light` / `dark`
- `ui_mode`:当前 UI 模式(固定 `embedded`
示例: 示例:
```text ```text
https://pay.example.com/pay?user_id=123&token=<jwt>&theme=light&ui_mode=embedded https://pay.example.com/pay?user_id=123&token=<jwt>&theme=light&ui_mode=embedded
``` ```
### 5) 失败处理建议 ### 5) 失败处理建议
- 支付成功与充值成功分状态落库
- 支付状态与充值状态分开落库。 - 回调验签成功后立即标记“支付成功”
- 收到并验证支付回调后,立即标记“支付成功”。 - 支付成功但充值失败的订单允许后续重试
- 支付成功但充值失败的订单应允许后续重试。 - 重试保持相同 `code`,并使用新的 `Idempotency-Key`
- 重试时继续使用同一 `code`,并使用新的 `Idempotency-Key`
### 6) `doc_url` 配置建议 ### 6) `doc_url` 配置建议
Sub2API 已支持系统设置中的 `doc_url` 字段。
推荐配置:
- 查看链接:`https://github.com/Wei-Shaw/sub2api/blob/main/ADMIN_PAYMENT_INTEGRATION_API.md` - 查看链接:`https://github.com/Wei-Shaw/sub2api/blob/main/ADMIN_PAYMENT_INTEGRATION_API.md`
- 下载链接:`https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/ADMIN_PAYMENT_INTEGRATION_API.md` - 下载链接:`https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/ADMIN_PAYMENT_INTEGRATION_API.md`
@@ -158,40 +125,35 @@ Sub2API 已支持系统设置中的 `doc_url` 字段。
## English ## English
### Overview ### Purpose
This document describes the minimal Sub2API Admin API surface for external payment integrations (for example, `sub2apipay`), including:
This document defines the minimum Admin API surface for integrating external payment systems (for example, sub2apipay) with Sub2API for recharge fulfillment and reconciliation. - Recharge after payment success
- User lookup
- Manual balance correction
- Purchase page query parameter forwarding
### Base URL ### Base URL
- Production: `https://<your-domain>` - Production: `https://<your-domain>`
- Beta: `http://<your-server-ip>:8084` - Beta: `http://<your-server-ip>:8084`
### Authentication ### Authentication
Recommended headers: Recommended headers:
- `x-api-key: admin-<64hex>`
- `x-api-key: admin-<64hex>` (recommended for server-to-server calls)
- `Content-Type: application/json` - `Content-Type: application/json`
- `Idempotency-Key` for idempotent endpoints
Note: Admin JWT is also accepted by admin routes, but Admin API key is recommended for machine integrations. Note: Admin JWT can also access admin routes, but Admin API Key is recommended for server-to-server integration.
### 1) One-step Create + Redeem
### 1) Create and Redeem in one step
`POST /api/v1/admin/redeem-codes/create-and-redeem` `POST /api/v1/admin/redeem-codes/create-and-redeem`
Purpose: Use case: atomically create a redeem code and redeem it to a target user.
- Atomically create a deterministic redeem code and redeem it to the target user.
- Typical usage: called right after payment callback success.
Required headers:
Headers:
- `x-api-key` - `x-api-key`
- `Idempotency-Key` - `Idempotency-Key`
Request body: Request body:
```json ```json
{ {
"code": "s2p_cm1234567890", "code": "s2p_cm1234567890",
@@ -202,21 +164,12 @@ Request body:
} }
``` ```
Rules:
- `code`: deterministic code mapped from external order id.
- `type`: `balance` is the recommended type.
- `value`: must be greater than 0.
- `user_id`: target user id.
Idempotency behavior: Idempotency behavior:
- Same `code` and same `used_by`: `200`
- Same `code` but different `used_by`: `409`
- Missing `Idempotency-Key`: `400` (`IDEMPOTENCY_KEY_REQUIRED`)
- Same `code` and same `used_by`: `200` (idempotent replay). curl example:
- Same `code` and different `used_by`: `409` (conflict).
- Missing `Idempotency-Key`: `400` (`IDEMPOTENCY_KEY_REQUIRED`).
Example:
```bash ```bash
curl -X POST "${BASE}/api/v1/admin/redeem-codes/create-and-redeem" \ curl -X POST "${BASE}/api/v1/admin/redeem-codes/create-and-redeem" \
-H "x-api-key: ${KEY}" \ -H "x-api-key: ${KEY}" \
@@ -231,32 +184,20 @@ curl -X POST "${BASE}/api/v1/admin/redeem-codes/create-and-redeem" \
}' }'
``` ```
### 2) Query User (Optional Pre-check) ### 2) Query User (optional pre-check)
`GET /api/v1/admin/users/:id` `GET /api/v1/admin/users/:id`
Purpose:
- Verify target user existence before final recharge/retry.
Example:
```bash ```bash
curl -s "${BASE}/api/v1/admin/users/123" \ curl -s "${BASE}/api/v1/admin/users/123" \
-H "x-api-key: ${KEY}" -H "x-api-key: ${KEY}"
``` ```
### 3) Balance Adjustment (Existing API) ### 3) Balance Adjustment (existing API)
`POST /api/v1/admin/users/:id/balance` `POST /api/v1/admin/users/:id/balance`
Purpose: Use case: manual correction with `set` / `add` / `subtract`.
- Reuse existing admin endpoint for manual reconciliation.
- Supports `set`, `add`, `subtract`.
Request body example (`subtract`): Request body example (`subtract`):
```json ```json
{ {
"balance": 100.0, "balance": 100.0,
@@ -265,8 +206,6 @@ Request body example (`subtract`):
} }
``` ```
Example:
```bash ```bash
curl -X POST "${BASE}/api/v1/admin/users/123/balance" \ curl -X POST "${BASE}/api/v1/admin/users/123/balance" \
-H "x-api-key: ${KEY}" \ -H "x-api-key: ${KEY}" \
@@ -279,35 +218,24 @@ curl -X POST "${BASE}/api/v1/admin/users/123/balance" \
}' }'
``` ```
### 4) Purchase URL Query Parameters (Iframe + New Tab) ### 4) Purchase URL query forwarding (iframe and new tab)
When Sub2API opens `purchase_subscription_url`, it appends:
When Sub2API frontend opens `purchase_subscription_url`, it appends the same query parameters for both iframe and “open in new tab” to keep context consistent. - `user_id`
- `token`
Appended parameters: - `theme` (`light` / `dark`)
- `ui_mode` (fixed: `embedded`)
- `user_id`: current logged-in user id
- `token`: current logged-in JWT token
- `theme`: current theme (`light` / `dark`)
- `ui_mode`: UI mode (fixed `embedded`)
Example: Example:
```text ```text
https://pay.example.com/pay?user_id=123&token=<jwt>&theme=light&ui_mode=embedded https://pay.example.com/pay?user_id=123&token=<jwt>&theme=light&ui_mode=embedded
``` ```
### 5) Failure Handling Recommendations ### 5) Failure handling recommendations
- Persist payment success and recharge success as separate states
- Store payment state and recharge state separately. - Mark payment as successful immediately after verified callback
- Mark payment success immediately after callback verification. - Allow retry for orders with payment success but recharge failure
- Keep orders retryable when payment succeeded but recharge failed. - Keep the same `code` for retry, and use a new `Idempotency-Key`
- Reuse the same deterministic `code` and a new `Idempotency-Key` when retrying.
### 6) Suggested `doc_url` Value
Sub2API already supports `doc_url` in system settings.
Recommended values:
### 6) Recommended `doc_url`
- View URL: `https://github.com/Wei-Shaw/sub2api/blob/main/ADMIN_PAYMENT_INTEGRATION_API.md` - View URL: `https://github.com/Wei-Shaw/sub2api/blob/main/ADMIN_PAYMENT_INTEGRATION_API.md`
- Download URL: `https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/ADMIN_PAYMENT_INTEGRATION_API.md` - Download URL: `https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/ADMIN_PAYMENT_INTEGRATION_API.md`

View File

@@ -290,6 +290,26 @@ const CreditCardIcon = {
) )
} }
const RechargeSubscriptionIcon = {
render: () =>
h(
'svg',
{ fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', 'stroke-width': '1.5' },
[
h('path', {
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
d: 'M2.25 7.5A2.25 2.25 0 014.5 5.25h15A2.25 2.25 0 0121.75 7.5v9A2.25 2.25 0 0119.5 18.75h-15A2.25 2.25 0 012.25 16.5v-9z'
}),
h('path', {
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
d: 'M6.75 12h3m4.5 0h3m-3-3v6'
})
]
)
}
const GlobeIcon = { const GlobeIcon = {
render: () => render: () =>
h( h(
@@ -490,7 +510,7 @@ const userNavItems = computed(() => {
{ {
path: '/purchase', path: '/purchase',
label: t('nav.buySubscription'), label: t('nav.buySubscription'),
icon: CreditCardIcon, icon: RechargeSubscriptionIcon,
hideInSimpleMode: true hideInSimpleMode: true
} }
] ]
@@ -515,7 +535,7 @@ const personalNavItems = computed(() => {
{ {
path: '/purchase', path: '/purchase',
label: t('nav.buySubscription'), label: t('nav.buySubscription'),
icon: CreditCardIcon, icon: RechargeSubscriptionIcon,
hideInSimpleMode: true hideInSimpleMode: true
} }
] ]

View File

@@ -280,7 +280,7 @@ export default {
logout: 'Logout', logout: 'Logout',
github: 'GitHub', github: 'GitHub',
mySubscriptions: 'My Subscriptions', mySubscriptions: 'My Subscriptions',
buySubscription: 'Purchase Subscription', buySubscription: 'Recharge / Subscription',
docs: 'Docs', docs: 'Docs',
sora: 'Sora Studio' sora: 'Sora Studio'
}, },
@@ -3582,11 +3582,11 @@ export default {
hideCcsImportButtonHint: 'When enabled, the "Import to CCS" button will be hidden on the API Keys page' hideCcsImportButtonHint: 'When enabled, the "Import to CCS" button will be hidden on the API Keys page'
}, },
purchase: { purchase: {
title: 'Purchase Page', title: 'Recharge / Subscription Page',
description: 'Show a "Purchase Subscription" entry in the sidebar and open the configured URL in an iframe', description: 'Show a "Recharge / Subscription" entry in the sidebar and open the configured URL in an iframe',
enabled: 'Show Purchase Entry', enabled: 'Show Recharge / Subscription Entry',
enabledHint: 'Only shown in standard mode (not simple mode)', enabledHint: 'Only shown in standard mode (not simple mode)',
url: 'Purchase URL', url: 'Recharge / Subscription URL',
urlPlaceholder: 'https://example.com/purchase', urlPlaceholder: 'https://example.com/purchase',
urlHint: 'Must be an absolute http(s) URL', urlHint: 'Must be an absolute http(s) URL',
iframeWarning: iframeWarning:
@@ -3874,16 +3874,16 @@ export default {
retry: 'Retry' retry: 'Retry'
}, },
// Purchase Subscription Page // Recharge / Subscription Page
purchase: { purchase: {
title: 'Purchase Subscription', title: 'Recharge / Subscription',
description: 'Purchase a subscription via the embedded page', description: 'Recharge balance or purchase subscription via the embedded page',
openInNewTab: 'Open in new tab', openInNewTab: 'Open in new tab',
notEnabledTitle: 'Feature not enabled', notEnabledTitle: 'Feature not enabled',
notEnabledDesc: 'The administrator has not enabled the purchase page. Please contact admin.', notEnabledDesc: 'The administrator has not enabled the recharge/subscription entry. Please contact admin.',
notConfiguredTitle: 'Purchase URL not configured', notConfiguredTitle: 'Recharge / Subscription URL not configured',
notConfiguredDesc: notConfiguredDesc:
'The administrator enabled the entry but has not configured a purchase URL. Please contact admin.' 'The administrator enabled the entry but has not configured a recharge/subscription URL. Please contact admin.'
}, },
// Announcements Page // Announcements Page

View File

@@ -280,7 +280,7 @@ export default {
logout: '退出登录', logout: '退出登录',
github: 'GitHub', github: 'GitHub',
mySubscriptions: '我的订阅', mySubscriptions: '我的订阅',
buySubscription: '购买订阅', buySubscription: '充值/订阅',
docs: '文档', docs: '文档',
sora: 'Sora 创作' sora: 'Sora 创作'
}, },
@@ -3754,11 +3754,11 @@ export default {
hideCcsImportButtonHint: '启用后将在 API Keys 页面隐藏"导入 CCS"按钮' hideCcsImportButtonHint: '启用后将在 API Keys 页面隐藏"导入 CCS"按钮'
}, },
purchase: { purchase: {
title: '购买订阅页面', title: '充值/订阅页面',
description: '在侧边栏展示”购买订阅”入口,并在页面内通过 iframe 打开指定链接', description: '在侧边栏展示“充值/订阅”入口,并在页面内通过 iframe 打开指定链接',
enabled: '显示购买订阅入口', enabled: '显示充值/订阅入口',
enabledHint: '仅在标准模式(非简单模式)下展示', enabledHint: '仅在标准模式(非简单模式)下展示',
url: '购买页面 URL', url: '充值/订阅页面 URL',
urlPlaceholder: 'https://example.com/purchase', urlPlaceholder: 'https://example.com/purchase',
urlHint: '必须是完整的 http(s) 链接', urlHint: '必须是完整的 http(s) 链接',
iframeWarning: iframeWarning:
@@ -4045,15 +4045,15 @@ export default {
retry: '重试' retry: '重试'
}, },
// Purchase Subscription Page // Recharge / Subscription Page
purchase: { purchase: {
title: '购买订阅', title: '充值/订阅',
description: '通过内嵌页面完成订阅购买', description: '通过内嵌页面完成充值/订阅',
openInNewTab: '新窗口打开', openInNewTab: '新窗口打开',
notEnabledTitle: '该功能未开启', notEnabledTitle: '该功能未开启',
notEnabledDesc: '管理员暂未开启购买订阅入口,请联系管理员。', notEnabledDesc: '管理员暂未开启充值/订阅入口,请联系管理员。',
notConfiguredTitle: '购买链接未配置', notConfiguredTitle: '充值/订阅链接未配置',
notConfiguredDesc: '管理员已开启入口,但尚未配置购买订阅链接,请联系管理员。' notConfiguredDesc: '管理员已开启入口,但尚未配置充值/订阅链接,请联系管理员。'
}, },
// Announcements Page // Announcements Page