按 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>
112 lines
4.3 KiB
Go
112 lines
4.3 KiB
Go
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,
|
||
},
|
||
})
|
||
}
|