Migrate from SQLite to MySQL: Update database configuration and schema

This commit is contained in:
wood chen 2025-02-23 03:37:10 +08:00
parent 8d198e45af
commit a2932ecf6b
12 changed files with 382 additions and 62 deletions

View File

@ -1,8 +1,18 @@
PORT=8080 # MySQL数据库配置
GIN_MODE=debug DB_HOST=localhost # 数据库主机地址
OAUTH_CLIENT_ID=your_client_id_here DB_PORT=3306 # 数据库端口
OAUTH_CLIENT_SECRET=your_client_secret_here DB_USER=root # 数据库用户名
OAUTH_REDIRECT_URI=https://aimodels-prices.q58.club/api/auth/callback DB_PASSWORD=your_password # 数据库密码
OAUTH_AUTHORIZE_URL=https://connect.q58.club/oauth/authorize DB_NAME=aimodels # 数据库名称
OAUTH_TOKEN_URL=https://connect.q58.club/api/oauth/access_token
OAUTH_USER_URL=https://connect.q58.club/api/oauth/user # 服务器配置
PORT=8080 # 服务器监听端口
# OAuth配置如果需要
OAUTH_CLIENT_ID=your_client_id
OAUTH_CLIENT_SECRET=your_client_secret
OAUTH_REDIRECT_URI=http://localhost:8080/api/auth/callback
OAUTH_AUTHORIZE_URL=https://example.com/oauth/authorize
# 其他配置
GIN_MODE=debug # Gin运行模式debug或release

238
backend/cmd/migrate/main.go Normal file
View File

@ -0,0 +1,238 @@
package main
import (
"database/sql"
"fmt"
"log"
"os"
"aimodels-prices/config"
"aimodels-prices/database"
)
func main() {
// 加载配置
cfg, err := config.LoadConfig()
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
// 检查SQLite数据库文件是否存在
if _, err := os.Stat(cfg.SQLitePath); os.IsNotExist(err) {
log.Printf("SQLite database file not found at %s, skipping migration", cfg.SQLitePath)
os.Exit(0)
}
// 连接SQLite数据库
sqliteDB, err := database.InitSQLiteDB(cfg.SQLitePath)
if err != nil {
log.Fatalf("Failed to connect to SQLite database: %v", err)
}
defer sqliteDB.Close()
// 初始化MySQL数据库
if err := database.InitDB(cfg); err != nil {
log.Fatalf("Failed to initialize MySQL database: %v", err)
}
defer database.DB.Close()
// 开始迁移数据
if err := migrateData(sqliteDB, database.DB); err != nil {
log.Fatalf("Failed to migrate data: %v", err)
}
log.Println("Data migration completed successfully!")
}
func migrateData(sqliteDB *sql.DB, mysqlDB *sql.DB) error {
// 迁移用户数据
if err := migrateUsers(sqliteDB, mysqlDB); err != nil {
return fmt.Errorf("failed to migrate users: %v", err)
}
// 迁移会话数据
if err := migrateSessions(sqliteDB, mysqlDB); err != nil {
return fmt.Errorf("failed to migrate sessions: %v", err)
}
// 迁移提供商数据
if err := migrateProviders(sqliteDB, mysqlDB); err != nil {
return fmt.Errorf("failed to migrate providers: %v", err)
}
// 迁移价格数据
if err := migratePrices(sqliteDB, mysqlDB); err != nil {
return fmt.Errorf("failed to migrate prices: %v", err)
}
return nil
}
func migrateUsers(sqliteDB *sql.DB, mysqlDB *sql.DB) error {
rows, err := sqliteDB.Query("SELECT id, username, email, role, created_at, updated_at, deleted_at FROM user")
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var (
id uint
username string
email string
role string
createdAt string
updatedAt string
deletedAt sql.NullString
)
if err := rows.Scan(&id, &username, &email, &role, &createdAt, &updatedAt, &deletedAt); err != nil {
return err
}
_, err = mysqlDB.Exec(
"INSERT INTO user (id, username, email, role, created_at, updated_at, deleted_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
id, username, email, role, createdAt, updatedAt, deletedAt.String,
)
if err != nil {
return err
}
}
return rows.Err()
}
func migrateSessions(sqliteDB *sql.DB, mysqlDB *sql.DB) error {
rows, err := sqliteDB.Query("SELECT id, user_id, expires_at, created_at, updated_at, deleted_at FROM session")
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var (
id string
userID uint
expiresAt string
createdAt string
updatedAt string
deletedAt sql.NullString
)
if err := rows.Scan(&id, &userID, &expiresAt, &createdAt, &updatedAt, &deletedAt); err != nil {
return err
}
_, err = mysqlDB.Exec(
"INSERT INTO session (id, user_id, expires_at, created_at, updated_at, deleted_at) VALUES (?, ?, ?, ?, ?, ?)",
id, userID, expiresAt, createdAt, updatedAt, deletedAt.String,
)
if err != nil {
return err
}
}
return rows.Err()
}
func migrateProviders(sqliteDB *sql.DB, mysqlDB *sql.DB) error {
rows, err := sqliteDB.Query("SELECT id, name, icon, created_at, updated_at, created_by FROM provider")
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var (
id uint
name string
icon sql.NullString
createdAt string
updatedAt string
createdBy string
)
if err := rows.Scan(&id, &name, &icon, &createdAt, &updatedAt, &createdBy); err != nil {
return err
}
_, err = mysqlDB.Exec(
"INSERT INTO provider (id, name, icon, created_at, updated_at, created_by) VALUES (?, ?, ?, ?, ?, ?)",
id, name, icon.String, createdAt, updatedAt, createdBy,
)
if err != nil {
return err
}
}
return rows.Err()
}
func migratePrices(sqliteDB *sql.DB, mysqlDB *sql.DB) error {
rows, err := sqliteDB.Query(`
SELECT id, model, model_type, billing_type, channel_type, currency,
input_price, output_price, price_source, status, created_at, updated_at,
created_by, temp_model, temp_model_type, temp_billing_type, temp_channel_type,
temp_currency, temp_input_price, temp_output_price, temp_price_source, updated_by
FROM price
`)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var (
id uint
model string
modelType string
billingType string
channelType string
currency string
inputPrice float64
outputPrice float64
priceSource string
status string
createdAt string
updatedAt string
createdBy string
tempModel sql.NullString
tempModelType sql.NullString
tempBillingType sql.NullString
tempChannelType sql.NullString
tempCurrency sql.NullString
tempInputPrice sql.NullFloat64
tempOutputPrice sql.NullFloat64
tempPriceSource sql.NullString
updatedBy sql.NullString
)
if err := rows.Scan(
&id, &model, &modelType, &billingType, &channelType, &currency,
&inputPrice, &outputPrice, &priceSource, &status, &createdAt, &updatedAt,
&createdBy, &tempModel, &tempModelType, &tempBillingType, &tempChannelType,
&tempCurrency, &tempInputPrice, &tempOutputPrice, &tempPriceSource, &updatedBy,
); err != nil {
return err
}
_, err = mysqlDB.Exec(`
INSERT INTO price (
id, model, model_type, billing_type, channel_type, currency,
input_price, output_price, price_source, status, created_at, updated_at,
created_by, temp_model, temp_model_type, temp_billing_type, temp_channel_type,
temp_currency, temp_input_price, temp_output_price, temp_price_source, updated_by
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`,
id, model, modelType, billingType, channelType, currency,
inputPrice, outputPrice, priceSource, status, createdAt, updatedAt,
createdBy, tempModel.String, tempModelType.String, tempBillingType.String, tempChannelType.String,
tempCurrency.String, tempInputPrice.Float64, tempOutputPrice.Float64, tempPriceSource.String, updatedBy.String,
)
if err != nil {
return err
}
}
return rows.Err()
}

View File

@ -9,8 +9,18 @@ import (
) )
type Config struct { type Config struct {
DBPath string // MySQL配置
DBHost string
DBPort string
DBUser string
DBPassword string
DBName string
// 其他配置
ServerPort string ServerPort string
// SQLite配置用于数据迁移
SQLitePath string
} }
func LoadConfig() (*Config, error) { func LoadConfig() (*Config, error) {
@ -31,8 +41,18 @@ func LoadConfig() (*Config, error) {
} }
config := &Config{ config := &Config{
DBPath: filepath.Join(dbDir, "aimodels.db"), // MySQL配置
DBHost: getEnv("DB_HOST", "localhost"),
DBPort: getEnv("DB_PORT", "3306"),
DBUser: getEnv("DB_USER", "root"),
DBPassword: getEnv("DB_PASSWORD", ""),
DBName: getEnv("DB_NAME", "aimodels"),
// 其他配置
ServerPort: getEnv("PORT", "8080"), ServerPort: getEnv("PORT", "8080"),
// SQLite路径用于数据迁移
SQLitePath: filepath.Join(dbDir, "aimodels.db"),
} }
return config, nil return config, nil

View File

@ -2,10 +2,13 @@ package database
import ( import (
"database/sql" "database/sql"
"fmt"
"log" "log"
_ "github.com/go-sql-driver/mysql"
_ "modernc.org/sqlite" _ "modernc.org/sqlite"
"aimodels-prices/config"
"aimodels-prices/models" "aimodels-prices/models"
) )
@ -13,16 +16,27 @@ import (
var DB *sql.DB var DB *sql.DB
// InitDB 初始化数据库连接 // InitDB 初始化数据库连接
func InitDB(dbPath string) error { func InitDB(cfg *config.Config) error {
var err error var err error
DB, err = sql.Open("sqlite", dbPath)
// 构建MySQL DSN
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
cfg.DBUser,
cfg.DBPassword,
cfg.DBHost,
cfg.DBPort,
cfg.DBName,
)
// 连接MySQL
DB, err = sql.Open("mysql", dsn)
if err != nil { if err != nil {
return err return fmt.Errorf("failed to connect to MySQL: %v", err)
} }
// 测试连接 // 测试连接
if err = DB.Ping(); err != nil { if err = DB.Ping(); err != nil {
return err return fmt.Errorf("failed to ping MySQL: %v", err)
} }
// 设置连接池参数 // 设置连接池参数
@ -31,12 +45,26 @@ func InitDB(dbPath string) error {
// 创建表结构 // 创建表结构
if err = createTables(); err != nil { if err = createTables(); err != nil {
return err return fmt.Errorf("failed to create tables: %v", err)
} }
return nil return nil
} }
// InitSQLiteDB 初始化SQLite数据库连接用于数据迁移
func InitSQLiteDB(dbPath string) (*sql.DB, error) {
db, err := sql.Open("sqlite", dbPath)
if err != nil {
return nil, fmt.Errorf("failed to connect to SQLite: %v", err)
}
if err = db.Ping(); err != nil {
return nil, fmt.Errorf("failed to ping SQLite: %v", err)
}
return db, nil
}
// createTables 创建数据库表 // createTables 创建数据库表
func createTables() error { func createTables() error {
// 创建用户表 // 创建用户表

View File

@ -4,6 +4,7 @@ go 1.21
require ( require (
github.com/gin-gonic/gin v1.9.1 github.com/gin-gonic/gin v1.9.1
github.com/go-sql-driver/mysql v1.7.1
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
modernc.org/sqlite v1.28.0 modernc.org/sqlite v1.28.0
) )

View File

@ -23,6 +23,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=

View File

@ -20,7 +20,7 @@ func main() {
} }
// 初始化数据库 // 初始化数据库
if err := database.InitDB(cfg.DBPath); err != nil { if err := database.InitDB(cfg); err != nil {
log.Fatalf("Failed to initialize database: %v", err) log.Fatalf("Failed to initialize database: %v", err)
} }
defer database.DB.Close() defer database.DB.Close()

View File

@ -34,28 +34,28 @@ type Price struct {
func CreatePriceTableSQL() string { func CreatePriceTableSQL() string {
return ` return `
CREATE TABLE IF NOT EXISTS price ( CREATE TABLE IF NOT EXISTS price (
id INTEGER PRIMARY KEY AUTOINCREMENT, id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
model TEXT NOT NULL, model VARCHAR(255) NOT NULL,
model_type TEXT NOT NULL, model_type VARCHAR(50) NOT NULL,
billing_type TEXT NOT NULL, billing_type VARCHAR(50) NOT NULL,
channel_type TEXT NOT NULL, channel_type VARCHAR(50) NOT NULL,
currency TEXT NOT NULL, currency VARCHAR(10) NOT NULL,
input_price REAL NOT NULL, input_price DECIMAL(10,6) NOT NULL,
output_price REAL NOT NULL, output_price DECIMAL(10,6) NOT NULL,
price_source TEXT NOT NULL, price_source VARCHAR(255) NOT NULL,
status TEXT NOT NULL DEFAULT 'pending', status VARCHAR(50) NOT NULL DEFAULT 'pending',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
created_by TEXT NOT NULL, created_by VARCHAR(255) NOT NULL,
temp_model TEXT, temp_model VARCHAR(255),
temp_model_type TEXT, temp_model_type VARCHAR(50),
temp_billing_type TEXT, temp_billing_type VARCHAR(50),
temp_channel_type TEXT, temp_channel_type VARCHAR(50),
temp_currency TEXT, temp_currency VARCHAR(10),
temp_input_price REAL, temp_input_price DECIMAL(10,6),
temp_output_price REAL, temp_output_price DECIMAL(10,6),
temp_price_source TEXT, temp_price_source VARCHAR(255),
updated_by TEXT, updated_by VARCHAR(255),
FOREIGN KEY (channel_type) REFERENCES provider(id) FOREIGN KEY (channel_type) REFERENCES provider(id)
)` ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`
} }

View File

@ -15,11 +15,11 @@ type Provider struct {
func CreateProviderTableSQL() string { func CreateProviderTableSQL() string {
return ` return `
CREATE TABLE IF NOT EXISTS provider ( CREATE TABLE IF NOT EXISTS provider (
id INTEGER PRIMARY KEY, id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name TEXT NOT NULL, name VARCHAR(255) NOT NULL,
icon TEXT, icon VARCHAR(1024),
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
created_by TEXT NOT NULL created_by VARCHAR(255) NOT NULL
)` ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`
} }

View File

@ -24,32 +24,32 @@ type Session struct {
DeletedAt *time.Time `json:"deleted_at,omitempty"` DeletedAt *time.Time `json:"deleted_at,omitempty"`
} }
// CreateTableSQL 返回创建用户表的 SQL // CreateUserTableSQL 返回创建用户表的 SQL
func CreateUserTableSQL() string { func CreateUserTableSQL() string {
return ` return `
CREATE TABLE IF NOT EXISTS user ( CREATE TABLE IF NOT EXISTS user (
id INTEGER PRIMARY KEY AUTOINCREMENT, id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
username TEXT UNIQUE NOT NULL, username VARCHAR(255) UNIQUE NOT NULL,
email TEXT UNIQUE NOT NULL, email VARCHAR(255) UNIQUE NOT NULL,
role TEXT NOT NULL DEFAULT 'user', role VARCHAR(50) NOT NULL DEFAULT 'user',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at DATETIME deleted_at TIMESTAMP NULL
)` ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`
} }
// CreateSessionTableSQL 返回创建会话表的 SQL // CreateSessionTableSQL 返回创建会话表的 SQL
func CreateSessionTableSQL() string { func CreateSessionTableSQL() string {
return ` return `
CREATE TABLE IF NOT EXISTS session ( CREATE TABLE IF NOT EXISTS session (
id TEXT PRIMARY KEY, id VARCHAR(255) PRIMARY KEY,
user_id INTEGER NOT NULL, user_id BIGINT UNSIGNED NOT NULL,
expires_at DATETIME NOT NULL, expires_at TIMESTAMP NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at DATETIME, deleted_at TIMESTAMP NULL,
FOREIGN KEY (user_id) REFERENCES user(id) FOREIGN KEY (user_id) REFERENCES user(id)
)` ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`
} }
// GetUser 获取会话关联的用户 // GetUser 获取会话关联的用户

19
scripts/build.sh Normal file
View File

@ -0,0 +1,19 @@
#!/bin/bash
# 设置Go环境变量
export CGO_ENABLED=0
export GOOS=linux
# 编译 AMD64 版本
echo "Building AMD64 version..."
export GOARCH=amd64
go build -o backend/main-amd64 backend/main.go
go build -o backend/migrate-amd64 backend/cmd/migrate/main.go
# 编译 ARM64 版本
echo "Building ARM64 version..."
export GOARCH=arm64
go build -o backend/main-arm64 backend/main.go
go build -o backend/migrate-arm64 backend/cmd/migrate/main.go
echo "Build completed!"

View File

@ -5,7 +5,9 @@ echo "执行数据库迁移..."
./migrate ./migrate
# 启动后端服务 # 启动后端服务
echo "启动后端服务..."
./main & ./main &
# 启动 nginx # 启动 nginx
echo "启动 Nginx..."
nginx -g 'daemon off;' nginx -g 'daemon off;'