Files
heicode-mananger/heicode/controller/heicode_self.go
T
chenchenandClaude Opus 4.8 349957360d 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>
2026-06-08 17:34:03 +08:00

112 lines
4.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package controller
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/heicode/manager/common"
"github.com/heicode/manager/i18n"
"github.com/heicode/manager/model"
"github.com/heicode/manager/setting/operation_setting"
)
// 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 operation_setting.QuotaDisplayTypeCNY
case operation_setting.QuotaDisplayTypeTokens:
return operation_setting.QuotaDisplayTypeTokens
case operation_setting.QuotaDisplayTypeCustom:
return operation_setting.QuotaDisplayTypeCustom
default:
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
// either:
// - a legacy sk- bearer (30-day transition window), or
// - a V2 device-signed request (after pair).
//
// Returns the *user-row* quota counters (not the device-token's
// UnlimitedQuota flag, which is just an auth-mechanism marker). This
// is the same data /api/user/self surfaces to the web dashboard, but
// trimmed to just the fields the desktop "balance pill" + usage panel
// need. No PII (email/social ids) so a leaked sk- can't enumerate
// account identity beyond what relay calls already expose.
func GetHeicodeSelf(c *gin.Context) {
userId := c.GetInt("id")
if userId == 0 {
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": common.TranslateMessage(c, i18n.MsgUnauthorized),
})
return
}
user, err := model.GetUserById(userId, false)
if err != nil {
common.SysLog("GetHeicodeSelf: " + err.Error())
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"message": common.TranslateMessage(c, i18n.MsgDatabaseError),
})
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,
// 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,
},
})
}