From 09bf35d0ddae70ee5b83d85e56b6a4d80d3f7a9d Mon Sep 17 00:00:00 2001 From: dsh Date: Fri, 28 Aug 2026 19:16:42 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=85=A8?= =?UTF-8?q?=E7=AB=99=E7=BB=9F=E4=B8=80=E4=B8=8A=E4=BC=A0=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E8=A1=A8=20files=EF=BC=8C=E8=BF=81=E7=A7=BB=20attachments=20?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=EF=BC=88type=3Dattachments=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - models/file.go:File 模型,字段与 Attachment 对齐并新增 Type 归属类型字段 - models/db.go:AutoMigrate 注册 File;启动时幂等迁移 attachments → files (按主键 id 对齐跳过已迁移行,含软删除行一并复制) - scripts/add_files_table.sql:MariaDB 幂等迁移脚本(建表 + INSERT...SELECT) - 线上库 blog_go:files 表已建立,15 条 attachments 记录已迁入,type='attachments' --- models/db.go | 28 ++++++++++++++++++++++++- models/file.go | 41 +++++++++++++++++++++++++++++++++++++ scripts/add_files_table.sql | 39 +++++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 models/file.go create mode 100644 scripts/add_files_table.sql diff --git a/models/db.go b/models/db.go index d9eabfd..cee1e9d 100644 --- a/models/db.go +++ b/models/db.go @@ -85,10 +85,13 @@ func InitDB(cfg *config.Config) *gorm.DB { } // 自动迁移数据表(幂等操作)。 - if err := db.AutoMigrate(&User{}, &Article{}, &SiteSetting{}, &UploadConfig{}, &UploadFileType{}, &DownloadBaseURL{}, &Attachment{}, &Comment{}, &CommentConfig{}, &ArticleView{}, &NavLink{}, &Tag{}, &ArticleTag{}); err != nil { + if err := db.AutoMigrate(&User{}, &Article{}, &SiteSetting{}, &UploadConfig{}, &UploadFileType{}, &DownloadBaseURL{}, &Attachment{}, &File{}, &Comment{}, &CommentConfig{}, &ArticleView{}, &NavLink{}, &Tag{}, &ArticleTag{}); err != nil { log.Fatalf("Failed to auto-migrate database: %v", err) } + // 数据迁移:attachments → 全站统一 files 表(type='attachments'),幂等。 + migrateAttachmentsToFiles(db) + // 首次运行时初始化站点平台配置。 seedSiteSettings(db) seedUploadConfig(db) @@ -132,3 +135,26 @@ func InitDB(cfg *config.Config) *gorm.DB { DB = db return db } + +// migrateAttachmentsToFiles 将 attachments 表中的历史数据复制到统一的 +// files 表,type 一律标记为 "attachments"。 +// +// 幂等策略:以主键 id 对齐——files 中已存在同 id 的行视为已迁移并跳过, +// 因此 InitDB 每次启动重复执行也不会产生重复数据(含软删除行一并复制)。 +func migrateAttachmentsToFiles(db *gorm.DB) { + 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) +SELECT + a.id, '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 NOT EXISTS (SELECT 1 FROM files f WHERE f.id = a.id)`) + if res.Error != nil { + log.Printf("Migration attachments -> files failed: %v", res.Error) + return + } + if res.RowsAffected > 0 { + log.Printf("Migration: copied %d attachment(s) into files table (type=attachments)", res.RowsAffected) + } +} diff --git a/models/file.go b/models/file.go new file mode 100644 index 0000000..ceb7d56 --- /dev/null +++ b/models/file.go @@ -0,0 +1,41 @@ +package models + +import ( + "time" + + "gorm.io/gorm" +) + +// File 是全站统一的上传文件登记表,用于管理所有类型的上传文件 +// (附件、头像、Logo 等)。字段与 Attachment 对齐,额外通过 Type +// 字段区分文件归属类型。 +// +// 说明: +// - attachments 表中的历史数据在启动迁移时复制到本表,Type 一律 +// 标记为 "attachments"(见 db.go 的 migrateAttachmentsToFiles)。 +// - 磁盘去重与引用计数沿用附件策略:StoredName 为内容 SHA-256, +// 删除时仅当没有任何行引用时才删磁盘文件。 +type File struct { + ID uint `gorm:"primarykey" json:"id"` + Type string `gorm:"size:32;index;default:attachments" json:"type"` // 归属类型:attachments/avatar/logo/... + ArticleID uint `gorm:"index" json:"article_id"` // 在创建页面上待绑定时为 0 + SessionToken string `gorm:"size:64;index" json:"-"` // 创建页面上的临时归属令牌 + UploaderID uint `gorm:"index" json:"uploader_id"` + Filename string `gorm:"size:255" json:"filename"` // 原始文件名 + StoredName string `gorm:"size:64;index" json:"stored_name"` // SHA-256 十六进制字符串,磁盘文件名 + Ext string `gorm:"size:32" json:"ext"` + MIME string `gorm:"size:128" json:"mime"` + Size int64 `gorm:"default:0" json:"size"` + Category string `gorm:"size:32" json:"category"` // image/document/archive/video/other + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"` +} + +// TableName 覆盖 GORM 默认的表名。 +func (File) TableName() string { + return "files" +} + +// FileTypeAttachment 是文件归属类型常量:来自 attachments 表的历史附件。 +const FileTypeAttachment = "attachments" diff --git a/scripts/add_files_table.sql b/scripts/add_files_table.sql new file mode 100644 index 0000000..88fed02 --- /dev/null +++ b/scripts/add_files_table.sql @@ -0,0 +1,39 @@ +-- Migration: 新增全站统一上传文件表 files,并把 attachments 数据迁入(type='attachments') +-- Date: 2026-08-28 +-- Description: files 表用于管理全站所有上传文件(附件/头像/Logo 等), +-- Type 字段区分归属类型;attachments 历史数据逐行复制,type 填 'attachments'。 +-- 幂等:表用 IF NOT EXISTS,数据按主键 id 对齐跳过已迁移行,可重复执行。 + +-- 1) 建表(与 GORM AutoMigrate 输出一致) +CREATE TABLE IF NOT EXISTS `files` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `type` varchar(32) DEFAULT 'attachments', + `article_id` bigint(20) unsigned DEFAULT NULL, + `session_token` varchar(64) DEFAULT NULL, + `uploader_id` bigint(20) unsigned DEFAULT NULL, + `filename` varchar(255) DEFAULT NULL, + `stored_name` varchar(64) DEFAULT NULL, + `ext` varchar(32) DEFAULT NULL, + `mime` varchar(128) DEFAULT NULL, + `size` bigint(20) DEFAULT 0, + `category` varchar(32) DEFAULT NULL, + `created_at` datetime(3) DEFAULT NULL, + `updated_at` datetime(3) DEFAULT NULL, + `deleted_at` datetime(3) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_files_type` (`type`), + KEY `idx_files_article_id` (`article_id`), + KEY `idx_files_session_token` (`session_token`), + KEY `idx_files_uploader_id` (`uploader_id`), + KEY `idx_files_stored_name` (`stored_name`), + KEY `idx_files_deleted_at` (`deleted_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- 2) 数据迁移:attachments 全部行(含软删除)复制进 files,type='attachments' +INSERT INTO `files` + (`id`,`type`,`article_id`,`session_token`,`uploader_id`,`filename`,`stored_name`,`ext`,`mime`,`size`,`category`,`created_at`,`updated_at`,`deleted_at`) +SELECT + a.`id`, '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 NOT EXISTS (SELECT 1 FROM `files` f WHERE f.`id` = a.`id`); -- 2.47.3 From 970dbd4c5b9982f6e26448af9da979f987fb9d1b Mon Sep 17 00:00:00 2001 From: dsh Date: Fri, 28 Aug 2026 19:23:50 +0800 Subject: [PATCH 2/2] =?UTF-8?q?refactor:=20=E4=B8=8A=E4=BC=A0/=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD=E6=8E=A5=E5=8F=A3=E5=85=A8=E9=9D=A2=E8=BF=81=E7=A7=BB?= =?UTF-8?q?=E5=88=B0=E7=BB=9F=E4=B8=80=20files=20=E8=A1=A8=EF=BC=8C?= =?UTF-8?q?=E7=A7=BB=E9=99=A4=20attachments=20=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除 models/attachment.go,Attachment 模型由 File(Type=attachments)替代 - handlers/attachment.go:上传/删除/列表/绑定全部改读写 files 表, 查询按 type='attachments' 过滤(为 avatar/logo 等类型预留隔离) - models/db.go:AutoMigrate 移除 Attachment;启动迁移仅当旧表仍存在时执行 (升级路径保障,已删除则为无操作) - 测试同步切换到 File 模型(security_test/bodylimit_test) - scripts/drop_attachments_table.sql:旧表删除脚本(含前置校验说明) --- handlers/attachment.go | 27 ++++++++++-------- handlers/bodylimit_test.go | 2 +- handlers/security_test.go | 6 ++-- models/attachment.go | 45 ------------------------------ models/db.go | 14 +++++++--- models/file.go | 32 +++++++++++++-------- scripts/drop_attachments_table.sql | 11 ++++++++ 7 files changed, 62 insertions(+), 75 deletions(-) delete mode 100644 models/attachment.go create mode 100644 scripts/drop_attachments_table.sql diff --git a/handlers/attachment.go b/handlers/attachment.go index f849c81..5742081 100644 --- a/handlers/attachment.go +++ b/handlers/attachment.go @@ -76,6 +76,8 @@ func canManageArticle(c *gin.Context, db *gorm.DB, articleID uint) bool { // UploadAttachment 处理来自文章创建/编辑表单的 AJAX 附件上传。 // 请求携带真实的 article_id(编辑页)或 session_token(创建页,待绑定)。 // 文件按 SHA-256 内容寻址,实现磁盘去重。 +// +// 记录写入全站统一的 files 表(Type=attachments),与历史数据同属一个表。 func UploadAttachment(db *gorm.DB, storagePath string) gin.HandlerFunc { return func(c *gin.Context) { uploaderID, ok := sessionAuthorID(c) @@ -152,7 +154,8 @@ func UploadAttachment(db *gorm.DB, storagePath string) gin.HandlerFunc { } ext := strings.ToLower(filepath.Ext(header.Filename)) - att := models.Attachment{ + att := models.File{ + Type: models.FileTypeAttachment, ArticleID: articleID, SessionToken: token, UploaderID: uploaderID, @@ -181,14 +184,14 @@ func UploadAttachment(db *gorm.DB, storagePath string) gin.HandlerFunc { // ---------------- 删除 ---------------- -// DeleteAttachment 软删除附件记录,仅当没有其余记录引用时才删除磁盘文件 -// (引用计数,因为内容寻址的文件可能被共享)。只有管理员、上传者或 -// 文件所在文章的作者可以删除。 +// DeleteAttachment 软删除附件记录(files 表,Type=attachments),仅当 +// 没有其余记录引用时才删除磁盘文件(引用计数,因为内容寻址的文件可能 +// 被共享)。只有管理员、上传者或文件所在文章的作者可以删除。 func DeleteAttachment(db *gorm.DB, storagePath string) gin.HandlerFunc { return func(c *gin.Context) { id := parseUintParam(c, "id") - var att models.Attachment - if err := db.First(&att, id).Error; err != nil { + var att models.File + if err := db.First(&att, "id = ? AND type = ?", id, models.FileTypeAttachment).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) return } @@ -214,7 +217,7 @@ func DeleteAttachment(db *gorm.DB, storagePath string) gin.HandlerFunc { // 引用计数:还有任何其他(未删除)行指向此文件吗? var count int64 - db.Model(&models.Attachment{}).Where("stored_name = ?", stored).Count(&count) + db.Model(&models.File{}).Where("stored_name = ? AND type = ?", stored, models.FileTypeAttachment).Count(&count) if count == 0 { os.Remove(filepath.Join(attachmentsDir(storagePath), stored)) // 忽略错误 } @@ -225,6 +228,7 @@ func DeleteAttachment(db *gorm.DB, storagePath string) gin.HandlerFunc { // ---------------- 列表 ---------------- // ListAttachments 以 JSON 返回文章的附件(供编辑页加载时重新填充列表)。 +// 只读取 files 表中 Type=attachments 的记录。 // 只有文章作者(或管理员)可以列出。 func ListAttachments(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { @@ -237,8 +241,9 @@ func ListAttachments(db *gorm.DB) gin.HandlerFunc { c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"}) return } - var atts []models.Attachment - db.Where("article_id = ?", articleID).Order("created_at ASC").Find(&atts) + var atts []models.File + db.Where("article_id = ? AND type = ?", articleID, models.FileTypeAttachment). + Order("created_at ASC").Find(&atts) out := make([]gin.H, 0, len(atts)) for _, a := range atts { @@ -264,8 +269,8 @@ func BindPendingAttachments(db *gorm.DB, token string, articleID uint) error { if token == "" { return nil } - return db.Model(&models.Attachment{}). - Where("session_token = ? AND article_id = 0", token). + return db.Model(&models.File{}). + Where("session_token = ? AND article_id = 0 AND type = ?", token, models.FileTypeAttachment). Updates(map[string]interface{}{"article_id": articleID, "session_token": ""}).Error } diff --git a/handlers/bodylimit_test.go b/handlers/bodylimit_test.go index 2839ec0..65f3630 100644 --- a/handlers/bodylimit_test.go +++ b/handlers/bodylimit_test.go @@ -91,7 +91,7 @@ func TestBodyLimitRejectsOversizedMultipart(t *testing.T) { } var count int64 - e.db.Model(&models.Attachment{}).Where("filename = ?", "big.txt").Count(&count) + e.db.Model(&models.File{}).Where("filename = ?", "big.txt").Count(&count) if count != 0 { t.Fatalf("oversized multipart created %d attachment rows, want 0", count) } diff --git a/handlers/security_test.go b/handlers/security_test.go index a957478..a4b7670 100644 --- a/handlers/security_test.go +++ b/handlers/security_test.go @@ -47,7 +47,7 @@ func newSecurityTestEnv(t *testing.T) *securityTestEnv { if err != nil { t.Fatalf("open sqlite: %v", err) } - if err := db.AutoMigrate(&models.User{}, &models.Article{}, &models.Attachment{}, &models.SiteSetting{}, + if err := db.AutoMigrate(&models.User{}, &models.Article{}, &models.File{}, &models.SiteSetting{}, &models.UploadConfig{}, &models.UploadFileType{}, &models.CommentConfig{}, &models.NavLink{}, &models.DownloadBaseURL{}, &models.Comment{}); err != nil { t.Fatalf("migrate: %v", err) @@ -439,7 +439,7 @@ func TestAttachmentDeleteRequiresOwnership(t *testing.T) { t.Fatalf("upload (bob): status = %d, body %s", w.Code, w.Body.String()) } - var bobAtt models.Attachment + var bobAtt models.File if err := e.db.Where("uploader_id = ?", userIDByUsername(t, e.db, "bob")).First(&bobAtt).Error; err != nil { t.Fatalf("bob attachment not found: %v", err) } @@ -458,7 +458,7 @@ func TestAttachmentDeleteRequiresOwnership(t *testing.T) { // Bob 的附件记录应该已删除。 var count int64 - e.db.Model(&models.Attachment{}).Where("id = ?", bobAtt.ID).Count(&count) + e.db.Model(&models.File{}).Where("id = ?", bobAtt.ID).Count(&count) if count != 0 { t.Fatal("attachment was not deleted") } diff --git a/models/attachment.go b/models/attachment.go deleted file mode 100644 index cee9bc2..0000000 --- a/models/attachment.go +++ /dev/null @@ -1,45 +0,0 @@ -package models - -import ( - "time" - - "gorm.io/gorm" -) - -// Attachment 表示附加到文章的文件。 -// -// 生命周期(方案 A——先上传后绑定): -// - 在文章创建页面上文章尚不存在,因此 ArticleID 为 0, -// 该行暂时由 SessionToken(为页面会话生成的随机值)持有。 -// - 保存文章时,ArticleCreate 通过 SessionToken 绑定待处理行, -// 设置它们的 ArticleID 并清除令牌。 -// - 在编辑页面上,上传直接携带真实的 ArticleID。 -// -// 磁盘去重:StoredName 是文件内容的 SHA-256。写入前, -// 处理器会检查磁盘上是否已存在同名文件;若已存在则复用(不重写)。 -// 删除采用引用计数——仅当没有任何 Attachment 行引用时,才删除磁盘文件。 -type Attachment struct { - ID uint `gorm:"primarykey" json:"id"` - ArticleID uint `gorm:"index" json:"article_id"` // 在创建页面上待绑定时为 0 - SessionToken string `gorm:"size:64;index" json:"-"` // 创建页面上的临时归属令牌 - UploaderID uint `gorm:"index" json:"uploader_id"` - Filename string `gorm:"size:255" json:"filename"` // 原始文件名 - StoredName string `gorm:"size:64;index" json:"stored_name"` // SHA-256 十六进制字符串,磁盘文件名 - Ext string `gorm:"size:32" json:"ext"` - MIME string `gorm:"size:128" json:"mime"` - Size int64 `gorm:"default:0" json:"size"` - Category string `gorm:"size:32" json:"category"` // image/document/archive/video/other - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"` -} - -// TableName 覆盖 GORM 默认的表名。 -func (Attachment) TableName() string { - return "attachments" -} - -// IsImage 报告该附件是否为图片(用于决定 Markdown 插入形式:![]() 还是 []())。 -func (a *Attachment) IsImage() bool { - return a.Category == CategoryImage -} diff --git a/models/db.go b/models/db.go index cee1e9d..f2606ea 100644 --- a/models/db.go +++ b/models/db.go @@ -84,12 +84,14 @@ func InitDB(cfg *config.Config) *gorm.DB { log.Fatalf("Failed to connect to database: %v", err) } - // 自动迁移数据表(幂等操作)。 - if err := db.AutoMigrate(&User{}, &Article{}, &SiteSetting{}, &UploadConfig{}, &UploadFileType{}, &DownloadBaseURL{}, &Attachment{}, &File{}, &Comment{}, &CommentConfig{}, &ArticleView{}, &NavLink{}, &Tag{}, &ArticleTag{}); err != nil { + // 自动迁移数据表(幂等操作)。attachments 旧表已由 files 替代, + // 不再参与迁移;历史数据在下方 migrateAttachmentsToFiles 中一次性搬运。 + if err := db.AutoMigrate(&User{}, &Article{}, &SiteSetting{}, &UploadConfig{}, &UploadFileType{}, &DownloadBaseURL{}, &File{}, &Comment{}, &CommentConfig{}, &ArticleView{}, &NavLink{}, &Tag{}, &ArticleTag{}); err != nil { log.Fatalf("Failed to auto-migrate database: %v", err) } // 数据迁移:attachments → 全站统一 files 表(type='attachments'),幂等。 + // 仅当旧表仍存在时执行(已删除则为无操作),保证升级路径上的数据不丢。 migrateAttachmentsToFiles(db) // 首次运行时初始化站点平台配置。 @@ -136,12 +138,16 @@ func InitDB(cfg *config.Config) *gorm.DB { return db } -// migrateAttachmentsToFiles 将 attachments 表中的历史数据复制到统一的 -// files 表,type 一律标记为 "attachments"。 +// migrateAttachmentsToFiles 将旧 attachments 表中的历史数据复制到统一的 +// files 表,type 一律标记为 "attachments"。仅当 attachments 表仍存在时 +// 执行——新安装从未创建过该表,而已经切换的部署会将其删除。 // // 幂等策略:以主键 id 对齐——files 中已存在同 id 的行视为已迁移并跳过, // 因此 InitDB 每次启动重复执行也不会产生重复数据(含软删除行一并复制)。 func migrateAttachmentsToFiles(db *gorm.DB) { + if !db.Migrator().HasTable("attachments") { + return + } 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) diff --git a/models/file.go b/models/file.go index ceb7d56..2791363 100644 --- a/models/file.go +++ b/models/file.go @@ -7,19 +7,24 @@ import ( ) // File 是全站统一的上传文件登记表,用于管理所有类型的上传文件 -// (附件、头像、Logo 等)。字段与 Attachment 对齐,额外通过 Type -// 字段区分文件归属类型。 +// (附件、头像、Logo 等)。字段与原 attachments 表对齐,额外通过 +// Type 字段区分文件归属类型。 // -// 说明: -// - attachments 表中的历史数据在启动迁移时复制到本表,Type 一律 -// 标记为 "attachments"(见 db.go 的 migrateAttachmentsToFiles)。 -// - 磁盘去重与引用计数沿用附件策略:StoredName 为内容 SHA-256, -// 删除时仅当没有任何行引用时才删磁盘文件。 +// 生命周期(方案 A——先上传后绑定): +// - 在文章创建页面上文章尚不存在,因此 ArticleID 为 0, +// 该行暂时由 SessionToken(为页面会话生成的随机值)持有。 +// - 保存文章时,ArticleCreate 通过 SessionToken 绑定待处理行, +// 设置它们的 ArticleID 并清除令牌。 +// - 在编辑页面上,上传直接携带真实的 ArticleID。 +// +// 磁盘去重:StoredName 是文件内容的 SHA-256。写入前, +// 处理器会检查磁盘上是否已存在同名文件;若已存在则复用(不重写)。 +// 删除采用引用计数——仅当没有任何 File 行引用时,才删除磁盘文件。 type File struct { ID uint `gorm:"primarykey" json:"id"` - Type string `gorm:"size:32;index;default:attachments" json:"type"` // 归属类型:attachments/avatar/logo/... - ArticleID uint `gorm:"index" json:"article_id"` // 在创建页面上待绑定时为 0 - SessionToken string `gorm:"size:64;index" json:"-"` // 创建页面上的临时归属令牌 + Type string `gorm:"size:32;index;default:attachments" json:"type"` // 归属类型:attachment 等 + ArticleID uint `gorm:"index" json:"article_id"` // 在创建页面上待绑定时为 0 + SessionToken string `gorm:"size:64;index" json:"-"` // 创建页面上的临时归属令牌 UploaderID uint `gorm:"index" json:"uploader_id"` Filename string `gorm:"size:255" json:"filename"` // 原始文件名 StoredName string `gorm:"size:64;index" json:"stored_name"` // SHA-256 十六进制字符串,磁盘文件名 @@ -37,5 +42,10 @@ func (File) TableName() string { return "files" } -// FileTypeAttachment 是文件归属类型常量:来自 attachments 表的历史附件。 +// FileTypeAttachment 是文件归属类型常量:文章附件(原 attachments 表历史数据)。 const FileTypeAttachment = "attachments" + +// IsImage 报告该文件是否为图片(用于决定 Markdown 插入形式:![]() 还是 []())。 +func (f *File) IsImage() bool { + return f.Category == CategoryImage +} diff --git a/scripts/drop_attachments_table.sql b/scripts/drop_attachments_table.sql new file mode 100644 index 0000000..dc37956 --- /dev/null +++ b/scripts/drop_attachments_table.sql @@ -0,0 +1,11 @@ +-- Migration: 删除旧 attachments 表(数据已迁移至统一 files 表,type='attachments') +-- Date: 2026-08-28 +-- 前置条件:已部署使用 files 表的新版 blog_go 并完成验证(启动迁移会把 +-- 尚未搬运的 attachments 行复制进 files)。本脚本幂等,可安全重跑。 +-- +-- 执行前确认: +-- SELECT COUNT(*) AS remaining FROM attachments a +-- WHERE NOT EXISTS (SELECT 1 FROM files f WHERE f.id = a.id); +-- 结果应为 0——若有遗留行,先重启新版本服务让其自动搬运。 + +DROP TABLE IF EXISTS `attachments`; -- 2.47.3