feat(*): 模型计费与渠道目录 - 支持视频 Token 报价结算和稳定模型展示

This commit is contained in:
ddCat
2026-09-18 23:26:54 +08:00
parent 90add8c466
commit 1a79005d25
57 changed files with 2904 additions and 341 deletions

View File

@@ -0,0 +1,40 @@
package database
import "testing"
func TestChannelModelDescriptionMigration(t *testing.T) {
db, err := Open(Config{Driver: "sqlite", DSN: ":memory:"})
if err != nil {
t.Fatal(err)
}
for _, sql := range []string{
`CREATE TABLE channel_models (id TEXT PRIMARY KEY, model_key TEXT, channel_label TEXT, unit_price_microcredits INTEGER)`,
`INSERT INTO channel_models VALUES ('model-a', 'seedance-2.0', '优惠渠道-993', 300000)`,
} {
if err := db.Exec(sql).Error; err != nil {
t.Fatal(err)
}
}
for i := 0; i < 2; i++ {
if err := migrateChannelModelDescription(db); err != nil {
t.Fatal(err)
}
}
var row struct {
ModelKey, ChannelLabel, Description string
UnitPriceMicrocredits int64
}
if err := db.Table("channel_models").Where("id = ?", "model-a").First(&row).Error; err != nil {
t.Fatal(err)
}
if row.ModelKey != "seedance-2.0" || row.ChannelLabel != "优惠渠道-993" || row.UnitPriceMicrocredits != 300000 || row.Description != "" {
t.Fatalf("migration changed existing data: %#v", row)
}
if err := db.Exec(`INSERT INTO channel_models (id) VALUES ('model-b')`).Error; err != nil {
t.Fatal(err)
}
var count int64
if err := db.Table("channel_models").Where("description = ''").Count(&count).Error; err != nil || count != 2 {
t.Fatalf("description default missing: count=%d err=%v", count, err)
}
}

View File

@@ -0,0 +1,40 @@
package database
import "testing"
func TestChannelModelLabelMigrationPreservesIdentityAndPricing(t *testing.T) {
db, err := Open(Config{Driver: "sqlite", DSN: ":memory:"})
if err != nil {
t.Fatal(err)
}
for _, sql := range []string{
`CREATE TABLE channel_models (id TEXT PRIMARY KEY, channel_id TEXT, model_key TEXT, provider_model_key TEXT, price_version INTEGER, unit_price_microcredits INTEGER)`,
`INSERT INTO channel_models VALUES ('model-a', 'channel-a', 'seedance-2.0', 'upstream-v2', 7, 300000)`,
} {
if err := db.Exec(sql).Error; err != nil {
t.Fatal(err)
}
}
for i := 0; i < 2; i++ {
if err := migrateChannelModelLabel(db); err != nil {
t.Fatal(err)
}
}
var row struct {
ID, ChannelID, ModelKey, ProviderModelKey, ChannelLabel string
PriceVersion, UnitPriceMicrocredits int64
}
if err := db.Table("channel_models").First(&row).Error; err != nil {
t.Fatal(err)
}
if row.ID != "model-a" || row.ChannelID != "channel-a" || row.ModelKey != "seedance-2.0" || row.ProviderModelKey != "upstream-v2" || row.ChannelLabel != "" || row.PriceVersion != 7 || row.UnitPriceMicrocredits != 300000 {
t.Fatalf("migration changed existing identity/pricing: %#v", row)
}
if err := db.Exec(`INSERT INTO channel_models (id) VALUES ('model-b')`).Error; err != nil {
t.Fatal(err)
}
var count int64
if err := db.Table("channel_models").Where("channel_label = ''").Count(&count).Error; err != nil || count != 2 {
t.Fatalf("empty label default missing: count=%d err=%v", count, err)
}
}

View File

@@ -11,7 +11,7 @@ import (
"gorm.io/gorm"
)
const CurrentSchemaVersion int64 = 23
const CurrentSchemaVersion int64 = 26
const baselineSchemaChecksum = "sha256:open-ai-canvas-schema-v1-20260830"
const schemaMigrationAppliedAtIndexChecksum = "sha256:schema-migrations-applied-at-index-v2-20260830"
@@ -89,6 +89,34 @@ var schemaMigrations = []migration{
{version: 23, name: "canvas_revision_history", checksum: "sha256:canvas-revision-history-v23-20260918", apply: func(tx *gorm.DB) error {
return tx.AutoMigrate(&model.CanvasProject{}, &model.CanvasSnapshot{}, &model.CanvasSnapshotResource{})
}},
{version: 24, name: "channel_model_label", checksum: "sha256:channel-model-label-v24", apply: migrateChannelModelLabel},
{version: 25, name: "video_token_formula_snapshot", checksum: "sha256:video-token-formula-snapshot-v25", apply: migrateVideoTokenFormulaSnapshot},
{version: 26, name: "channel_model_description", checksum: "sha256:channel-model-description-v26", apply: migrateChannelModelDescription},
}
func migrateChannelModelDescription(tx *gorm.DB) error {
if tx.Migrator().HasColumn(&model.ChannelModel{}, "Description") {
return nil
}
return tx.Migrator().AddColumn(&model.ChannelModel{}, "Description")
}
func migrateVideoTokenFormulaSnapshot(tx *gorm.DB) error {
for _, field := range []string{"VideoFormulaTokens", "UsageSource"} {
if !tx.Migrator().HasColumn(&model.BillingOrder{}, field) {
if err := tx.Migrator().AddColumn(&model.BillingOrder{}, field); err != nil {
return fmt.Errorf("增加视频 Token 结算字段 %s:%w", field, err)
}
}
}
return nil
}
func migrateChannelModelLabel(tx *gorm.DB) error {
if tx.Migrator().HasColumn(&model.ChannelModel{}, "ChannelLabel") {
return nil
}
return tx.Migrator().AddColumn(&model.ChannelModel{}, "ChannelLabel")
}
func migrateSchemaV14(tx *gorm.DB) error {

View File

@@ -446,7 +446,7 @@ func TestMigrateSchemaV4AddsResourceUploadKeyToExistingSchema(t *testing.T) {
if err := db.Exec(`CREATE TABLE resources (id TEXT PRIMARY KEY, user_id TEXT NOT NULL)`).Error; err != nil {
t.Fatal(err)
}
if err := db.AutoMigrate(&model.ModelChannel{}, &model.ChannelModel{}); err != nil {
if err := db.AutoMigrate(&model.ModelChannel{}, &model.ChannelModel{}, &model.BillingOrder{}); err != nil {
t.Fatal(err)
}
if err := db.AutoMigrate(&schemaMigration{}); err != nil {
@@ -490,7 +490,7 @@ func TestMigrateSchemaRepairsLegacyAssetFoldersMigrationOrder(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if err := db.AutoMigrate(&model.Resource{}, &model.Asset{}, &model.AssetFolder{}, &model.ModelChannel{}, &model.ChannelModel{}); err != nil {
if err := db.AutoMigrate(&model.Resource{}, &model.Asset{}, &model.AssetFolder{}, &model.ModelChannel{}, &model.ChannelModel{}, &model.BillingOrder{}); err != nil {
t.Fatal(err)
}
if err := db.AutoMigrate(&schemaMigration{}); err != nil {

View File

@@ -0,0 +1,44 @@
package database
import (
"testing"
"infinite-canvas/backend/internal/model"
)
func TestMigrateSchemaV25PreservesHistoricalBillingOrders(t *testing.T) {
db, err := Open(Config{Driver: "sqlite", DSN: "file:migration-video-token-formula-v25?mode=memory&cache=shared"})
if err != nil {
t.Fatal(err)
}
if err := MigrateSchema(db); err != nil {
t.Fatal(err)
}
for _, field := range []string{"VideoFormulaTokens", "UsageSource"} {
if err := db.Migrator().DropColumn(&model.BillingOrder{}, field); err != nil {
t.Fatal(err)
}
}
if err := db.Exec(`INSERT INTO billing_orders (id, user_id, idempotency_key, capability, billing_mode, quantity, amount_microcredits, output_tokens, usage_available, status) VALUES ('legacy', 'user', 'task', 'video', 'token', 118800, 1900800, 108000, true, 'settled')`).Error; err != nil {
t.Fatal(err)
}
if err := db.Where("version = ?", 25).Delete(&schemaMigration{}).Error; err != nil {
t.Fatal(err)
}
for range 2 {
if err := MigrateSchema(db); err != nil {
t.Fatalf("upgrade to v25: %v", err)
}
}
var order model.BillingOrder
if err := db.First(&order, "id = ?", "legacy").Error; err != nil {
t.Fatal(err)
}
if order.VideoFormulaTokens != 0 || order.UsageSource != "" || order.Quantity != 118800 || order.AmountMicrocredits != 1900800 || order.OutputTokens != 108000 || !order.UsageAvailable || order.Status != model.BillingStatusSettled {
t.Fatalf("historical billing changed during migration: %+v", order)
}
status, err := ReadSchemaStatus(db)
if err != nil || !status.Ready || status.Current != CurrentSchemaVersion {
t.Fatalf("schema not ready after upgrade: %+v, %v", status, err)
}
}