feat(monitoring): attribute SaaS citations by registrable domain
Persist host/registrable_domain/site_key on monitoring article URL aliases, populate them on desktop-publish completion, and use them in the citation pipeline to count how often SaaS-published content appears as a citation source. Expose a /citation-summary endpoint with a 7/30-day window switch and surface the new source-share metrics in the tracking dashboard. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -74,12 +74,25 @@ func (s *MonitoringService) WithRedis(redis *goredis.Client) *MonitoringService
|
||||
type MonitoringDashboardCompositeResponse struct {
|
||||
Overview MonitoringOverview `json:"overview"`
|
||||
Runtime MonitoringDashboardRuntime `json:"runtime"`
|
||||
CitationWindow MonitoringDashboardWindow `json:"citation_window"`
|
||||
PlatformBreakdown []MonitoringPlatformDaily `json:"platform_breakdown"`
|
||||
HotQuestions []MonitoringHotQuestion `json:"hot_questions"`
|
||||
CitationRanking []MonitoringCitationRanking `json:"citation_ranking"`
|
||||
CitedArticles []MonitoringCitedArticle `json:"cited_articles"`
|
||||
}
|
||||
|
||||
type MonitoringCitationSummaryResponse struct {
|
||||
CitationWindow MonitoringDashboardWindow `json:"citation_window"`
|
||||
CitationRanking []MonitoringCitationRanking `json:"citation_ranking"`
|
||||
CitedArticles []MonitoringCitedArticle `json:"cited_articles"`
|
||||
}
|
||||
|
||||
type MonitoringDashboardWindow struct {
|
||||
Days int `json:"days"`
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
}
|
||||
|
||||
type MonitoringDashboardRuntime struct {
|
||||
CurrentUserClientOnline bool `json:"current_user_client_online"`
|
||||
PlatformAuthorizationStatus string `json:"platform_authorization_status"`
|
||||
@@ -126,12 +139,15 @@ type MonitoringHotQuestion struct {
|
||||
}
|
||||
|
||||
type MonitoringCitationRanking struct {
|
||||
AIPlatformID string `json:"ai_platform_id"`
|
||||
PlatformName string `json:"platform_name"`
|
||||
SampleStatus string `json:"sample_status"`
|
||||
CitedAnswerCount int64 `json:"cited_answer_count"`
|
||||
CitedArticleCount int64 `json:"cited_article_count"`
|
||||
CitationRate *float64 `json:"citation_rate"`
|
||||
AIPlatformID string `json:"ai_platform_id"`
|
||||
PlatformName string `json:"platform_name"`
|
||||
SampleStatus string `json:"sample_status"`
|
||||
CitedAnswerCount int64 `json:"cited_answer_count"`
|
||||
CitedArticleCount int64 `json:"cited_article_count"`
|
||||
CitationSourceCount int64 `json:"citation_source_count"`
|
||||
SaaSSourceCount int64 `json:"saas_source_count"`
|
||||
SaaSSourceRate *float64 `json:"saas_source_rate"`
|
||||
CitationRate *float64 `json:"citation_rate"`
|
||||
}
|
||||
|
||||
type MonitoringCitedArticle struct {
|
||||
@@ -140,6 +156,7 @@ type MonitoringCitedArticle struct {
|
||||
PublishPlatform string `json:"publish_platform"`
|
||||
CitationCount int64 `json:"citation_count"`
|
||||
CitationRate *float64 `json:"citation_rate"`
|
||||
SourceShare *float64 `json:"source_share"`
|
||||
}
|
||||
|
||||
type MonitoringQuestionDetailResponse struct {
|
||||
@@ -347,7 +364,6 @@ func (s *MonitoringService) DashboardComposite(
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
questionIDs := configuredQuestionIDs(configuredQuestions)
|
||||
|
||||
platforms := defaultMonitoringPlatformMetadata()
|
||||
selectedPlatformID := normalizedOptionalMonitoringPlatformPointer(aiPlatformID)
|
||||
@@ -380,23 +396,59 @@ func (s *MonitoringService) DashboardComposite(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
citationRanking, err := s.loadCitationRanking(ctx, actor.TenantID, brand.ID, questionIDs, startDate, endDate, accessStates, selectedPlatformID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
citedArticles, err := s.loadCitedArticles(ctx, actor.TenantID, brand.ID, questionIDs, startDate, endDate, selectedPlatformID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &MonitoringDashboardCompositeResponse{
|
||||
Overview: overview,
|
||||
Runtime: runtime,
|
||||
Overview: overview,
|
||||
Runtime: runtime,
|
||||
CitationWindow: MonitoringDashboardWindow{
|
||||
Days: defaultTrackingDays,
|
||||
From: endDate.AddDate(0, 0, -(defaultTrackingDays - 1)).Format("2006-01-02"),
|
||||
To: endDate.Format("2006-01-02"),
|
||||
},
|
||||
PlatformBreakdown: platformBreakdown,
|
||||
HotQuestions: hotQuestions,
|
||||
CitationRanking: citationRanking,
|
||||
CitedArticles: citedArticles,
|
||||
CitationRanking: []MonitoringCitationRanking{},
|
||||
CitedArticles: []MonitoringCitedArticle{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *MonitoringService) CitationSummary(
|
||||
ctx context.Context,
|
||||
days int,
|
||||
) (*MonitoringCitationSummaryResponse, error) {
|
||||
actor := auth.MustActor(ctx)
|
||||
workspaceID := auth.CurrentWorkspaceID(ctx)
|
||||
|
||||
quota, err := s.loadQuota(ctx, actor, workspaceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
citationWindowDays := normalizeCitationWindowDays(days)
|
||||
startDate, endDate := trackingDateWindow(citationWindowDays)
|
||||
|
||||
accessStates, err := s.loadAccessStates(ctx, actor.TenantID, workspaceID, quota.PrimaryClientID, endDate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
citationRanking, err := s.loadCitationRanking(ctx, actor.TenantID, 0, nil, startDate, endDate, accessStates, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
citedArticles, err := s.loadCitedArticles(ctx, actor.TenantID, 0, nil, startDate, endDate, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &MonitoringCitationSummaryResponse{
|
||||
CitationWindow: MonitoringDashboardWindow{
|
||||
Days: citationWindowDays,
|
||||
From: startDate.Format("2006-01-02"),
|
||||
To: endDate.Format("2006-01-02"),
|
||||
},
|
||||
CitationRanking: citationRanking,
|
||||
CitedArticles: citedArticles,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -509,12 +561,12 @@ func (s *MonitoringService) QuestionDetail(ctx context.Context, brandID, questio
|
||||
})
|
||||
}
|
||||
|
||||
citationAnalysis, err := s.loadQuestionCitationAnalysis(ctx, actor.TenantID, brand.ID, questionID, hashBytes, fromDate, toDate, selectedPlatformID)
|
||||
citationAnalysis, err := s.loadQuestionCitationAnalysis(ctx, actor.TenantID, runIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
contentCitations, err := s.loadQuestionContentCitations(ctx, actor.TenantID, brand.ID, questionID, hashBytes, fromDate, toDate, selectedPlatformID)
|
||||
contentCitations, err := s.loadQuestionContentCitations(ctx, actor.TenantID, runIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -2430,13 +2482,26 @@ func (s *MonitoringService) loadCitationRanking(
|
||||
platformQueryIDs := monitoringPlatformQueryIDs(aiPlatformID)
|
||||
|
||||
rows, err := s.monitoringPool.Query(ctx, `
|
||||
WITH run_counts AS (
|
||||
WITH published_domains AS (
|
||||
SELECT DISTINCT domain_key
|
||||
FROM (
|
||||
SELECT NULLIF(LOWER(TRIM(host)), '') AS domain_key
|
||||
FROM monitoring_article_url_aliases
|
||||
WHERE tenant_id = $1
|
||||
UNION
|
||||
SELECT NULLIF(LOWER(TRIM(site_key)), '') AS domain_key
|
||||
FROM monitoring_article_url_aliases
|
||||
WHERE tenant_id = $1
|
||||
) domains
|
||||
WHERE domain_key IS NOT NULL
|
||||
),
|
||||
run_counts AS (
|
||||
SELECT
|
||||
r.ai_platform_id,
|
||||
COUNT(*) AS sample_count
|
||||
FROM question_monitor_runs r
|
||||
WHERE r.tenant_id = $1
|
||||
AND r.brand_id = $2
|
||||
AND ($2::bigint = 0 OR r.brand_id = $2)
|
||||
AND r.collector_type = $3
|
||||
AND r.status = 'succeeded'
|
||||
AND r.business_date BETWEEN $4::date AND $5::date
|
||||
@@ -2444,36 +2509,65 @@ func (s *MonitoringService) loadCitationRanking(
|
||||
AND ($7::text[] IS NULL OR r.ai_platform_id = ANY($7))
|
||||
GROUP BY r.ai_platform_id
|
||||
),
|
||||
citation_counts AS (
|
||||
citation_sources AS (
|
||||
SELECT
|
||||
r.ai_platform_id,
|
||||
COUNT(DISTINCT cf.run_id) FILTER (WHERE cf.article_id IS NOT NULL AND r.ai_platform_id <> 'qwen') AS cited_answer_count,
|
||||
COUNT(DISTINCT cf.article_id) FILTER (WHERE cf.article_id IS NOT NULL AND r.ai_platform_id <> 'qwen') AS cited_article_count
|
||||
cf.id,
|
||||
cf.run_id,
|
||||
cf.article_id,
|
||||
(
|
||||
cf.article_id IS NOT NULL
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM published_domains pd
|
||||
WHERE pd.domain_key = ANY(ARRAY[
|
||||
LOWER(NULLIF(cf.registrable_domain, '')),
|
||||
LOWER(NULLIF(cf.site_key, '')),
|
||||
LOWER(NULLIF(cf.host, ''))
|
||||
])
|
||||
)
|
||||
) AS matched_saas_source
|
||||
FROM question_monitor_runs r
|
||||
JOIN monitoring_citation_facts cf
|
||||
ON cf.tenant_id = r.tenant_id AND cf.run_id = r.id
|
||||
WHERE r.tenant_id = $1
|
||||
AND r.brand_id = $2
|
||||
AND ($2::bigint = 0 OR r.brand_id = $2)
|
||||
AND r.collector_type = $3
|
||||
AND r.status = 'succeeded'
|
||||
AND r.business_date BETWEEN $4::date AND $5::date
|
||||
AND ($6::bigint[] IS NULL OR r.question_id = ANY($6))
|
||||
AND ($7::text[] IS NULL OR r.ai_platform_id = ANY($7))
|
||||
GROUP BY r.ai_platform_id
|
||||
),
|
||||
citation_counts AS (
|
||||
SELECT
|
||||
ai_platform_id,
|
||||
COUNT(*) AS citation_source_count,
|
||||
COUNT(*) FILTER (WHERE matched_saas_source AND ai_platform_id <> 'qwen') AS saas_source_count,
|
||||
COUNT(DISTINCT run_id) FILTER (WHERE matched_saas_source AND ai_platform_id <> 'qwen') AS cited_answer_count,
|
||||
COUNT(DISTINCT article_id) FILTER (WHERE article_id IS NOT NULL AND ai_platform_id <> 'qwen') AS cited_article_count
|
||||
FROM citation_sources
|
||||
GROUP BY ai_platform_id
|
||||
)
|
||||
SELECT
|
||||
rc.ai_platform_id,
|
||||
rc.sample_count,
|
||||
COALESCE(cc.cited_answer_count, 0) AS cited_answer_count,
|
||||
COALESCE(cc.cited_article_count, 0) AS cited_article_count,
|
||||
COALESCE(cc.citation_source_count, 0) AS citation_source_count,
|
||||
COALESCE(cc.saas_source_count, 0) AS saas_source_count,
|
||||
CASE
|
||||
WHEN COALESCE(cc.citation_source_count, 0) > 0
|
||||
THEN COALESCE(cc.saas_source_count, 0)::double precision / cc.citation_source_count::double precision
|
||||
ELSE NULL
|
||||
END AS saas_source_rate,
|
||||
CASE
|
||||
WHEN rc.sample_count > 0 THEN COALESCE(cc.cited_answer_count, 0)::double precision / rc.sample_count
|
||||
ELSE NULL
|
||||
END AS citation_rate
|
||||
FROM run_counts rc
|
||||
LEFT JOIN citation_counts cc ON cc.ai_platform_id = rc.ai_platform_id
|
||||
WHERE COALESCE(cc.cited_answer_count, 0) > 0
|
||||
ORDER BY cited_answer_count DESC, cited_article_count DESC, rc.ai_platform_id ASC
|
||||
WHERE COALESCE(cc.saas_source_count, 0) > 0
|
||||
ORDER BY cited_answer_count DESC, saas_source_count DESC, cited_article_count DESC, rc.ai_platform_id ASC
|
||||
`, tenantID, brandID, monitoringCollectorType, startDate.Format("2006-01-02"), endDate.Format("2006-01-02"), nullableInt64Array(questionIDs), nullableStringArray(platformQueryIDs))
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50041, "query_failed", "failed to load citation ranking")
|
||||
@@ -2481,9 +2575,11 @@ func (s *MonitoringService) loadCitationRanking(
|
||||
defer rows.Close()
|
||||
|
||||
type citationRankingAggregate struct {
|
||||
CitedAnswerCount int64
|
||||
CitedArticleCount int64
|
||||
SampleCount int64
|
||||
CitedAnswerCount int64
|
||||
CitedArticleCount int64
|
||||
CitationSourceCount int64
|
||||
SaaSSourceCount int64
|
||||
SampleCount int64
|
||||
}
|
||||
|
||||
aggregates := make(map[string]citationRankingAggregate)
|
||||
@@ -2492,7 +2588,9 @@ func (s *MonitoringService) loadCitationRanking(
|
||||
var sampleCount int64
|
||||
var citedAnswerCount int64
|
||||
var citedArticleCount int64
|
||||
if scanErr := rows.Scan(&platformID, &sampleCount, &citedAnswerCount, &citedArticleCount, new(sql.NullFloat64)); scanErr != nil {
|
||||
var citationSourceCount int64
|
||||
var saasSourceCount int64
|
||||
if scanErr := rows.Scan(&platformID, &sampleCount, &citedAnswerCount, &citedArticleCount, &citationSourceCount, &saasSourceCount, new(sql.NullFloat64), new(sql.NullFloat64)); scanErr != nil {
|
||||
return nil, response.ErrInternal(50041, "scan_failed", "failed to parse citation ranking")
|
||||
}
|
||||
platformID = normalizeMonitoringPlatformID(platformID)
|
||||
@@ -2502,6 +2600,8 @@ func (s *MonitoringService) loadCitationRanking(
|
||||
item := aggregates[platformID]
|
||||
item.CitedAnswerCount += citedAnswerCount
|
||||
item.CitedArticleCount += citedArticleCount
|
||||
item.CitationSourceCount += citationSourceCount
|
||||
item.SaaSSourceCount += saasSourceCount
|
||||
item.SampleCount += sampleCount
|
||||
aggregates[platformID] = item
|
||||
}
|
||||
@@ -2509,18 +2609,24 @@ func (s *MonitoringService) loadCitationRanking(
|
||||
items := make([]MonitoringCitationRanking, 0, len(aggregates))
|
||||
for platformID, aggregate := range aggregates {
|
||||
items = append(items, MonitoringCitationRanking{
|
||||
AIPlatformID: platformID,
|
||||
PlatformName: platformDisplayName(platformID),
|
||||
SampleStatus: derivePlatformSampleStatus("", accessStates[platformID]),
|
||||
CitedAnswerCount: aggregate.CitedAnswerCount,
|
||||
CitedArticleCount: aggregate.CitedArticleCount,
|
||||
CitationRate: divideAsPointer(aggregate.CitedAnswerCount, aggregate.SampleCount),
|
||||
AIPlatformID: platformID,
|
||||
PlatformName: platformDisplayName(platformID),
|
||||
SampleStatus: derivePlatformSampleStatus("", accessStates[platformID]),
|
||||
CitedAnswerCount: aggregate.CitedAnswerCount,
|
||||
CitedArticleCount: aggregate.CitedArticleCount,
|
||||
CitationSourceCount: aggregate.CitationSourceCount,
|
||||
SaaSSourceCount: aggregate.SaaSSourceCount,
|
||||
SaaSSourceRate: divideAsPointer(aggregate.SaaSSourceCount, aggregate.CitationSourceCount),
|
||||
CitationRate: divideAsPointer(aggregate.CitedAnswerCount, aggregate.SampleCount),
|
||||
})
|
||||
}
|
||||
sort.Slice(items, func(left, right int) bool {
|
||||
if items[left].CitedAnswerCount != items[right].CitedAnswerCount {
|
||||
return items[left].CitedAnswerCount > items[right].CitedAnswerCount
|
||||
}
|
||||
if items[left].SaaSSourceCount != items[right].SaaSSourceCount {
|
||||
return items[left].SaaSSourceCount > items[right].SaaSSourceCount
|
||||
}
|
||||
if items[left].CitedArticleCount != items[right].CitedArticleCount {
|
||||
return items[left].CitedArticleCount > items[right].CitedArticleCount
|
||||
}
|
||||
@@ -2547,7 +2653,7 @@ func (s *MonitoringService) loadCitedArticles(
|
||||
SELECT COUNT(*)
|
||||
FROM question_monitor_runs r
|
||||
WHERE r.tenant_id = $1
|
||||
AND r.brand_id = $2
|
||||
AND ($2::bigint = 0 OR r.brand_id = $2)
|
||||
AND r.collector_type = $3
|
||||
AND r.status = 'succeeded'
|
||||
AND r.business_date BETWEEN $4::date AND $5::date
|
||||
@@ -2558,26 +2664,51 @@ func (s *MonitoringService) loadCitedArticles(
|
||||
}
|
||||
|
||||
rows, err := s.monitoringPool.Query(ctx, `
|
||||
WITH article_counts AS (
|
||||
SELECT
|
||||
cf.article_id,
|
||||
COALESCE(MAX(alias.article_title_snapshot), '未命名文章') AS article_title,
|
||||
COALESCE(MAX(alias.publish_platform_name_snapshot), '已发布内容') AS publish_platform,
|
||||
COUNT(*) AS citation_count
|
||||
FROM monitoring_citation_facts cf
|
||||
JOIN question_monitor_runs r
|
||||
ON r.tenant_id = cf.tenant_id AND r.id = cf.run_id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
alias.article_title_snapshot,
|
||||
alias.publish_platform_name_snapshot
|
||||
FROM monitoring_article_url_aliases alias
|
||||
WHERE alias.tenant_id = cf.tenant_id
|
||||
AND alias.article_id = cf.article_id
|
||||
ORDER BY
|
||||
(alias.publish_record_id = cf.publish_record_id) DESC NULLS LAST,
|
||||
alias.updated_at DESC,
|
||||
alias.id DESC
|
||||
LIMIT 1
|
||||
) alias ON TRUE
|
||||
WHERE cf.tenant_id = $1
|
||||
AND ($2::bigint = 0 OR cf.brand_id = $2)
|
||||
AND cf.article_id IS NOT NULL
|
||||
AND r.ai_platform_id <> 'qwen'
|
||||
AND r.collector_type = $3
|
||||
AND r.status = 'succeeded'
|
||||
AND r.business_date BETWEEN $4::date AND $5::date
|
||||
AND ($6::bigint[] IS NULL OR r.question_id = ANY($6))
|
||||
AND ($7::text[] IS NULL OR r.ai_platform_id = ANY($7))
|
||||
GROUP BY cf.article_id
|
||||
),
|
||||
totals AS (
|
||||
SELECT COALESCE(SUM(citation_count), 0) AS total_article_citation_count
|
||||
FROM article_counts
|
||||
)
|
||||
SELECT
|
||||
cf.article_id,
|
||||
COALESCE(MAX(alias.article_title_snapshot), '未命名文章') AS article_title,
|
||||
COALESCE(MAX(alias.publish_platform_name_snapshot), '已发布内容') AS publish_platform,
|
||||
COUNT(*) AS citation_count
|
||||
FROM monitoring_citation_facts cf
|
||||
JOIN question_monitor_runs r
|
||||
ON r.tenant_id = cf.tenant_id AND r.id = cf.run_id
|
||||
LEFT JOIN monitoring_article_url_aliases alias
|
||||
ON alias.tenant_id = cf.tenant_id AND alias.article_id = cf.article_id
|
||||
WHERE cf.tenant_id = $1
|
||||
AND cf.brand_id = $2
|
||||
AND cf.article_id IS NOT NULL
|
||||
AND r.ai_platform_id <> 'qwen'
|
||||
AND r.collector_type = $3
|
||||
AND r.status = 'succeeded'
|
||||
AND r.business_date BETWEEN $4::date AND $5::date
|
||||
AND ($6::bigint[] IS NULL OR r.question_id = ANY($6))
|
||||
AND ($7::text[] IS NULL OR r.ai_platform_id = ANY($7))
|
||||
GROUP BY cf.article_id
|
||||
cf.article_title,
|
||||
cf.publish_platform,
|
||||
cf.citation_count,
|
||||
t.total_article_citation_count
|
||||
FROM article_counts cf
|
||||
CROSS JOIN totals t
|
||||
ORDER BY citation_count DESC, cf.article_id ASC
|
||||
LIMIT 10
|
||||
`, tenantID, brandID, monitoringCollectorType, startDate.Format("2006-01-02"), endDate.Format("2006-01-02"), nullableInt64Array(questionIDs), nullableStringArray(platformQueryIDs))
|
||||
@@ -2592,7 +2723,8 @@ func (s *MonitoringService) loadCitedArticles(
|
||||
var title string
|
||||
var publishPlatform string
|
||||
var citationCount int64
|
||||
if scanErr := rows.Scan(&articleID, &title, &publishPlatform, &citationCount); scanErr != nil {
|
||||
var totalArticleCitationCount int64
|
||||
if scanErr := rows.Scan(&articleID, &title, &publishPlatform, &citationCount, &totalArticleCitationCount); scanErr != nil {
|
||||
return nil, response.ErrInternal(50041, "scan_failed", "failed to parse cited articles")
|
||||
}
|
||||
items = append(items, MonitoringCitedArticle{
|
||||
@@ -2601,6 +2733,7 @@ func (s *MonitoringService) loadCitedArticles(
|
||||
PublishPlatform: publishPlatform,
|
||||
CitationCount: citationCount,
|
||||
CitationRate: divideAsPointer(citationCount, totalSampleCount),
|
||||
SourceShare: divideAsPointer(citationCount, totalArticleCitationCount),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2902,8 +3035,10 @@ func (s *MonitoringService) loadCitationsForRuns(ctx context.Context, tenantID i
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *MonitoringService) loadQuestionCitationAnalysis(ctx context.Context, tenantID, brandID, questionID int64, hashBytes []byte, fromDate, toDate time.Time, aiPlatformID *string) ([]MonitoringQuestionCitationStats, error) {
|
||||
platformQueryIDs := monitoringPlatformQueryIDs(aiPlatformID)
|
||||
func (s *MonitoringService) loadQuestionCitationAnalysis(ctx context.Context, tenantID int64, runIDs []int64) ([]MonitoringQuestionCitationStats, error) {
|
||||
if len(runIDs) == 0 {
|
||||
return []MonitoringQuestionCitationStats{}, nil
|
||||
}
|
||||
|
||||
rows, err := s.monitoringPool.Query(ctx, `
|
||||
WITH filtered_facts AS (
|
||||
@@ -2918,13 +3053,8 @@ func (s *MonitoringService) loadQuestionCitationAnalysis(ctx context.Context, te
|
||||
JOIN monitoring_citation_facts cf
|
||||
ON cf.tenant_id = r.tenant_id AND cf.run_id = r.id
|
||||
WHERE r.tenant_id = $1
|
||||
AND r.brand_id = $2
|
||||
AND r.question_id = $3
|
||||
AND r.question_hash = $4
|
||||
AND r.collector_type = $5
|
||||
AND r.status = 'succeeded'
|
||||
AND r.business_date BETWEEN $6::date AND $7::date
|
||||
AND ($8::text[] IS NULL OR r.ai_platform_id = ANY($8))
|
||||
AND r.id = ANY($2)
|
||||
),
|
||||
domain_keys AS (
|
||||
SELECT DISTINCT host, registrable_domain, site_key
|
||||
@@ -3003,7 +3133,7 @@ func (s *MonitoringService) loadQuestionCitationAnalysis(ctx context.Context, te
|
||||
JOIN totals t
|
||||
ON t.ai_platform_id = g.ai_platform_id
|
||||
ORDER BY g.ai_platform_id ASC, g.citation_count DESC, g.site_name ASC
|
||||
`, tenantID, brandID, questionID, hashBytes, monitoringCollectorType, fromDate.Format("2006-01-02"), toDate.Format("2006-01-02"), nullableStringArray(platformQueryIDs))
|
||||
`, tenantID, runIDs)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50041, "query_failed", "failed to load question citation analysis")
|
||||
}
|
||||
@@ -3071,8 +3201,10 @@ func (s *MonitoringService) loadQuestionCitationAnalysis(ctx context.Context, te
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *MonitoringService) loadQuestionContentCitations(ctx context.Context, tenantID, brandID, questionID int64, hashBytes []byte, fromDate, toDate time.Time, aiPlatformID *string) ([]MonitoringQuestionContentCitation, error) {
|
||||
platformQueryIDs := monitoringPlatformQueryIDs(aiPlatformID)
|
||||
func (s *MonitoringService) loadQuestionContentCitations(ctx context.Context, tenantID int64, runIDs []int64) ([]MonitoringQuestionContentCitation, error) {
|
||||
if len(runIDs) == 0 {
|
||||
return []MonitoringQuestionContentCitation{}, nil
|
||||
}
|
||||
|
||||
rows, err := s.monitoringPool.Query(ctx, `
|
||||
WITH platform_totals AS (
|
||||
@@ -3083,13 +3215,8 @@ func (s *MonitoringService) loadQuestionContentCitations(ctx context.Context, te
|
||||
JOIN monitoring_citation_facts cf
|
||||
ON cf.tenant_id = r.tenant_id AND cf.run_id = r.id
|
||||
WHERE r.tenant_id = $1
|
||||
AND r.brand_id = $2
|
||||
AND r.question_id = $3
|
||||
AND r.question_hash = $4
|
||||
AND r.collector_type = $5
|
||||
AND r.status = 'succeeded'
|
||||
AND r.business_date BETWEEN $6::date AND $7::date
|
||||
AND ($8::text[] IS NULL OR r.ai_platform_id = ANY($8))
|
||||
AND r.id = ANY($2)
|
||||
GROUP BY r.ai_platform_id
|
||||
)
|
||||
SELECT
|
||||
@@ -3107,18 +3234,13 @@ func (s *MonitoringService) loadQuestionContentCitations(ctx context.Context, te
|
||||
LEFT JOIN monitoring_article_url_aliases alias
|
||||
ON alias.tenant_id = cf.tenant_id AND alias.article_id = cf.article_id
|
||||
WHERE r.tenant_id = $1
|
||||
AND r.brand_id = $2
|
||||
AND r.question_id = $3
|
||||
AND r.question_hash = $4
|
||||
AND r.collector_type = $5
|
||||
AND r.status = 'succeeded'
|
||||
AND r.business_date BETWEEN $6::date AND $7::date
|
||||
AND cf.article_id IS NOT NULL
|
||||
AND r.ai_platform_id <> 'qwen'
|
||||
AND ($8::text[] IS NULL OR r.ai_platform_id = ANY($8))
|
||||
AND r.id = ANY($2)
|
||||
GROUP BY cf.article_id, r.ai_platform_id
|
||||
ORDER BY citation_count DESC, cf.article_id ASC
|
||||
`, tenantID, brandID, questionID, hashBytes, monitoringCollectorType, fromDate.Format("2006-01-02"), toDate.Format("2006-01-02"), nullableStringArray(platformQueryIDs))
|
||||
`, tenantID, runIDs)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50041, "query_failed", "failed to load content citations")
|
||||
}
|
||||
@@ -3326,6 +3448,13 @@ func normalizeTrackingDays(days int) int {
|
||||
return days
|
||||
}
|
||||
|
||||
func normalizeCitationWindowDays(days int) int {
|
||||
if days == maxTrackingDays {
|
||||
return maxTrackingDays
|
||||
}
|
||||
return defaultTrackingDays
|
||||
}
|
||||
|
||||
func resolveMonitoringPlatforms(enabled []string) []monitoringPlatformMetadata {
|
||||
if len(enabled) == 0 {
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user