From 30af358dd795653a7e7dfb175d5e9880fea9b31b Mon Sep 17 00:00:00 2001 From: dsh Date: Fri, 28 Aug 2026 19:50:58 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=BF=81=E7=A7=BB=20attachments?= =?UTF-8?q?=E2=86=92files=20=E5=A2=9E=E5=8A=A0=E5=86=85=E5=AE=B9=E5=9B=9E?= =?UTF-8?q?=E9=80=80=E7=AD=96=E7=95=A5=EF=BC=8C=E9=98=B2=E9=87=8D=E5=BB=BA?= =?UTF-8?q?=E8=A1=A8=E5=90=8E=20id=20=E6=92=9E=E5=8F=B7=E6=BC=8F=E6=90=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 场景:中途部署过旧版二进制时 AutoMigrate 会把 attachments 以 id=1 重新编号;此前按 id 对齐的 NOT EXISTS 会误判已迁移, 导致新上传行静默丢失。现在第二段按 stored_name(内容 SHA-256) 补齐 id 冲突的行(省略 id 由数据库重新分配,内容已登记则跳过)。 --- models/db.go | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/models/db.go b/models/db.go index f2606ea..c225daf 100644 --- a/models/db.go +++ b/models/db.go @@ -142,12 +142,18 @@ func InitDB(cfg *config.Config) *gorm.DB { // files 表,type 一律标记为 "attachments"。仅当 attachments 表仍存在时 // 执行——新安装从未创建过该表,而已经切换的部署会将其删除。 // -// 幂等策略:以主键 id 对齐——files 中已存在同 id 的行视为已迁移并跳过, -// 因此 InitDB 每次启动重复执行也不会产生重复数据(含软删除行一并复制)。 +// 幂等与异常恢复策略: +// 1. 按主键 id 对齐——files 中已存在同 id 的行视为已迁移并跳过; +// 2. 若 attachments 表被重建(例如中途部署过旧版二进制,AutoMigrate 把表 +// 从 id=1 重新编号),旧 id 已被其他内容占用,改用 stored_name(内容 +// SHA-256)比对补齐,避免新上传被静默漏搬。 +// 含软删除行一并复制;重复执行不会产生重复数据。 func migrateAttachmentsToFiles(db *gorm.DB) { if !db.Migrator().HasTable("attachments") { return } + + // 1) id 对齐迁移(常规升级路径)。 res := db.Exec(` INSERT INTO files (id, type, article_id, session_token, uploader_id, filename, stored_name, ext, mime, size, category, created_at, updated_at, deleted_at) @@ -163,4 +169,23 @@ WHERE NOT EXISTS (SELECT 1 FROM files f WHERE f.id = a.id)`) if res.RowsAffected > 0 { log.Printf("Migration: copied %d attachment(s) into files table (type=attachments)", res.RowsAffected) } + + // 2) 内容补齐:id 已存在但指向不同内容(重建表后 id 撞号)的行, + // 省略 id 让数据库重新分配,且跳过内容已在 files 中登记的行(去重)。 + res = db.Exec(` +INSERT INTO files + (type, article_id, session_token, uploader_id, filename, stored_name, ext, mime, size, category, created_at, updated_at, deleted_at) +SELECT + 'attachments', a.article_id, a.session_token, a.uploader_id, a.filename, + a.stored_name, a.ext, a.mime, a.size, a.category, a.created_at, a.updated_at, a.deleted_at +FROM attachments a +WHERE EXISTS (SELECT 1 FROM files f WHERE f.id = a.id AND f.stored_name <> a.stored_name) + AND NOT EXISTS (SELECT 1 FROM files f WHERE f.stored_name = a.stored_name)`) + if res.Error != nil { + log.Printf("Migration attachments -> files (content fallback) failed: %v", res.Error) + return + } + if res.RowsAffected > 0 { + log.Printf("Migration: recovered %d colliding attachment(s) into files table by content", res.RowsAffected) + } } -- 2.47.3