thread-master/apps/backend/internal/module/radar/domain/sweep.go

90 lines
3.0 KiB
Go
Raw Permalink Normal View History

2026-08-03 05:52:02 +00:00
package domain
import (
"fmt"
"strings"
)
// Sweep fetch path — reuses the dual-path split (api | crawler); no third path.
const (
SweepPathAPI = "api"
SweepPathCrawler = "crawler"
)
/*
RadarSweep 是一次每日巡或手動觸發的執行紀錄
計數以累加更新為主Job 中途失敗時已寫入的進度保留供續跑判斷
judged_external_ids 標記已判過的貼文避免重跑
*/
type RadarSweep struct {
ID string `bson:"_id" json:"id"`
OwnerUID int64 `bson:"owner_uid" json:"owner_uid"`
WatchID string `bson:"watch_id" json:"watch_id"`
JobID string `bson:"job_id,omitempty" json:"job_id,omitempty"`
Path string `bson:"path" json:"path"`
HitCount int `bson:"hit_count" json:"hit_count"`
JudgedCount int `bson:"judged_count" json:"judged_count"`
CreatedCount int `bson:"created_count" json:"created_count"`
TruncatedCount int `bson:"truncated_count" json:"truncated_count"`
FailedReason string `bson:"failed_reason,omitempty" json:"failed_reason,omitempty"`
CreditsUsed int `bson:"credits_used" json:"credits_used"`
JudgedExternalIDs []string `bson:"judged_external_ids,omitempty" json:"judged_external_ids,omitempty"`
StartedAt int64 `bson:"started_at" json:"started_at"`
EndedAt int64 `bson:"ended_at,omitempty" json:"ended_at,omitempty"`
}
// SweepDelta is an incremental progress patch applied with $inc / $addToSet.
type SweepDelta struct {
HitCount int
JudgedCount int
CreatedCount int
TruncatedCount int
CreditsUsed int
JudgedExternalIDs []string
FailedReason *string // nil = leave unchanged; non-nil (incl. empty) = set
EndedAt int64 // 0 = leave unchanged
}
// SweepListFilter pages sweeps for an owner, optionally scoped to one watch.
type SweepListFilter struct {
WatchID string
Page int
PageSize int
}
func IsSweepPath(s string) bool {
switch s {
case SweepPathAPI, SweepPathCrawler:
return true
}
return false
}
func (s *RadarSweep) Normalize() error {
if s.OwnerUID <= 0 {
return fmt.Errorf("%w: owner_uid required", ErrValidation)
}
if strings.TrimSpace(s.WatchID) == "" {
return fmt.Errorf("%w: watch_id required", ErrValidation)
}
if s.Path == "" {
s.Path = SweepPathAPI
}
if !IsSweepPath(s.Path) {
return fmt.Errorf("%w: unknown sweep path %q", ErrValidation, s.Path)
}
// failed_reason must never hold tokens; strip obvious bearer-like blobs is out of scope —
// callers are responsible. We only reject empty path/owner.
if s.FailedReason != "" {
s.FailedReason = strings.TrimSpace(s.FailedReason)
}
s.JudgedExternalIDs = dedupeStrings(s.JudgedExternalIDs)
return nil
}
// MergeJudgedExternalIDs appends new IDs without duplicates (order preserved).
func MergeJudgedExternalIDs(existing, add []string) []string {
return dedupeStrings(append(append([]string{}, existing...), add...))
}