diff --git a/.env.example b/.env.example index f2a61c8..1620837 100644 --- a/.env.example +++ b/.env.example @@ -6,8 +6,9 @@ ADMIN_PASSWORD=infinite-canvas JWT_SECRET=infinite-canvas JWT_EXPIRE_HOURS=168 -# 后端监听端口 -PORT=8080 +# 后端默认监听 8080,如需本地开发修改端口再取消注释。 +# Docker 镜像内前端固定监听 3000,避免覆盖前端 PORT。 +# PORT=8080 # 公开访问地址,用于把本地上传的 Seedance 参考图/视频暴露给火山方舟拉取。 # 线上部署时填写站点根地址,例如:https://your-domain.example.com @@ -21,6 +22,8 @@ NEXT_PUBLIC_DOC_URL=https://docs.canvas.best STORAGE_DRIVER=sqlite # sqlite: DATABASE_DSN=data/infinite-canvas.db # Docker 部署时建议使用绝对路径,避免工作目录变化后写入临时库:DATABASE_DSN=/app/data/infinite-canvas.db +# mysql 目标库不存在时会尝试自动创建,账号需有 CREATE 权限。 # mysql: DATABASE_DSN=user:password@tcp(127.0.0.1:3306)/infinite_canvas?parseTime=true +# postgres 目标库不存在时会尝试自动创建,账号需有 CREATEDB 权限。 # postgres: DATABASE_DSN=postgres://user:password@127.0.0.1:5432/infinite_canvas?sslmode=disable DATABASE_DSN=data/infinite-canvas.db diff --git a/CHANGELOG.md b/CHANGELOG.md index 0becd45..5f53ef2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased ++ [新增] 新增图片放大工具。 ++ [优化] 优化图片工具条,增加自定义功能。 ++ [修复] 修复端口冲突问题、pg/mysql未初始化问题。 + ## v0.2.1 - 2026-06-03 + [新增] 新增文档站点页面。 diff --git a/Dockerfile b/Dockerfile index 854d1d5..ccf710c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -43,4 +43,4 @@ RUN mkdir -p /app/data/prompts EXPOSE 3000 # 先启动内部 Go API,再由 Next.js 提供页面并代理 /api/*。 -CMD ["sh", "-c", "PORT=8080 /app/server & cd /app/web && node server.js"] +CMD ["sh", "-c", "PORT=8080 /app/server & cd /app/web && PORT=3000 node server.js"] diff --git a/go.mod b/go.mod index 5891d74..24c404d 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,8 @@ require ( github.com/gin-gonic/gin v1.11.0 github.com/glebarez/sqlite v1.11.0 github.com/golang-jwt/jwt/v5 v5.3.1 + github.com/go-sql-driver/mysql v1.8.1 + github.com/jackc/pgx/v5 v5.6.0 github.com/joho/godotenv v1.5.1 github.com/robfig/cron/v3 v3.0.1 golang.org/x/crypto v0.48.0 @@ -27,13 +29,11 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.27.0 // indirect - github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-yaml v1.18.0 // indirect github.com/google/uuid v1.3.0 github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgx/v5 v5.6.0 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect diff --git a/repository/db.go b/repository/db.go index aed850a..4948d76 100644 --- a/repository/db.go +++ b/repository/db.go @@ -1,6 +1,9 @@ package repository import ( + "context" + "database/sql" + "errors" "os" "path/filepath" "strings" @@ -9,7 +12,10 @@ import ( "github.com/basketikun/infinite-canvas/config" "github.com/basketikun/infinite-canvas/model" "github.com/glebarez/sqlite" - "gorm.io/driver/mysql" + mysqldriver "github.com/go-sql-driver/mysql" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + gormmysql "gorm.io/driver/mysql" "gorm.io/driver/postgres" "gorm.io/gorm" ) @@ -41,6 +47,18 @@ func DB() (*gorm.DB, error) { if driver == "sqlite" && dsn != ":memory:" { _ = os.MkdirAll(filepath.Dir(dsn), 0755) } + if isPostgresDriver(driver) { + dbErr = ensurePostgresDatabase(dsn) + if dbErr != nil { + return + } + } + if driver == "mysql" { + dbErr = ensureMySQLDatabase(dsn) + if dbErr != nil { + return + } + } db, dbErr = gorm.Open(dialector(driver, dsn), &gorm.Config{}) if dbErr != nil { return @@ -59,10 +77,103 @@ func DB() (*gorm.DB, error) { func dialector(driver string, dsn string) gorm.Dialector { switch driver { case "mysql": - return mysql.Open(dsn) + return gormmysql.Open(dsn) case "postgres", "postgresql": return postgres.Open(dsn) default: return sqlite.Open(dsn) } } + +func isPostgresDriver(driver string) bool { + return driver == "postgres" || driver == "postgresql" +} + +func ensureMySQLDatabase(dsn string) error { + cfg, err := mysqldriver.ParseDSN(dsn) + if err != nil { + return err + } + target := strings.TrimSpace(cfg.DBName) + if target == "" { + return nil + } + ctx := context.Background() + targetDB, err := sql.Open("mysql", dsn) + if err != nil { + return err + } + err = targetDB.PingContext(ctx) + _ = targetDB.Close() + if err == nil { + return nil + } + if !isMySQLError(err, 1049) { + return err + } + + maintenance := cfg.Clone() + maintenance.DBName = "" + serverDB, err := sql.Open("mysql", maintenance.FormatDSN()) + if err != nil { + return err + } + defer serverDB.Close() + + _, err = serverDB.ExecContext(ctx, "CREATE DATABASE "+quoteMySQLIdentifier(target)+" CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci") + if isMySQLError(err, 1007) { + return nil + } + return err +} + +func ensurePostgresDatabase(dsn string) error { + cfg, err := pgx.ParseConfig(dsn) + if err != nil { + return err + } + target := strings.TrimSpace(cfg.Database) + if target == "" { + return nil + } + ctx := context.Background() + conn, err := pgx.ConnectConfig(ctx, cfg) + if err == nil { + _ = conn.Close(ctx) + return nil + } + if !isPostgresError(err, "3D000") { + return err + } + + maintenance := cfg.Copy() + maintenance.Database = "postgres" + if strings.EqualFold(target, "postgres") { + maintenance.Database = "template1" + } + conn, err = pgx.ConnectConfig(ctx, maintenance) + if err != nil { + return err + } + defer conn.Close(ctx) + + _, err = conn.Exec(ctx, "CREATE DATABASE "+pgx.Identifier{target}.Sanitize(), pgx.QueryExecModeExec) + if isPostgresError(err, "42P04") { + return nil + } + return err +} + +func isMySQLError(err error, number uint16) bool { + var mysqlErr *mysqldriver.MySQLError + return errors.As(err, &mysqlErr) && mysqlErr.Number == number +} + +func isPostgresError(err error, code string) bool { + var pgErr *pgconn.PgError + return errors.As(err, &pgErr) && pgErr.Code == code +} + +func quoteMySQLIdentifier(name string) string { + return "`" + strings.ReplaceAll(name, "`", "``") + "`" +}