thread-master/old/backend/internal/library/viral/mission_topic.go

132 lines
3.1 KiB
Go
Raw Normal View History

2026-06-28 08:28:42 +00:00
package viral
import (
"strings"
"unicode"
"haixun-backend/internal/library/placement"
)
type topicMatchTerms struct {
Anchors []string
Terms []string
}
func missionTopicMatchTerms(seed, label string, hints []string) topicMatchTerms {
seenAnchors := map[string]struct{}{}
seenTerms := map[string]struct{}{}
out := topicMatchTerms{}
addTerm := func(term string, anchor bool) {
term = normaliseTopicToken(term)
if term == "" || isGenericTopicToken(term) {
return
}
if _, ok := seenTerms[term]; !ok {
seenTerms[term] = struct{}{}
out.Terms = append(out.Terms, term)
}
if anchor {
if _, ok := seenAnchors[term]; !ok {
seenAnchors[term] = struct{}{}
out.Anchors = append(out.Anchors, term)
}
}
}
addSource := func(raw string, anchor bool) {
raw = strings.TrimSpace(raw)
if raw == "" {
return
}
compact := compactTopicPhrase(raw)
addTerm(compact, anchor)
for _, token := range splitTopicTokens(raw) {
addTerm(token, anchor)
}
}
addSource(seed, true)
addSource(label, true)
anchorHints := len(out.Anchors) == 0
for _, hint := range hints {
addSource(hint, anchorHints)
}
return out
}
// missionPostMatchesTopic is intentionally stricter than topicTopicHits:
// mission scan candidates must match the post body, not only the query/tag
// that produced the crawl result. This prevents crawler/search drift from
// admitting high-engagement but unrelated posts.
func missionPostMatchesTopic(post placement.ScanCandidate, terms topicMatchTerms) bool {
text := strings.ToLower(strings.TrimSpace(post.Text))
if len(terms.Anchors) == 0 && len(terms.Terms) == 0 {
return text != ""
}
if text == "" {
return false
}
for _, term := range terms.Anchors {
if term != "" && strings.Contains(text, term) {
return true
}
}
hits := 0
for _, term := range terms.Terms {
if term != "" && strings.Contains(text, term) {
hits++
if hits >= 2 {
return true
}
}
}
return false
}
func splitTopicTokens(raw string) []string {
return strings.FieldsFunc(raw, func(r rune) bool {
if unicode.IsSpace(r) || unicode.IsPunct(r) || unicode.IsSymbol(r) {
return true
}
switch r {
case '', '。', '、', '', '', '', '', '「', '」', '『', '』', '', '', '【', '】':
return true
default:
return false
}
})
}
func compactTopicPhrase(raw string) string {
var b strings.Builder
for _, r := range raw {
if unicode.IsSpace(r) || unicode.IsPunct(r) || unicode.IsSymbol(r) {
continue
}
b.WriteRune(r)
}
return b.String()
}
func normaliseTopicToken(raw string) string {
token := strings.ToLower(strings.TrimSpace(raw))
if token == "" {
return ""
}
token = strings.Trim(token, "#,.。:;!?()[]【】\"'「」『』")
if len([]rune(token)) < 2 {
return ""
}
return token
}
func isGenericTopicToken(token string) bool {
switch token {
case "分享", "心得", "推薦", "請問", "求助", "問題", "方法", "技巧", "經驗",
"熱門", "話題", "最近", "大家", "有人", "可以", "如何", "怎麼", "為什麼",
"品質", "閱讀", "更多", "多閱讀", "老公", "老婆", "男友", "女友":
return true
default:
return false
}
}