diff --git a/backend/db/init.go b/backend/db/init.go index 265f697..687caea 100644 --- a/backend/db/init.go +++ b/backend/db/init.go @@ -35,12 +35,8 @@ func Init(version string) error { } switch cfg.DbType { - case "mysql": - Instance, err = xorm.NewEngine("mysql", dsn) - case "sqlite": - Instance, err = xorm.NewEngine("sqlite", dsn) - case "postgres": - Instance, err = xorm.NewEngine("postgres", dsn) + case "mysql", "sqlite", "postgres": + Instance, err = newDatabaseEngine(cfg.DbType, dsn) default: return errors.New("Database Type Error!") } @@ -110,6 +106,17 @@ func Init(version string) error { return nil } +func newDatabaseEngine(driver, dsn string) (*xorm.Engine, error) { + engine, err := xorm.NewEngine(driver, dsn) + if err != nil { + return nil, err + } + // 数据库存储与跨库迁移统一使用 UTC,读取后仍按应用时区展示。 + // 不得把驱动返回的 UTC 时间按本地时区重新解释。 + engine.SetTZDatabase(time.UTC) + return engine, nil +} + func WithContext(ctx *requestcontext.Context, sql string) string { if ctx != nil { logId := ctx.GetValue(requestcontext.LogID) diff --git a/backend/db/init_migration_test.go b/backend/db/init_migration_test.go index 5c55249..bad1c4f 100644 --- a/backend/db/init_migration_test.go +++ b/backend/db/init_migration_test.go @@ -4,11 +4,55 @@ import ( "context" "path/filepath" "testing" + "time" "git.pcux.cn/xuans/Smail/config" "git.pcux.cn/xuans/Smail/db/migration" + "xorm.io/xorm/convert" ) +func TestDatabaseEnginePreservesMigratedUTCTime(t *testing.T) { + previousLocal := time.Local + time.Local = time.FixedZone("CST", 8*3600) + t.Cleanup(func() { time.Local = previousLocal }) + + for _, tc := range []struct { + driver string + dsn string + }{ + {"sqlite", ":memory:"}, + {"mysql", "smail@tcp(127.0.0.1:3306)/smail?parseTime=true"}, + {"postgres", "host=127.0.0.1 user=smail dbname=smail sslmode=require"}, + } { + t.Run(tc.driver, func(t *testing.T) { + engine, err := newDatabaseEngine(tc.driver, tc.dsn) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = engine.Close() }) + if engine.GetTZDatabase() != time.UTC { + t.Fatalf("database timezone = %v, want UTC", engine.GetTZDatabase()) + } + if engine.GetTZLocation() != time.Local { + t.Fatalf("display timezone = %v, want CST", engine.GetTZLocation()) + } + want := time.Date(2026, 10, 3, 16, 56, 0, 0, time.UTC) + for _, value := range []any{want, "2026-10-03 16:56:00"} { + got, err := convert.AsTime(value, engine.GetTZDatabase(), engine.GetTZLocation()) + if err != nil { + t.Fatal(err) + } + if got == nil || !got.Equal(want) { + t.Fatalf("converted time = %v, want instant %v", got, want) + } + if display := got.Format("2006-01-02 15:04:05"); display != "2026-10-04 00:56:00" { + t.Fatalf("display time = %s, want 2026-10-04 00:56:00", display) + } + } + }) + } +} + func TestInitAppliesSchemaMigration(t *testing.T) { databasePath := filepath.Join(t.TempDir(), "init.db") previousConfig := config.Instance