From 773acd079f990ae3fd8f64292c0de6d0abc1d897 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sun, 13 Sep 2026 16:22:00 +0800 Subject: [PATCH] =?UTF-8?q?harden(migrate):=20=E6=8A=84=E9=80=81=E5=9B=9E?= =?UTF-8?q?=E5=A1=AB=E5=8F=96=E5=88=87=E6=8D=A2=E6=97=B6=E5=88=BB=E6=94=B9?= =?UTF-8?q?=20CAST(TEXT)=20=E2=80=94=E2=80=94=20PG=20=E7=9A=84=20timestamp?= =?UTF-8?q?tz=20=E4=BC=9A=E8=A2=AB=E6=89=AB=E6=88=90=20time.Time?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 判据只在 SQLite 上跑过:把 MIN(read_at) 扫进 sql.NullString 在 PG 上依赖驱动返回类型, 不可靠。改成 CAST(... AS TEXT) 再解析,并把 PG 的文本形态(+00 时区后缀)加进可识别的 layout 列表。认不出来时退回现在= 把当下已有的已读邮件全算作迁移前 —— 首次升级时 正是对的,且有一次标记守着不会反复跑。 --- server/internal/db/migrate.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/server/internal/db/migrate.go b/server/internal/db/migrate.go index c04319b..105bd6f 100644 --- a/server/internal/db/migrate.go +++ b/server/internal/db/migrate.go @@ -120,13 +120,21 @@ func switchoverAt(ctx context.Context) (time.Time, error) { return time.Time{}, err } var raw sql.NullString - if err := DB.QueryRowContext(ctx, `SELECT MIN(read_at) FROM mail_reads`).Scan(&raw); err != nil { + // CAST 成文本再解析:两种方言的驱动对 timestamptz 返回的类型不一样 + // (PG 给 time.Time,SQLite 给字符串),文本形态才是共通的。 + if err := DB.QueryRowContext(ctx, `SELECT CAST(MIN(read_at) AS TEXT) FROM mail_reads`).Scan(&raw); err != nil { return time.Time{}, err } if !raw.Valid { return time.Now().UTC(), nil // 表还是空的:本机没有"老邮件"要照顾 } - for _, layout := range []string{"2006-01-02 15:04:05.000", "2006-01-02 15:04:05", time.RFC3339} { + // 认不出来的形态退回"现在":那等于把当下已有的已读邮件全算作迁移前 —— + // 首次升级时正是对的;而且这条路有一次性标记守着,不会反复跑。 + for _, layout := range []string{ + "2006-01-02 15:04:05.000", "2006-01-02 15:04:05", + "2006-01-02 15:04:05.999999-07", "2006-01-02 15:04:05-07", + time.RFC3339, + } { if t, perr := time.Parse(layout, raw.String); perr == nil { return t.UTC(), nil }