Author SHA1 Message Date
dsh a14b154a3c fix: 迁移 attachments→files 增加内容回退策略,防重建表后 id 撞号漏搬
场景:中途部署过旧版二进制时 AutoMigrate 会把 attachments 以
id=1 重新编号;此前按 id 对齐的 NOT EXISTS 会误判已迁移,
导致新上传行静默丢失。现在第二段按 stored_name(内容 SHA-256)
补齐 id 冲突的行(省略 id 由数据库重新分配,内容已登记则跳过)。
2026-08-28 20:01:16 +08:00
+27 -2
View File
@@ -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)
}
}