Files
mce/internal/service/audit_service.go
Victor_Jay 97adf54d6d 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+ 文件)
2026-06-22 02:27:35 +08:00

241 lines
7.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package service
import (
"metazone.cc/mce/internal/common"
"metazone.cc/mce/internal/config"
"metazone.cc/mce/internal/model"
)
// auditRepo AuditService 所需的最小仓储接口ISP
type auditRepo interface {
Create(audit *model.AuditSubmission) error
FindByID(id uint) (*model.AuditSubmission, error)
FindPendingByUserAndType(userID uint, auditType string) (*model.AuditSubmission, error)
Update(audit *model.AuditSubmission) error
ListAudits(auditType, status string, offset, limit int) ([]model.AuditSubmission, error)
CountAudits(auditType, status string) (int64, error)
FindPendingByUserID(userID uint) ([]model.AuditSubmission, error)
ExistsPendingByTypeAndValue(auditType, newValue string, excludeUserID uint) (bool, error)
}
// userProfileStore 审核服务所需的最小用户仓储接口ISP
type userProfileStore interface {
FindByID(id uint) (*model.User, error)
Update(user *model.User) error
ExistsByUsername(username string) (bool, error)
}
// siteSettingsChecker 审核服务所需的站点设置接口ISP仅读取审核开关
type siteSettingsChecker interface {
IsAuditEnabled() bool
IsAuditTypeEnabled(auditType string, defaultVal bool) bool
}
// auditNotifier AuditService 所需的通知接口ISP审核通过/拒绝时发送通知)
type auditNotifier interface {
NotifyAuditApproved(userID uint, auditType string, relatedID uint)
NotifyAuditRejected(userID uint, auditType, reason string, relatedID uint)
}
// auditTaskCompleter 审核通过时触发首次任务奖励的接口
type auditTaskCompleter interface {
CompleteTask(userID uint, taskType string) (newExp int, levelUp bool, err error)
HasCompletedTask(userID uint, taskType string) (bool, error)
}
// energyRenameRefunder 改名审核被拒时返还域能的接口ISP
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
userRepo userProfileStore
settings siteSettingsChecker
defaultCfg config.AuditConfig
notifier auditNotifier
levelSvc auditTaskCompleter
energyRefundSvc energyRenameRefunder
avatarCleaner avatarFileCleaner
}
// NewAuditService 构造函数
func NewAuditService(auditRepo auditRepo, userRepo userProfileStore, settings siteSettingsChecker, cfg config.AuditConfig, notifier auditNotifier, levelSvc auditTaskCompleter) *AuditService {
return &AuditService{
auditRepo: auditRepo,
userRepo: userRepo,
settings: settings,
defaultCfg: cfg,
notifier: notifier,
levelSvc: levelSvc,
}
}
// WithEnergyRefundService 链式注入域能退款服务
func (s *AuditService) WithEnergyRefundService(svc energyRenameRefunder) *AuditService {
s.energyRefundSvc = svc
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) {
user, err := s.userRepo.FindByID(userID)
if err != nil {
return false, err
}
return !model.HasMinRole(user.Role, model.RoleAdmin), nil
}
// Submit 提交审核
// - 总开关未开 → 返回 ErrAuditDisabled
// - 类型开关未开 → 返回 ErrAuditTypeOff
// - 用户名类型 → 先检查是否已被占用
// - 若存在同类型 pending → 标记旧记录为 rejected理由"用户重新提交"),创建新 pending
func (s *AuditService) Submit(userID uint, auditType, newValue string) error {
// 检查总开关
if !s.settings.IsAuditEnabled() {
return common.ErrAuditDisabled
}
// 检查类型开关
if !s.settings.IsAuditTypeEnabled(auditType, s.typeDefault(auditType)) {
return common.ErrAuditTypeOff
}
// 用户名冲突检查(提交时即拦截,避免提交到后台后审核失败)
if auditType == model.AuditTypeUsername {
// 1) users 表中已被占用
exists, err := s.userRepo.ExistsByUsername(newValue)
if err != nil {
return err
}
if exists {
return common.ErrUsernameTaken
}
// 2) 审核表中已有其他用户提交了同名 pending
pendingExists, err := s.auditRepo.ExistsPendingByTypeAndValue(auditType, newValue, userID)
if err != nil {
return err
}
if pendingExists {
return common.ErrUsernameTaken
}
}
// 覆盖旧的同类型 pending
existing, err := s.auditRepo.FindPendingByUserAndType(userID, auditType)
if err == nil {
reason := "用户重新提交"
existing.Status = model.AuditStatusRejected
existing.RejectReason = &reason
_ = s.auditRepo.Update(existing)
}
submission := &model.AuditSubmission{
UserID: userID,
AuditType: auditType,
NewValue: newValue,
Status: model.AuditStatusPending,
}
return s.auditRepo.Create(submission)
}
// SubmitProfileChanges 批量提交资料变更(用户名/签名),每个变更字段独立提交
// 仅提交有变更的字段,无变更的跳过
func (s *AuditService) SubmitProfileChanges(userID uint, currentUser *model.User, newUsername, newBio string) error {
if newUsername != "" && newUsername != currentUser.Username {
if err := s.Submit(userID, model.AuditTypeUsername, newUsername); err != nil {
return err
}
}
if newBio != "" && newBio != currentUser.Bio {
if err := s.Submit(userID, model.AuditTypeBio, newBio); err != nil {
return err
}
}
return nil
}
// Approve 通过审核 → 更新 User 表对应字段
// 仅审核人可操作,不可审核自己的提交
func (s *AuditService) Approve(reviewerID uint, submissionID uint) error {
return s.review(reviewerID, submissionID, true, "")
}
// Reject 拒绝审核 → 记录拒绝理由
func (s *AuditService) Reject(reviewerID uint, submissionID uint, reason string) error {
return s.review(reviewerID, submissionID, false, reason)
}
// List 分页查询审核列表
func (s *AuditService) List(auditType, status string, page, pageSize int) (*AuditListResult, error) {
p := common.Pagination{Page: page, PageSize: pageSize}
p.DefaultPagination()
total, err := s.auditRepo.CountAudits(auditType, status)
if err != nil {
return nil, err
}
items, err := s.auditRepo.ListAudits(auditType, status, p.Offset(), p.PageSize)
if err != nil {
return nil, err
}
if items == nil {
items = []model.AuditSubmission{}
}
return &AuditListResult{
Items: items,
Total: total,
Page: p.Page,
}, nil
}
// GetPendingTypes 获取用户所有待审核类型(用于个人中心提示)
func (s *AuditService) GetPendingTypes(userID uint) ([]string, error) {
submissions, err := s.auditRepo.FindPendingByUserID(userID)
if err != nil {
return nil, err
}
types := make([]string, 0, len(submissions))
for _, sub := range submissions {
types = append(types, sub.AuditType)
}
return types, nil
}
// typeDefault 获取某审核类型在 config.yaml 中的默认开关值
func (s *AuditService) typeDefault(auditType string) bool {
switch auditType {
case model.AuditTypeUsername:
return s.defaultCfg.UsernameAudit
case model.AuditTypeAvatar:
return s.defaultCfg.AvatarAudit
case model.AuditTypeBio:
return s.defaultCfg.BioAudit
}
return true
}
// AuditListResult 审核列表查询结果
type AuditListResult struct {
Items []model.AuditSubmission `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
}