account_upstream_billing_rates.go 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. package admin
  2. import (
  3. "crypto/sha256"
  4. "encoding/hex"
  5. "encoding/json"
  6. "net/http"
  7. "strconv"
  8. "strings"
  9. "github.com/Wei-Shaw/sub2api/internal/pkg/response"
  10. "github.com/Wei-Shaw/sub2api/internal/service"
  11. "github.com/gin-gonic/gin"
  12. )
  13. // upstreamBillingRatesResponse is deliberately smaller than the regular
  14. // account-list response. The frontend uses the returned ID order to detect a
  15. // page boundary/order change and only applies snapshots when the page is
  16. // otherwise stable.
  17. type upstreamBillingRatesResponse struct {
  18. Items []service.UpstreamBillingRateSnapshotItem `json:"items"`
  19. Total int64 `json:"total"`
  20. Page int `json:"page"`
  21. PageSize int `json:"page_size"`
  22. }
  23. // GetUpstreamBillingRates returns persisted upstream billing snapshots for the
  24. // current account-list page. It never probes an upstream and never loads
  25. // today-stats, usage, concurrency, or credentials for the response.
  26. //
  27. // GET /api/v1/admin/accounts/upstream-billing-rates
  28. func (h *AccountHandler) GetUpstreamBillingRates(c *gin.Context) {
  29. if h.adminService == nil {
  30. response.Error(c, http.StatusServiceUnavailable, "account service unavailable")
  31. return
  32. }
  33. page, pageSize := response.ParsePagination(c)
  34. platform := c.Query("platform")
  35. accountType := c.Query("type")
  36. status := c.Query("status")
  37. search := strings.TrimSpace(c.Query("search"))
  38. if len(search) > 100 {
  39. search = search[:100]
  40. }
  41. privacyMode := strings.TrimSpace(c.Query("privacy_mode"))
  42. sortBy := c.DefaultQuery("sort_by", "name")
  43. sortOrder := c.DefaultQuery("sort_order", "asc")
  44. var groupID int64
  45. if groupQuery := c.Query("group"); groupQuery != "" {
  46. if groupQuery == accountListGroupUngroupedQueryValue {
  47. groupID = service.AccountListGroupUngrouped
  48. } else {
  49. parsed, err := strconv.ParseInt(groupQuery, 10, 64)
  50. if err != nil || parsed < 0 {
  51. response.BadRequest(c, "invalid group filter")
  52. return
  53. }
  54. groupID = parsed
  55. }
  56. }
  57. accounts, total, err := h.adminService.ListAccounts(
  58. c.Request.Context(), page, pageSize, platform, accountType, status,
  59. search, groupID, privacyMode, sortBy, sortOrder,
  60. )
  61. if err != nil {
  62. response.ErrorFrom(c, err)
  63. return
  64. }
  65. payload := upstreamBillingRatesResponse{
  66. Items: service.BuildUpstreamBillingRateSnapshotItems(accounts),
  67. Total: total,
  68. Page: page,
  69. PageSize: pageSize,
  70. }
  71. c.Header("Cache-Control", "private, no-cache")
  72. etag := buildUpstreamBillingRatesETag(payload)
  73. if etag != "" {
  74. c.Header("ETag", etag)
  75. if ifNoneMatchMatched(c.GetHeader("If-None-Match"), etag) {
  76. c.Status(http.StatusNotModified)
  77. return
  78. }
  79. }
  80. response.Success(c, payload)
  81. }
  82. func buildUpstreamBillingRatesETag(payload upstreamBillingRatesResponse) string {
  83. raw, err := json.Marshal(payload)
  84. if err != nil {
  85. return ""
  86. }
  87. sum := sha256.Sum256(raw)
  88. return "\"" + hex.EncodeToString(sum[:]) + "\""
  89. }