fix(billing): reject rate_multiplier <= 0 on save; clamp negatives to 0 in compute

分组倍率和用户专属倍率在保存时没有校验,0 会触发计费层的 `<=0 → 1.0`
防御条款,结果订阅/余额分组按标准价扣费;完全是沉默地绕过了业务规则。

- 保存校验(admin_service):CreateGroup / UpdateGroup / BatchSetGroupRateMultipliers /
  UpdateUser.SyncUserGroupRates 全部要求 > 0
- 计算层(billing_service):三处 `<=0 → 1.0` 改为 `<0 → 0`;负数按 0 结算,
  避免配置异常被静默按 1x 收费
- 前端:分组倍率 / 用户专属倍率输入 min 统一到 0.001
- 删除未使用的 IsFreeSubscription 方法

测试:新增 billing_service_rate_multiplier_test.go 端到端验证;更新原有锁定
旧 `<=0 → 1.0` 行为的测试。
This commit is contained in:
erio
2026-04-17 18:32:12 +08:00
parent 948d8e6d02
commit df57d2776b
11 changed files with 119 additions and 72 deletions

View File

@@ -448,8 +448,9 @@ func (s *BillingService) CalculateCostUnified(input CostInput) (*CostBreakdown,
})
}
if input.RateMultiplier <= 0 {
input.RateMultiplier = 1.0
// 保存时强制 > 0若仍有负数泄漏缓存/迁移残留),按 0 处理避免按 1x 误扣。
if input.RateMultiplier < 0 {
input.RateMultiplier = 0
}
var breakdown *CostBreakdown
@@ -493,8 +494,9 @@ func (s *BillingService) computeTokenBreakdown(
rateMultiplier float64, serviceTier string,
applyLongCtx bool,
) *CostBreakdown {
if rateMultiplier <= 0 {
rateMultiplier = 1.0
// 保存时强制 > 0若仍有负数泄漏按 0 处理避免按 1x 误扣。
if rateMultiplier < 0 {
rateMultiplier = 0
}
inputPrice := pricing.InputPricePerToken
@@ -831,9 +833,9 @@ func (s *BillingService) CalculateImageCost(model string, imageSize string, imag
// 计算总费用
totalCost := unitPrice * float64(imageCount)
// 应用倍率
if rateMultiplier <= 0 {
rateMultiplier = 1.0
// 应用倍率(保存时强制 > 0负数按 0 处理避免按 1x 误扣)
if rateMultiplier < 0 {
rateMultiplier = 0
}
actualCost := totalCost * rateMultiplier