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`);