thread-master/apps/backend/internal/module/radar/usecase/judge_cache.go

91 lines
2.7 KiB
Go
Raw Normal View History

2026-08-13 02:22:24 +00:00
package usecase
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"strings"
"apps/backend/internal/module/radar/domain"
)
// JudgeCandidate is the metered boundary. A successful structured result is
// cached by source identity and DemandMap version; cache hits return zero
// credits and never call the provider again.
func (s *Service) JudgeCandidate(ctx context.Context, ownerUID int64, profile *domain.ServiceProfile, watch *domain.RadarWatch, cand *domain.CandidatePost) (*JudgeResult, int, error) {
key := s.judgeCacheKey(ctx, ownerUID, watch, cand)
if key != "" {
if result, ok := s.cachedJudgeResult(key); ok {
return result, 0, nil
}
}
result, credits, err := s.judgeCandidateUncached(ctx, ownerUID, profile, watch, cand)
if err == nil && result != nil && result.Cacheable && key != "" {
s.storeJudgeResult(key, result)
}
return result, credits, err
}
func (s *Service) judgeCacheKey(ctx context.Context, ownerUID int64, watch *domain.RadarWatch, cand *domain.CandidatePost) string {
if cand == nil || strings.TrimSpace(cand.ExternalID) == "" {
return ""
}
contextKey := "generic"
mapVersion := int64(0)
if watch != nil {
contextKey = watch.ContextMode + ":" + watch.ProductID
if watch.ContextMode == domain.WatchContextProduct && s.Repo != nil {
if dm, err := s.GetDemandMap(ctx, ownerUID, watch.ProductID); err == nil && dm != nil {
mapVersion = dm.MapVersion
}
}
}
raw := fmt.Sprintf("%d|%s|%d|%s|%s|%d", ownerUID, contextKey, mapVersion, cand.ExternalID, cand.Text, cand.PostedAt)
sum := sha256.Sum256([]byte(raw))
return hex.EncodeToString(sum[:])
}
func (s *Service) cachedJudgeResult(key string) (*JudgeResult, bool) {
s.judgeCacheMu.Lock()
defer s.judgeCacheMu.Unlock()
if s.judgeCache == nil {
s.judgeCache = map[string]cachedJudge{}
}
value, ok := s.judgeCache[key]
if !ok || domain.NowNano()-value.createdAt > int64(24*60*60*1e9) {
if ok {
delete(s.judgeCache, key)
}
return nil, false
}
return cloneJudgeResult(value.result), true
}
func (s *Service) storeJudgeResult(key string, result *JudgeResult) {
s.judgeCacheMu.Lock()
defer s.judgeCacheMu.Unlock()
if s.judgeCache == nil {
s.judgeCache = map[string]cachedJudge{}
}
if len(s.judgeCache) >= 2048 {
for oldKey := range s.judgeCache {
delete(s.judgeCache, oldKey)
break
}
}
s.judgeCache[key] = cachedJudge{result: cloneJudgeResult(result), createdAt: domain.NowNano()}
}
func cloneJudgeResult(in *JudgeResult) *JudgeResult {
if in == nil {
return nil
}
out := *in
out.Reasons = append([]domain.OpportunityReason(nil), in.Reasons...)
if in.ProductMatch != nil {
out.ProductMatch = domain.CloneProductMatch(in.ProductMatch)
}
return &out
}