From ca21ae7216b1feaf3c114fdcdb0f6fe77bff8c0b Mon Sep 17 00:00:00 2001 From: Victor_Jay Date: Tue, 2 Jun 2026 22:26:01 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=A4=B4=E5=83=8F?= =?UTF-8?q?=E5=AE=A1=E6=A0=B8=E6=97=A7=E5=9B=BE=E8=A3=82=E5=BC=80=E9=97=AE?= =?UTF-8?q?=E9=A2=98=20+=20CSP=20=E9=85=8D=E7=BD=AE=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 头像提交审核不再删除旧文件,审核通过后才删除旧头像文件 - 头像审核被拒时删除已上传的新头像文件,保留旧头像 - CSP style-src 移除 nonce 以启用 unsafe-inline(JS 动态样式需要) - CSP script-src 移除 nonce 改用 unsafe-inline + unsafe-eval(Vditor 编辑器和动态样式需要) --- internal/middleware/security.go | 4 ++-- internal/router/deps_extra.go | 1 + internal/service/audit_review.go | 14 ++++++++++++++ internal/service/audit_service.go | 13 +++++++++++++ internal/service/avatar_service.go | 26 ++++++++++++++++++++------ 5 files changed, 50 insertions(+), 8 deletions(-) diff --git a/internal/middleware/security.go b/internal/middleware/security.go index 59a42bf..1d55ee0 100644 --- a/internal/middleware/security.go +++ b/internal/middleware/security.go @@ -32,8 +32,8 @@ func SecurityHeaders() gin.HandlerFunc { // img-src: 本站 + data: URI(头像裁切)+ blob:(粘贴图片) c.Header("Content-Security-Policy", "default-src 'self'; "+ - "script-src 'self' 'nonce-"+nonce+"'; "+ - "style-src 'self' 'nonce-"+nonce+"'; "+ + "script-src 'self' 'unsafe-inline' 'unsafe-eval'; "+ + "style-src 'self' 'unsafe-inline'; "+ "img-src 'self' data: blob:; "+ "connect-src 'self'") diff --git a/internal/router/deps_extra.go b/internal/router/deps_extra.go index 519ac8c..561709a 100644 --- a/internal/router/deps_extra.go +++ b/internal/router/deps_extra.go @@ -38,6 +38,7 @@ func buildDeps(db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSetting auditService := service.NewAuditService(auditRepo, userRepo, siteSettings, cfg.Audit, notificationService, levelSvc) auditService.WithEnergyRefundService(energyService) + auditService.WithAvatarFileCleaner(avatarSvc) auditController := adminCtrl.NewAuditController(auditService, userRepo) messageController := controller.NewMessageController(notificationService) diff --git a/internal/service/audit_review.go b/internal/service/audit_review.go index 2fe3564..b753175 100644 --- a/internal/service/audit_review.go +++ b/internal/service/audit_review.go @@ -67,6 +67,11 @@ func (s *AuditService) review(reviewerID uint, submissionID uint, approved bool, } s.notifier.NotifyAuditRejected(submission.UserID, submission.AuditType, reasonMsg, submission.ID) } + + // 头像审核被拒 → 删除提交的新头像文件 + if submission.AuditType == model.AuditTypeAvatar && s.avatarCleaner != nil { + _ = s.avatarCleaner.DeleteAvatarFile(submission.NewValue) + } } return s.auditRepo.Update(submission) } @@ -90,7 +95,16 @@ func (s *AuditService) applyApproval(submission *model.AuditSubmission) error { } user.Username = submission.NewValue case model.AuditTypeAvatar: + oldAvatar := user.Avatar user.Avatar = submission.NewValue + if err := s.userRepo.Update(user); err != nil { + return err + } + // 审核通过,删除旧头像文件 + if s.avatarCleaner != nil { + _ = s.avatarCleaner.DeleteAvatarFile(oldAvatar) + } + return nil case model.AuditTypeBio: user.Bio = submission.NewValue } diff --git a/internal/service/audit_service.go b/internal/service/audit_service.go index c49d4c0..d8f8c9a 100644 --- a/internal/service/audit_service.go +++ b/internal/service/audit_service.go @@ -48,6 +48,12 @@ type energyRenameRefunder interface { RefundOnRenameReject(userID uint) error } +// avatarFileCleaner 头像审核通过/拒绝时清理头像文件的接口(ISP) +type avatarFileCleaner interface { + CleanOldAvatars(userID uint) + DeleteAvatarFile(url string) error +} + // AuditService 审核业务逻辑 type AuditService struct { auditRepo auditRepo @@ -57,6 +63,7 @@ type AuditService struct { notifier auditNotifier levelSvc auditTaskCompleter energyRefundSvc energyRenameRefunder + avatarCleaner avatarFileCleaner } // NewAuditService 构造函数 @@ -77,6 +84,12 @@ func (s *AuditService) WithEnergyRefundService(svc energyRenameRefunder) *AuditS return s } +// WithAvatarFileCleaner 链式注入头像文件清理服务 +func (s *AuditService) WithAvatarFileCleaner(svc avatarFileCleaner) *AuditService { + s.avatarCleaner = svc + return s +} + // ShouldAudit 判断指定用户是否需要走审核流程 // 规则:admin 及以上角色免审 func (s *AuditService) ShouldAudit(userID uint) (bool, error) { diff --git a/internal/service/avatar_service.go b/internal/service/avatar_service.go index 6093231..85c021f 100644 --- a/internal/service/avatar_service.go +++ b/internal/service/avatar_service.go @@ -57,6 +57,9 @@ func (s *AvatarService) ProcessAvatar(userID uint, file io.Reader, contentType s return "", err } + // 直接更新场景:删除旧头像文件,仅保留新文件 + s.CleanOldAvatars(userID) + user, err := s.userRepo.FindByID(userID) if err != nil { return "", fmt.Errorf("用户不存在") @@ -134,10 +137,8 @@ func (s *AvatarService) ProcessImage(userID uint, file io.Reader, contentType st return "", fmt.Errorf("创建存储目录失败") } - // 8. 删除该用户的旧头像,仅保留最新 - s.cleanOldAvatars(userID) - - // 9. 原子写入:先写临时文件,再 rename 到最终路径 + // 8. 原子写入:先写临时文件,再 rename 到最终路径 + // 注意:此处不删除旧头像文件,审核场景下审核通过前旧头像仍需正常显示 ts := time.Now().Unix() filename := fmt.Sprintf("%d_%d.webp", userID, ts) tmpPath := filepath.Join(s.storageDir, filename+".tmp") @@ -153,8 +154,8 @@ func (s *AvatarService) ProcessImage(userID uint, file io.Reader, contentType st return fmt.Sprintf("/uploads/avatars/%d_%d.webp", userID, ts), nil } -// cleanOldAvatars 删除指定用户的所有旧头像文件(仅保留最新一次上传) -func (s *AvatarService) cleanOldAvatars(userID uint) { +// CleanOldAvatars 删除指定用户的所有旧头像文件,调用方负责确保新文件不受影响 +func (s *AvatarService) CleanOldAvatars(userID uint) { prefix := strconv.FormatUint(uint64(userID), 10) + "_" entries, err := os.ReadDir(s.storageDir) if err != nil { @@ -167,6 +168,19 @@ func (s *AvatarService) cleanOldAvatars(userID uint) { } } +// DeleteAvatarFile 根据头像 URL 删除对应的磁盘文件 +func (s *AvatarService) DeleteAvatarFile(url string) error { + if url == "" { + return nil + } + filename := filepath.Base(url) + if filename == "." || filename == "/" { + return nil + } + fullPath := filepath.Join(s.storageDir, filename) + return os.Remove(fullPath) +} + // subImage 从原图中裁切指定正方形区域 func subImage(img image.Image, x, y, size int) image.Image { cropped := image.NewNRGBA(image.Rect(0, 0, size, size))