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:
@@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user