From c8116037bea4ce26eaa5ef4b159019d30e8c85d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E4=B8=AD=E6=B4=8B?= Date: Tue, 2 Jun 2026 00:30:51 +0800 Subject: [PATCH] =?UTF-8?q?feat(billing):=20=E5=9F=9F=E5=90=8D=E8=AE=A1?= =?UTF-8?q?=E8=B4=B9=E7=B3=BB=E6=95=B0(=E5=90=8Ckey=E4=B8=8D=E5=90=8C?= =?UTF-8?q?=E5=9F=9F=E5=90=8D=E4=B8=8D=E5=90=8C=E5=80=8D=E7=8E=87,=20?= =?UTF-8?q?=E4=BF=9D=E7=95=99auto)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 setting/ratio_setting/domain_ratio.go: domain_ratio 配置(RWMap+注册), GetDomainRatio(规整host)/HasDomainRatioConfigured 等; 默认空map不影响现有计费 - relay/helper/price.go HandleGroupRatio: 末尾按请求域名(X-Forwarded-Host回退Host) 乘域名系数, 含 HasSpecialRatio 同乘; 覆盖普通/阶梯/按次三条计费路径 - model/option.go: DomainRatio 接入后台 option 读写(同 GroupRatio) - auto 分组选择/熔断完全不受影响; 同上游纯价差 Co-Authored-By: Claude Opus 4.8 (1M context) --- model/option.go | 3 + relay/helper/price.go | 21 +++++++ setting/ratio_setting/domain_ratio.go | 82 +++++++++++++++++++++++++++ 3 files changed, 106 insertions(+) create mode 100644 setting/ratio_setting/domain_ratio.go diff --git a/model/option.go b/model/option.go index ed1af72e..b3cc2936 100644 --- a/model/option.go +++ b/model/option.go @@ -143,6 +143,7 @@ func InitOptionMap() { common.OptionMap["CreateCacheRatio"] = ratio_setting.CreateCacheRatio2JSONString() common.OptionMap["GroupRatio"] = ratio_setting.GroupRatio2JSONString() common.OptionMap["GroupGroupRatio"] = ratio_setting.GroupGroupRatio2JSONString() + common.OptionMap["DomainRatio"] = ratio_setting.DomainRatio2JSONString() common.OptionMap["UserUsableGroups"] = setting.UserUsableGroups2JSONString() common.OptionMap["CompletionRatio"] = ratio_setting.CompletionRatio2JSONString() common.OptionMap["ImageRatio"] = ratio_setting.ImageRatio2JSONString() @@ -524,6 +525,8 @@ func updateOptionMap(key string, value string) (err error) { err = ratio_setting.UpdateGroupRatioByJSONString(value) case "GroupGroupRatio": err = ratio_setting.UpdateGroupGroupRatioByJSONString(value) + case "DomainRatio": + err = ratio_setting.UpdateDomainRatioByJSONString(value) case "UserUsableGroups": err = setting.UpdateUserUsableGroupsByJSONString(value) case "CompletionRatio": diff --git a/relay/helper/price.go b/relay/helper/price.go index d1e16bca..9d070b2d 100644 --- a/relay/helper/price.go +++ b/relay/helper/price.go @@ -61,6 +61,27 @@ func HandleGroupRatio(ctx *gin.Context, relayInfo *relaycommon.RelayInfo) types. groupRatioInfo.GroupRatio = ratio_setting.GetGroupRatio(relayInfo.UsingGroup) } + // 域名计费系数:同一个令牌(含 auto 分组)按请求所用的对外域名再乘一个系数。 + // 用于"同一个 key、不同域名、不同价"的纯价差场景,完全不影响 auto 的分组选择/熔断。 + // 未配置任何域名系数时直接跳过,对现有计费零影响。 + if ratio_setting.HasDomainRatioConfigured() { + host := ctx.GetHeader("X-Forwarded-Host") + if host == "" { + host = ctx.Request.Host + } + // X-Forwarded-Host 可能是逗号分隔的代理链,取第一个(最外层对外域名) + if i := strings.IndexByte(host, ','); i != -1 { + host = host[:i] + } + if dr := ratio_setting.GetDomainRatio(host); dr != 1 { + groupRatioInfo.GroupRatio *= dr + if groupRatioInfo.HasSpecialRatio { + groupRatioInfo.GroupSpecialRatio *= dr + } + logger.LogDebug(ctx, fmt.Sprintf("domain ratio applied: host=%s ratio=%f finalGroupRatio=%f", host, dr, groupRatioInfo.GroupRatio)) + } + } + return groupRatioInfo } diff --git a/setting/ratio_setting/domain_ratio.go b/setting/ratio_setting/domain_ratio.go new file mode 100644 index 00000000..497338c0 --- /dev/null +++ b/setting/ratio_setting/domain_ratio.go @@ -0,0 +1,82 @@ +package ratio_setting + +import ( + "strings" + + "github.com/QuantumNous/new-api/setting/config" + "github.com/QuantumNous/new-api/types" +) + +// 域名计费系数:同一个令牌(可为 auto 分组),按请求所用的对外域名再乘一个额外系数。 +// 最终倍率 = auto命中分组倍率 × domainRatio。 +// 用于"同一个 key、不同域名、不同价"的纯价差场景(同上游, 不破坏 auto 的选择/熔断)。 +// key 为对外域名(小写, 不含端口), value 为系数。例: +// +// { "api1.example.com": 1.2, "api2.example.com": 1.5 } +var defaultDomainRatio = map[string]float64{} + +var domainRatioMap = types.NewRWMap[string, float64]() + +type DomainRatioSetting struct { + DomainRatio *types.RWMap[string, float64] `json:"domain_ratio"` +} + +var domainRatioSetting DomainRatioSetting + +func init() { + domainRatioMap.AddAll(defaultDomainRatio) + domainRatioSetting = DomainRatioSetting{ + DomainRatio: domainRatioMap, + } + config.GlobalConfig.Register("domain_ratio_setting", &domainRatioSetting) +} + +// GetDomainRatio 按域名返回系数;未配置时返回 1(不影响计费)。 +// host 会被规整为小写并去掉端口。 +func GetDomainRatio(host string) float64 { + h := normalizeDomainHost(host) + if h == "" { + return 1 + } + ratio, ok := domainRatioMap.Get(h) + if !ok { + return 1 + } + return ratio +} + +// HasDomainRatioConfigured 是否配置了任何域名系数(用于跳过无谓计算)。 +func HasDomainRatioConfigured() bool { + return domainRatioMap.Len() > 0 +} + +// normalizeDomainHost 规整域名:小写、去首尾空格、去端口。 +func normalizeDomainHost(host string) string { + h := strings.TrimSpace(strings.ToLower(host)) + if h == "" { + return "" + } + // 去端口:host:port → host。IPv6 形如 [::1]:8080 → 去掉 ]:8080 之后的端口。 + if strings.HasPrefix(h, "[") { + if end := strings.Index(h, "]"); end != -1 { + return h[:end+1] + } + return h + } + if i := strings.LastIndexByte(h, ':'); i != -1 { + return h[:i] + } + return h +} + +func DomainRatio2JSONString() string { + return domainRatioMap.MarshalJSONString() +} + +func UpdateDomainRatioByJSONString(jsonStr string) error { + return types.LoadFromJsonString(domainRatioMap, jsonStr) +} + +func GetDomainRatioCopy() map[string]float64 { + return domainRatioMap.ReadAll() +}