From 729d335a6949c31d58db574defb190bf69bfe402 Mon Sep 17 00:00:00 2001 From: hanxuanyu <2252193204@qq.com> Date: Tue, 27 Jan 2026 10:27:25 +0800 Subject: [PATCH] =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=E8=BF=81=E7=A7=BB?= =?UTF-8?q?=E4=BC=98=E5=8C=96=EF=BC=8C=E5=A2=9E=E5=BC=BA=E5=A4=96=E9=94=AE?= =?UTF-8?q?=E7=BA=A6=E6=9D=9F=E6=8E=A7=E5=88=B6=EF=BC=8C=E6=94=B9=E8=BF=9B?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E8=BE=93=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + internal/bootstrap/bootstrap.go | 17 +++++----- internal/model/models.go | 4 +-- internal/repo/db.go | 7 +++- internal/service/fetcher/fetcher.go | 43 +++++++++++++++++++------ internal/service/image/image_service.go | 12 ++++--- 6 files changed, 59 insertions(+), 25 deletions(-) diff --git a/.gitignore b/.gitignore index 22532b2..32066fd 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,4 @@ desktop.ini /bing_paper.db /req.txt /BingPaper +/temp/ diff --git a/internal/bootstrap/bootstrap.go b/internal/bootstrap/bootstrap.go index d9e0437..f2bd961 100644 --- a/internal/bootstrap/bootstrap.go +++ b/internal/bootstrap/bootstrap.go @@ -37,26 +37,25 @@ func Init(webFS embed.FS, configPath string) *gin.Engine { util.InitLogger(cfg.Log.Level) // 输出配置信息 - util.Logger.Info("Application configuration loaded", - zap.String("config_file", config.GetRawViper().ConfigFileUsed()), - zap.String("db_type", cfg.DB.Type), - zap.String("storage_type", cfg.Storage.Type), - zap.Int("server_port", cfg.Server.Port), - ) + util.Logger.Info("Application configuration loaded") + util.Logger.Info("├─ Config file", zap.String("path", config.GetRawViper().ConfigFileUsed())) + util.Logger.Info("├─ Database ", zap.String("type", cfg.DB.Type)) + util.Logger.Info("├─ Storage ", zap.String("type", cfg.Storage.Type)) + util.Logger.Info("└─ Server ", zap.Int("port", cfg.Server.Port)) // 根据存储类型输出更多信息 switch cfg.Storage.Type { case "s3": - util.Logger.Info("S3 storage info", + util.Logger.Info("S3 storage detail", zap.String("endpoint", cfg.Storage.S3.Endpoint), zap.String("bucket", cfg.Storage.S3.Bucket), ) case "webdav": - util.Logger.Info("WebDAV storage info", + util.Logger.Info("WebDAV storage detail", zap.String("url", cfg.Storage.WebDAV.URL), ) default: - util.Logger.Info("Local storage info", + util.Logger.Info("Local storage detail", zap.String("root", cfg.Storage.Local.Root), ) } diff --git a/internal/model/models.go b/internal/model/models.go index a64662e..84f46e5 100644 --- a/internal/model/models.go +++ b/internal/model/models.go @@ -16,12 +16,12 @@ type Image struct { CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - Variants []ImageVariant `gorm:"foreignKey:ImageID" json:"variants"` + Variants []ImageVariant `gorm:"foreignKey:ImageID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"variants"` } type ImageVariant struct { ID uint `gorm:"primaryKey" json:"id"` - ImageID uint `gorm:"uniqueIndex:idx_image_variant_format" json:"image_id"` + ImageID uint `gorm:"index;uniqueIndex:idx_image_variant_format" json:"image_id"` Variant string `gorm:"uniqueIndex:idx_image_variant_format;type:varchar(20)" json:"variant"` // UHD, 1920x1080, etc. Format string `gorm:"uniqueIndex:idx_image_variant_format;type:varchar(10)" json:"format"` // jpg, webp StorageKey string `json:"storage_key"` diff --git a/internal/repo/db.go b/internal/repo/db.go index d7578c2..6cdc3b6 100644 --- a/internal/repo/db.go +++ b/internal/repo/db.go @@ -32,7 +32,8 @@ func InitDB() error { } gormConfig := &gorm.Config{ - Logger: logger.Default.LogMode(logger.Info), + Logger: logger.Default.LogMode(logger.Info), + DisableForeignKeyConstraintWhenMigrating: true, } db, err := gorm.Open(dialector, gormConfig) @@ -40,8 +41,12 @@ func InitDB() error { return err } + // 针对 MySQL 的额外处理:如果数据库不存在,GORM 的 mysql 驱动通常无法直接创建库。 + // 但此处假设 DSN 中指定的数据库已经存在。AutoMigrate 会负责创建表。 + // 迁移 if err := db.AutoMigrate(&model.Image{}, &model.ImageVariant{}, &model.Token{}); err != nil { + util.Logger.Error("Database migration failed", zap.Error(err)) return err } diff --git a/internal/service/fetcher/fetcher.go b/internal/service/fetcher/fetcher.go index c8c70c9..60e6dda 100644 --- a/internal/service/fetcher/fetcher.go +++ b/internal/service/fetcher/fetcher.go @@ -18,8 +18,10 @@ import ( "BingPaper/internal/repo" "BingPaper/internal/storage" "BingPaper/internal/util" + "github.com/disintegration/imaging" "go.uber.org/zap" + "gorm.io/gorm/clause" ) type BingResponse struct { @@ -109,10 +111,22 @@ func (f *Fetcher) processImage(ctx context.Context, bingImg BingImage) error { Quiz: bingImg.Quiz, } - if err := repo.DB.Create(&dbImg).Error; err != nil { + if err := repo.DB.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "date"}}, + DoNothing: true, + }).Create(&dbImg).Error; err != nil { return err } + // 再次检查 dbImg.ID 是否被填充,如果没有被填充(说明由于冲突未插入),则需要查询出已有的 ID + if dbImg.ID == 0 { + var existing model.Image + if err := repo.DB.Where("date = ?", dateStr).First(&existing).Error; err != nil { + return err + } + dbImg = existing + } + // 保存各种分辨率 variants := []struct { name string @@ -194,27 +208,38 @@ func (f *Fetcher) saveVariant(ctx context.Context, img *model.Image, variant, fo Size: int64(len(data)), } - return repo.DB.Create(&vRecord).Error + return repo.DB.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "image_id"}, {Name: "variant"}, {Name: "format"}}, + DoNothing: true, + }).Create(&vRecord).Error } func (f *Fetcher) saveDailyFiles(srcImg image.Image, originalData []byte) { util.Logger.Info("Saving daily files") localRoot := config.GetConfig().Storage.Local.Root if config.GetConfig().Storage.Type != "local" { - // 如果不是本地存储,保存在临时目录或指定缓存目录 + // 如果不是本地存储,保存在静态资源目录 localRoot = "static" } - os.MkdirAll(filepath.Join(localRoot, "static"), 0755) + + if err := os.MkdirAll(localRoot, 0755); err != nil { + util.Logger.Error("Failed to create directory", zap.String("path", localRoot), zap.Error(err)) + return + } // daily.jpeg (quality 95) - jpegPath := filepath.Join(localRoot, "static", "daily.jpeg") - fJpeg, _ := os.Create(jpegPath) - if fJpeg != nil { + jpegPath := filepath.Join(localRoot, "daily.jpeg") + fJpeg, err := os.Create(jpegPath) + if err != nil { + util.Logger.Error("Failed to create daily.jpeg", zap.Error(err)) + } else { jpeg.Encode(fJpeg, srcImg, &jpeg.Options{Quality: 95}) fJpeg.Close() } // original.jpeg (quality 100) - originalPath := filepath.Join(localRoot, "static", "original.jpeg") - os.WriteFile(originalPath, originalData, 0644) + originalPath := filepath.Join(localRoot, "original.jpeg") + if err := os.WriteFile(originalPath, originalData, 0644); err != nil { + util.Logger.Error("Failed to write original.jpeg", zap.Error(err)) + } } diff --git a/internal/service/image/image_service.go b/internal/service/image/image_service.go index 78ff9c8..349d37c 100644 --- a/internal/service/image/image_service.go +++ b/internal/service/image/image_service.go @@ -35,10 +35,14 @@ func CleanupOldImages(ctx context.Context) error { util.Logger.Warn("Failed to delete storage object", zap.String("key", v.StorageKey), zap.Error(err)) } } - // 删除 DB 记录 (级联删除由代码处理,或者 GORM 会处理已加载的关联吗?) - // 简单起见,手动删除关联 - repo.DB.Where("image_id = ?", img.ID).Delete(&model.ImageVariant{}) - repo.DB.Delete(&img) + // 删除关联记录(逻辑外键控制) + if err := repo.DB.Where("image_id = ?", img.ID).Delete(&model.ImageVariant{}).Error; err != nil { + util.Logger.Error("Failed to delete variants", zap.Uint("image_id", img.ID), zap.Error(err)) + } + // 删除主表记录 + if err := repo.DB.Delete(&img).Error; err != nil { + util.Logger.Error("Failed to delete image", zap.Uint("id", img.ID), zap.Error(err)) + } } util.Logger.Info("Cleanup task completed", zap.Int("deleted_count", len(images)))