feat(*): 模型计费与渠道目录 - 支持视频 Token 报价结算和稳定模型展示
This commit is contained in:
40
backend/internal/database/channel_model_description_test.go
Normal file
40
backend/internal/database/channel_model_description_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
40
backend/internal/database/channel_model_label_test.go
Normal file
40
backend/internal/database/channel_model_label_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
44
backend/internal/database/migrations_video_token_test.go
Normal file
44
backend/internal/database/migrations_video_token_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user