119 lines
4.9 KiB
Go
119 lines
4.9 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"`
|
||
MatchEvaluatedCount int `bson:"match_evaluated_count" json:"match_evaluated_count"`
|
||
MatchMergedCount int `bson:"match_merged_count" json:"match_merged_count"`
|
||
FitRejectedCount int `bson:"fit_rejected_count" json:"fit_rejected_count"`
|
||
FailedReason string `bson:"failed_reason,omitempty" json:"failed_reason,omitempty"`
|
||
CreditsUsed int `bson:"credits_used" json:"credits_used"`
|
||
DedupedCount int `bson:"deduped_count" json:"deduped_count"`
|
||
PrefilterPassCount int `bson:"prefilter_pass_count" json:"prefilter_pass_count"`
|
||
PrefilterReviewCount int `bson:"prefilter_review_count" json:"prefilter_review_count"`
|
||
PrefilterRejectedCount int `bson:"prefilter_rejected_count" json:"prefilter_rejected_count"`
|
||
CachedJudgmentCount int `bson:"cached_judgment_count" json:"cached_judgment_count"`
|
||
TombstoneMatchedCount int `bson:"tombstone_matched_count" json:"tombstone_matched_count"`
|
||
BudgetDeferredCount int `bson:"budget_deferred_count" json:"budget_deferred_count"`
|
||
DemandInputVersion string `bson:"demand_input_version,omitempty" json:"demand_input_version,omitempty"`
|
||
DemandMapVersion int64 `bson:"demand_map_version,omitempty" json:"demand_map_version,omitempty"`
|
||
CreditSearch int `bson:"credit_search" json:"credit_search"`
|
||
CreditDemandMap int `bson:"credit_demand_map" json:"credit_demand_map"`
|
||
CreditJudge int `bson:"credit_judge" json:"credit_judge"`
|
||
CreditReply int `bson:"credit_reply" json:"credit_reply"`
|
||
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
|
||
MatchEvaluatedCount int
|
||
MatchMergedCount int
|
||
FitRejectedCount int
|
||
CreditsUsed int
|
||
DedupedCount int
|
||
PrefilterPassCount int
|
||
PrefilterReviewCount int
|
||
PrefilterRejectedCount int
|
||
CachedJudgmentCount int
|
||
TombstoneMatchedCount int
|
||
BudgetDeferredCount int
|
||
CreditSearch int
|
||
CreditDemandMap int
|
||
CreditJudge int
|
||
CreditReply 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)
|
||
}
|
||
// WatchID may be empty for on-demand explore (no subscription); required for scheduled sweeps.
|
||
s.WatchID = strings.TrimSpace(s.WatchID)
|
||
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...))
|
||
}
|