feat(billing): 域名计费系数(同key不同域名不同倍率, 保留auto)

- 新增 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) <noreply@anthropic.com>
This commit is contained in:
2026-06-02 00:30:51 +08:00
parent 9a2e60dff2
commit c8116037be
3 changed files with 106 additions and 0 deletions

View File

@@ -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":

View File

@@ -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
}

View File

@@ -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()
}