fix(billing): 视频时长按实际秒数线性计费

将五秒基础价按 duration / 5 比例结算,保留 provider 返回的小数时长,避免六秒视频被按两个完整单位收费。

影响:所有使用 5s 视频基础价的模型,quantity 与 durationUnitCount 允许小数。新增迁移同步现存规则的旧 ceil 公式元数据。

验证:go vet ./...;pnpm lint;pnpm test;pnpm build;./tests/ci/migrations-test.sh
This commit is contained in:
2026-07-21 13:32:23 +08:00
parent e533ec2367
commit e3dfe8162b
8 changed files with 178 additions and 24 deletions
+15 -7
View File
@@ -10,6 +10,8 @@ import (
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)
const videoBillingUnitSeconds = 5
type EstimateResult struct {
Items []any `json:"items"`
Resolver string `json:"resolver"`
@@ -135,7 +137,7 @@ func (s *Service) billings(ctx context.Context, user *auth.User, kind string, bo
baseKey = "videoBase"
duration, durationSource := billingDurationSeconds(body, response)
audioEnabled, audioSource := billingAudioEnabled(body, response)
durationUnits := math.Max(1, math.Ceil(duration/5))
durationUnits := videoDurationUnits(duration)
amount := float64(count) *
durationUnits *
resourcePrice(config, resource, baseKey, "basePrice") *
@@ -144,7 +146,7 @@ func (s *Service) billings(ctx context.Context, user *auth.User, kind string, bo
resourceWeight(config, resource, "referenceVideoWeights", boolWeightKey(requestHasReferenceVideo(body))) *
resourceWeight(config, resource, "voiceSpecifiedWeights", boolWeightKey(requestHasVoiceID(body, audioEnabled))) *
discount
return []any{billingLineWithDetails(candidate, resource, unit, count*int(durationUnits), roundPrice(amount), discount, simulated, map[string]any{
return []any{billingLineWithDetails(candidate, resource, unit, videoDurationQuantity(duration, count), roundPrice(amount), discount, simulated, map[string]any{
"count": count,
"audio": audioEnabled,
"audioSource": audioSource,
@@ -416,6 +418,16 @@ func weightValueAliases(key string, name string) []string {
}
}
func videoDurationUnits(durationSeconds float64) float64 {
return videoDurationQuantity(durationSeconds, 1)
}
func videoDurationQuantity(durationSeconds float64, count int) float64 {
const durationPrecision = 1_000_000_000
scaledDuration := math.Round(durationSeconds * durationPrecision)
return scaledDuration * float64(count) / (durationPrecision * videoBillingUnitSeconds)
}
func requestOutputCount(body map[string]any) int {
for _, key := range []string{"n", "count", "batch_size", "batchSize"} {
if value := int(math.Ceil(floatFromAny(body[key]))); value > 0 {
@@ -476,11 +488,7 @@ func generatedVideoDurationSeconds(result map[string]any) (float64, bool) {
if duration <= 0 {
continue
}
rounded := math.Round(duration)
if rounded <= 0 {
rounded = 1
}
return rounded, true
return duration, true
}
return 0, false
}