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.
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
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])
|
||||
}
|
||||
Reference in New Issue
Block a user