6066f43a7d
- 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.
247 lines
6.5 KiB
Go
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])
|
|
}
|