package app import ( "strings" "unicode" ) type monitoringStoredParseFields struct { BrandMentioned *bool BrandMentionPosition *string FirstRecommended *bool SentimentLabel *string MatchedBrandTerms []string } type monitoringAnswerParseSummary struct { BrandMentioned bool BrandMentionPosition string FirstRecommended bool SentimentLabel string MatchedBrandTerms []string } var monitoringPositiveSignals = []string{ "推荐", "首选", "优先", "更推荐", "值得", "靠谱", "专业", "优质", "高端", "不错", "合适", "满意", "口碑", "领先", } var monitoringNegativeSignals = []string{ "不推荐", "慎选", "避坑", "不好", "较差", "很差", "问题", "投诉", "负面", "不靠谱", "一般", "短板", "缺点", "不足", } func resolveMonitoringAnswerParseSummary( answer string, brandName string, stored monitoringStoredParseFields, ) monitoringAnswerParseSummary { return overlayMonitoringStoredParseSummary( deriveMonitoringAnswerParseSummary(answer, brandName), stored, ) } func overlayMonitoringStoredParseSummary( base monitoringAnswerParseSummary, stored monitoringStoredParseFields, ) monitoringAnswerParseSummary { result := base if stored.BrandMentioned != nil { result.BrandMentioned = *stored.BrandMentioned } if value := strings.TrimSpace(monitoringStringValue(stored.BrandMentionPosition)); value != "" { result.BrandMentionPosition = value } if stored.FirstRecommended != nil { result.FirstRecommended = *stored.FirstRecommended } if value := strings.TrimSpace(monitoringStringValue(stored.SentimentLabel)); value != "" { result.SentimentLabel = value } if len(stored.MatchedBrandTerms) > 0 { result.MatchedBrandTerms = normalizeMonitoringBrandTermList(stored.MatchedBrandTerms) } if result.BrandMentioned && result.BrandMentionPosition == "" { result.BrandMentionPosition = "mentioned" } if !result.BrandMentioned && result.BrandMentionPosition == "" { result.BrandMentionPosition = "not_mentioned" } if result.BrandMentioned && result.SentimentLabel == "" { result.SentimentLabel = "neutral" } if !result.BrandMentioned && result.SentimentLabel == "" { result.SentimentLabel = "unknown" } return result } func deriveMonitoringAnswerParseSummary(answer string, brandName string) monitoringAnswerParseSummary { result := monitoringAnswerParseSummary{ BrandMentionPosition: "not_mentioned", SentimentLabel: "unknown", MatchedBrandTerms: []string{}, } answer = strings.TrimSpace(answer) if answer == "" { return result } brandTerms := buildMonitoringBrandTerms(brandName) if len(brandTerms) == 0 { return result } answerLower := strings.ToLower(answer) answerNormalized := normalizeMonitoringTextForMatch(answer) matchedTerms := findMonitoringMatchedBrandTerms(answerLower, answerNormalized, brandTerms) if len(matchedTerms) == 0 { return result } result.BrandMentioned = true result.BrandMentionPosition = "mentioned" result.MatchedBrandTerms = matchedTerms if isMonitoringTop1Mention(answerLower, matchedTerms) { result.BrandMentionPosition = "top1" } result.FirstRecommended = isMonitoringFirstRecommended(answerLower, matchedTerms, result.BrandMentionPosition) result.SentimentLabel = deriveMonitoringSentimentLabel(answerLower, matchedTerms, result.FirstRecommended) return result } func buildMonitoringBrandTerms(brandName string) []string { return normalizeMonitoringBrandTermList([]string{strings.TrimSpace(brandName)}) } func normalizeMonitoringBrandTermList(values []string) []string { result := make([]string, 0, len(values)) seen := make(map[string]struct{}, len(values)) for _, value := range values { trimmed := strings.TrimSpace(value) if trimmed == "" { continue } if _, ok := seen[trimmed]; ok { continue } seen[trimmed] = struct{}{} result = append(result, trimmed) } return result } func normalizeMonitoringTextForMatch(value string) string { var builder strings.Builder builder.Grow(len(value)) for _, item := range strings.ToLower(value) { if unicode.IsLetter(item) || unicode.IsNumber(item) { builder.WriteRune(item) } } return builder.String() } func findMonitoringMatchedBrandTerms(answerLower, answerNormalized string, brandTerms []string) []string { matched := make([]string, 0, len(brandTerms)) for _, term := range brandTerms { termLower := strings.ToLower(strings.TrimSpace(term)) if termLower == "" { continue } termNormalized := normalizeMonitoringTextForMatch(termLower) if strings.Contains(answerLower, termLower) || (termNormalized != "" && strings.Contains(answerNormalized, termNormalized)) { matched = append(matched, term) } } return normalizeMonitoringBrandTermList(matched) } func isMonitoringTop1Mention(answerLower string, matchedTerms []string) bool { segment := strings.ToLower(firstMonitoringRunes(strings.TrimSpace(answerLower), 96)) if segment == "" { return false } for _, term := range matchedTerms { termLower := strings.ToLower(strings.TrimSpace(term)) if termLower != "" && strings.Contains(segment, termLower) { return true } } return false } func isMonitoringFirstRecommended(answerLower string, matchedTerms []string, mentionPosition string) bool { if mentionPosition == "top1" { return true } for _, term := range matchedTerms { termLower := strings.ToLower(strings.TrimSpace(term)) if termLower == "" { continue } patterns := []string{ "推荐" + termLower, "首选" + termLower, "优先" + termLower, "建议选择" + termLower, "建议选" + termLower, "更推荐" + termLower, termLower + "是首选", termLower + "更合适", termLower + "更值得", termLower + "值得推荐", } for _, pattern := range patterns { if strings.Contains(answerLower, pattern) { return true } } } return false } func deriveMonitoringSentimentLabel(answerLower string, matchedTerms []string, firstRecommended bool) string { negativeCount := 0 for _, signal := range monitoringNegativeSignals { if strings.Contains(answerLower, signal) { negativeCount++ } } if negativeCount > 0 { return "negative" } positiveCount := 0 for _, signal := range monitoringPositiveSignals { if strings.Contains(answerLower, signal) { positiveCount++ } } if firstRecommended || positiveCount > 0 { return "positive" } if len(matchedTerms) > 0 { return "neutral" } return "unknown" } func firstMonitoringRunes(value string, limit int) string { if limit <= 0 { return "" } runes := []rune(value) if len(runes) <= limit { return string(runes) } return string(runes[:limit]) }