package usecase import ( "crypto/sha256" "encoding/hex" "net/url" "strconv" "strings" "apps/backend/internal/module/scout/domain" ) type classifiedPost struct { classification string score int reason string } func planScanTerms(brief *domain.RunBrief) []string { return dedupeTerms(brief.Pains, brief.Tags, brief.Periphery, []string{brief.Intent}) } func dedupeTerms(groups ...[]string) []string { seen := make(map[string]struct{}) var out []string for _, group := range groups { for _, term := range group { term = strings.Join(strings.Fields(strings.TrimSpace(term)), " ") key := strings.ToLower(term) if term == "" { continue } if _, ok := seen[key]; ok { continue } seen[key] = struct{}{} out = append(out, term) } } return out } func classifyPost(mode, text string, terms []string) classifiedPost { lower := strings.ToLower(text) signals := matchedSignals(lower, terms) if hasAny(lower, "giveaway", "抽獎", "follow for follow", "互追", "crypto", "賺錢") { return classifiedPost{domain.ClassificationNoise, 0, "noise signal"} } if mode == domain.ModeActivity { return classifyActivity(lower, signals) } if hasAny(lower, "dm me", "私訊我", "服務洽詢", "立即購買", "限時優惠", "團購", "業配") { return scored(domain.ClassificationProviderOffer, 35, signals, "provider-offer signal") } if hasAny(lower, "推薦", "求推", "有沒有推薦", "any recommendation", "what do you recommend") { return scored(domain.ClassificationSeekingRecommendation, 60, signals, "recommendation signal") } if strings.Contains(lower, "?") || strings.Contains(lower, "?") || hasAny(lower, "怎麼", "如何", "請問", "求助", "help") { return scored(domain.ClassificationSeekingHelp, 55, signals, "help-seeking signal") } return scored(domain.ClassificationDiscussion, 40, signals, "discussion signal") } func classifyActivity(text string, signals []string) classifiedPost { if hasAny(text, "dm me", "私訊我", "服務洽詢", "立即購買", "限時優惠", "團購", "業配") { return scored(domain.ClassificationProviderOffer, 35, signals, "provider-offer signal; recency unavailable (neutral)") } if strings.Contains(text, "?") || strings.Contains(text, "?") || hasAny(text, "請問", "怎麼", "如何", "有人知道", "求") { return scored(domain.ClassificationAsking, 60, signals, "asking signal; recency unavailable (neutral)") } if hasAny(text, "活動", "event", "開幕", "launch", "發布", "報名", "登記", "公告") { return scored(domain.ClassificationAnnouncement, 50, signals, "announcement signal; recency unavailable (neutral)") } return scored(domain.ClassificationDiscussion, 40, signals, "discussion signal; recency unavailable (neutral)") } func scored(classification string, base int, signals []string, label string) classifiedPost { score := base + len(signals)*15 if score > 100 { score = 100 } reason := label if len(signals) > 0 { reason += "; matched: " + strings.Join(signals, ", ") } return classifiedPost{classification, score, reason} } func matchedSignals(text string, terms []string) []string { var signals []string for _, term := range dedupeTerms(terms) { if strings.Contains(text, strings.ToLower(term)) { signals = append(signals, term) } } return signals } func hasAny(text string, signals ...string) bool { for _, signal := range signals { if strings.Contains(text, signal) { return true } } return false } func canonicalPermalink(raw string) string { u, err := url.Parse(strings.TrimSpace(raw)) if err != nil || u.Host == "" { return "" } u.Scheme = "https" u.Host = strings.ToLower(u.Host) u.RawQuery = "" u.Fragment = "" u.Path = strings.TrimRight(u.Path, "/") return u.String() } func permalinkID(ownerUID int64, permalink string) string { sum := sha256.Sum256([]byte(strings.TrimSpace(permalink) + "|" + formatOwnerUID(ownerUID))) return "sp_" + hex.EncodeToString(sum[:])[:20] } func formatOwnerUID(ownerUID int64) string { return strconv.FormatInt(ownerUID, 10) }