Skip to content

Commit 0765d10

Browse files
committed
feat: 增加上游 Sub2API 计费倍率探测与账号展示
1 parent eb2b863 commit 0765d10

49 files changed

Lines changed: 5747 additions & 74 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎backend/cmd/server/wire.go‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ func provideCleanup(
103103
paymentOrderExpiry *service.PaymentOrderExpiryService,
104104
channelMonitorRunner *service.ChannelMonitorRunner,
105105
quotaFlusher *service.UserPlatformQuotaUsageFlusher,
106+
upstreamBillingProbe *service.UpstreamBillingProbeService,
106107
) func() {
107108
return func() {
108109
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
@@ -279,6 +280,12 @@ func provideCleanup(
279280
}
280281
return nil
281282
}},
283+
{"UpstreamBillingProbeService", func() error {
284+
if upstreamBillingProbe != nil {
285+
upstreamBillingProbe.Stop()
286+
}
287+
return nil
288+
}},
282289
}
283290

284291
infraSteps := []cleanupStep{

‎backend/cmd/server/wire_gen.go‎

Lines changed: 10 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎backend/cmd/server/wire_gen_test.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ func TestProvideCleanup_WithMinimalDependencies_NoPanic(t *testing.T) {
8383
nil, // paymentOrderExpiry
8484
nil, // channelMonitorRunner
8585
nil, // quotaFlusher
86+
nil, // upstreamBillingProbe
8687
)
8788

8889
require.NotPanics(t, func() {

‎backend/internal/handler/admin/account_handler.go‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ type AccountHandler struct {
6262
rpmCache service.RPMCache
6363
tokenCacheInvalidator service.TokenCacheInvalidator
6464
grokImportProber grokUsageProber
65+
upstreamBillingProbe *service.UpstreamBillingProbeService
66+
}
67+
68+
// SetUpstreamBillingProbeService attaches the optional remote billing probe service.
69+
func (h *AccountHandler) SetUpstreamBillingProbeService(probe *service.UpstreamBillingProbeService) {
70+
h.upstreamBillingProbe = probe
6571
}
6672

6773
// NewAccountHandler creates a new admin account handler
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package admin
2+
3+
import (
4+
"strconv"
5+
6+
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
7+
"github.com/Wei-Shaw/sub2api/internal/service"
8+
"github.com/gin-gonic/gin"
9+
)
10+
11+
type upstreamBillingProbeEnabledRequest struct {
12+
Enabled *bool `json:"enabled" binding:"required"`
13+
}
14+
15+
type upstreamBillingProbeBatchRequest struct {
16+
AccountIDs []int64 `json:"account_ids" binding:"required"`
17+
}
18+
19+
func (h *AccountHandler) GetUpstreamBillingProbeSettings(c *gin.Context) {
20+
if h.upstreamBillingProbe == nil {
21+
response.ErrorFrom(c, service.ErrUpstreamBillingProbeUnavailable)
22+
return
23+
}
24+
settings, err := h.upstreamBillingProbe.GetSettings(c.Request.Context())
25+
if err != nil {
26+
response.ErrorFrom(c, err)
27+
return
28+
}
29+
response.Success(c, settings)
30+
}
31+
32+
func (h *AccountHandler) UpdateUpstreamBillingProbeSettings(c *gin.Context) {
33+
if h.upstreamBillingProbe == nil {
34+
response.ErrorFrom(c, service.ErrUpstreamBillingProbeUnavailable)
35+
return
36+
}
37+
var req service.UpstreamBillingProbeSettings
38+
if err := c.ShouldBindJSON(&req); err != nil {
39+
response.BadRequest(c, "Invalid request: "+err.Error())
40+
return
41+
}
42+
if err := h.upstreamBillingProbe.UpdateSettings(c.Request.Context(), &req); err != nil {
43+
response.ErrorFrom(c, err)
44+
return
45+
}
46+
settings, err := h.upstreamBillingProbe.GetSettings(c.Request.Context())
47+
if err != nil {
48+
response.ErrorFrom(c, err)
49+
return
50+
}
51+
response.Success(c, settings)
52+
}
53+
54+
func (h *AccountHandler) SetUpstreamBillingProbeEnabled(c *gin.Context) {
55+
if h.upstreamBillingProbe == nil {
56+
response.ErrorFrom(c, service.ErrUpstreamBillingProbeUnavailable)
57+
return
58+
}
59+
accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
60+
if err != nil || accountID <= 0 {
61+
response.BadRequest(c, "Invalid account ID")
62+
return
63+
}
64+
var req upstreamBillingProbeEnabledRequest
65+
if err := c.ShouldBindJSON(&req); err != nil {
66+
response.BadRequest(c, "Invalid request: "+err.Error())
67+
return
68+
}
69+
if err := h.upstreamBillingProbe.SetAccountEnabled(c.Request.Context(), accountID, *req.Enabled); err != nil {
70+
response.ErrorFrom(c, err)
71+
return
72+
}
73+
response.Success(c, gin.H{"account_id": accountID, "enabled": *req.Enabled})
74+
}
75+
76+
func (h *AccountHandler) ProbeUpstreamBilling(c *gin.Context) {
77+
if h.upstreamBillingProbe == nil {
78+
response.ErrorFrom(c, service.ErrUpstreamBillingProbeUnavailable)
79+
return
80+
}
81+
accountID, err := strconv.ParseInt(c.Param("id"), 10, 64)
82+
if err != nil || accountID <= 0 {
83+
response.BadRequest(c, "Invalid account ID")
84+
return
85+
}
86+
snapshot, err := h.upstreamBillingProbe.ProbeAccount(c.Request.Context(), accountID)
87+
if err != nil {
88+
response.ErrorFrom(c, err)
89+
return
90+
}
91+
response.Success(c, service.UpstreamBillingProbeResult{AccountID: accountID, Snapshot: snapshot})
92+
}
93+
94+
func (h *AccountHandler) ProbeUpstreamBillingBatch(c *gin.Context) {
95+
if h.upstreamBillingProbe == nil {
96+
response.ErrorFrom(c, service.ErrUpstreamBillingProbeUnavailable)
97+
return
98+
}
99+
var req upstreamBillingProbeBatchRequest
100+
if err := c.ShouldBindJSON(&req); err != nil {
101+
response.BadRequest(c, "Invalid request: "+err.Error())
102+
return
103+
}
104+
if len(req.AccountIDs) == 0 || len(req.AccountIDs) > service.UpstreamBillingProbeMaxBatchSize {
105+
response.BadRequest(c, "account_ids must contain between 1 and 20 items")
106+
return
107+
}
108+
seen := make(map[int64]struct{}, len(req.AccountIDs))
109+
accountIDs := make([]int64, 0, len(req.AccountIDs))
110+
for _, accountID := range req.AccountIDs {
111+
if accountID <= 0 {
112+
response.BadRequest(c, "account_ids must contain positive IDs")
113+
return
114+
}
115+
if _, exists := seen[accountID]; exists {
116+
continue
117+
}
118+
seen[accountID] = struct{}{}
119+
accountIDs = append(accountIDs, accountID)
120+
}
121+
response.Success(c, gin.H{"results": h.upstreamBillingProbe.ProbeAccounts(c.Request.Context(), accountIDs)})
122+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package admin
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"net/http"
7+
"net/http/httptest"
8+
"testing"
9+
10+
"github.com/Wei-Shaw/sub2api/internal/service"
11+
"github.com/gin-gonic/gin"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
func setupUpstreamBillingProbeRouter() *gin.Engine {
16+
gin.SetMode(gin.TestMode)
17+
handler := NewAccountHandler(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil)
18+
handler.SetUpstreamBillingProbeService(service.NewUpstreamBillingProbeService(nil, nil, nil))
19+
20+
router := gin.New()
21+
router.GET("/admin/accounts/upstream-billing-probe/settings", handler.GetUpstreamBillingProbeSettings)
22+
router.POST("/admin/accounts/upstream-billing-probe/batch", handler.ProbeUpstreamBillingBatch)
23+
router.PUT("/admin/accounts/:id/upstream-billing-probe", handler.SetUpstreamBillingProbeEnabled)
24+
return router
25+
}
26+
27+
func TestAccountHandlerGetUpstreamBillingProbeSettingsReturnsDefaults(t *testing.T) {
28+
router := setupUpstreamBillingProbeRouter()
29+
recorder := httptest.NewRecorder()
30+
router.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/admin/accounts/upstream-billing-probe/settings", nil))
31+
32+
require.Equal(t, http.StatusOK, recorder.Code)
33+
var response struct {
34+
Data service.UpstreamBillingProbeSettings `json:"data"`
35+
}
36+
require.NoError(t, json.Unmarshal(recorder.Body.Bytes(), &response))
37+
require.True(t, response.Data.Enabled)
38+
require.Equal(t, 30, response.Data.IntervalMinutes)
39+
}
40+
41+
func TestAccountHandlerProbeUpstreamBillingBatchValidatesIDs(t *testing.T) {
42+
router := setupUpstreamBillingProbeRouter()
43+
44+
for _, body := range []string{`{"account_ids":[]}`, `{"account_ids":[0]}`} {
45+
recorder := httptest.NewRecorder()
46+
request := httptest.NewRequest(http.MethodPost, "/admin/accounts/upstream-billing-probe/batch", bytes.NewBufferString(body))
47+
request.Header.Set("Content-Type", "application/json")
48+
router.ServeHTTP(recorder, request)
49+
require.Equal(t, http.StatusBadRequest, recorder.Code)
50+
}
51+
}
52+
53+
func TestAccountHandlerSetUpstreamBillingProbeEnabledRejectsInvalidID(t *testing.T) {
54+
router := setupUpstreamBillingProbeRouter()
55+
recorder := httptest.NewRecorder()
56+
request := httptest.NewRequest(http.MethodPut, "/admin/accounts/not-an-id/upstream-billing-probe", bytes.NewBufferString(`{"enabled":true}`))
57+
request.Header.Set("Content-Type", "application/json")
58+
router.ServeHTTP(recorder, request)
59+
60+
require.Equal(t, http.StatusBadRequest, recorder.Code)
61+
}
62+
63+
func TestAccountHandlerSetUpstreamBillingProbeEnabledRequiresValue(t *testing.T) {
64+
router := setupUpstreamBillingProbeRouter()
65+
recorder := httptest.NewRecorder()
66+
request := httptest.NewRequest(http.MethodPut, "/admin/accounts/1/upstream-billing-probe", bytes.NewBufferString(`{}`))
67+
request.Header.Set("Content-Type", "application/json")
68+
router.ServeHTTP(recorder, request)
69+
70+
require.Equal(t, http.StatusBadRequest, recorder.Code)
71+
}

‎backend/internal/handler/wire.go‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ func ProvideAdminHandlers(
4141
paymentHandler *admin.PaymentHandler,
4242
affiliateHandler *admin.AffiliateHandler,
4343
complianceHandler *admin.ComplianceHandler,
44+
upstreamBillingProbe *service.UpstreamBillingProbeService,
4445
) *AdminHandlers {
46+
accountHandler.SetUpstreamBillingProbeService(upstreamBillingProbe)
4547
return &AdminHandlers{
4648
Dashboard: dashboardHandler,
4749
User: userHandler,

0 commit comments

Comments
 (0)