fix: golangci-lint 零告警通过 (57→0) + gofumpt/goimports 全量格式化

## CI 修复 (P0/P1)

P0 — 编译阻塞:
  - interfaces.go: postUseCase ISP 接口移除不用的 Create/Update/Delete

P1 — 必须修复:
  - ST1000: 为 13 个包添加包注释 (common/config/model/service/...)
  - ST1005: redis_store.go 全部错误消息改为小写开头
  - errcheck (19处): defer Close()→闭包忽略, notifier.Create→_=, r.Run→检查error
  - errorlint (9处): switch-on-error→errors.Is 链, ==→errors.Is
  - ST1020/ST1022: 导出符号注释以符号名开头

P2 — 安全评审:
  - gosec (12处): G203/G301/G304/G306 添加 nolint 注释并附理由

P3 — 清理:
  - unused: 移除 hasUnicode/energyStore/current/energyAdminUseCase
  - gofumpt + goimports 全量格式化 (35+ 文件)
This commit is contained in:
2026-06-22 02:27:35 +08:00
parent e65f903362
commit 97adf54d6d
75 changed files with 418 additions and 420 deletions

View File

@ -1,3 +1,4 @@
// Package service 包含核心业务逻辑实现,通过 Store 接口隔离数据访问层。
package service
import (

View File

@ -1,6 +1,8 @@
package service
import (
"errors"
"metazone.cc/mce/internal/common"
"metazone.cc/mce/internal/model"
@ -12,7 +14,7 @@ func (s *AuditService) review(reviewerID uint, submissionID uint, approved bool,
// 1. 查审核记录
submission, err := s.auditRepo.FindByID(submissionID)
if err != nil {
if err == gorm.ErrRecordNotFound {
if errors.Is(err, gorm.ErrRecordNotFound) {
return common.ErrAuditNotFound
}
return err

View File

@ -56,14 +56,14 @@ type avatarFileCleaner interface {
// AuditService 审核业务逻辑
type AuditService struct {
auditRepo auditRepo
userRepo userProfileStore
settings siteSettingsChecker
defaultCfg config.AuditConfig
notifier auditNotifier
levelSvc auditTaskCompleter
energyRefundSvc energyRenameRefunder
avatarCleaner avatarFileCleaner
auditRepo auditRepo
userRepo userProfileStore
settings siteSettingsChecker
defaultCfg config.AuditConfig
notifier auditNotifier
levelSvc auditTaskCompleter
energyRefundSvc energyRenameRefunder
avatarCleaner avatarFileCleaner
}
// NewAuditService 构造函数

View File

@ -4,7 +4,6 @@ import (
"regexp"
"strings"
"time"
"unicode"
"metazone.cc/mce/internal/common"
"metazone.cc/mce/internal/model"
@ -13,10 +12,12 @@ import (
"gorm.io/gorm"
)
var pwLower = regexp.MustCompile(`[a-z]`)
var pwUpper = regexp.MustCompile(`[A-Z]`)
var pwDigit = regexp.MustCompile(`\d`)
var pwSymbol = regexp.MustCompile(`[^a-zA-Z0-9]`)
var (
pwLower = regexp.MustCompile(`[a-z]`)
pwUpper = regexp.MustCompile(`[A-Z]`)
pwDigit = regexp.MustCompile(`\d`)
pwSymbol = regexp.MustCompile(`[^a-zA-Z0-9]`)
)
// 键盘水平序列QWERTY 布局)
var keyboardHoriz = []string{
@ -181,16 +182,6 @@ func PasswordStrengthHint(level string) string {
return "密码至少 8 位"
}
// hasUnicode 检查是否包含非 ASCII 字符
func hasUnicode(s string) bool {
for _, r := range s {
if r > unicode.MaxASCII {
return true
}
}
return false
}
// ChangePassword 修改密码
// 流程:验证当前密码 → 校验新密码强度 → 哈希 → 更新 → 删除所有 session强制重新登录
func (s *AuthService) ChangePassword(userID uint, currentPassword, newPassword string) error {

View File

@ -22,8 +22,8 @@ import (
// 头像处理常量
const (
avatarMaxFileSize = 5 * 1024 * 1024 // 5MB
avatarMinWidth = 128 // 最小宽高,防止马赛克图被拉升
avatarMaxFileSize = 5 * 1024 * 1024 // 5MB
avatarMinWidth = 128 // 最小宽高,防止马赛克图被拉升
avatarMinHeight = 128
avatarMaxWidth = 3840
avatarMaxHeight = 3840
@ -97,7 +97,7 @@ func (s *AvatarService) ProcessImage(userID uint, file io.Reader, contentType st
img, _, err = image.Decode(bytes.NewReader(data))
}
if err != nil {
return "", fmt.Errorf("图片解码失败:%v", err)
return "", fmt.Errorf("图片解码失败:%w", err)
}
bounds := img.Bounds()
w, h := bounds.Dx(), bounds.Dy()
@ -129,11 +129,11 @@ func (s *AvatarService) ProcessImage(userID uint, file io.Reader, contentType st
// 6. 编码为 WebP目标 ≤100KB
webpData, err := encodeWebP(resized, avatarTargetKB*1024)
if err != nil {
return "", fmt.Errorf("图片编码失败:%v", err)
return "", fmt.Errorf("图片编码失败:%w", err)
}
// 7. 确保存储目录存在
if err := os.MkdirAll(s.storageDir, 0755); err != nil {
if err := os.MkdirAll(s.storageDir, 0o755); err != nil { //nolint:gosec // 头像存储目录标准权限
return "", fmt.Errorf("创建存储目录失败")
}
@ -143,11 +143,11 @@ func (s *AvatarService) ProcessImage(userID uint, file io.Reader, contentType st
filename := fmt.Sprintf("%d_%d.webp", userID, ts)
tmpPath := filepath.Join(s.storageDir, filename+".tmp")
finalPath := filepath.Join(s.storageDir, filename)
if err := os.WriteFile(tmpPath, webpData, 0644); err != nil {
if err := os.WriteFile(tmpPath, webpData, 0o644); err != nil { //nolint:gosec // 上传头像标准权限
return "", fmt.Errorf("写入临时文件失败")
}
if err := os.Rename(tmpPath, finalPath); err != nil {
os.Remove(tmpPath)
_ = os.Remove(tmpPath)
return "", fmt.Errorf("保存文件失败")
}
@ -163,7 +163,7 @@ func (s *AvatarService) CleanOldAvatars(userID uint) {
}
for _, e := range entries {
if !e.IsDir() && strings.HasPrefix(e.Name(), prefix) && strings.HasSuffix(e.Name(), ".webp") {
os.Remove(filepath.Join(s.storageDir, e.Name()))
_ = os.Remove(filepath.Join(s.storageDir, e.Name()))
}
}
}
@ -242,4 +242,3 @@ func encodeWebP(img image.Image, targetBytes int) ([]byte, error) {
}
return buf.Bytes(), nil
}

View File

@ -64,7 +64,7 @@ func (s *CommentService) CreateRoot(userID uint, postID uint, body string) (*mod
// 通知文章作者(评论者 ≠ 作者RelatedID 存 postID 以便消息页生成跳转链接
if s.notifier != nil && userID != postAuthorID {
s.notifier.Create(postAuthorID, model.NotifyComment,
_ = s.notifier.Create(postAuthorID, model.NotifyComment,
"新评论",
fmt.Sprintf("%s 评论了你的文章", commenterName),
&comment.PostID, &comment.ID)
@ -112,7 +112,7 @@ func (s *CommentService) CreateReply(userID uint, parentID uint, body string) (*
// 通知被回复者不自己回复自己RelatedID 存 postID 以便消息页生成跳转链接
if s.notifier != nil && parent.UserID != userID {
s.notifier.Create(parent.UserID, model.NotifyCommentReply,
_ = s.notifier.Create(parent.UserID, model.NotifyCommentReply,
"新回复",
fmt.Sprintf("%s 回复了你的评论", commenterName),
&parent.PostID, &comment.ID)
@ -235,7 +235,7 @@ func (s *CommentService) parseMentions(commentID uint, postID uint, commenterID
// 向被@用户发送通知(排除自己@自己、重复通知)
if s.notifier != nil && mentionedUID != commenterID && !notified[mentionedUID] {
notified[mentionedUID] = true
s.notifier.Create(mentionedUID, model.NotifyCommentMention,
_ = s.notifier.Create(mentionedUID, model.NotifyCommentMention,
"新提及",
fmt.Sprintf("%s 在评论中 @ 了你", commenterName),
&postID, &commentID)

View File

@ -30,22 +30,22 @@ func (s *EnergyService) SetFundRepo(repo FundStore) {
// EnergyInfo 域能信息(含每日赋能经验状态)
type EnergyInfo struct {
Energy int // 域能余额乘10存储
DailyEnergizeExp int // 当日已通过赋能获得的经验值
DailyExpCap int // 每日赋能经验上限
DailyExpCapReached bool // 是否已达上限
PostEnergizeTotal int // 当前文章已赋能量DB乘10仅在传 postID 时返回)
Energy int // 域能余额乘10存储
DailyEnergizeExp int // 当日已通过赋能获得的经验值
DailyExpCap int // 每日赋能经验上限
DailyExpCapReached bool // 是否已达上限
PostEnergizeTotal int // 当前文章已赋能量DB乘10仅在传 postID 时返回)
}
// 域能相关常量乘10存储DB值
const (
EnergyCheckIn = 10 // +1.0 域能
EnergyRename = -60 // -6.0 域能
EnergyRenameRefund = 60 // +6.0 域能
EnergyDeletePost = -20 // -2.0 域能
EnergyEnergize1 = 10 // 轻赋 1 域能
EnergyEnergize2 = 20 // 重赋 2 域能
EnergizeExpPerEnergy = 10 // 每 1 域能 = 10 经验
DailyEnergizeExpCap = 50 // 每日赋能经验上限
MaxEnergizePerPost = 20 // 单用户单文章最多赋能 2 域能×10
EnergyCheckIn = 10 // +1.0 域能
EnergyRename = -60 // -6.0 域能
EnergyRenameRefund = 60 // +6.0 域能
EnergyDeletePost = -20 // -2.0 域能
EnergyEnergize1 = 10 // 轻赋 1 域能
EnergyEnergize2 = 20 // 重赋 2 域能
EnergizeExpPerEnergy = 10 // 每 1 域能 = 10 经验
DailyEnergizeExpCap = 50 // 每日赋能经验上限
MaxEnergizePerPost = 20 // 单用户单文章最多赋能 2 域能×10
)

View File

@ -19,7 +19,6 @@ func (s *FavoriteService) AddToFolder(userID, folderID, postID uint) error {
}
return s.repo.Transaction(func(txRepo FavoriteStore) error {
// 如果该文章已在其他收藏夹中,先移除
existing, err := txRepo.FindItemByUserAndPost(userID, postID)
if err == nil && existing.FolderID != folderID {

View File

@ -51,8 +51,8 @@ type FolderItemsResult struct {
// FavoriteStatus 某文章的收藏状态
type FavoriteStatus struct {
Favorited bool `json:"favorited"`
FolderID *uint `json:"folder_id,omitempty"`
Favorited bool `json:"favorited"`
FolderID *uint `json:"folder_id,omitempty"`
FolderName *string `json:"folder_name,omitempty"`
}

View File

@ -9,10 +9,10 @@ import (
// LevelService 积分与等级业务逻辑
type LevelService struct {
userRepo userLevelStore
checkRepo checkInStore
taskRepo taskStore
notifSvc levelNotifier
userRepo userLevelStore
checkRepo checkInStore
taskRepo taskStore
notifSvc levelNotifier
energyCheckInSvc energyCheckIner
}

View File

@ -15,11 +15,11 @@ import (
// PostService 帖子业务逻辑
type PostService struct {
repo postStore
ss *config.SiteSettings
notifier postNotifier
userRepo userExpReader
onHomePageChanged func()
repo postStore
ss *config.SiteSettings
notifier postNotifier
userRepo userExpReader
onHomePageChanged func()
}
// NewPostService 构造函数
@ -146,12 +146,12 @@ func (s *PostService) ListByUser(userID uint, status string, page, pageSize int)
// StudioOverview 创作中心数据概览
type StudioOverview struct {
TotalPosts int64 `json:"total_posts"`
DraftCount int64 `json:"draft_count"`
PendingCount int64 `json:"pending_count"`
TotalPosts int64 `json:"total_posts"`
DraftCount int64 `json:"draft_count"`
PendingCount int64 `json:"pending_count"`
ApprovedCount int64 `json:"approved_count"`
RejectedCount int64 `json:"rejected_count"`
TotalViews int64 `json:"total_views"`
TotalViews int64 `json:"total_views"`
}
// GetOverview 获取创作中心数据概览(单次 GROUP BY 查询)

View File

@ -11,8 +11,8 @@ import (
type ReactionType string
const (
ReactionNone ReactionType = "none"
ReactionLiked ReactionType = "liked"
ReactionNone ReactionType = "none"
ReactionLiked ReactionType = "liked"
ReactionDisliked ReactionType = "disliked"
)

View File

@ -125,9 +125,6 @@ type EnergyStore interface {
Transaction(fn func(txEnergy EnergyStore, txFund FundStore) error) error
}
// energyStore 向后兼容别名
type energyStore = EnergyStore
// FundStore 公户仓储接口ISP
type FundStore interface {
GetBalance() (int, error)

View File

@ -98,10 +98,11 @@ func parseShortcode(raw string) (model.Shortcode, bool) {
// renderPlaceholder 根据 shortcode 类型生成前端占位 HTML
//
// 占位 div 结构约定:
// <div class="zone-card" data-zone-type="类型" data-zone-id="参数"
// data-zone-extra="附加信息">
// <span class="zone-card-loading">卡片加载中...</span>
// </div>
//
// <div class="zone-card" data-zone-type="类型" data-zone-id="参数"
// data-zone-extra="附加信息">
// <span class="zone-card-loading">卡片加载中...</span>
// </div>
//
// 前端 shortcode.js 根据 data-zone-* 属性决定如何渲染。
// "加载中..." 文本会被 shortcode.js 在渲染时替换,仅作降级显示。