From 39ca192c41ea8e040aee99b015b7761bec6964d1 Mon Sep 17 00:00:00 2001 From: erio Date: Sun, 1 Mar 2026 00:41:38 +0800 Subject: [PATCH 1/5] feat(admin): add create-and-redeem API and payment integration docs --- ADMIN_PAYMENT_INTEGRATION_API.md | 134 ++++++++++++++++++ README.md | 1 + backend/cmd/server/wire_gen.go | 2 +- .../admin/admin_basic_handlers_test.go | 2 +- .../internal/handler/admin/redeem_handler.go | 94 +++++++++++- backend/internal/server/routes/admin.go | 1 + backend/internal/service/redeem_service.go | 27 ++++ 7 files changed, 256 insertions(+), 5 deletions(-) create mode 100644 ADMIN_PAYMENT_INTEGRATION_API.md diff --git a/ADMIN_PAYMENT_INTEGRATION_API.md b/ADMIN_PAYMENT_INTEGRATION_API.md new file mode 100644 index 00000000..fa2ff2bc --- /dev/null +++ b/ADMIN_PAYMENT_INTEGRATION_API.md @@ -0,0 +1,134 @@ +# Sub2API Admin API: Payment Integration + +This document describes the minimum Admin API surface for external payment systems (for example, sub2apipay) to complete balance top-up and reconciliation. + +## Base URL + +- Production: `https://` +- Beta: `http://:8084` + +All endpoints below use: + +- Header: `x-api-key: admin-<64hex>` (recommended for server-to-server) +- Header: `Content-Type: application/json` + +Note: Admin JWT is also accepted by admin routes, but machine-to-machine integration should use admin API key. + +## 1) Create + Redeem in One Step + +`POST /api/v1/admin/redeem-codes/create-and-redeem` + +Purpose: + +- Atomically create a deterministic redeem code and redeem it to a target user. +- Typical usage: called after payment callback succeeds. + +Required headers: + +- `x-api-key` +- `Idempotency-Key` + +Request body: + +```json +{ + "code": "s2p_cm1234567890", + "type": "balance", + "value": 100.0, + "user_id": 123, + "notes": "sub2apipay order: cm1234567890" +} +``` + +Rules: + +- `code`: external deterministic order-mapped code. +- `type`: currently recommended `balance`. +- `value`: must be `> 0`. +- `user_id`: target user id. + +Idempotency semantics: + +- Same `code`, same `used_by` user: return `200` (idempotent replay). +- Same `code`, different `used_by` user: return `409` conflict. +- Missing `Idempotency-Key`: return `400` (`IDEMPOTENCY_KEY_REQUIRED`). + +Example: + +```bash +curl -X POST "${BASE}/api/v1/admin/redeem-codes/create-and-redeem" \ + -H "x-api-key: ${KEY}" \ + -H "Idempotency-Key: pay-cm1234567890-success" \ + -H "Content-Type: application/json" \ + -d '{ + "code":"s2p_cm1234567890", + "type":"balance", + "value":100.00, + "user_id":123, + "notes":"sub2apipay order: cm1234567890" + }' +``` + +## 2) Query User (Optional Pre-check) + +`GET /api/v1/admin/users/:id` + +Purpose: + +- Check whether target user exists before payment finalize/retry. + +Example: + +```bash +curl -s "${BASE}/api/v1/admin/users/123" \ + -H "x-api-key: ${KEY}" +``` + +## 3) Balance Adjustment (Existing Interface) + +`POST /api/v1/admin/users/:id/balance` + +Purpose: + +- Existing reusable admin interface for manual correction. +- Supports `set`, `add`, `subtract`. + +Request body example (`subtract`): + +```json +{ + "balance": 100.0, + "operation": "subtract", + "notes": "manual correction" +} +``` + +Example: + +```bash +curl -X POST "${BASE}/api/v1/admin/users/123/balance" \ + -H "x-api-key: ${KEY}" \ + -H "Idempotency-Key: balance-subtract-cm1234567890" \ + -H "Content-Type: application/json" \ + -d '{ + "balance":100.00, + "operation":"subtract", + "notes":"manual correction" + }' +``` + +## 4) Error Handling Recommendations + +- Persist upstream payment result independently from recharge result. +- Mark payment success immediately after callback verification. +- If recharge fails after payment success, keep order retryable by admin operation. +- For retry, always reuse deterministic `code` + new `Idempotency-Key`. + +## 5) Suggested `doc_url` Setting + +Sub2API already supports `doc_url` in system settings. + +Recommended values: + +- View URL: `https://github.com/Wei-Shaw/sub2api/blob/main/ADMIN_PAYMENT_INTEGRATION_API.md` +- Direct download URL: `https://raw.githubusercontent.com/Wei-Shaw/sub2api/main/ADMIN_PAYMENT_INTEGRATION_API.md` diff --git a/README.md b/README.md index a5f680bf..6381b301 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ Sub2API is an AI API gateway platform designed to distribute and manage API quot ## Documentation - Dependency Security: `docs/dependency-security.md` +- Admin Payment Integration API: `ADMIN_PAYMENT_INTEGRATION_API.md` --- diff --git a/backend/cmd/server/wire_gen.go b/backend/cmd/server/wire_gen.go index 37ad5d9f..b8337733 100644 --- a/backend/cmd/server/wire_gen.go +++ b/backend/cmd/server/wire_gen.go @@ -148,7 +148,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) { geminiOAuthHandler := admin.NewGeminiOAuthHandler(geminiOAuthService) antigravityOAuthHandler := admin.NewAntigravityOAuthHandler(antigravityOAuthService) proxyHandler := admin.NewProxyHandler(adminService) - adminRedeemHandler := admin.NewRedeemHandler(adminService) + adminRedeemHandler := admin.NewRedeemHandler(adminService, redeemService) promoHandler := admin.NewPromoHandler(promoService) opsRepository := repository.NewOpsRepository(db) pricingRemoteClient := repository.ProvidePricingRemoteClient(configConfig) diff --git a/backend/internal/handler/admin/admin_basic_handlers_test.go b/backend/internal/handler/admin/admin_basic_handlers_test.go index aeb4097f..4de10d3e 100644 --- a/backend/internal/handler/admin/admin_basic_handlers_test.go +++ b/backend/internal/handler/admin/admin_basic_handlers_test.go @@ -19,7 +19,7 @@ func setupAdminRouter() (*gin.Engine, *stubAdminService) { userHandler := NewUserHandler(adminSvc, nil) groupHandler := NewGroupHandler(adminSvc) proxyHandler := NewProxyHandler(adminSvc) - redeemHandler := NewRedeemHandler(adminSvc) + redeemHandler := NewRedeemHandler(adminSvc, nil) router.GET("/api/v1/admin/users", userHandler.List) router.GET("/api/v1/admin/users/:id", userHandler.GetByID) diff --git a/backend/internal/handler/admin/redeem_handler.go b/backend/internal/handler/admin/redeem_handler.go index 7073061d..0a932ee9 100644 --- a/backend/internal/handler/admin/redeem_handler.go +++ b/backend/internal/handler/admin/redeem_handler.go @@ -4,11 +4,13 @@ import ( "bytes" "context" "encoding/csv" + "errors" "fmt" "strconv" "strings" "github.com/Wei-Shaw/sub2api/internal/handler/dto" + infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors" "github.com/Wei-Shaw/sub2api/internal/pkg/response" "github.com/Wei-Shaw/sub2api/internal/service" @@ -17,13 +19,15 @@ import ( // RedeemHandler handles admin redeem code management type RedeemHandler struct { - adminService service.AdminService + adminService service.AdminService + redeemService *service.RedeemService } // NewRedeemHandler creates a new admin redeem handler -func NewRedeemHandler(adminService service.AdminService) *RedeemHandler { +func NewRedeemHandler(adminService service.AdminService, redeemService *service.RedeemService) *RedeemHandler { return &RedeemHandler{ - adminService: adminService, + adminService: adminService, + redeemService: redeemService, } } @@ -36,6 +40,15 @@ type GenerateRedeemCodesRequest struct { ValidityDays int `json:"validity_days" binding:"omitempty,max=36500"` // 订阅类型使用,默认30天,最大100年 } +// CreateAndRedeemCodeRequest represents creating a fixed code and redeeming it for a target user. +type CreateAndRedeemCodeRequest struct { + Code string `json:"code" binding:"required,min=3,max=128"` + Type string `json:"type" binding:"required,oneof=balance concurrency subscription invitation"` + Value float64 `json:"value" binding:"required,gt=0"` + UserID int64 `json:"user_id" binding:"required,gt=0"` + Notes string `json:"notes"` +} + // List handles listing all redeem codes with pagination // GET /api/v1/admin/redeem-codes func (h *RedeemHandler) List(c *gin.Context) { @@ -109,6 +122,81 @@ func (h *RedeemHandler) Generate(c *gin.Context) { }) } +// CreateAndRedeem creates a fixed redeem code and redeems it for a target user in one step. +// POST /api/v1/admin/redeem-codes/create-and-redeem +func (h *RedeemHandler) CreateAndRedeem(c *gin.Context) { + if h.redeemService == nil { + response.InternalError(c, "redeem service not configured") + return + } + + var req CreateAndRedeemCodeRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "Invalid request: "+err.Error()) + return + } + req.Code = strings.TrimSpace(req.Code) + + executeAdminIdempotentJSON(c, "admin.redeem_codes.create_and_redeem", req, service.DefaultWriteIdempotencyTTL(), func(ctx context.Context) (any, error) { + existing, err := h.redeemService.GetByCode(ctx, req.Code) + if err == nil { + return h.resolveCreateAndRedeemExisting(ctx, existing, req.UserID) + } + if !errors.Is(err, service.ErrRedeemCodeNotFound) { + return nil, err + } + + createErr := h.redeemService.CreateCode(ctx, &service.RedeemCode{ + Code: req.Code, + Type: req.Type, + Value: req.Value, + Status: service.StatusUnused, + Notes: req.Notes, + }) + if createErr != nil { + // Unique code race: if code now exists, use idempotent semantics by used_by. + existingAfterCreateErr, getErr := h.redeemService.GetByCode(ctx, req.Code) + if getErr == nil { + return h.resolveCreateAndRedeemExisting(ctx, existingAfterCreateErr, req.UserID) + } + return nil, createErr + } + + redeemed, redeemErr := h.redeemService.Redeem(ctx, req.UserID, req.Code) + if redeemErr != nil { + return nil, redeemErr + } + return gin.H{"redeem_code": dto.RedeemCodeFromServiceAdmin(redeemed)}, nil + }) +} + +func (h *RedeemHandler) resolveCreateAndRedeemExisting(ctx context.Context, existing *service.RedeemCode, userID int64) (any, error) { + if existing == nil { + return nil, infraerrors.Conflict("REDEEM_CODE_CONFLICT", "redeem code conflict") + } + + // If previous run created the code but crashed before redeem, redeem it now. + if existing.CanUse() { + redeemed, err := h.redeemService.Redeem(ctx, userID, existing.Code) + if err == nil { + return gin.H{"redeem_code": dto.RedeemCodeFromServiceAdmin(redeemed)}, nil + } + if !errors.Is(err, service.ErrRedeemCodeUsed) { + return nil, err + } + latest, getErr := h.redeemService.GetByCode(ctx, existing.Code) + if getErr == nil { + existing = latest + } + } + + if existing.UsedBy != nil && *existing.UsedBy == userID { + return gin.H{"redeem_code": dto.RedeemCodeFromServiceAdmin(existing)}, nil + } + + return nil, infraerrors.Conflict("REDEEM_CODE_CONFLICT", "redeem code already used by another user") +} + // Delete handles deleting a redeem code // DELETE /api/v1/admin/redeem-codes/:id func (h *RedeemHandler) Delete(c *gin.Context) { diff --git a/backend/internal/server/routes/admin.go b/backend/internal/server/routes/admin.go index 4d0a33c2..ade4d462 100644 --- a/backend/internal/server/routes/admin.go +++ b/backend/internal/server/routes/admin.go @@ -351,6 +351,7 @@ func registerRedeemCodeRoutes(admin *gin.RouterGroup, h *handler.Handlers) { codes.GET("/stats", h.Admin.Redeem.GetStats) codes.GET("/export", h.Admin.Redeem.Export) codes.GET("/:id", h.Admin.Redeem.GetByID) + codes.POST("/create-and-redeem", h.Admin.Redeem.CreateAndRedeem) codes.POST("/generate", h.Admin.Redeem.Generate) codes.DELETE("/:id", h.Admin.Redeem.Delete) codes.POST("/batch-delete", h.Admin.Redeem.BatchDelete) diff --git a/backend/internal/service/redeem_service.go b/backend/internal/service/redeem_service.go index ad277ca0..b22da752 100644 --- a/backend/internal/service/redeem_service.go +++ b/backend/internal/service/redeem_service.go @@ -174,6 +174,33 @@ func (s *RedeemService) GenerateCodes(ctx context.Context, req GenerateCodesRequ return codes, nil } +// CreateCode creates a redeem code with caller-provided code value. +// It is primarily used by admin integrations that require an external order ID +// to be mapped to a deterministic redeem code. +func (s *RedeemService) CreateCode(ctx context.Context, code *RedeemCode) error { + if code == nil { + return errors.New("redeem code is required") + } + code.Code = strings.TrimSpace(code.Code) + if code.Code == "" { + return errors.New("code is required") + } + if code.Type == "" { + code.Type = RedeemTypeBalance + } + if code.Type != RedeemTypeInvitation && code.Value <= 0 { + return errors.New("value must be greater than 0") + } + if code.Status == "" { + code.Status = StatusUnused + } + + if err := s.redeemRepo.Create(ctx, code); err != nil { + return fmt.Errorf("create redeem code: %w", err) + } + return nil +} + // checkRedeemRateLimit 检查用户兑换错误次数是否超限 func (s *RedeemService) checkRedeemRateLimit(ctx context.Context, userID int64) error { if s.cache == nil { From 2e88e23002a58e176936923fe87cd9ee5af17a99 Mon Sep 17 00:00:00 2001 From: erio Date: Sun, 1 Mar 2026 00:57:26 +0800 Subject: [PATCH 2/5] feat(frontend): append purchase query params and make integration doc bilingual --- ADMIN_PAYMENT_INTEGRATION_API.md | 233 ++++++++++++++++-- .../views/user/PurchaseSubscriptionView.vue | 141 ++++++++--- 2 files changed, 317 insertions(+), 57 deletions(-) diff --git a/ADMIN_PAYMENT_INTEGRATION_API.md b/ADMIN_PAYMENT_INTEGRATION_API.md index fa2ff2bc..db25c8d8 100644 --- a/ADMIN_PAYMENT_INTEGRATION_API.md +++ b/ADMIN_PAYMENT_INTEGRATION_API.md @@ -1,27 +1,189 @@ -# Sub2API Admin API: Payment Integration +# Sub2API Admin API: Payment Integration / 支付集成接口文档 -This document describes the minimum Admin API surface for external payment systems (for example, sub2apipay) to complete balance top-up and reconciliation. +## 中文 -## Base URL +### 概述 + +本文档描述外部支付系统(例如 sub2apipay)对接 Sub2API 时的最小 Admin API 集合,用于完成充值发放与对账。 + +### 基础地址 + +- 生产环境:`https://` +- Beta 环境:`http://:8084` + +### 认证方式 + +以下接口均建议使用: + +- 请求头:`x-api-key: admin-<64hex>`(服务间调用推荐) +- 请求头:`Content-Type: application/json` + +说明:管理员 JWT 也可访问 admin 路由,但机器对机器调用建议使用 Admin API Key。 + +### 1) 一步完成:创建兑换码并兑换 + +`POST /api/v1/admin/redeem-codes/create-and-redeem` + +用途: + +- 原子化完成“创建固定兑换码 + 兑换给指定用户”。 +- 常用于支付回调成功后的自动充值。 + +必需请求头: + +- `x-api-key` +- `Idempotency-Key` + +请求体: + +```json +{ + "code": "s2p_cm1234567890", + "type": "balance", + "value": 100.0, + "user_id": 123, + "notes": "sub2apipay order: cm1234567890" +} +``` + +规则: + +- `code`:外部订单映射的确定性兑换码。 +- `type`:当前推荐使用 `balance`。 +- `value`:必须大于 0。 +- `user_id`:目标用户 ID。 + +幂等语义: + +- 同一 `code` 且 `used_by` 一致:返回 `200`(幂等回放)。 +- 同一 `code` 但 `used_by` 不一致:返回 `409`(冲突)。 +- 缺少 `Idempotency-Key`:返回 `400`(`IDEMPOTENCY_KEY_REQUIRED`)。 + +示例: + +```bash +curl -X POST "${BASE}/api/v1/admin/redeem-codes/create-and-redeem" \ + -H "x-api-key: ${KEY}" \ + -H "Idempotency-Key: pay-cm1234567890-success" \ + -H "Content-Type: application/json" \ + -d '{ + "code":"s2p_cm1234567890", + "type":"balance", + "value":100.00, + "user_id":123, + "notes":"sub2apipay order: cm1234567890" + }' +``` + +### 2) 查询用户(可选前置检查) + +`GET /api/v1/admin/users/:id` + +用途: + +- 支付成功后充值前,确认目标用户是否存在。 + +示例: + +```bash +curl -s "${BASE}/api/v1/admin/users/123" \ + -H "x-api-key: ${KEY}" +``` + +### 3) 余额调整(已存在接口) + +`POST /api/v1/admin/users/:id/balance` + +用途: + +- 复用现有管理员接口做人工纠偏。 +- 支持 `set`、`add`、`subtract`。 + +示例(扣减): + +```json +{ + "balance": 100.0, + "operation": "subtract", + "notes": "manual correction" +} +``` + +```bash +curl -X POST "${BASE}/api/v1/admin/users/123/balance" \ + -H "x-api-key: ${KEY}" \ + -H "Idempotency-Key: balance-subtract-cm1234567890" \ + -H "Content-Type: application/json" \ + -d '{ + "balance":100.00, + "operation":"subtract", + "notes":"manual correction" + }' +``` + +### 4) 购买页跳转 URL Query 参数(iframe 与新窗口统一) + +Sub2API 前端在打开 `purchase_subscription_url` 时,会给 iframe 和“新窗口打开”统一追加 query 参数,确保外部支付页拿到一致上下文。 + +追加参数: + +- `user_id`:当前登录用户 ID +- `token`:当前登录 JWT token +- `theme`:当前主题(`light` / `dark`) +- `ui_mode`:当前 UI 模式(固定 `embedded`) + +示例: + +```text +https://pay.example.com/pay?user_id=123&token=&theme=light&ui_mode=embedded +``` + +### 5) 失败处理建议 + +- 支付状态与充值状态分开落库。 +- 收到并验证支付回调后,立即标记“支付成功”。 +- 支付成功但充值失败的订单应允许后续重试。 +- 重试时继续使用同一 `code`,并使用新的 `Idempotency-Key`。 + +### 6) `doc_url` 配置建议 + +Sub2API 已支持系统设置中的 `doc_url` 字段。 + +推荐配置: + +- 查看链接:`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` + +--- + +## English + +### Overview + +This document defines the minimum Admin API surface for integrating external payment systems (for example, sub2apipay) with Sub2API for recharge fulfillment and reconciliation. + +### Base URL - Production: `https://` - Beta: `http://:8084` -All endpoints below use: +### Authentication -- Header: `x-api-key: admin-<64hex>` (recommended for server-to-server) -- Header: `Content-Type: application/json` +Recommended headers: -Note: Admin JWT is also accepted by admin routes, but machine-to-machine integration should use admin API key. +- `x-api-key: admin-<64hex>` (recommended for server-to-server calls) +- `Content-Type: application/json` -## 1) Create + Redeem in One Step +Note: Admin JWT is also accepted by admin routes, but Admin API key is recommended for machine integrations. + +### 1) One-step Create + Redeem `POST /api/v1/admin/redeem-codes/create-and-redeem` Purpose: -- Atomically create a deterministic redeem code and redeem it to a target user. -- Typical usage: called after payment callback succeeds. +- Atomically create a deterministic redeem code and redeem it to the target user. +- Typical usage: called right after payment callback success. Required headers: @@ -42,16 +204,16 @@ Request body: Rules: -- `code`: external deterministic order-mapped code. -- `type`: currently recommended `balance`. -- `value`: must be `> 0`. +- `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 semantics: +Idempotency behavior: -- Same `code`, same `used_by` user: return `200` (idempotent replay). -- Same `code`, different `used_by` user: return `409` conflict. -- Missing `Idempotency-Key`: return `400` (`IDEMPOTENCY_KEY_REQUIRED`). +- Same `code` and same `used_by`: `200` (idempotent replay). +- Same `code` and different `used_by`: `409` (conflict). +- Missing `Idempotency-Key`: `400` (`IDEMPOTENCY_KEY_REQUIRED`). Example: @@ -69,13 +231,13 @@ 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` Purpose: -- Check whether target user exists before payment finalize/retry. +- Verify target user existence before final recharge/retry. Example: @@ -84,13 +246,13 @@ curl -s "${BASE}/api/v1/admin/users/123" \ -H "x-api-key: ${KEY}" ``` -## 3) Balance Adjustment (Existing Interface) +### 3) Balance Adjustment (Existing API) `POST /api/v1/admin/users/:id/balance` Purpose: -- Existing reusable admin interface for manual correction. +- Reuse existing admin endpoint for manual reconciliation. - Supports `set`, `add`, `subtract`. Request body example (`subtract`): @@ -117,18 +279,35 @@ curl -X POST "${BASE}/api/v1/admin/users/123/balance" \ }' ``` -## 4) Error Handling Recommendations +### 4) Purchase URL Query Parameters (Iframe + New Tab) -- Persist upstream payment result independently from recharge result. +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. + +Appended parameters: + +- `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: + +```text +https://pay.example.com/pay?user_id=123&token=&theme=light&ui_mode=embedded +``` + +### 5) Failure Handling Recommendations + +- Store payment state and recharge state separately. - Mark payment success immediately after callback verification. -- If recharge fails after payment success, keep order retryable by admin operation. -- For retry, always reuse deterministic `code` + new `Idempotency-Key`. +- Keep orders retryable when payment succeeded but recharge failed. +- Reuse the same deterministic `code` and a new `Idempotency-Key` when retrying. -## 5) Suggested `doc_url` Setting +### 6) Suggested `doc_url` Value Sub2API already supports `doc_url` in system settings. Recommended values: - View URL: `https://github.com/Wei-Shaw/sub2api/blob/main/ADMIN_PAYMENT_INTEGRATION_API.md` -- Direct 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` diff --git a/frontend/src/views/user/PurchaseSubscriptionView.vue b/frontend/src/views/user/PurchaseSubscriptionView.vue index 55bcf307..f9460720 100644 --- a/frontend/src/views/user/PurchaseSubscriptionView.vue +++ b/frontend/src/views/user/PurchaseSubscriptionView.vue @@ -1,30 +1,6 @@ - From 997cd1e33294ebfd39b66f9333780a20ae36f4e2 Mon Sep 17 00:00:00 2001 From: erio Date: Sun, 1 Mar 2026 01:53:14 +0800 Subject: [PATCH 3/5] docs+ui: add bilingual payment integration doc and rename purchase entry to recharge/subscription --- ADMIN_PAYMENT_INTEGRATION_API.md | 216 ++++++------------ frontend/src/components/layout/AppSidebar.vue | 24 +- frontend/src/i18n/locales/en.ts | 22 +- frontend/src/i18n/locales/zh.ts | 22 +- 4 files changed, 116 insertions(+), 168 deletions(-) diff --git a/ADMIN_PAYMENT_INTEGRATION_API.md b/ADMIN_PAYMENT_INTEGRATION_API.md index db25c8d8..4cc21594 100644 --- a/ADMIN_PAYMENT_INTEGRATION_API.md +++ b/ADMIN_PAYMENT_INTEGRATION_API.md @@ -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://` +- Beta:`http://:8084` -- 生产环境:`https://` -- Beta 环境:`http://:8084` +### 认证 +推荐使用: +- `x-api-key: admin-<64hex>` +- `Content-Type: application/json` +- 幂等接口额外传:`Idempotency-Key` -### 认证方式 - -以下接口均建议使用: - -- 请求头:`x-api-key: admin-<64hex>`(服务间调用推荐) -- 请求头:`Content-Type: application/json` - -说明:管理员 JWT 也可访问 admin 路由,但机器对机器调用建议使用 Admin API Key。 - -### 1) 一步完成:创建兑换码并兑换 +说明:管理员 JWT 也可访问 admin 路由,但服务间调用建议使用 Admin API Key。 +### 1) 一步完成创建并兑换 `POST /api/v1/admin/redeem-codes/create-and-redeem` -用途: - -- 原子化完成“创建固定兑换码 + 兑换给指定用户”。 -- 常用于支付回调成功后的自动充值。 - -必需请求头: +用途:原子完成“创建兑换码 + 兑换到指定用户”。 +请求头: - `x-api-key` - `Idempotency-Key` -请求体: - +请求体示例: ```json { "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`(幂等回放)。 -- 同一 `code` 但 `used_by` 不一致:返回 `409`(冲突)。 -- 缺少 `Idempotency-Key`:返回 `400`(`IDEMPOTENCY_KEY_REQUIRED`)。 - -示例: - +curl 示例: ```bash curl -X POST "${BASE}/api/v1/admin/redeem-codes/create-and-redeem" \ -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` -用途: - -- 支付成功后充值前,确认目标用户是否存在。 - -示例: - ```bash curl -s "${BASE}/api/v1/admin/users/123" \ -H "x-api-key: ${KEY}" ``` -### 3) 余额调整(已存在接口) - +### 3) 余额调整(已有接口) `POST /api/v1/admin/users/:id/balance` -用途: - -- 复用现有管理员接口做人工纠偏。 -- 支持 `set`、`add`、`subtract`。 - -示例(扣减): +用途:人工补偿 / 扣减,支持 `set` / `add` / `subtract`。 +请求体示例(扣减): ```json { "balance": 100.0, @@ -121,36 +99,25 @@ curl -X POST "${BASE}/api/v1/admin/users/123/balance" \ }' ``` -### 4) 购买页跳转 URL Query 参数(iframe 与新窗口统一) - -Sub2API 前端在打开 `purchase_subscription_url` 时,会给 iframe 和“新窗口打开”统一追加 query 参数,确保外部支付页拿到一致上下文。 - -追加参数: - -- `user_id`:当前登录用户 ID -- `token`:当前登录 JWT token -- `theme`:当前主题(`light` / `dark`) -- `ui_mode`:当前 UI 模式(固定 `embedded`) +### 4) 购买页 URL Query 透传(iframe / 新窗口一致) +当 Sub2API 打开 `purchase_subscription_url` 时,会统一追加: +- `user_id` +- `token` +- `theme`(`light` / `dark`) +- `ui_mode`(固定 `embedded`) 示例: - ```text https://pay.example.com/pay?user_id=123&token=&theme=light&ui_mode=embedded ``` ### 5) 失败处理建议 - -- 支付状态与充值状态分开落库。 -- 收到并验证支付回调后,立即标记“支付成功”。 -- 支付成功但充值失败的订单应允许后续重试。 -- 重试时继续使用同一 `code`,并使用新的 `Idempotency-Key`。 +- 支付成功与充值成功分状态落库 +- 回调验签成功后立即标记“支付成功” +- 支付成功但充值失败的订单允许后续重试 +- 重试保持相同 `code`,并使用新的 `Idempotency-Key` ### 6) `doc_url` 配置建议 - -Sub2API 已支持系统设置中的 `doc_url` 字段。 - -推荐配置: - - 查看链接:`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` @@ -158,40 +125,35 @@ Sub2API 已支持系统设置中的 `doc_url` 字段。 ## English -### Overview - -This document defines the minimum Admin API surface for integrating external payment systems (for example, sub2apipay) with Sub2API for recharge fulfillment and reconciliation. +### Purpose +This document describes the minimal Sub2API Admin API surface for external payment integrations (for example, `sub2apipay`), including: +- Recharge after payment success +- User lookup +- Manual balance correction +- Purchase page query parameter forwarding ### Base URL - - Production: `https://` - Beta: `http://:8084` ### Authentication - Recommended headers: - -- `x-api-key: admin-<64hex>` (recommended for server-to-server calls) +- `x-api-key: admin-<64hex>` - `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. - -### 1) One-step Create + Redeem +Note: Admin JWT can also access admin routes, but Admin API Key is recommended for server-to-server integration. +### 1) Create and Redeem in one step `POST /api/v1/admin/redeem-codes/create-and-redeem` -Purpose: - -- Atomically create a deterministic redeem code and redeem it to the target user. -- Typical usage: called right after payment callback success. - -Required headers: +Use case: atomically create a redeem code and redeem it to a target user. +Headers: - `x-api-key` - `Idempotency-Key` Request body: - ```json { "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: +- 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). -- Same `code` and different `used_by`: `409` (conflict). -- Missing `Idempotency-Key`: `400` (`IDEMPOTENCY_KEY_REQUIRED`). - -Example: - +curl example: ```bash curl -X POST "${BASE}/api/v1/admin/redeem-codes/create-and-redeem" \ -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` -Purpose: - -- Verify target user existence before final recharge/retry. - -Example: - ```bash curl -s "${BASE}/api/v1/admin/users/123" \ -H "x-api-key: ${KEY}" ``` -### 3) Balance Adjustment (Existing API) - +### 3) Balance Adjustment (existing API) `POST /api/v1/admin/users/:id/balance` -Purpose: - -- Reuse existing admin endpoint for manual reconciliation. -- Supports `set`, `add`, `subtract`. +Use case: manual correction with `set` / `add` / `subtract`. Request body example (`subtract`): - ```json { "balance": 100.0, @@ -265,8 +206,6 @@ Request body example (`subtract`): } ``` -Example: - ```bash curl -X POST "${BASE}/api/v1/admin/users/123/balance" \ -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) - -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. - -Appended parameters: - -- `user_id`: current logged-in user id -- `token`: current logged-in JWT token -- `theme`: current theme (`light` / `dark`) -- `ui_mode`: UI mode (fixed `embedded`) +### 4) Purchase URL query forwarding (iframe and new tab) +When Sub2API opens `purchase_subscription_url`, it appends: +- `user_id` +- `token` +- `theme` (`light` / `dark`) +- `ui_mode` (fixed: `embedded`) Example: - ```text https://pay.example.com/pay?user_id=123&token=&theme=light&ui_mode=embedded ``` -### 5) Failure Handling Recommendations - -- Store payment state and recharge state separately. -- Mark payment success immediately after callback verification. -- Keep orders retryable when payment succeeded but recharge failed. -- 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: +### 5) Failure handling recommendations +- Persist payment success and recharge success as separate states +- Mark payment as successful immediately after verified callback +- Allow retry for orders with payment success but recharge failure +- Keep the same `code` for retry, and use a new `Idempotency-Key` +### 6) Recommended `doc_url` - 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` diff --git a/frontend/src/components/layout/AppSidebar.vue b/frontend/src/components/layout/AppSidebar.vue index e89e73b1..b356e3e5 100644 --- a/frontend/src/components/layout/AppSidebar.vue +++ b/frontend/src/components/layout/AppSidebar.vue @@ -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 = { render: () => h( @@ -490,7 +510,7 @@ const userNavItems = computed(() => { { path: '/purchase', label: t('nav.buySubscription'), - icon: CreditCardIcon, + icon: RechargeSubscriptionIcon, hideInSimpleMode: true } ] @@ -515,7 +535,7 @@ const personalNavItems = computed(() => { { path: '/purchase', label: t('nav.buySubscription'), - icon: CreditCardIcon, + icon: RechargeSubscriptionIcon, hideInSimpleMode: true } ] diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 50dce51b..34e23179 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -280,7 +280,7 @@ export default { logout: 'Logout', github: 'GitHub', mySubscriptions: 'My Subscriptions', - buySubscription: 'Purchase Subscription', + buySubscription: 'Recharge / Subscription', docs: 'Docs', 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' }, purchase: { - title: 'Purchase Page', - description: 'Show a "Purchase Subscription" entry in the sidebar and open the configured URL in an iframe', - enabled: 'Show Purchase Entry', + title: 'Recharge / Subscription Page', + description: 'Show a "Recharge / Subscription" entry in the sidebar and open the configured URL in an iframe', + enabled: 'Show Recharge / Subscription Entry', enabledHint: 'Only shown in standard mode (not simple mode)', - url: 'Purchase URL', + url: 'Recharge / Subscription URL', urlPlaceholder: 'https://example.com/purchase', urlHint: 'Must be an absolute http(s) URL', iframeWarning: @@ -3874,16 +3874,16 @@ export default { retry: 'Retry' }, - // Purchase Subscription Page + // Recharge / Subscription Page purchase: { - title: 'Purchase Subscription', - description: 'Purchase a subscription via the embedded page', + title: 'Recharge / Subscription', + description: 'Recharge balance or purchase subscription via the embedded page', openInNewTab: 'Open in new tab', notEnabledTitle: 'Feature not enabled', - notEnabledDesc: 'The administrator has not enabled the purchase page. Please contact admin.', - notConfiguredTitle: 'Purchase URL not configured', + notEnabledDesc: 'The administrator has not enabled the recharge/subscription entry. Please contact admin.', + notConfiguredTitle: 'Recharge / Subscription URL not configured', 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 diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index 56055a53..42d3bbb5 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -280,7 +280,7 @@ export default { logout: '退出登录', github: 'GitHub', mySubscriptions: '我的订阅', - buySubscription: '购买订阅', + buySubscription: '充值/订阅', docs: '文档', sora: 'Sora 创作' }, @@ -3754,11 +3754,11 @@ export default { hideCcsImportButtonHint: '启用后将在 API Keys 页面隐藏"导入 CCS"按钮' }, purchase: { - title: '购买订阅页面', - description: '在侧边栏展示”购买订阅”入口,并在页面内通过 iframe 打开指定链接', - enabled: '显示购买订阅入口', + title: '充值/订阅页面', + description: '在侧边栏展示“充值/订阅”入口,并在页面内通过 iframe 打开指定链接', + enabled: '显示充值/订阅入口', enabledHint: '仅在标准模式(非简单模式)下展示', - url: '购买页面 URL', + url: '充值/订阅页面 URL', urlPlaceholder: 'https://example.com/purchase', urlHint: '必须是完整的 http(s) 链接', iframeWarning: @@ -4045,15 +4045,15 @@ export default { retry: '重试' }, - // Purchase Subscription Page + // Recharge / Subscription Page purchase: { - title: '购买订阅', - description: '通过内嵌页面完成订阅购买', + title: '充值/订阅', + description: '通过内嵌页面完成充值/订阅', openInNewTab: '新窗口打开', notEnabledTitle: '该功能未开启', - notEnabledDesc: '管理员暂未开启购买订阅入口,请联系管理员。', - notConfiguredTitle: '购买链接未配置', - notConfiguredDesc: '管理员已开启入口,但尚未配置购买订阅链接,请联系管理员。' + notEnabledDesc: '管理员暂未开启充值/订阅入口,请联系管理员。', + notConfiguredTitle: '充值/订阅链接未配置', + notConfiguredDesc: '管理员已开启入口,但尚未配置充值/订阅链接,请联系管理员。' }, // Announcements Page From fcc77d1383f8d49190b7e2dc655d74c15847a212 Mon Sep 17 00:00:00 2001 From: erio Date: Sun, 1 Mar 2026 02:04:19 +0800 Subject: [PATCH 4/5] refactor(purchase): use URL/searchParams only for purchase query merge --- frontend/src/views/user/PurchaseSubscriptionView.vue | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/frontend/src/views/user/PurchaseSubscriptionView.vue b/frontend/src/views/user/PurchaseSubscriptionView.vue index f9460720..fdcd0d34 100644 --- a/frontend/src/views/user/PurchaseSubscriptionView.vue +++ b/frontend/src/views/user/PurchaseSubscriptionView.vue @@ -117,17 +117,7 @@ function buildPurchaseUrl( url.searchParams.set(PURCHASE_UI_MODE_QUERY_KEY, PURCHASE_UI_MODE_EMBEDDED) return url.toString() } catch { - const params: string[] = [] - if (userId) { - params.push(`${PURCHASE_USER_ID_QUERY_KEY}=${encodeURIComponent(String(userId))}`) - } - if (authToken) { - params.push(`${PURCHASE_AUTH_TOKEN_QUERY_KEY}=${encodeURIComponent(authToken)}`) - } - params.push(`${PURCHASE_THEME_QUERY_KEY}=${encodeURIComponent(theme)}`) - params.push(`${PURCHASE_UI_MODE_QUERY_KEY}=${encodeURIComponent(PURCHASE_UI_MODE_EMBEDDED)}`) - const separator = baseUrl.includes('?') ? '&' : '?' - return `${baseUrl}${separator}${params.join('&')}` + return baseUrl } } From 23686b13915515a70018f57c5e112446a7a6432d Mon Sep 17 00:00:00 2001 From: erio Date: Sun, 1 Mar 2026 18:08:42 +0800 Subject: [PATCH 5/5] refactor(docs): move integration doc to docs/ and add download link in settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move ADMIN_PAYMENT_INTEGRATION_API.md → docs/ADMIN_PAYMENT_INTEGRATION_API.md - Update README.md reference path - Add payment integration doc download link in admin settings UI (Purchase section) - Add i18n keys: integrationDoc / integrationDocHint (zh + en) --- README.md | 2 +- .../ADMIN_PAYMENT_INTEGRATION_API.md | 0 frontend/src/i18n/locales/en.ts | 4 +++- frontend/src/i18n/locales/zh.ts | 4 +++- frontend/src/views/admin/SettingsView.vue | 20 +++++++++++++++++++ 5 files changed, 27 insertions(+), 3 deletions(-) rename ADMIN_PAYMENT_INTEGRATION_API.md => docs/ADMIN_PAYMENT_INTEGRATION_API.md (100%) diff --git a/README.md b/README.md index 6381b301..1e2f2290 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ Sub2API is an AI API gateway platform designed to distribute and manage API quot ## Documentation - Dependency Security: `docs/dependency-security.md` -- Admin Payment Integration API: `ADMIN_PAYMENT_INTEGRATION_API.md` +- Admin Payment Integration API: `docs/ADMIN_PAYMENT_INTEGRATION_API.md` --- diff --git a/ADMIN_PAYMENT_INTEGRATION_API.md b/docs/ADMIN_PAYMENT_INTEGRATION_API.md similarity index 100% rename from ADMIN_PAYMENT_INTEGRATION_API.md rename to docs/ADMIN_PAYMENT_INTEGRATION_API.md diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 34e23179..3b83e609 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -3590,7 +3590,9 @@ export default { urlPlaceholder: 'https://example.com/purchase', urlHint: 'Must be an absolute http(s) URL', iframeWarning: - '⚠️ iframe note: Some websites block embedding via X-Frame-Options or CSP (frame-ancestors). If the page is blank, provide an "Open in new tab" alternative.' + '⚠️ iframe note: Some websites block embedding via X-Frame-Options or CSP (frame-ancestors). If the page is blank, provide an "Open in new tab" alternative.', + integrationDoc: 'Payment Integration Docs', + integrationDocHint: 'Covers endpoint specs, idempotency semantics, and code samples' }, soraClient: { title: 'Sora Client', diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index 42d3bbb5..ae52b372 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -3762,7 +3762,9 @@ export default { urlPlaceholder: 'https://example.com/purchase', urlHint: '必须是完整的 http(s) 链接', iframeWarning: - '⚠️ iframe 提示:部分网站会通过 X-Frame-Options 或 CSP(frame-ancestors)禁止被 iframe 嵌入,出现空白时可引导用户使用”新窗口打开”。' + '⚠️ iframe 提示:部分网站会通过 X-Frame-Options 或 CSP(frame-ancestors)禁止被 iframe 嵌入,出现空白时可引导用户使用”新窗口打开”。', + integrationDoc: '支付集成文档', + integrationDocHint: '包含接口说明、幂等语义及示例代码' }, soraClient: { title: 'Sora 客户端', diff --git a/frontend/src/views/admin/SettingsView.vue b/frontend/src/views/admin/SettingsView.vue index 13b4d1e9..bd444210 100644 --- a/frontend/src/views/admin/SettingsView.vue +++ b/frontend/src/views/admin/SettingsView.vue @@ -991,6 +991,26 @@ {{ t('admin.settings.purchase.iframeWarning') }}

+ + +
+ + + + + {{ t('admin.settings.purchase.integrationDoc') }} + + + + {{ t('admin.settings.purchase.integrationDocHint') }} + +