168 lines
4.8 KiB
Go
168 lines
4.8 KiB
Go
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
|
||
}
|