topfans/backend/services/moderationService/service/auto_hide_service.go
2026-06-22 17:19:48 +08:00

168 lines
4.8 KiB
Go
Raw 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 (
"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.45s 锁覆盖整个 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: &note,
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 逐个 DELspec §6.4 v2.2 B1DEL 不支持通配)
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
}