fix(billing): 封住发布前计费竞态
ci / verify (pull_request) Failing after 8s

阻止上游提交状态不明的任务被租约接管后重复执行,并为人工复核保留可操作的结算记录。

将生产提交绑定到当前估价签名,统一复用预处理快照,并补强规则形状、定点溢出与历史规则兼容校验。

已通过 PostgreSQL 16 集成测试、Go 全量测试与静态检查、前端测试与构建、OpenAPI、依赖审计、镜像、迁移、流水线和 SemVer 门禁。
This commit is contained in:
2026-07-21 10:23:58 +08:00
parent 257ee09e58
commit 8beb8501fa
18 changed files with 762 additions and 75 deletions
+50 -2
View File
@@ -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