diff --git a/.env.example b/.env.example index 932e8e1..06beaa4 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,18 @@ -PORT=8080 -GIN_MODE=debug -OAUTH_CLIENT_ID=your_client_id_here -OAUTH_CLIENT_SECRET=your_client_secret_here -OAUTH_REDIRECT_URI=https://aimodels-prices.q58.club/api/auth/callback -OAUTH_AUTHORIZE_URL=https://connect.q58.club/oauth/authorize -OAUTH_TOKEN_URL=https://connect.q58.club/api/oauth/access_token -OAUTH_USER_URL=https://connect.q58.club/api/oauth/user \ No newline at end of file +# MySQL数据库配置 +DB_HOST=localhost # 数据库主机地址 +DB_PORT=3306 # 数据库端口 +DB_USER=root # 数据库用户名 +DB_PASSWORD=your_password # 数据库密码 +DB_NAME=aimodels # 数据库名称 + +# 服务器配置 +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 \ No newline at end of file diff --git a/backend/cmd/migrate/main.go b/backend/cmd/migrate/main.go new file mode 100644 index 0000000..c2ecfe6 --- /dev/null +++ b/backend/cmd/migrate/main.go @@ -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, ¤cy, + &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() +} diff --git a/backend/config/config.go b/backend/config/config.go index d35e6e8..f2e6d6f 100644 --- a/backend/config/config.go +++ b/backend/config/config.go @@ -9,8 +9,18 @@ import ( ) type Config struct { - DBPath string + // MySQL配置 + DBHost string + DBPort string + DBUser string + DBPassword string + DBName string + + // 其他配置 ServerPort string + + // SQLite配置(用于数据迁移) + SQLitePath string } func LoadConfig() (*Config, error) { @@ -31,8 +41,18 @@ func LoadConfig() (*Config, error) { } 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"), + + // SQLite路径(用于数据迁移) + SQLitePath: filepath.Join(dbDir, "aimodels.db"), } return config, nil diff --git a/backend/database/db.go b/backend/database/db.go index ad9e98c..7f24500 100644 --- a/backend/database/db.go +++ b/backend/database/db.go @@ -2,10 +2,13 @@ package database import ( "database/sql" + "fmt" "log" + _ "github.com/go-sql-driver/mysql" _ "modernc.org/sqlite" + "aimodels-prices/config" "aimodels-prices/models" ) @@ -13,16 +16,27 @@ import ( var DB *sql.DB // InitDB 初始化数据库连接 -func InitDB(dbPath string) error { +func InitDB(cfg *config.Config) 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 { - return err + return fmt.Errorf("failed to connect to MySQL: %v", err) } // 测试连接 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 { - return err + return fmt.Errorf("failed to create tables: %v", err) } 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 创建数据库表 func createTables() error { // 创建用户表 diff --git a/backend/go.mod b/backend/go.mod index c7064ed..a40d875 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -4,6 +4,7 @@ go 1.21 require ( github.com/gin-gonic/gin v1.9.1 + github.com/go-sql-driver/mysql v1.7.1 github.com/joho/godotenv v1.5.1 modernc.org/sqlite v1.28.0 ) diff --git a/backend/go.sum b/backend/go.sum index 7a1109b..3e755f3 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -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/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-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/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= diff --git a/backend/main.go b/backend/main.go index e2b5fee..c4143cf 100644 --- a/backend/main.go +++ b/backend/main.go @@ -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) } defer database.DB.Close() diff --git a/backend/models/price.go b/backend/models/price.go index 0c7ca05..2c79a59 100644 --- a/backend/models/price.go +++ b/backend/models/price.go @@ -34,28 +34,28 @@ type Price struct { func CreatePriceTableSQL() string { return ` CREATE TABLE IF NOT EXISTS price ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - model TEXT NOT NULL, - model_type TEXT NOT NULL, - billing_type TEXT NOT NULL, - channel_type TEXT NOT NULL, - currency TEXT NOT NULL, - input_price REAL NOT NULL, - output_price REAL NOT NULL, - price_source TEXT NOT NULL, - status TEXT NOT NULL DEFAULT 'pending', - created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_by TEXT NOT NULL, - temp_model TEXT, - temp_model_type TEXT, - temp_billing_type TEXT, - temp_channel_type TEXT, - temp_currency TEXT, - temp_input_price REAL, - temp_output_price REAL, - temp_price_source TEXT, - updated_by TEXT, + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + model VARCHAR(255) NOT NULL, + model_type VARCHAR(50) NOT NULL, + billing_type VARCHAR(50) NOT NULL, + channel_type VARCHAR(50) NOT NULL, + currency VARCHAR(10) NOT NULL, + input_price DECIMAL(10,6) NOT NULL, + output_price DECIMAL(10,6) NOT NULL, + price_source VARCHAR(255) NOT NULL, + status VARCHAR(50) NOT NULL DEFAULT 'pending', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + created_by VARCHAR(255) NOT NULL, + temp_model VARCHAR(255), + temp_model_type VARCHAR(50), + temp_billing_type VARCHAR(50), + temp_channel_type VARCHAR(50), + temp_currency VARCHAR(10), + temp_input_price DECIMAL(10,6), + temp_output_price DECIMAL(10,6), + temp_price_source VARCHAR(255), + updated_by VARCHAR(255), FOREIGN KEY (channel_type) REFERENCES provider(id) - )` + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci` } diff --git a/backend/models/provider.go b/backend/models/provider.go index cd3172a..83fd494 100644 --- a/backend/models/provider.go +++ b/backend/models/provider.go @@ -15,11 +15,11 @@ type Provider struct { func CreateProviderTableSQL() string { return ` CREATE TABLE IF NOT EXISTS provider ( - id INTEGER PRIMARY KEY, - name TEXT NOT NULL, - icon TEXT, - created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - created_by TEXT NOT NULL - )` + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + icon VARCHAR(1024), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + created_by VARCHAR(255) NOT NULL + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci` } diff --git a/backend/models/user.go b/backend/models/user.go index 7be300b..495ce64 100644 --- a/backend/models/user.go +++ b/backend/models/user.go @@ -24,32 +24,32 @@ type Session struct { DeletedAt *time.Time `json:"deleted_at,omitempty"` } -// CreateTableSQL 返回创建用户表的 SQL +// CreateUserTableSQL 返回创建用户表的 SQL func CreateUserTableSQL() string { return ` CREATE TABLE IF NOT EXISTS user ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT UNIQUE NOT NULL, - email TEXT UNIQUE NOT NULL, - role TEXT NOT NULL DEFAULT 'user', - created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - deleted_at DATETIME - )` + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(255) UNIQUE NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + role VARCHAR(50) NOT NULL DEFAULT 'user', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci` } // CreateSessionTableSQL 返回创建会话表的 SQL func CreateSessionTableSQL() string { return ` CREATE TABLE IF NOT EXISTS session ( - id TEXT PRIMARY KEY, - user_id INTEGER NOT NULL, - expires_at DATETIME NOT NULL, - created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - deleted_at DATETIME, + id VARCHAR(255) PRIMARY KEY, + user_id BIGINT UNSIGNED NOT NULL, + expires_at TIMESTAMP NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL, FOREIGN KEY (user_id) REFERENCES user(id) - )` + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci` } // GetUser 获取会话关联的用户 diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100644 index 0000000..14f319f --- /dev/null +++ b/scripts/build.sh @@ -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!" \ No newline at end of file diff --git a/scripts/start.sh b/scripts/start.sh index 66f387f..f0db2a6 100644 --- a/scripts/start.sh +++ b/scripts/start.sh @@ -5,7 +5,9 @@ echo "执行数据库迁移..." ./migrate # 启动后端服务 +echo "启动后端服务..." ./main & # 启动 nginx +echo "启动 Nginx..." nginx -g 'daemon off;' \ No newline at end of file