fix(heicode-self): quota_display_type 补 CUSTOM + 统一大写 + 加 displayQuota 测试

按 Mem0ried 复审:
- quotaDisplayUnitLabel 补 QuotaDisplayTypeCustom 分支(原落 default→"USD",
  导致自定义币种站点 display_quota 算对但 type 误报 USD、客户端配错符号)。
- tokens 标签由小写 "tokens" 改为规范常量 TOKENS,displayQuota 的判断同步用常量,
  对外 quota_display_type 统一大写(与 USD/CNY/CUSTOM 一致,客户端按常量比较不再 mismatch)。
- 新增表驱动测试 heicode_self_test.go:displayQuota 四模式(USD 除 / CNY·CUSTOM 乘 /
  TOKENS 原值)+ per_unit<=0 防除零;quotaDisplayUnitLabel 四模式(含 CUSTOM 回归)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-08 17:34:03 +08:00
co-authored by Claude Opus 4.8
parent ce733defcb
commit 349957360d
2 changed files with 70 additions and 8 deletions
+15 -8
View File
@@ -11,16 +11,23 @@ import (
"github.com/heicode/manager/setting/operation_setting" "github.com/heicode/manager/setting/operation_setting"
) )
// quotaDisplayUnitLabel names the unit the raw quota is denominated in. // quotaDisplayUnitLabel names the unit the raw quota is denominated in. Returns
// Returns USD / CNY / tokens (mirrors the web dashboard's quota display type). // 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 { func quotaDisplayUnitLabel() string {
switch operation_setting.GetQuotaDisplayType() { switch operation_setting.GetQuotaDisplayType() {
case operation_setting.QuotaDisplayTypeCNY: case operation_setting.QuotaDisplayTypeCNY:
return "CNY" return operation_setting.QuotaDisplayTypeCNY
case operation_setting.QuotaDisplayTypeTokens: case operation_setting.QuotaDisplayTypeTokens:
return "tokens" return operation_setting.QuotaDisplayTypeTokens
case operation_setting.QuotaDisplayTypeCustom:
return operation_setting.QuotaDisplayTypeCustom
default: default:
return "USD" return operation_setting.QuotaDisplayTypeUSD
} }
} }
@@ -31,12 +38,12 @@ func quotaDisplayUnitLabel() string {
// USD : raw / quota_per_unit // USD : raw / quota_per_unit
// CNY : raw / quota_per_unit * usd_exchange_rate // CNY : raw / quota_per_unit * usd_exchange_rate
// CUSTOM : raw / quota_per_unit * custom_currency_exchange_rate // CUSTOM : raw / quota_per_unit * custom_currency_exchange_rate
// tokens : raw (no division) // TOKENS : raw (no division)
// //
// usd_exchange_rate is the USD→display-currency multiplier returned alongside, // usd_exchange_rate is the USD→display-currency multiplier returned alongside,
// so clients that prefer raw can recompute the same value. // so clients that prefer raw can recompute the same value.
func displayQuota(raw int, label string, perUnit, rate float64) float64 { func displayQuota(raw int, label string, perUnit, rate float64) float64 {
if label == "tokens" || perUnit <= 0 { if label == operation_setting.QuotaDisplayTypeTokens || perUnit <= 0 {
return float64(raw) return float64(raw)
} }
return float64(raw) / perUnit * rate return float64(raw) / perUnit * rate
@@ -93,7 +100,7 @@ func GetHeicodeSelf(c *gin.Context) {
"quota": user.Quota, "quota": user.Quota,
"used_quota": user.UsedQuota, "used_quota": user.UsedQuota,
"quota_per_unit": common.QuotaPerUnit, "quota_per_unit": common.QuotaPerUnit,
"quota_display_type": label, // USD / CNY / tokens "quota_display_type": label, // USD / CNY / TOKENS / CUSTOM (pick currency symbol only)
"usd_exchange_rate": rate, // USD→display-currency multiplier (1 for USD/tokens) "usd_exchange_rate": rate, // USD→display-currency multiplier (1 for USD/tokens)
// ready-to-show values (server applies the display-type formula above). // ready-to-show values (server applies the display-type formula above).
"display_quota": displayQuota(user.Quota, label, perUnit, rate), "display_quota": displayQuota(user.Quota, label, perUnit, rate),
+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())
}
}