Files
geo/server/internal/tenant/app/monitoring_answer_parse.go
T
root 6066f43a7d Add monitoring service and database schema
- Implement monitoring service with heartbeat, lease tasks, resume tasks, and task result handling.
- Create monitoring time utilities for business date calculations.
- Add unit tests for date window resolution and business day handling.
- Define database schema for monitoring-related tables including quotas, daily reports, and task management.
- Establish migration scripts for creating and dropping monitoring tables.
2026-04-12 09:56:18 +08:00

247 lines
6.5 KiB
Go

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])
}