package service import ( "context" _ "embed" "fmt" "github.com/redis/go-redis/v9" "gorm.io/gorm" "github.com/topfans/backend/pkg/logger" "github.com/topfans/backend/pkg/models" "github.com/topfans/backend/services/moderationService/config" "github.com/topfans/backend/services/moderationService/repository" ) //go:embed lua/auto_hide.lua var autoHideLuaScript string type AutoHideService struct { repo *repository.ModerationRepository redis *redis.Client tx *TxHelper cfg config.ModerationConfig } func NewAutoHideService( repo *repository.ModerationRepository, rdb *redis.Client, tx *TxHelper, cfg config.ModerationConfig, ) *AutoHideService { return &AutoHideService{repo: repo, redis: rdb, tx: tx, cfg: cfg} } // TryTrigger - 提交举报后调用(spec §6.1 step 6) // 返回值:(triggered bool, counter int64) func (s *AutoHideService) TryTrigger( ctx context.Context, targetType string, targetID int64, reporterID int64, ) (bool, int64, error) { // 应用层短锁(spec §6.4:5s 锁覆盖整个 auto-hide 流程) lockKey := fmt.Sprintf("%s:lock:%s:%d", s.cfg.RedisKeyPrefix, targetType, targetID) ok, err := s.redis.SetNX(ctx, lockKey, "1", s.cfg.LockTTL).Result() if err != nil { return false, 0, err } if !ok { return false, 0, nil // 5s 内已有并发请求在跑 } defer func() { if err := s.redis.Del(ctx, lockKey).Err(); err != nil { logger.Sugar.Warnw("failed to release auto-hide lock", "key", lockKey, "err", err) } }() counterKey := fmt.Sprintf("%s:counter:%s:%d", s.cfg.RedisKeyPrefix, targetType, targetID) userMarkerKey := fmt.Sprintf("%s:user:%s:%d:%d", s.cfg.RedisKeyPrefix, targetType, targetID, reporterID) result, err := s.redis.Eval(ctx, autoHideLuaScript, []string{counterKey, userMarkerKey}, s.cfg.AutoHideThreshold, int(s.cfg.CounterTTL.Seconds()), int(s.cfg.UserMarkerTTL.Seconds()), ).Result() if err != nil { return false, 0, err } arr := result.([]interface{}) triggered := arr[0].(int64) == 1 counter := arr[1].(int64) return triggered, counter, nil } // ExecuteAutoHide - 触发自动隐藏(事务内:业务表软删除 + reports UPDATE + mts UPSERT + 流水) func (s *AutoHideService) ExecuteAutoHide( ctx context.Context, targetType string, targetID int64, triggerReportID int64, ) error { now := repository.Now() return s.tx.WithTx(ctx, func(tx *gorm.DB) error { var affected int64 var err error // ① 业务表软删除(按 target_type 路由) switch targetType { case "asset": affected, err = s.repo.SoftDeleteAsset(ctx, targetID, now) case "user_profile": affected, err = s.repo.SoftDeleteUser(ctx, targetID, now) default: return fmt.Errorf("unsupported target_type: %s", targetType) } if err != nil { return err } // ② reports UPDATE pending → auto_hidden if _, err := s.repo.UpgradePendingToAutoHidden(ctx, targetType, targetID, triggerReportID, now); err != nil { return err } // ③ mts UPSERT mts := &models.ModerationTargetStatus{ TargetType: targetType, TargetID: targetID, LastActionType: "autohide", Source: "auto", SourceReportID: &triggerReportID, CreatedAt: now, UpdatedAt: now, } if err := s.repo.UpsertTargetStatus(ctx, mts); err != nil { return err } // ③.5 写 moderation_actions 流水(v2.4 V7 conditional) actionType := "takedown" note := "auto-hide threshold reached" if affected == 0 { actionType = "autohide_noop" note = "auto-hide no-op: target already is_active=false" } reportID := triggerReportID tt := targetType tid := targetID action := &models.ModerationAction{ ReportID: &reportID, AdminID: 0, // 系统自动 sentinel (v2.2 B4) ActionType: actionType, TargetType: &tt, TargetID: &tid, Note: ¬e, Success: true, CreatedAt: now, } return s.repo.CreateAction(ctx, action) }) } // ResetCounter - spec §6.2 path C: admin 判定误报,重置 Redis 计数 func (s *AutoHideService) ResetCounter(ctx context.Context, targetType string, targetID int64) error { counterKey := fmt.Sprintf("%s:counter:%s:%d", s.cfg.RedisKeyPrefix, targetType, targetID) lockKey := fmt.Sprintf("%s:lock:%s:%d", s.cfg.RedisKeyPrefix, targetType, targetID) userMarkerPattern := fmt.Sprintf("%s:user:%s:%d:*", s.cfg.RedisKeyPrefix, targetType, targetID) // DEL counter + lock if err := s.redis.Del(ctx, counterKey, lockKey).Err(); err != nil { return err } // SCAN MATCH user_marker 逐个 DEL(spec §6.4 v2.2 B1:DEL 不支持通配) iter := s.redis.Scan(ctx, 0, userMarkerPattern, 100).Iterator() var keys []string for iter.Next(ctx) { keys = append(keys, iter.Val()) } if err := iter.Err(); err != nil { return err } if len(keys) > 0 { if err := s.redis.Del(ctx, keys...).Err(); err != nil { return err } } return nil }