feat: add monitoring marked articles and retention updates
Frontend CI / Frontend (push) Successful in 7m47s
Backend CI / Backend (push) Successful in 19m26s

This commit is contained in:
2026-06-17 12:48:41 +08:00
parent 9ed857e159
commit 31c4dd9358
40 changed files with 2373 additions and 488 deletions
@@ -279,6 +279,8 @@ type monitoringAliasResolution struct {
ArticleID *int64
PublishRecordID *int64
NormalizedURLKey string
SourceType string
SourceID *int64
ResolutionStatus string
ResolutionConfidence string
}
@@ -298,6 +300,9 @@ type monitoringAliasCandidate struct {
MatchKeys []string
ArticleID *int64
PublishRecordID *int64
SourceType string
SourceID *int64
SourcePriority int
RegistrableDomain string
SiteKey string
Host string
@@ -316,6 +321,8 @@ type monitoringCitationFact struct {
SiteKey string
ArticleID *int64
PublishRecordID *int64
ContentSourceType *string
ContentSourceID *int64
ResolutionStatus string
ResolutionConfidence string
}
@@ -2119,7 +2126,7 @@ func (s *MonitoringCallbackService) processTaskResult(ctx context.Context, task
return nil, err
}
citationSourceCount++
if fact.ArticleID != nil {
if fact.ContentSourceType != nil && fact.ContentSourceID != nil {
contentCitationCount++
}
}
@@ -2591,23 +2598,61 @@ func (s *MonitoringCallbackService) loadAliasResolutions(ctx context.Context, tx
}
rows, err := tx.Query(ctx, `
WITH alias_sources AS (
SELECT
normalized_url,
article_id,
publish_record_id,
'published_article' AS source_type,
article_id AS source_id,
1 AS source_priority,
registrable_domain,
site_key,
host,
last_path_segment,
article_title_snapshot,
updated_at,
id
FROM monitoring_article_url_aliases
WHERE tenant_id = $1
AND confidence = 'high'
UNION ALL
SELECT
normalized_url,
NULL AS article_id,
NULL AS publish_record_id,
'manual_mark' AS source_type,
id AS source_id,
2 AS source_priority,
registrable_domain,
site_key,
host,
last_path_segment,
article_title AS article_title_snapshot,
updated_at,
id
FROM monitoring_user_marked_articles
WHERE tenant_id = $1
AND expires_at > NOW()
)
SELECT
normalized_url,
article_id,
publish_record_id,
registrable_domain,
source_type,
source_id,
source_priority,
registrable_domain,
site_key,
host,
last_path_segment,
article_title_snapshot
FROM monitoring_article_url_aliases
WHERE tenant_id = $1
AND (
normalized_url = ANY($2::text[])
OR NULLIF(LOWER(TRIM(registrable_domain)), '') = ANY($3::text[])
OR NULLIF(LOWER(TRIM(site_key)), '') = ANY($3::text[])
OR NULLIF(LOWER(TRIM(host)), '') = ANY($3::text[])
)
FROM alias_sources
WHERE normalized_url = ANY($2::text[])
OR NULLIF(LOWER(TRIM(registrable_domain)), '') = ANY($3::text[])
OR NULLIF(LOWER(TRIM(site_key)), '') = ANY($3::text[])
OR NULLIF(LOWER(TRIM(host)), '') = ANY($3::text[])
ORDER BY updated_at DESC, id DESC
`, tenantID, normalizedURLs, domainKeys)
if err != nil {
return nil, response.ErrInternal(50041, "alias_lookup_failed", "failed to resolve article aliases")
@@ -2619,6 +2664,7 @@ func (s *MonitoringCallbackService) loadAliasResolutions(ctx context.Context, tx
var candidate monitoringAliasCandidate
var articleID sql.NullInt64
var publishRecordID sql.NullInt64
var sourceID sql.NullInt64
var registrableDomain sql.NullString
var siteKey sql.NullString
var host sql.NullString
@@ -2628,6 +2674,9 @@ func (s *MonitoringCallbackService) loadAliasResolutions(ctx context.Context, tx
&candidate.NormalizedURL,
&articleID,
&publishRecordID,
&candidate.SourceType,
&sourceID,
&candidate.SourcePriority,
&registrableDomain,
&siteKey,
&host,
@@ -2638,6 +2687,7 @@ func (s *MonitoringCallbackService) loadAliasResolutions(ctx context.Context, tx
}
candidate.ArticleID = nullableInt64Value(articleID)
candidate.PublishRecordID = nullableInt64Value(publishRecordID)
candidate.SourceID = nullableInt64Value(sourceID)
candidate.RegistrableDomain = strings.ToLower(strings.TrimSpace(nullableStringText(registrableDomain)))
candidate.SiteKey = strings.ToLower(strings.TrimSpace(nullableStringText(siteKey)))
candidate.Host = strings.ToLower(strings.TrimSpace(nullableStringText(host)))
@@ -2665,8 +2715,14 @@ func (s *MonitoringCallbackService) loadAliasResolutions(ctx context.Context, tx
best = candidate
continue
}
if score == bestScore && !sameInt64Pointer(candidate.ArticleID, best.ArticleID) {
bestAmbiguous = true
if score == bestScore && !sameMonitoringAliasCandidateSource(candidate, best) {
if best.SourcePriority == 0 || candidate.SourcePriority == best.SourcePriority {
bestAmbiguous = true
}
if best.SourcePriority == 0 || (candidate.SourcePriority > 0 && candidate.SourcePriority < best.SourcePriority) {
bestAmbiguous = false
best = candidate
}
}
}
@@ -2677,6 +2733,8 @@ func (s *MonitoringCallbackService) loadAliasResolutions(ctx context.Context, tx
ArticleID: cloneInt64(best.ArticleID),
PublishRecordID: cloneInt64(best.PublishRecordID),
NormalizedURLKey: best.NormalizedURL,
SourceType: strings.TrimSpace(best.SourceType),
SourceID: cloneInt64(best.SourceID),
ResolutionStatus: "resolved",
ResolutionConfidence: "high",
}
@@ -2745,19 +2803,28 @@ func sameInt64Pointer(left, right *int64) bool {
return *left == *right
}
func sameMonitoringAliasCandidateSource(left, right monitoringAliasCandidate) bool {
leftType := strings.TrimSpace(left.SourceType)
rightType := strings.TrimSpace(right.SourceType)
if leftType == "" && rightType == "" {
return sameInt64Pointer(left.ArticleID, right.ArticleID)
}
return leftType == rightType && sameInt64Pointer(left.SourceID, right.SourceID)
}
func (s *MonitoringCallbackService) insertCitationFact(ctx context.Context, tx pgx.Tx, task *monitoringCollectTask, runID int64, fact monitoringCitationFact) error {
if _, err := tx.Exec(ctx, `
INSERT INTO monitoring_citation_facts (
tenant_id, run_id, brand_id, ai_platform_id, business_date,
cited_url, cited_title, normalized_url, host, registrable_domain,
subdomain, suffix, site_key, article_id, publish_record_id,
resolution_status, resolution_confidence
content_source_type, content_source_id, resolution_status, resolution_confidence
)
VALUES (
$1, $2, $3, $4, $5::date,
$6, $7, $8, $9, $10,
$11, $12, $13, $14, $15,
$16, $17
$16, $17, $18, $19
)
`,
task.TenantID,
@@ -2775,6 +2842,8 @@ func (s *MonitoringCallbackService) insertCitationFact(ctx context.Context, tx p
fact.SiteKey,
nullableInt64(fact.ArticleID),
nullableInt64(fact.PublishRecordID),
nullableString(fact.ContentSourceType),
nullableInt64(fact.ContentSourceID),
fact.ResolutionStatus,
fact.ResolutionConfidence,
); err != nil {
@@ -2926,10 +2995,10 @@ func (s *MonitoringCallbackService) loadExistingRunSummary(ctx context.Context,
var citationSourceCount int
var contentCitationCount int
if err := s.monitoringPool.QueryRow(ctx, `
SELECT
COUNT(*)::int AS citation_source_count,
COUNT(*) FILTER (WHERE article_id IS NOT NULL AND $3::text <> 'qwen')::int AS content_citation_count
FROM monitoring_citation_facts
SELECT
COUNT(*)::int AS citation_source_count,
COUNT(*) FILTER (WHERE content_source_type IS NOT NULL AND content_source_id IS NOT NULL AND $3::text <> 'qwen')::int AS content_citation_count
FROM monitoring_citation_facts
WHERE tenant_id = $1
AND run_id = $2
`, task.TenantID, runID.Int64, normalizeMonitoringPlatformID(task.AIPlatformID)).Scan(&citationSourceCount, &contentCitationCount); err != nil {
@@ -3173,6 +3242,8 @@ func buildMonitoringCitationFact(input MonitoringSourceItem, aliasMap map[string
var articleID *int64
var publishRecordID *int64
var contentSourceType *string
var contentSourceID *int64
var aliasResolution *monitoringAliasResolution
if resolveContentCitations {
articleID = cloneInt64(input.ArticleID)
@@ -3186,6 +3257,10 @@ func buildMonitoringCitationFact(input MonitoringSourceItem, aliasMap map[string
if publishRecordID == nil {
publishRecordID = cloneInt64(alias.PublishRecordID)
}
if strings.TrimSpace(alias.SourceType) != "" && alias.SourceID != nil {
contentSourceType = optionalString(strings.TrimSpace(alias.SourceType))
contentSourceID = cloneInt64(alias.SourceID)
}
}
}
@@ -3217,6 +3292,8 @@ func buildMonitoringCitationFact(input MonitoringSourceItem, aliasMap map[string
SiteKey: siteKey,
ArticleID: articleID,
PublishRecordID: publishRecordID,
ContentSourceType: contentSourceType,
ContentSourceID: contentSourceID,
ResolutionStatus: resolutionStatus,
ResolutionConfidence: resolutionConfidence,
}, true