|
@@ -1,15 +1,15 @@
|
|
|
-"""文章域 DB 读写 —— demand_search_article_relation + demand_search_article_detail"""
|
|
|
|
|
|
|
+"""供给域 DB 读写 —— relation + detail + queue + accounts + articles 跨表访问"""
|
|
|
|
|
|
|
|
import json
|
|
import json
|
|
|
from typing import Dict, List, Optional
|
|
from typing import Dict, List, Optional
|
|
|
|
|
|
|
|
from src.infra.database.mysql.manager import MysqlManager
|
|
from src.infra.database.mysql.manager import MysqlManager
|
|
|
|
|
|
|
|
-from ._const import DemandSearchArticleConst
|
|
|
|
|
|
|
+from ._const import ContentSupplyConst
|
|
|
|
|
|
|
|
|
|
|
|
|
-class ArticleSearchRelationMapper(DemandSearchArticleConst):
|
|
|
|
|
- """demand_search_article_relation + demand_search_queue 的读写"""
|
|
|
|
|
|
|
+class ArticleSearchRelationMapper(ContentSupplyConst):
|
|
|
|
|
+ """demand_search_article_relation + demand_search_queue + demand_search_cache 的读写"""
|
|
|
|
|
|
|
|
def __init__(self, pool: MysqlManager):
|
|
def __init__(self, pool: MysqlManager):
|
|
|
self.pool = pool
|
|
self.pool = pool
|
|
@@ -132,7 +132,7 @@ class ArticleSearchRelationMapper(DemandSearchArticleConst):
|
|
|
return await self.pool.save(query=query, params=params, batch=True)
|
|
return await self.pool.save(query=query, params=params, batch=True)
|
|
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
- # 读取
|
|
|
|
|
|
|
+ # relation 表 — 读取
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
async def fetch_pending(
|
|
async def fetch_pending(
|
|
@@ -176,7 +176,7 @@ class ArticleSearchRelationMapper(DemandSearchArticleConst):
|
|
|
return row["cnt"] if row else 0
|
|
return row["cnt"] if row else 0
|
|
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
- # 状态更新
|
|
|
|
|
|
|
+ # relation 表 — 状态更新
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
async def mark_processing(self, ids: List[int]) -> int:
|
|
async def mark_processing(self, ids: List[int]) -> int:
|
|
@@ -270,16 +270,12 @@ class ArticleSearchRelationMapper(DemandSearchArticleConst):
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
-class ArticleDetailMapper(DemandSearchArticleConst):
|
|
|
|
|
|
|
+class ArticleDetailMapper(ContentSupplyConst):
|
|
|
"""demand_search_article_detail 表的数据访问层"""
|
|
"""demand_search_article_detail 表的数据访问层"""
|
|
|
|
|
|
|
|
def __init__(self, pool: MysqlManager):
|
|
def __init__(self, pool: MysqlManager):
|
|
|
self.pool = pool
|
|
self.pool = pool
|
|
|
|
|
|
|
|
- # ═══════════════════════════════════════════════════════════════
|
|
|
|
|
- # 写入
|
|
|
|
|
- # ═══════════════════════════════════════════════════════════════
|
|
|
|
|
-
|
|
|
|
|
async def insert_one(self, detail: Dict) -> int:
|
|
async def insert_one(self, detail: Dict) -> int:
|
|
|
"""写入单篇详情,channel_content_id 冲突则跳过
|
|
"""写入单篇详情,channel_content_id 冲突则跳过
|
|
|
|
|
|
|
@@ -325,4 +321,258 @@ class ArticleDetailMapper(DemandSearchArticleConst):
|
|
|
return await self.pool.save(query=query, params=params)
|
|
return await self.pool.save(query=query, params=params)
|
|
|
|
|
|
|
|
|
|
|
|
|
-__all__ = ["ArticleSearchRelationMapper", "ArticleDetailMapper"]
|
|
|
|
|
|
|
+class AccountMapper(ContentSupplyConst):
|
|
|
|
|
+ """demand_search_accounts 表 + relation.save_account_status + demand_account_articles 的读写"""
|
|
|
|
|
+
|
|
|
|
|
+ def __init__(self, pool: MysqlManager):
|
|
|
|
|
+ self.pool = pool
|
|
|
|
|
+
|
|
|
|
|
+ # ═══════════════════════════════════════════════════════════════
|
|
|
|
|
+ # relation 表 — 读取待抓账号
|
|
|
|
|
+ # ═══════════════════════════════════════════════════════════════
|
|
|
|
|
+
|
|
|
|
|
+ async def fetch_pending_relations(self, *, limit: int = 100) -> List[Dict]:
|
|
|
|
|
+ """取 save_account_status=0 的行(DISTINCT biz,按 id ASC)"""
|
|
|
|
|
+ query = f"""
|
|
|
|
|
+ SELECT MIN(id) AS id, biz, MIN(url) AS url
|
|
|
|
|
+ FROM {self.RELATION_TABLE}
|
|
|
|
|
+ WHERE save_account_status = %s AND biz IS NOT NULL AND biz != ''
|
|
|
|
|
+ GROUP BY biz
|
|
|
|
|
+ ORDER BY id ASC
|
|
|
|
|
+ LIMIT %s
|
|
|
|
|
+ """
|
|
|
|
|
+ return await self.pool.fetch(
|
|
|
|
|
+ query=query,
|
|
|
|
|
+ params=(self.AccountSaveStatus.INIT, limit),
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ async def fetch_relation_ids_by_biz(self, biz: str) -> List[int]:
|
|
|
|
|
+ """取某个 biz 下所有 save_account_status=0 的 relation id"""
|
|
|
|
|
+ rows = await self.pool.fetch(
|
|
|
|
|
+ query=f"SELECT id FROM {self.RELATION_TABLE} WHERE biz = %s AND save_account_status = %s",
|
|
|
|
|
+ params=(biz, self.AccountSaveStatus.INIT),
|
|
|
|
|
+ )
|
|
|
|
|
+ return [r["id"] for r in rows]
|
|
|
|
|
+
|
|
|
|
|
+ async def fetch_relation_ids_by_biz_any_status(self, biz: str) -> List[int]:
|
|
|
|
|
+ """取某个 biz 下未完成(INIT/PROCESSING)的 relation id,异常恢复用"""
|
|
|
|
|
+ rows = await self.pool.fetch(
|
|
|
|
|
+ query=f"SELECT id FROM {self.RELATION_TABLE} WHERE biz = %s AND save_account_status IN (%s, %s)",
|
|
|
|
|
+ params=(
|
|
|
|
|
+ biz,
|
|
|
|
|
+ self.AccountSaveStatus.INIT,
|
|
|
|
|
+ self.AccountSaveStatus.PROCESSING,
|
|
|
|
|
+ ),
|
|
|
|
|
+ )
|
|
|
|
|
+ return [r["id"] for r in rows]
|
|
|
|
|
+
|
|
|
|
|
+ # ═══════════════════════════════════════════════════════════════
|
|
|
|
|
+ # relation 表 — save_account_status 状态更新
|
|
|
|
|
+ # ═══════════════════════════════════════════════════════════════
|
|
|
|
|
+
|
|
|
|
|
+ async def _mark_batch(
|
|
|
|
|
+ self, target_status: int, ids: List[int], from_statuses: List[int]
|
|
|
|
|
+ ) -> int:
|
|
|
|
|
+ """通用:将 ids 从 from_statuses 任一改为 target_status"""
|
|
|
|
|
+ if not ids:
|
|
|
|
|
+ return 0
|
|
|
|
|
+ placeholders = ",".join(["%s"] * len(ids))
|
|
|
|
|
+ status_placeholders = ",".join(["%s"] * len(from_statuses))
|
|
|
|
|
+ query = f"""
|
|
|
|
|
+ UPDATE {self.RELATION_TABLE}
|
|
|
|
|
+ SET save_account_status = %s
|
|
|
|
|
+ WHERE id IN ({placeholders}) AND save_account_status IN ({status_placeholders})
|
|
|
|
|
+ """
|
|
|
|
|
+ return await self.pool.save(
|
|
|
|
|
+ query=query,
|
|
|
|
|
+ params=(target_status, *ids, *from_statuses),
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ async def relation_mark_processing(self, ids: List[int]) -> int:
|
|
|
|
|
+ """INIT → PROCESSING,CAS 防重复"""
|
|
|
|
|
+ return await self._mark_batch(
|
|
|
|
|
+ self.AccountSaveStatus.PROCESSING, ids, [self.AccountSaveStatus.INIT]
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ async def relation_mark_success(self, ids: List[int]) -> int:
|
|
|
|
|
+ """PROCESSING → SUCCESS"""
|
|
|
|
|
+ return await self._mark_batch(
|
|
|
|
|
+ self.AccountSaveStatus.SUCCESS, ids, [self.AccountSaveStatus.PROCESSING]
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ async def relation_mark_success_direct(self, ids: List[int]) -> int:
|
|
|
|
|
+ """INIT → SUCCESS,跳过 PROCESSING(查重命中时用)"""
|
|
|
|
|
+ return await self._mark_batch(
|
|
|
|
|
+ self.AccountSaveStatus.SUCCESS, ids, [self.AccountSaveStatus.INIT]
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ async def relation_mark_failed(self, ids: List[int]) -> int:
|
|
|
|
|
+ """INIT/PROCESSING → FAIL,异常恢复用"""
|
|
|
|
|
+ return await self._mark_batch(
|
|
|
|
|
+ self.AccountSaveStatus.FAIL,
|
|
|
|
|
+ ids,
|
|
|
|
|
+ [self.AccountSaveStatus.INIT, self.AccountSaveStatus.PROCESSING],
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ # ═══════════════════════════════════════════════════════════════
|
|
|
|
|
+ # demand_search_accounts 表
|
|
|
|
|
+ # ═══════════════════════════════════════════════════════════════
|
|
|
|
|
+
|
|
|
|
|
+ async def find_by_biz(self, biz: str) -> Optional[Dict]:
|
|
|
|
|
+ """按 biz 查账号是否已存在"""
|
|
|
|
|
+ return await self.pool.fetch_one(
|
|
|
|
|
+ query=f"SELECT * FROM {self.ACCOUNT_TABLE} WHERE biz = %s LIMIT 1",
|
|
|
|
|
+ params=(biz,),
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ async def insert_account(self, account: Dict) -> int:
|
|
|
|
|
+ """写入账号,channel_account_id 冲突则跳过"""
|
|
|
|
|
+ query = f"""
|
|
|
|
|
+ INSERT IGNORE INTO {self.ACCOUNT_TABLE}
|
|
|
|
|
+ (channel_account_id, gh_id, account_name, biz, biz_info, description)
|
|
|
|
|
+ VALUES (%s, %s, %s, %s, %s, %s)
|
|
|
|
|
+ """
|
|
|
|
|
+ return await self.pool.save(
|
|
|
|
|
+ query=query,
|
|
|
|
|
+ params=(
|
|
|
|
|
+ account["channel_account_id"],
|
|
|
|
|
+ account["gh_id"],
|
|
|
|
|
+ account["account_name"],
|
|
|
|
|
+ account["biz"],
|
|
|
|
|
+ account["biz_info"],
|
|
|
|
|
+ account["description"],
|
|
|
|
|
+ ),
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ async def list_accounts_for_article_crawl(self, *, limit: int) -> List[Dict]:
|
|
|
|
|
+ """列出有 gh_id 的账号,用于文章抓取(crawl_deep_done=0 优先)"""
|
|
|
|
|
+ query = f"""
|
|
|
|
|
+ SELECT channel_account_id, gh_id, crawl_cursor, crawl_deep_done
|
|
|
|
|
+ FROM {self.ACCOUNT_TABLE}
|
|
|
|
|
+ WHERE gh_id IS NOT NULL AND gh_id != ''
|
|
|
|
|
+ ORDER BY crawl_deep_done ASC, gh_id ASC
|
|
|
|
|
+ LIMIT %s
|
|
|
|
|
+ """
|
|
|
|
|
+ return await self.pool.fetch(query=query, params=(limit,))
|
|
|
|
|
+
|
|
|
|
|
+ async def update_crawl_status(
|
|
|
|
|
+ self,
|
|
|
|
|
+ gh_id: str,
|
|
|
|
|
+ *,
|
|
|
|
|
+ cursor: str = "",
|
|
|
|
|
+ deep_done: int = 0,
|
|
|
|
|
+ ) -> int:
|
|
|
|
|
+ """更新账号的爬取游标和深翻状态"""
|
|
|
|
|
+ query = f"""
|
|
|
|
|
+ UPDATE {self.ACCOUNT_TABLE}
|
|
|
|
|
+ SET crawl_cursor = %s, crawl_deep_done = %s, last_crawl_at = NOW()
|
|
|
|
|
+ WHERE gh_id = %s
|
|
|
|
|
+ """
|
|
|
|
|
+ return await self.pool.save(query=query, params=(cursor, deep_done, gh_id))
|
|
|
|
|
+
|
|
|
|
|
+ # ═══════════════════════════════════════════════════════════════
|
|
|
|
|
+ # demand_account_articles 表
|
|
|
|
|
+ # ═══════════════════════════════════════════════════════════════
|
|
|
|
|
+
|
|
|
|
|
+ async def insert_articles_batch(self, articles: List[Dict]) -> int:
|
|
|
|
|
+ """批量写入文章,wx_sn 冲突则跳过"""
|
|
|
|
|
+ if not articles:
|
|
|
|
|
+ return 0
|
|
|
|
|
+ query = f"""
|
|
|
|
|
+ INSERT IGNORE INTO {self.ARTICLES_TABLE}
|
|
|
|
|
+ (gh_id, app_msg_id, item_index, title, content_url, wx_sn,
|
|
|
|
|
+ digest, is_original, item_show_type, source_url, cover_img_url, show_desc,
|
|
|
|
|
+ show_view_count, show_like_count, show_pay_count, show_zs_count,
|
|
|
|
|
+ type, send_time, create_time, update_time, date_time, base_info)
|
|
|
|
|
+ VALUES
|
|
|
|
|
+ (%s, %s, %s, %s, %s, %s,
|
|
|
|
|
+ %s, %s, %s, %s, %s, %s,
|
|
|
|
|
+ %s, %s, %s, %s,
|
|
|
|
|
+ %s, %s, %s, %s, %s, %s)
|
|
|
|
|
+ """
|
|
|
|
|
+ params = [
|
|
|
|
|
+ (
|
|
|
|
|
+ a["gh_id"],
|
|
|
|
|
+ a["app_msg_id"],
|
|
|
|
|
+ a["item_index"],
|
|
|
|
|
+ a["title"],
|
|
|
|
|
+ a["content_url"],
|
|
|
|
|
+ a["wx_sn"],
|
|
|
|
|
+ a["digest"],
|
|
|
|
|
+ a["is_original"],
|
|
|
|
|
+ a["item_show_type"],
|
|
|
|
|
+ a["source_url"],
|
|
|
|
|
+ a["cover_img_url"],
|
|
|
|
|
+ a["show_desc"],
|
|
|
|
|
+ a["show_view_count"],
|
|
|
|
|
+ a["show_like_count"],
|
|
|
|
|
+ a["show_pay_count"],
|
|
|
|
|
+ a["show_zs_count"],
|
|
|
|
|
+ a["type"],
|
|
|
|
|
+ a["send_time"],
|
|
|
|
|
+ a["create_time"],
|
|
|
|
|
+ a["update_time"],
|
|
|
|
|
+ a["date_time"],
|
|
|
|
|
+ json.dumps(a["base_info"], ensure_ascii=False),
|
|
|
|
|
+ )
|
|
|
|
|
+ for a in articles
|
|
|
|
|
+ ]
|
|
|
|
|
+ return await self.pool.save(query=query, params=params, batch=True)
|
|
|
|
|
+
|
|
|
|
|
+ async def count_articles_by_gh_id(self, gh_id: str) -> int:
|
|
|
|
|
+ """统计某账号已抓文章数"""
|
|
|
|
|
+ row = await self.pool.fetch_one(
|
|
|
|
|
+ query=f"SELECT COUNT(*) AS cnt FROM {self.ARTICLES_TABLE} WHERE gh_id = %s",
|
|
|
|
|
+ params=(gh_id,),
|
|
|
|
|
+ )
|
|
|
|
|
+ return row["cnt"] if row else 0
|
|
|
|
|
+
|
|
|
|
|
+ # ═══════════════════════════════════════════════════════════════
|
|
|
|
|
+ # demand_account_articles 表 — 指标更新
|
|
|
|
|
+ # ═══════════════════════════════════════════════════════════════
|
|
|
|
|
+
|
|
|
|
|
+ async def list_accounts_for_article_update(self, *, limit: int) -> List[Dict]:
|
|
|
|
|
+ """列出已完成深翻的账号,按最久未更新排序,用于定期刷新文章指标"""
|
|
|
|
|
+ query = f"""
|
|
|
|
|
+ SELECT channel_account_id, gh_id, last_crawl_at
|
|
|
|
|
+ FROM {self.ACCOUNT_TABLE}
|
|
|
|
|
+ WHERE gh_id IS NOT NULL AND gh_id != '' AND crawl_deep_done = 1
|
|
|
|
|
+ ORDER BY last_crawl_at ASC
|
|
|
|
|
+ LIMIT %s
|
|
|
|
|
+ """
|
|
|
|
|
+ return await self.pool.fetch(query=query, params=(limit,))
|
|
|
|
|
+
|
|
|
|
|
+ async def update_article_metrics_batch(self, updates: List[Dict]) -> int:
|
|
|
|
|
+ """批量更新文章数值指标,按 wx_sn 匹配"""
|
|
|
|
|
+ if not updates:
|
|
|
|
|
+ return 0
|
|
|
|
|
+ query = f"""
|
|
|
|
|
+ UPDATE {self.ARTICLES_TABLE}
|
|
|
|
|
+ SET show_view_count = %s, show_like_count = %s, show_pay_count = %s,
|
|
|
|
|
+ show_zs_count = %s, show_desc = %s
|
|
|
|
|
+ WHERE wx_sn = %s
|
|
|
|
|
+ """
|
|
|
|
|
+ params = [
|
|
|
|
|
+ (
|
|
|
|
|
+ u["show_view_count"],
|
|
|
|
|
+ u["show_like_count"],
|
|
|
|
|
+ u["show_pay_count"],
|
|
|
|
|
+ u["show_zs_count"],
|
|
|
|
|
+ u["show_desc"],
|
|
|
|
|
+ u["wx_sn"],
|
|
|
|
|
+ )
|
|
|
|
|
+ for u in updates
|
|
|
|
|
+ ]
|
|
|
|
|
+ return await self.pool.save(query=query, params=params, batch=True)
|
|
|
|
|
+
|
|
|
|
|
+ async def touch_last_crawl_at(self, gh_id: str) -> int:
|
|
|
|
|
+ """更新 last_crawl_at = NOW(),不改变 cursor 和 deep_done 状态"""
|
|
|
|
|
+ query = f"""
|
|
|
|
|
+ UPDATE {self.ACCOUNT_TABLE}
|
|
|
|
|
+ SET last_crawl_at = NOW()
|
|
|
|
|
+ WHERE gh_id = %s
|
|
|
|
|
+ """
|
|
|
|
|
+ return await self.pool.save(query=query, params=(gh_id,))
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+__all__ = ["ArticleSearchRelationMapper", "ArticleDetailMapper", "AccountMapper"]
|