90 lines
3.0 KiB
Go
90 lines
3.0 KiB
Go
|
|
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...))
|
|||
|
|
}
|