diff --git a/README.md b/README.md
index a5f680bf..1e2f2290 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: `docs/ADMIN_PAYMENT_INTEGRATION_API.md`
---
diff --git a/backend/cmd/server/wire_gen.go b/backend/cmd/server/wire_gen.go
index 5e78886c..ef5d142e 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 {
diff --git a/docs/ADMIN_PAYMENT_INTEGRATION_API.md b/docs/ADMIN_PAYMENT_INTEGRATION_API.md
new file mode 100644
index 00000000..4cc21594
--- /dev/null
+++ b/docs/ADMIN_PAYMENT_INTEGRATION_API.md
@@ -0,0 +1,241 @@
+# ADMIN_PAYMENT_INTEGRATION_API
+
+> 单文件中英双语文档 / Single-file bilingual documentation (Chinese + English)
+
+---
+
+## 中文
+
+### 目标
+本文档用于对接外部支付系统(如 `sub2apipay`)与 Sub2API 的 Admin API,覆盖:
+- 支付成功后充值
+- 用户查询
+- 人工余额修正
+- 前端购买页参数透传
+
+### 基础地址
+- 生产:`https://`
+- Beta:`http://:8084`
+
+### 认证
+推荐使用:
+- `x-api-key: admin-<64hex>`
+- `Content-Type: application/json`
+- 幂等接口额外传:`Idempotency-Key`
+
+说明:管理员 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` 且 `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}" \
+ -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` 时,会统一追加:
+- `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`
+
+### 6) `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
+
+### 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>`
+- `Content-Type: application/json`
+- `Idempotency-Key` for idempotent endpoints
+
+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`
+
+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",
+ "type": "balance",
+ "value": 100.0,
+ "user_id": 123,
+ "notes": "sub2apipay order: cm1234567890"
+}
+```
+
+Idempotency behavior:
+- Same `code` and same `used_by`: `200`
+- Same `code` but different `used_by`: `409`
+- Missing `Idempotency-Key`: `400` (`IDEMPOTENCY_KEY_REQUIRED`)
+
+curl 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`
+
+```bash
+curl -s "${BASE}/api/v1/admin/users/123" \
+ -H "x-api-key: ${KEY}"
+```
+
+### 3) Balance Adjustment (existing API)
+`POST /api/v1/admin/users/:id/balance`
+
+Use case: manual correction with `set` / `add` / `subtract`.
+
+Request body example (`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) 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
+- 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 4e70ddda..9e41ea76 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'
},
@@ -3590,15 +3590,17 @@ 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:
- '⚠️ 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',
@@ -3882,16 +3884,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 bc5e6b69..36986867 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 创作'
},
@@ -3761,15 +3761,17 @@ 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:
- '⚠️ iframe 提示:部分网站会通过 X-Frame-Options 或 CSP(frame-ancestors)禁止被 iframe 嵌入,出现空白时可引导用户使用”新窗口打开”。'
+ '⚠️ iframe 提示:部分网站会通过 X-Frame-Options 或 CSP(frame-ancestors)禁止被 iframe 嵌入,出现空白时可引导用户使用”新窗口打开”。',
+ integrationDoc: '支付集成文档',
+ integrationDocHint: '包含接口说明、幂等语义及示例代码'
},
soraClient: {
title: 'Sora 客户端',
@@ -4052,15 +4054,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
diff --git a/frontend/src/views/admin/SettingsView.vue b/frontend/src/views/admin/SettingsView.vue
index 90d07c45..c87ced78 100644
--- a/frontend/src/views/admin/SettingsView.vue
+++ b/frontend/src/views/admin/SettingsView.vue
@@ -1020,6 +1020,26 @@
{{ t('admin.settings.purchase.iframeWarning') }}
+
+
+
+
+
+ {{ t('admin.settings.purchase.integrationDoc') }}
+
+
—
+
+ {{ t('admin.settings.purchase.integrationDocHint') }}
+
+
diff --git a/frontend/src/views/user/PurchaseSubscriptionView.vue b/frontend/src/views/user/PurchaseSubscriptionView.vue
index 55bcf307..fdcd0d34 100644
--- a/frontend/src/views/user/PurchaseSubscriptionView.vue
+++ b/frontend/src/views/user/PurchaseSubscriptionView.vue
@@ -1,30 +1,6 @@
-
-
-
- {{ t('purchase.title') }}
-
-
- {{ t('purchase.description') }}
-
-
-
-
-
-
-