From f1e6c1bf7796d80bcf7c0a3a425b868d634ef7e5 Mon Sep 17 00:00:00 2001 From: CaIon Date: Wed, 4 Feb 2026 01:42:55 +0800 Subject: [PATCH] feat(subscription): implement SQLite support for SubscriptionPlan table creation and migration #2823 --- model/main.go | 106 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 100 insertions(+), 6 deletions(-) diff --git a/model/main.go b/model/main.go index b1d363355..e78970950 100644 --- a/model/main.go +++ b/model/main.go @@ -271,7 +271,6 @@ func migrateDB() error { &TwoFA{}, &TwoFABackupCode{}, &Checkin{}, - &SubscriptionPlan{}, &SubscriptionOrder{}, &UserSubscription{}, &SubscriptionPreConsumeRecord{}, @@ -279,6 +278,15 @@ func migrateDB() error { if err != nil { return err } + if common.UsingSQLite { + if err := ensureSubscriptionPlanTableSQLite(); err != nil { + return err + } + } else { + if err := DB.AutoMigrate(&SubscriptionPlan{}); err != nil { + return err + } + } return nil } @@ -309,7 +317,6 @@ func migrateDBFast() error { {&TwoFA{}, "TwoFA"}, {&TwoFABackupCode{}, "TwoFABackupCode"}, {&Checkin{}, "Checkin"}, - {&SubscriptionPlan{}, "SubscriptionPlan"}, {&SubscriptionOrder{}, "SubscriptionOrder"}, {&UserSubscription{}, "UserSubscription"}, {&SubscriptionPreConsumeRecord{}, "SubscriptionPreConsumeRecord"}, @@ -337,6 +344,15 @@ func migrateDBFast() error { return err } } + if common.UsingSQLite { + if err := ensureSubscriptionPlanTableSQLite(); err != nil { + return err + } + } else { + if err := DB.AutoMigrate(&SubscriptionPlan{}); err != nil { + return err + } + } common.SysLog("database migrated") return nil } @@ -349,9 +365,91 @@ func migrateLOGDB() error { return nil } +type sqliteColumnDef struct { + Name string + DDL string +} + +func ensureSubscriptionPlanTableSQLite() error { + if !common.UsingSQLite { + return nil + } + tableName := "subscription_plans" + if !DB.Migrator().HasTable(tableName) { + createSQL := `CREATE TABLE ` + "`" + tableName + "`" + ` ( +` + "`id`" + ` integer, +` + "`title`" + ` varchar(128) NOT NULL, +` + "`subtitle`" + ` varchar(255) DEFAULT '', +` + "`price_amount`" + ` decimal(10,6) NOT NULL, +` + "`currency`" + ` varchar(8) NOT NULL DEFAULT 'USD', +` + "`duration_unit`" + ` varchar(16) NOT NULL DEFAULT 'month', +` + "`duration_value`" + ` integer NOT NULL DEFAULT 1, +` + "`custom_seconds`" + ` bigint NOT NULL DEFAULT 0, +` + "`enabled`" + ` numeric DEFAULT 1, +` + "`sort_order`" + ` integer DEFAULT 0, +` + "`stripe_price_id`" + ` varchar(128) DEFAULT '', +` + "`creem_product_id`" + ` varchar(128) DEFAULT '', +` + "`max_purchase_per_user`" + ` integer DEFAULT 0, +` + "`upgrade_group`" + ` varchar(64) DEFAULT '', +` + "`total_amount`" + ` bigint NOT NULL DEFAULT 0, +` + "`quota_reset_period`" + ` varchar(16) DEFAULT 'never', +` + "`quota_reset_custom_seconds`" + ` bigint DEFAULT 0, +` + "`created_at`" + ` bigint, +` + "`updated_at`" + ` bigint, +PRIMARY KEY (` + "`id`" + `) +)` + return DB.Exec(createSQL).Error + } + var cols []struct { + Name string `gorm:"column:name"` + } + if err := DB.Raw("PRAGMA table_info(`" + tableName + "`)").Scan(&cols).Error; err != nil { + return err + } + existing := make(map[string]struct{}, len(cols)) + for _, c := range cols { + existing[c.Name] = struct{}{} + } + required := []sqliteColumnDef{ + {Name: "title", DDL: "`title` varchar(128) NOT NULL"}, + {Name: "subtitle", DDL: "`subtitle` varchar(255) DEFAULT ''"}, + {Name: "price_amount", DDL: "`price_amount` decimal(10,6) NOT NULL"}, + {Name: "currency", DDL: "`currency` varchar(8) NOT NULL DEFAULT 'USD'"}, + {Name: "duration_unit", DDL: "`duration_unit` varchar(16) NOT NULL DEFAULT 'month'"}, + {Name: "duration_value", DDL: "`duration_value` integer NOT NULL DEFAULT 1"}, + {Name: "custom_seconds", DDL: "`custom_seconds` bigint NOT NULL DEFAULT 0"}, + {Name: "enabled", DDL: "`enabled` numeric DEFAULT 1"}, + {Name: "sort_order", DDL: "`sort_order` integer DEFAULT 0"}, + {Name: "stripe_price_id", DDL: "`stripe_price_id` varchar(128) DEFAULT ''"}, + {Name: "creem_product_id", DDL: "`creem_product_id` varchar(128) DEFAULT ''"}, + {Name: "max_purchase_per_user", DDL: "`max_purchase_per_user` integer DEFAULT 0"}, + {Name: "upgrade_group", DDL: "`upgrade_group` varchar(64) DEFAULT ''"}, + {Name: "total_amount", DDL: "`total_amount` bigint NOT NULL DEFAULT 0"}, + {Name: "quota_reset_period", DDL: "`quota_reset_period` varchar(16) DEFAULT 'never'"}, + {Name: "quota_reset_custom_seconds", DDL: "`quota_reset_custom_seconds` bigint DEFAULT 0"}, + {Name: "created_at", DDL: "`created_at` bigint"}, + {Name: "updated_at", DDL: "`updated_at` bigint"}, + } + for _, col := range required { + if _, ok := existing[col.Name]; ok { + continue + } + if err := DB.Exec("ALTER TABLE `" + tableName + "` ADD COLUMN " + col.DDL).Error; err != nil { + return err + } + } + return nil +} + // migrateSubscriptionPlanPriceAmount migrates price_amount column from float/double to decimal(10,6) // This is safe to run multiple times - it checks the column type first func migrateSubscriptionPlanPriceAmount() { + // SQLite doesn't support ALTER COLUMN, and its type affinity handles this automatically + // Skip early to avoid GORM parsing the existing table DDL which may cause issues + if common.UsingSQLite { + return + } + tableName := "subscription_plans" columnName := "price_amount" @@ -387,10 +485,6 @@ func migrateSubscriptionPlanPriceAmount() { } alterSQL = fmt.Sprintf("ALTER TABLE %s MODIFY COLUMN %s decimal(10,6) NOT NULL DEFAULT 0", tableName, columnName) - } else if common.UsingSQLite { - // SQLite doesn't support ALTER COLUMN, but its type affinity handles this automatically - // The column will accept decimal values without modification - return } else { return }