Merge pull request #14 from xmindlab-heicode/feat/heicode-self-quota-display-type

feat(heicode-self): /api/heicode/self 返回 raw quota + quota_per_unit(与网页台同源换算)
This commit is contained in:
Fasthei
2026-06-08 17:52:46 +08:00
committed by GitHub
2 changed files with 107 additions and 17 deletions
+52 -17
View File
@@ -11,22 +11,44 @@ import (
"github.com/heicode/manager/setting/operation_setting"
)
// quotaToDisplayUnit converts a raw quota integer (DB unit) to the
// site-configured display unit (USD / CNY / tokens). Mirrors the
// conversion in billing.go so the desktop "balance pill" shows the
// same number the user sees in the web dashboard.
func quotaToDisplayUnit(raw int) float64 {
amount := float64(raw)
// quotaDisplayUnitLabel names the unit the raw quota is denominated in. Returns
// the canonical (UPPER-CASE) display-type constant — USD / CNY / TOKENS / CUSTOM
// — mirroring the web dashboard. The client uses this only to pick a currency
// symbol; the actual conversion is done server-side (display_quota). Returning the
// constants verbatim keeps the wire value byte-identical to what the client
// compares against, and covers CUSTOM (previously fell through to "USD", so a
// custom-currency site mislabelled its display_quota and the client showed "$").
func quotaDisplayUnitLabel() string {
switch operation_setting.GetQuotaDisplayType() {
case operation_setting.QuotaDisplayTypeCNY:
return amount / common.QuotaPerUnit * operation_setting.USDExchangeRate
return operation_setting.QuotaDisplayTypeCNY
case operation_setting.QuotaDisplayTypeTokens:
return amount
return operation_setting.QuotaDisplayTypeTokens
case operation_setting.QuotaDisplayTypeCustom:
return operation_setting.QuotaDisplayTypeCustom
default:
return amount / common.QuotaPerUnit
return operation_setting.QuotaDisplayTypeUSD
}
}
// displayQuota converts a raw quota counter to the value the user should SEE,
// for the current站点 display type — server-side so the client never has to
// special-case a mode. This is the contract of record (no hardcoded ×500000):
//
// USD : raw / quota_per_unit
// CNY : raw / quota_per_unit * usd_exchange_rate
// CUSTOM : raw / quota_per_unit * custom_currency_exchange_rate
// TOKENS : raw (no division)
//
// usd_exchange_rate is the USD→display-currency multiplier returned alongside,
// so clients that prefer raw can recompute the same value.
func displayQuota(raw int, label string, perUnit, rate float64) float64 {
if label == operation_setting.QuotaDisplayTypeTokens || perUnit <= 0 {
return float64(raw)
}
return float64(raw) / perUnit * rate
}
// GetHeicodeSelf serves GET /api/heicode/self.
//
// Mounted on TokenAuth so the cc-haha desktop client can read it with
@@ -60,17 +82,30 @@ func GetHeicodeSelf(c *gin.Context) {
return
}
// Conversion params + server-computed display values so every quota_display_type
// (USD / CNY / tokens / custom) renders correctly without a client-side hack.
label := quotaDisplayUnitLabel()
perUnit := float64(common.QuotaPerUnit)
rate := operation_setting.GetUsdToCurrencyRate(operation_setting.USDExchangeRate) // USD→1, CNY→7.3, tokens→1
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"id": user.Id,
"username": user.Username,
"display_name": user.DisplayName,
"group": user.Group,
"role": user.Role,
"quota": quotaToDisplayUnit(user.Quota),
"used_quota": quotaToDisplayUnit(user.UsedQuota),
"request_count": user.RequestCount,
"id": user.Id,
"username": user.Username,
"display_name": user.DisplayName,
"group": user.Group,
"role": user.Role,
// raw counters (NewAPI internal units) + the params to convert them.
"quota": user.Quota,
"used_quota": user.UsedQuota,
"quota_per_unit": common.QuotaPerUnit,
"quota_display_type": label, // USD / CNY / TOKENS / CUSTOM (pick currency symbol only)
"usd_exchange_rate": rate, // USD→display-currency multiplier (1 for USD/tokens)
// ready-to-show values (server applies the display-type formula above).
"display_quota": displayQuota(user.Quota, label, perUnit, rate),
"display_used_quota": displayQuota(user.UsedQuota, label, perUnit, rate),
"request_count": user.RequestCount,
},
})
}
+55
View File
@@ -0,0 +1,55 @@
package controller
import (
"testing"
"github.com/heicode/manager/setting/operation_setting"
"github.com/stretchr/testify/require"
)
// displayQuota is the /api/heicode/self conversion contract: the server applies
// the display-type formula so the client never special-cases a mode. Pin all
// four modes (USD divides, CNY/CUSTOM scale, TOKENS passes through) and the
// divide-by-zero guard.
func TestDisplayQuota_AllModes(t *testing.T) {
const perUnit = 500000.0
cases := []struct {
name string
raw int
label string
rate float64
want float64
}{
{"USD divides by per-unit", 1_000_000, operation_setting.QuotaDisplayTypeUSD, 1, 2},
{"CNY multiplies by usd rate", 1_000_000, operation_setting.QuotaDisplayTypeCNY, 7.3, 14.6},
{"CUSTOM uses custom rate", 1_000_000, operation_setting.QuotaDisplayTypeCustom, 2.5, 5},
{"TOKENS returns raw, no division", 1_000_000, operation_setting.QuotaDisplayTypeTokens, 1, 1_000_000},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
require.InDelta(t, tc.want, displayQuota(tc.raw, tc.label, perUnit, tc.rate), 1e-9)
})
}
// per_unit<=0 must never divide by zero — return raw unchanged.
require.Equal(t, float64(1_000_000), displayQuota(1_000_000, operation_setting.QuotaDisplayTypeUSD, 0, 1))
}
// quotaDisplayUnitLabel must echo the canonical UPPER-CASE constant for every
// mode. The CUSTOM case is the regression guard: it previously fell through to
// "USD", mislabelling a custom-currency site's display_quota.
func TestQuotaDisplayUnitLabel_CanonicalAndCustom(t *testing.T) {
gs := operation_setting.GetGeneralSetting()
orig := gs.QuotaDisplayType
t.Cleanup(func() { gs.QuotaDisplayType = orig })
for _, want := range []string{
operation_setting.QuotaDisplayTypeUSD,
operation_setting.QuotaDisplayTypeCNY,
operation_setting.QuotaDisplayTypeTokens,
operation_setting.QuotaDisplayTypeCustom,
} {
gs.QuotaDisplayType = want
require.Equal(t, want, quotaDisplayUnitLabel())
}
}