fix media supply citation attribution
Desktop Client Build / Resolve Build Metadata (push) Successful in 26s
Frontend CI / Frontend (push) Successful in 2m53s
Backend CI / Backend (push) Successful in 16m37s
Desktop Client Build / Build Desktop Client (push) Successful in 23m40s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 28s
Desktop Client Build / Resolve Build Metadata (push) Successful in 26s
Frontend CI / Frontend (push) Successful in 2m53s
Backend CI / Backend (push) Successful in 16m37s
Desktop Client Build / Build Desktop Client (push) Successful in 23m40s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 28s
This commit is contained in:
@@ -46,7 +46,9 @@ func main() {
|
||||
if mediaSupplyService == nil {
|
||||
mediaSupplyService = tenantapp.NewMediaSupplyService(app.DB, app.Redis, app.Logger, app.ConfigStore).WithObjectStorage(app.ObjectStorage)
|
||||
}
|
||||
tenantapp.NewMediaSupplyWorker(mediaSupplyService, app.DB, app.Logger).Start(workerCtx)
|
||||
tenantapp.NewMediaSupplyWorker(mediaSupplyService, app.DB, app.Logger).
|
||||
WithMonitoringPool(app.MonitoringDB).
|
||||
Start(workerCtx)
|
||||
knowledgeCleanupService := tenantapp.NewKnowledgeService(
|
||||
app.DB,
|
||||
app.RetrievalProvider,
|
||||
|
||||
@@ -17,9 +17,10 @@ import (
|
||||
)
|
||||
|
||||
type MediaSupplyWorker struct {
|
||||
service *MediaSupplyService
|
||||
pool *pgxpool.Pool
|
||||
logger *zap.Logger
|
||||
service *MediaSupplyService
|
||||
pool *pgxpool.Pool
|
||||
monitoringPool *pgxpool.Pool
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
type mediaSupplySyncJob struct {
|
||||
@@ -59,6 +60,13 @@ func NewMediaSupplyWorker(service *MediaSupplyService, pool *pgxpool.Pool, logge
|
||||
return &MediaSupplyWorker{service: service, pool: pool, logger: logger}
|
||||
}
|
||||
|
||||
func (w *MediaSupplyWorker) WithMonitoringPool(monitoringPool *pgxpool.Pool) *MediaSupplyWorker {
|
||||
if w != nil {
|
||||
w.monitoringPool = monitoringPool
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
func (w *MediaSupplyWorker) Start(ctx context.Context) {
|
||||
if w == nil || w.service == nil || w.pool == nil {
|
||||
return
|
||||
@@ -917,7 +925,165 @@ func (w *MediaSupplyWorker) updateOrderBacklinks(ctx context.Context, order medi
|
||||
`, order.ID, "published", remaining, firstMatchedMeijiequanOrderCode(matches), firstMatchedMeijiequanOrderID(matches)); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return updatedLinks, tx.Commit(ctx)
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
w.refreshMonitoringAttributionForMediaSupplyBacklinks(ctx, order.ID)
|
||||
return updatedLinks, nil
|
||||
}
|
||||
|
||||
func (w *MediaSupplyWorker) refreshMonitoringAttributionForMediaSupplyBacklinks(ctx context.Context, orderID int64) {
|
||||
if w == nil || w.monitoringPool == nil || w.pool == nil || orderID <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := w.pool.Query(ctx, `
|
||||
SELECT
|
||||
o.tenant_id,
|
||||
i.id,
|
||||
i.external_article_url
|
||||
FROM media_supply_order_items i
|
||||
JOIN media_supply_orders o ON o.id = i.order_id
|
||||
WHERE o.id = $1
|
||||
AND o.deleted_at IS NULL
|
||||
AND NULLIF(TRIM(COALESCE(i.external_article_url, '')), '') IS NOT NULL
|
||||
`, orderID)
|
||||
if err != nil {
|
||||
w.logWarn("media supply monitoring attribution backlink lookup failed", zap.Int64("order_id", orderID), zap.Error(err))
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
type backlinkSource struct {
|
||||
tenantID int64
|
||||
itemID int64
|
||||
url string
|
||||
}
|
||||
sources := make([]backlinkSource, 0)
|
||||
for rows.Next() {
|
||||
var source backlinkSource
|
||||
if scanErr := rows.Scan(&source.tenantID, &source.itemID, &source.url); scanErr != nil {
|
||||
w.logWarn("media supply monitoring attribution backlink scan failed", zap.Int64("order_id", orderID), zap.Error(scanErr))
|
||||
return
|
||||
}
|
||||
source.url = strings.TrimSpace(source.url)
|
||||
if source.tenantID <= 0 || source.itemID <= 0 || source.url == "" {
|
||||
continue
|
||||
}
|
||||
sources = append(sources, source)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
w.logWarn("media supply monitoring attribution backlink iterate failed", zap.Int64("order_id", orderID), zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
for _, source := range sources {
|
||||
if _, err := w.refreshMonitoringAttributionForMediaSupplyBacklink(ctx, source.tenantID, source.itemID, source.url); err != nil {
|
||||
w.logWarn(
|
||||
"media supply monitoring attribution refresh failed",
|
||||
zap.Int64("tenant_id", source.tenantID),
|
||||
zap.Int64("order_id", orderID),
|
||||
zap.Int64("item_id", source.itemID),
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *MediaSupplyWorker) refreshMonitoringAttributionForMediaSupplyBacklink(ctx context.Context, tenantID, itemID int64, rawURL string) (int64, error) {
|
||||
matchKeys := monitoringCitationCandidateKeys(rawURL, normalizeCitationURL(rawURL))
|
||||
if len(matchKeys) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
normalizedURL := normalizeCitationURL(rawURL)
|
||||
host, registrableDomain, _, _ := deriveCitationURLParts(rawURL, normalizedURL)
|
||||
candidateURLs := uniqueTrimmedStrings(rawURL, normalizedURL)
|
||||
rows, err := w.monitoringPool.Query(ctx, `
|
||||
SELECT id, cited_url, normalized_url
|
||||
FROM monitoring_citation_facts
|
||||
WHERE tenant_id = $1
|
||||
AND (
|
||||
(content_source_type IS NULL AND content_source_id IS NULL)
|
||||
OR content_source_type = ANY($5::text[])
|
||||
)
|
||||
AND (
|
||||
cited_url = ANY($2::text[])
|
||||
OR normalized_url = ANY($2::text[])
|
||||
OR NULLIF(LOWER(TRIM(host)), '') = NULLIF($3, '')
|
||||
OR NULLIF(LOWER(TRIM(registrable_domain)), '') = NULLIF($4, '')
|
||||
OR NULLIF(LOWER(TRIM(site_key)), '') = NULLIF($4, '')
|
||||
)
|
||||
`, tenantID, candidateURLs, strings.ToLower(strings.TrimSpace(host)), strings.ToLower(strings.TrimSpace(registrableDomain)), []string{
|
||||
monitoringContentSourcePublishedArticle,
|
||||
monitoringContentSourceManualMark,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
matchKeySet := make(map[string]struct{}, len(matchKeys))
|
||||
for _, key := range matchKeys {
|
||||
matchKeySet[strings.TrimSpace(key)] = struct{}{}
|
||||
}
|
||||
matchedIDs := make([]int64, 0)
|
||||
for rows.Next() {
|
||||
var id int64
|
||||
var citedURL string
|
||||
var factNormalizedURL string
|
||||
if scanErr := rows.Scan(&id, &citedURL, &factNormalizedURL); scanErr != nil {
|
||||
return 0, scanErr
|
||||
}
|
||||
for _, factKey := range monitoringCitationCandidateKeys(citedURL, factNormalizedURL) {
|
||||
if _, ok := matchKeySet[strings.TrimSpace(factKey)]; ok {
|
||||
matchedIDs = append(matchedIDs, id)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(matchedIDs) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
tag, err := w.monitoringPool.Exec(ctx, `
|
||||
UPDATE monitoring_citation_facts
|
||||
SET content_source_type = $3,
|
||||
content_source_id = $2
|
||||
WHERE tenant_id = $1
|
||||
AND id = ANY($4::bigint[])
|
||||
AND (
|
||||
(content_source_type IS NULL AND content_source_id IS NULL)
|
||||
OR content_source_type = ANY($5::text[])
|
||||
)
|
||||
`, tenantID, itemID, monitoringContentSourceMediaSupplyOrderItem, matchedIDs, []string{
|
||||
monitoringContentSourcePublishedArticle,
|
||||
monitoringContentSourceManualMark,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return tag.RowsAffected(), nil
|
||||
}
|
||||
|
||||
func uniqueTrimmedStrings(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 _, exists := seen[trimmed]; exists {
|
||||
continue
|
||||
}
|
||||
seen[trimmed] = struct{}{}
|
||||
result = append(result, trimmed)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (w *MediaSupplyWorker) claimSyncJob(ctx context.Context) (*mediaSupplySyncJob, bool, error) {
|
||||
|
||||
@@ -2819,10 +2819,10 @@ func (s *MonitoringCallbackService) loadAliasResolutions(ctx context.Context, tx
|
||||
normalized_url,
|
||||
article_id,
|
||||
publish_record_id,
|
||||
'published_article' AS source_type,
|
||||
article_id AS source_id,
|
||||
1 AS source_priority,
|
||||
registrable_domain,
|
||||
'published_article' AS source_type,
|
||||
article_id AS source_id,
|
||||
2 AS source_priority,
|
||||
registrable_domain,
|
||||
site_key,
|
||||
host,
|
||||
last_path_segment,
|
||||
@@ -2837,10 +2837,10 @@ func (s *MonitoringCallbackService) loadAliasResolutions(ctx context.Context, tx
|
||||
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,
|
||||
'manual_mark' AS source_type,
|
||||
id AS source_id,
|
||||
3 AS source_priority,
|
||||
registrable_domain,
|
||||
site_key,
|
||||
host,
|
||||
last_path_segment,
|
||||
@@ -2855,10 +2855,10 @@ func (s *MonitoringCallbackService) loadAliasResolutions(ctx context.Context, tx
|
||||
normalized_url,
|
||||
article_id,
|
||||
publish_record_id,
|
||||
source_type,
|
||||
source_id,
|
||||
source_priority,
|
||||
registrable_domain,
|
||||
source_type,
|
||||
source_id,
|
||||
source_priority,
|
||||
registrable_domain,
|
||||
site_key,
|
||||
host,
|
||||
last_path_segment,
|
||||
@@ -2916,6 +2916,36 @@ func (s *MonitoringCallbackService) loadAliasResolutions(ctx context.Context, tx
|
||||
return nil, response.ErrInternal(50041, "alias_scan_failed", "failed to iterate article aliases")
|
||||
}
|
||||
|
||||
mediaAliases, err := loadMonitoringMediaSupplyPublishedAliases(ctx, s.businessPool, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, alias := range mediaAliases {
|
||||
candidate := monitoringAliasCandidate{
|
||||
NormalizedURL: alias.NormalizedURL,
|
||||
SourceType: strings.TrimSpace(alias.SourceType),
|
||||
SourcePriority: alias.SourcePriority,
|
||||
TitleKey: normalizeMonitoringAliasTitle(alias.ArticleTitle),
|
||||
MatchKeys: monitoringCitationCandidateKeys(alias.OriginalURL, alias.NormalizedURL),
|
||||
}
|
||||
if alias.ArticleID > 0 {
|
||||
value := alias.ArticleID
|
||||
candidate.ArticleID = &value
|
||||
}
|
||||
if alias.SourceID > 0 {
|
||||
value := alias.SourceID
|
||||
candidate.SourceID = &value
|
||||
}
|
||||
if lastPathSegment := monitoringAliasLastPathSegment(alias.NormalizedURL); lastPathSegment != nil {
|
||||
candidate.LastPathSegment = strings.TrimSpace(*lastPathSegment)
|
||||
}
|
||||
host, registrableDomain, _, _ := deriveCitationURLParts(alias.OriginalURL, alias.NormalizedURL)
|
||||
candidate.Host = strings.ToLower(strings.TrimSpace(host))
|
||||
candidate.RegistrableDomain = strings.ToLower(strings.TrimSpace(registrableDomain))
|
||||
candidate.SiteKey = candidate.RegistrableDomain
|
||||
candidates = append(candidates, candidate)
|
||||
}
|
||||
|
||||
for _, input := range lookupInputs {
|
||||
bestScore := -1
|
||||
bestAmbiguous := false
|
||||
|
||||
@@ -583,6 +583,38 @@ func TestBuildMonitoringCitationFactResolvesContentForQwen(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildMonitoringCitationFactResolvesMediaSupplyContentSource(t *testing.T) {
|
||||
aliasArticleID := int64(42)
|
||||
sourceID := int64(9001)
|
||||
normalizedURL := "https://example.com/submitted/article"
|
||||
input := MonitoringSourceItem{
|
||||
URL: "https://example.com/submitted/article?utm_source=chat",
|
||||
NormalizedURL: &normalizedURL,
|
||||
}
|
||||
aliasMap := map[string]monitoringAliasResolution{
|
||||
normalizedURL: {
|
||||
ArticleID: &aliasArticleID,
|
||||
NormalizedURLKey: normalizedURL,
|
||||
SourceType: monitoringContentSourceMediaSupplyOrderItem,
|
||||
SourceID: &sourceID,
|
||||
},
|
||||
}
|
||||
|
||||
fact, ok := buildMonitoringCitationFact(input, aliasMap, true)
|
||||
if !ok {
|
||||
t.Fatal("buildMonitoringCitationFact() returned !ok")
|
||||
}
|
||||
if fact.ArticleID == nil || *fact.ArticleID != aliasArticleID {
|
||||
t.Fatalf("ArticleID = %v, want %d", fact.ArticleID, aliasArticleID)
|
||||
}
|
||||
if fact.ContentSourceType == nil || *fact.ContentSourceType != monitoringContentSourceMediaSupplyOrderItem {
|
||||
t.Fatalf("ContentSourceType = %v, want %q", fact.ContentSourceType, monitoringContentSourceMediaSupplyOrderItem)
|
||||
}
|
||||
if fact.ContentSourceID == nil || *fact.ContentSourceID != sourceID {
|
||||
t.Fatalf("ContentSourceID = %v, want %d", fact.ContentSourceID, sourceID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScoreMonitoringAliasCandidateUsesCanonicalArticleKey(t *testing.T) {
|
||||
input := monitoringAliasLookupInput{
|
||||
NormalizedURL: "https://m.163.com/dy/article/KRPRE8DJ0556MCR0.html",
|
||||
@@ -613,6 +645,70 @@ func TestScoreMonitoringAliasCandidateRejectsSameDomainDifferentArticle(t *testi
|
||||
}
|
||||
}
|
||||
|
||||
func TestMonitoringPublishedAliasIndexMatchesMediaSupplySubmission(t *testing.T) {
|
||||
index := make(monitoringPublishedAliasIndex)
|
||||
sourceID := int64(9001)
|
||||
mediaAlias := monitoringPublishedAlias{
|
||||
ArticleID: 42,
|
||||
ArticleTitle: "投稿文章",
|
||||
PublishPlatform: monitoringMediaSupplySubmissionCitationLabel,
|
||||
OriginalURL: "https://m.example.com/submitted/article?id=123&utm_source=chat",
|
||||
NormalizedURL: "https://www.example.com/submitted/article?id=123",
|
||||
SourceType: monitoringContentSourceMediaSupplyOrderItem,
|
||||
SourceID: sourceID,
|
||||
SourcePriority: 1,
|
||||
}
|
||||
|
||||
addMonitoringPublishedAliasToIndex(index, mediaAlias)
|
||||
|
||||
match, ok := index.Match(
|
||||
"https://www.example.com/submitted/article?id=123&utm_medium=ai",
|
||||
"https://www.example.com/submitted/article?id=123",
|
||||
)
|
||||
if !ok {
|
||||
t.Fatal("Match() = false, want media supply alias match")
|
||||
}
|
||||
if match.SourceType != monitoringContentSourceMediaSupplyOrderItem {
|
||||
t.Fatalf("SourceType = %q, want %q", match.SourceType, monitoringContentSourceMediaSupplyOrderItem)
|
||||
}
|
||||
if match.SourceID != sourceID {
|
||||
t.Fatalf("SourceID = %d, want %d", match.SourceID, sourceID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMonitoringPublishedAliasIndexPrefersMediaSupplySubmission(t *testing.T) {
|
||||
index := make(monitoringPublishedAliasIndex)
|
||||
normalizedURL := "https://www.example.com/submitted/article?id=123"
|
||||
|
||||
addMonitoringPublishedAliasToIndex(index, monitoringPublishedAlias{
|
||||
ArticleID: 42,
|
||||
OriginalURL: normalizedURL,
|
||||
NormalizedURL: normalizedURL,
|
||||
SourceType: monitoringContentSourcePublishedArticle,
|
||||
SourceID: 42,
|
||||
SourcePriority: 2,
|
||||
})
|
||||
addMonitoringPublishedAliasToIndex(index, monitoringPublishedAlias{
|
||||
ArticleID: 42,
|
||||
OriginalURL: normalizedURL,
|
||||
NormalizedURL: normalizedURL,
|
||||
SourceType: monitoringContentSourceMediaSupplyOrderItem,
|
||||
SourceID: 9001,
|
||||
SourcePriority: 1,
|
||||
})
|
||||
|
||||
match, ok := index.Match(normalizedURL, normalizedURL)
|
||||
if !ok {
|
||||
t.Fatal("Match() = false, want media supply alias match")
|
||||
}
|
||||
if match.SourceType != monitoringContentSourceMediaSupplyOrderItem {
|
||||
t.Fatalf("SourceType = %q, want %q", match.SourceType, monitoringContentSourceMediaSupplyOrderItem)
|
||||
}
|
||||
if match.SourceID != 9001 {
|
||||
t.Fatalf("SourceID = %d, want 9001", match.SourceID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildMonitoringRawPayloadDeepSeekIncludesSearchResultsInCitationInputs(t *testing.T) {
|
||||
searchTitle := "搜索网页结果"
|
||||
|
||||
|
||||
@@ -32,7 +32,78 @@ type monitoringAliasQuerier interface {
|
||||
Query(ctx context.Context, sql string, args ...interface{}) (pgx.Rows, error)
|
||||
}
|
||||
|
||||
func loadMonitoringPublishedAliasIndex(ctx context.Context, q monitoringAliasQuerier, tenantID int64) (monitoringPublishedAliasIndex, error) {
|
||||
const (
|
||||
monitoringContentSourcePublishedArticle = "published_article"
|
||||
monitoringContentSourceManualMark = "manual_mark"
|
||||
monitoringContentSourceMediaSupplyOrderItem = "media_supply_order_item"
|
||||
monitoringMediaSupplySubmissionCitationLabel = "投稿引用"
|
||||
)
|
||||
|
||||
func loadMonitoringMediaSupplyPublishedAliases(ctx context.Context, q monitoringAliasQuerier, tenantID int64) ([]monitoringPublishedAlias, error) {
|
||||
if q == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
rows, err := q.Query(ctx, `
|
||||
SELECT
|
||||
o.article_id,
|
||||
i.id,
|
||||
COALESCE(NULLIF(o.title, ''), NULLIF(i.resource_name_snapshot, ''), '未命名文章') AS article_title,
|
||||
COALESCE(NULLIF(i.resource_name_snapshot, ''), $2) AS publish_platform,
|
||||
i.external_article_url
|
||||
FROM media_supply_order_items i
|
||||
JOIN media_supply_orders o ON o.id = i.order_id
|
||||
WHERE o.tenant_id = $1
|
||||
AND o.deleted_at IS NULL
|
||||
AND NULLIF(TRIM(COALESCE(i.external_article_url, '')), '') IS NOT NULL
|
||||
ORDER BY i.updated_at DESC, i.id DESC
|
||||
`, tenantID, monitoringMediaSupplySubmissionCitationLabel)
|
||||
if err != nil {
|
||||
return nil, responseInternalAliasLookupError()
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := make([]monitoringPublishedAlias, 0)
|
||||
for rows.Next() {
|
||||
var articleID sql.NullInt64
|
||||
var itemID int64
|
||||
var articleTitle string
|
||||
var publishPlatform string
|
||||
var originalURL string
|
||||
if scanErr := rows.Scan(
|
||||
&articleID,
|
||||
&itemID,
|
||||
&articleTitle,
|
||||
&publishPlatform,
|
||||
&originalURL,
|
||||
); scanErr != nil {
|
||||
return nil, responseInternalAliasScanError()
|
||||
}
|
||||
normalizedURL := normalizeCitationURL(originalURL)
|
||||
if normalizedURL == "" {
|
||||
continue
|
||||
}
|
||||
item := monitoringPublishedAlias{
|
||||
ArticleTitle: articleTitle,
|
||||
PublishPlatform: publishPlatform,
|
||||
OriginalURL: strings.TrimSpace(originalURL),
|
||||
NormalizedURL: normalizedURL,
|
||||
SourceType: monitoringContentSourceMediaSupplyOrderItem,
|
||||
SourceID: itemID,
|
||||
SourcePriority: 1,
|
||||
}
|
||||
if articleID.Valid {
|
||||
item.ArticleID = articleID.Int64
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, responseInternalAliasScanError()
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func loadMonitoringPublishedAliasIndex(ctx context.Context, q monitoringAliasQuerier, businessQ monitoringAliasQuerier, tenantID int64) (monitoringPublishedAliasIndex, error) {
|
||||
rows, err := q.Query(ctx, `
|
||||
WITH alias_sources AS (
|
||||
SELECT
|
||||
@@ -42,11 +113,11 @@ func loadMonitoringPublishedAliasIndex(ctx context.Context, q monitoringAliasQue
|
||||
COALESCE(publish_platform_name_snapshot, '已发布内容') AS publish_platform,
|
||||
original_url,
|
||||
normalized_url,
|
||||
'published_article' AS source_type,
|
||||
article_id AS source_id,
|
||||
1 AS source_priority,
|
||||
updated_at AS sort_updated_at,
|
||||
id AS sort_id
|
||||
'published_article' AS source_type,
|
||||
article_id AS source_id,
|
||||
2 AS source_priority,
|
||||
updated_at AS sort_updated_at,
|
||||
id AS sort_id
|
||||
FROM monitoring_article_url_aliases
|
||||
WHERE tenant_id = $1
|
||||
AND confidence = 'high'
|
||||
@@ -58,11 +129,11 @@ func loadMonitoringPublishedAliasIndex(ctx context.Context, q monitoringAliasQue
|
||||
COALESCE(NULLIF(publish_platform, ''), '外部文章') AS publish_platform,
|
||||
original_url,
|
||||
normalized_url,
|
||||
'manual_mark' AS source_type,
|
||||
id AS source_id,
|
||||
2 AS source_priority,
|
||||
updated_at AS sort_updated_at,
|
||||
id AS sort_id
|
||||
'manual_mark' AS source_type,
|
||||
id AS source_id,
|
||||
3 AS source_priority,
|
||||
updated_at AS sort_updated_at,
|
||||
id AS sort_id
|
||||
FROM monitoring_user_marked_articles
|
||||
WHERE tenant_id = $1
|
||||
AND expires_at > NOW()
|
||||
@@ -74,9 +145,9 @@ func loadMonitoringPublishedAliasIndex(ctx context.Context, q monitoringAliasQue
|
||||
publish_platform,
|
||||
original_url,
|
||||
normalized_url,
|
||||
source_type,
|
||||
source_id,
|
||||
source_priority
|
||||
source_type,
|
||||
source_id,
|
||||
source_priority
|
||||
FROM alias_sources
|
||||
ORDER BY source_priority ASC, sort_updated_at DESC, sort_id DESC
|
||||
`, tenantID)
|
||||
@@ -111,32 +182,44 @@ func loadMonitoringPublishedAliasIndex(ctx context.Context, q monitoringAliasQue
|
||||
item.PublishRecordID = nullableInt64Value(publishRecordID)
|
||||
item.OriginalURL = originalURL
|
||||
item.NormalizedURL = normalizedURL
|
||||
for _, key := range monitoringCitationCandidateKeys(originalURL, normalizedURL) {
|
||||
existing, exists := index[key]
|
||||
if exists && existing.SourcePriority < item.SourcePriority {
|
||||
continue
|
||||
}
|
||||
if exists && item.SourcePriority < existing.SourcePriority {
|
||||
index[key] = item
|
||||
continue
|
||||
}
|
||||
if exists && (existing.SourceType != item.SourceType || existing.SourceID != item.SourceID) {
|
||||
existing.Ambiguous = true
|
||||
index[key] = existing
|
||||
continue
|
||||
}
|
||||
if !exists {
|
||||
index[key] = item
|
||||
}
|
||||
}
|
||||
addMonitoringPublishedAliasToIndex(index, item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, responseInternalAliasScanError()
|
||||
}
|
||||
|
||||
mediaAliases, err := loadMonitoringMediaSupplyPublishedAliases(ctx, businessQ, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range mediaAliases {
|
||||
addMonitoringPublishedAliasToIndex(index, item)
|
||||
}
|
||||
|
||||
return index, nil
|
||||
}
|
||||
|
||||
func addMonitoringPublishedAliasToIndex(index monitoringPublishedAliasIndex, item monitoringPublishedAlias) {
|
||||
for _, key := range monitoringCitationCandidateKeys(item.OriginalURL, item.NormalizedURL) {
|
||||
existing, exists := index[key]
|
||||
if exists && existing.SourcePriority < item.SourcePriority {
|
||||
continue
|
||||
}
|
||||
if exists && item.SourcePriority < existing.SourcePriority {
|
||||
index[key] = item
|
||||
continue
|
||||
}
|
||||
if exists && (existing.SourceType != item.SourceType || existing.SourceID != item.SourceID) {
|
||||
existing.Ambiguous = true
|
||||
index[key] = existing
|
||||
continue
|
||||
}
|
||||
if !exists {
|
||||
index[key] = item
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (index monitoringPublishedAliasIndex) Match(rawURL, normalizedURL string) (monitoringPublishedAlias, bool) {
|
||||
for _, key := range monitoringCitationCandidateKeys(rawURL, normalizedURL) {
|
||||
item, ok := index[key]
|
||||
|
||||
@@ -3733,9 +3733,9 @@ func (s *MonitoringService) loadCitedArticleSourceMeta(ctx context.Context, tena
|
||||
manualIDs := make([]int64, 0, len(sources))
|
||||
for _, source := range sources {
|
||||
switch source.SourceType {
|
||||
case "published_article":
|
||||
case monitoringContentSourcePublishedArticle:
|
||||
publishedIDs = append(publishedIDs, source.SourceID)
|
||||
case "manual_mark":
|
||||
case monitoringContentSourceManualMark:
|
||||
manualIDs = append(manualIDs, source.SourceID)
|
||||
}
|
||||
}
|
||||
@@ -3766,7 +3766,7 @@ func (s *MonitoringService) loadCitedArticleSourceMeta(ctx context.Context, tena
|
||||
item.ArticleID = &value
|
||||
item.ArticleTitle = repairMonitoringMojibake(item.ArticleTitle)
|
||||
item.PublishPlatform = repairMonitoringMojibake(item.PublishPlatform)
|
||||
item.SourceType = "published_article"
|
||||
item.SourceType = monitoringContentSourcePublishedArticle
|
||||
item.SourceID = articleID
|
||||
result[monitoringContentSourceKey(item.SourceType, item.SourceID)] = item
|
||||
}
|
||||
@@ -3796,13 +3796,68 @@ func (s *MonitoringService) loadCitedArticleSourceMeta(ctx context.Context, tena
|
||||
}
|
||||
item.ArticleTitle = repairMonitoringMojibake(item.ArticleTitle)
|
||||
item.PublishPlatform = repairMonitoringMojibake(item.PublishPlatform)
|
||||
item.SourceType = "manual_mark"
|
||||
item.SourceType = monitoringContentSourceManualMark
|
||||
result[monitoringContentSourceKey(item.SourceType, item.SourceID)] = item
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, response.ErrInternal(50041, "scan_failed", "failed to iterate marked article metadata")
|
||||
}
|
||||
}
|
||||
mediaItems, err := s.loadMediaSupplyCitedArticleSourceMeta(ctx, tenantID, sources)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for key, item := range mediaItems {
|
||||
result[key] = item
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *MonitoringService) loadMediaSupplyCitedArticleSourceMeta(ctx context.Context, tenantID int64, sources []monitoringCitedArticleSourceRow) (map[string]MonitoringCitedArticle, error) {
|
||||
result := make(map[string]MonitoringCitedArticle)
|
||||
mediaItemIDs := make([]int64, 0, len(sources))
|
||||
for _, source := range sources {
|
||||
if source.SourceType == monitoringContentSourceMediaSupplyOrderItem {
|
||||
mediaItemIDs = append(mediaItemIDs, source.SourceID)
|
||||
}
|
||||
}
|
||||
if len(mediaItemIDs) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
rows, err := s.businessPool.Query(ctx, `
|
||||
SELECT
|
||||
i.id,
|
||||
o.article_id,
|
||||
COALESCE(NULLIF(o.title, ''), NULLIF(i.resource_name_snapshot, ''), '未命名文章') AS article_title,
|
||||
COALESCE(NULLIF(i.resource_name_snapshot, ''), $3) AS publish_platform,
|
||||
i.external_article_url
|
||||
FROM media_supply_order_items i
|
||||
JOIN media_supply_orders o ON o.id = i.order_id
|
||||
WHERE o.tenant_id = $1
|
||||
AND i.id = ANY($2)
|
||||
AND o.deleted_at IS NULL
|
||||
`, tenantID, mediaItemIDs, monitoringMediaSupplySubmissionCitationLabel)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50041, "query_failed", "failed to load media supply citation metadata")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var item MonitoringCitedArticle
|
||||
var articleID sql.NullInt64
|
||||
if scanErr := rows.Scan(&item.SourceID, &articleID, &item.ArticleTitle, &item.PublishPlatform, &item.SourceURL); scanErr != nil {
|
||||
return nil, response.ErrInternal(50041, "scan_failed", "failed to parse media supply citation metadata")
|
||||
}
|
||||
item.ArticleID = nullableInt64Value(articleID)
|
||||
item.ArticleTitle = repairMonitoringMojibake(item.ArticleTitle)
|
||||
item.PublishPlatform = repairMonitoringMojibake(item.PublishPlatform)
|
||||
item.SourceType = monitoringContentSourceMediaSupplyOrderItem
|
||||
result[monitoringContentSourceKey(item.SourceType, item.SourceID)] = item
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, response.ErrInternal(50041, "scan_failed", "failed to iterate media supply citation metadata")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -4012,7 +4067,7 @@ func (s *MonitoringService) loadCitationsForRuns(ctx context.Context, tenantID i
|
||||
return result, nil
|
||||
}
|
||||
|
||||
aliasIndex, err := loadMonitoringPublishedAliasIndex(ctx, s.monitoringPool, tenantID)
|
||||
aliasIndex, err := loadMonitoringPublishedAliasIndex(ctx, s.monitoringPool, s.businessPool, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -4120,7 +4175,7 @@ func (s *MonitoringService) loadCitationsForRuns(ctx context.Context, tenantID i
|
||||
articleTitle = &title
|
||||
aliasSourceType := strings.TrimSpace(alias.SourceType)
|
||||
if aliasSourceType == "" {
|
||||
aliasSourceType = "published_article"
|
||||
aliasSourceType = monitoringContentSourcePublishedArticle
|
||||
}
|
||||
sourceType = &aliasSourceType
|
||||
aliasSourceID := alias.SourceID
|
||||
|
||||
Reference in New Issue
Block a user