fix(billing): 封住发布前计费竞态
ci / verify (pull_request) Failing after 8s
ci / verify (pull_request) Failing after 8s
阻止上游提交状态不明的任务被租约接管后重复执行,并为人工复核保留可操作的结算记录。 将生产提交绑定到当前估价签名,统一复用预处理快照,并补强规则形状、定点溢出与历史规则兼容校验。 已通过 PostgreSQL 16 集成测试、Go 全量测试与静态检查、前端测试与构建、OpenAPI、依赖审计、镜像、迁移、流水线和 SemVer 门禁。
This commit is contained in:
@@ -335,6 +335,9 @@ ORDER BY priority ASC, resource_type ASC, rule_key ASC`, id)
|
||||
default:
|
||||
return EffectivePricingConfig{}, fmt.Errorf("pricing rule %s uses unsupported calculator %s", ruleKey, calculatorType)
|
||||
}
|
||||
if err := ValidateEffectivePricingRuleShape(resourceType, unit, calculatorType); err != nil {
|
||||
return EffectivePricingConfig{}, fmt.Errorf("pricing rule %s: %w", ruleKey, err)
|
||||
}
|
||||
if strings.HasPrefix(strings.TrimSpace(basePrice), "-") {
|
||||
return EffectivePricingConfig{}, fmt.Errorf("pricing rule %s has a negative base price", ruleKey)
|
||||
}
|
||||
@@ -372,6 +375,51 @@ ORDER BY priority ASC, resource_type ASC, rule_key ASC`, id)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ValidateEffectivePricingRuleShape(resourceType string, unit string, calculatorType string) error {
|
||||
unit = NormalizeEffectivePricingRuleUnit(resourceType, unit)
|
||||
if strings.TrimSpace(calculatorType) == "" {
|
||||
calculatorType = DefaultEffectivePricingCalculator(resourceType)
|
||||
}
|
||||
allowed := false
|
||||
switch resourceType {
|
||||
case "text_input", "text_cached_input", "text_output", "text_total":
|
||||
allowed = unit == "1k_tokens" && calculatorType == "token_usage"
|
||||
case "image", "image_edit":
|
||||
allowed = unit == "image" && calculatorType == "unit_weight"
|
||||
case "video":
|
||||
allowed = unit == "5s" && calculatorType == "duration_weight"
|
||||
case "music":
|
||||
allowed = (unit == "song" || unit == "item") && calculatorType == "unit_weight"
|
||||
case "audio":
|
||||
allowed = unit == "character" && calculatorType == "unit_weight"
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
if !allowed {
|
||||
return fmt.Errorf("unit %q and calculator %q do not match resource %q", unit, calculatorType, resourceType)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NormalizeEffectivePricingRuleUnit(resourceType string, unit string) string {
|
||||
unit = strings.TrimSpace(unit)
|
||||
if resourceType == "video" && unit == "video" {
|
||||
return "5s"
|
||||
}
|
||||
return unit
|
||||
}
|
||||
|
||||
func DefaultEffectivePricingCalculator(resourceType string) string {
|
||||
switch strings.TrimSpace(resourceType) {
|
||||
case "text_input", "text_cached_input", "text_output", "text_total":
|
||||
return "token_usage"
|
||||
case "video":
|
||||
return "duration_weight"
|
||||
default:
|
||||
return "unit_weight"
|
||||
}
|
||||
}
|
||||
|
||||
func addPricingRuleToConfig(config map[string]any, resourceType string, basePrice string, dynamicWeight map[string]any, formulaConfig map[string]any) {
|
||||
resourceConfig := map[string]any{"basePrice": basePrice}
|
||||
if len(dynamicWeight) > 0 {
|
||||
@@ -591,7 +639,7 @@ func normalizePricingRule(input PricingRuleInput, index int, defaultCurrency str
|
||||
input.RuleKey = strings.TrimSpace(input.RuleKey)
|
||||
input.DisplayName = strings.TrimSpace(input.DisplayName)
|
||||
input.ResourceType = strings.TrimSpace(input.ResourceType)
|
||||
input.Unit = strings.TrimSpace(input.Unit)
|
||||
input.Unit = NormalizeEffectivePricingRuleUnit(input.ResourceType, input.Unit)
|
||||
input.Currency = strings.TrimSpace(input.Currency)
|
||||
input.CalculatorType = strings.TrimSpace(input.CalculatorType)
|
||||
input.Status = strings.TrimSpace(input.Status)
|
||||
@@ -608,7 +656,7 @@ func normalizePricingRule(input PricingRuleInput, index int, defaultCurrency str
|
||||
input.Currency = defaultCurrency
|
||||
}
|
||||
if input.CalculatorType == "" {
|
||||
input.CalculatorType = "unit_weight"
|
||||
input.CalculatorType = DefaultEffectivePricingCalculator(input.ResourceType)
|
||||
}
|
||||
if input.Priority == 0 {
|
||||
input.Priority = (index + 1) * 10
|
||||
|
||||
Reference in New Issue
Block a user