@@ -582,11 +582,75 @@ function buildJudgementCursor(time: Date, userId: number) {
582582 return `${ time . getTime ( ) . toString ( 36 ) } -${ userId . toString ( 36 ) } ` ;
583583}
584584
585+ const ARTICLE_COPRA_SCHEMA_ERROR_CODES = new Set ( [ "42P01" , "42703" ] ) ;
586+ let skipArticleCopraJoin = false ;
587+
585588// Pulls recent文章候选,连同最新快照/作者信息/近期回复数。
586589async function fetchArticleRows ( ) {
587590 const since = new Date ( Date . now ( ) - ARTICLE_LOOKBACK_MS ) ;
588591 const recentSince = new Date ( Date . now ( ) - ARTICLE_RECENT_REPLY_WINDOW_MS ) ;
589592
593+ if ( skipArticleCopraJoin ) {
594+ return executeArticleRowsQuery ( { since, recentSince, includeCopra : false } ) ;
595+ }
596+
597+ try {
598+ return await executeArticleRowsQuery ( {
599+ since,
600+ recentSince,
601+ includeCopra : true ,
602+ } ) ;
603+ } catch ( error ) {
604+ if ( ! isArticleCopraSchemaError ( error ) ) {
605+ throw error ;
606+ }
607+
608+ skipArticleCopraJoin = true ;
609+ console . warn (
610+ "[feed] ArticleCopra join disabled after schema error" ,
611+ error instanceof Error ? error . message : error ,
612+ ) ;
613+
614+ return executeArticleRowsQuery ( { since, recentSince, includeCopra : false } ) ;
615+ }
616+ }
617+
618+ function isArticleCopraSchemaError ( error : unknown ) {
619+ const code = extractPostgresErrorCode ( error ) ;
620+ return Boolean ( code && ARTICLE_COPRA_SCHEMA_ERROR_CODES . has ( code ) ) ;
621+ }
622+
623+ function extractPostgresErrorCode ( error : unknown ) : string | null {
624+ if ( ! error || typeof error !== "object" ) return null ;
625+ const candidate = error as { code ?: unknown ; cause ?: unknown } ;
626+ if ( typeof candidate . code === "string" ) {
627+ return candidate . code ;
628+ }
629+ const { cause } = candidate ;
630+ if ( cause && typeof cause === "object" ) {
631+ const causeWithCode = cause as { code ?: unknown } ;
632+ if ( typeof causeWithCode . code === "string" ) {
633+ return causeWithCode . code ;
634+ }
635+ }
636+ return null ;
637+ }
638+
639+ async function executeArticleRowsQuery ( {
640+ since,
641+ recentSince,
642+ includeCopra,
643+ } : {
644+ since : Date ;
645+ recentSince : Date ;
646+ includeCopra : boolean ;
647+ } ) {
648+ const summarySelection = includeCopra ? sql `ac."summary"` : sql `NULL::text` ;
649+ const tagsSelection = includeCopra ? sql `ac."tags"` : sql `NULL::text` ;
650+ const copraJoin = includeCopra
651+ ? sql `LEFT JOIN "ArticleCopra" ac ON ac."articleId" = a."lid"`
652+ : sql `` ;
653+
590654 const { rows } = await db . execute < ArticleRow > ( sql `
591655 WITH latest_user AS (
592656 SELECT DISTINCT ON ("userId")
@@ -623,8 +687,8 @@ async function fetchArticleRows() {
623687 COALESCE(rar."recentReplyCount", 0) AS "recentReplyCount",
624688 las."title" AS "title",
625689 las."category" AS "category",
626- ac."summary" AS "summary",
627- ac."tags" AS "tags",
690+ ${ summarySelection } AS "summary",
691+ ${ tagsSelection } AS "tags",
628692 au."userId" AS "authorId",
629693 au."name" AS "authorName",
630694 au."badge" AS "authorBadge",
@@ -635,7 +699,7 @@ async function fetchArticleRows() {
635699 JOIN latest_article_snapshot las ON las."articleId" = a."lid"
636700 LEFT JOIN recent_article_replies rar ON rar."articleId" = a."lid"
637701 LEFT JOIN latest_user au ON au."userId" = a."authorId"
638- LEFT JOIN "ArticleCopra" ac ON ac."articleId" = a."lid"
702+ ${ copraJoin }
639703 WHERE a."updatedAt" >= ${ since }
640704 ORDER BY a."updatedAt" DESC
641705 LIMIT ${ ARTICLE_CANDIDATE_LIMIT }
0 commit comments