91 lines
2.7 KiB
Go
91 lines
2.7 KiB
Go
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
|
|
}
|