From c944fbb0ea1e797f5214553aa4c5a9f022a0f1ab Mon Sep 17 00:00:00 2001 From: stefanfeng Date: Wed, 8 Apr 2026 11:47:36 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BB=8A=E6=97=A5=E6=96=87=E7=AB=A0?= =?UTF-8?q?=E9=85=8D=E9=A2=9D=E6=8E=A7=E5=88=B6=EF=BC=8C=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E5=85=A8=E9=83=A8=E8=99=9A=E6=8B=9F=E7=94=A8=E6=88=B7=E9=9B=86?= =?UTF-8?q?=E4=B8=AD=E4=BA=92=E5=8A=A8=E5=90=8C=E4=B8=80=E7=AF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:今日只有1篇文章时,所有虚拟用户全部互动该文章,历史文章无人问津 修复方案(配额制): - 新增 count_today_articles():轻量统计今日广场文章数 - 配额规则:每篇今日文章最多吸引3个虚拟用户(可调) - 今日1篇 → 最多3人互动今日,其余全走历史 - 今日5篇 → 最多15人互动今日,其余走历史 - 今日10篇以上 → 批次内所有人均可互动今日文章 - get_news_list() 新增 force_history 参数,强制走 Phase 2 - 调度器在分发任务前计算配额,超出配额的用户透传 force_history=True 效果:新文章获得合理曝光,历史文章持续被互动,分布更自然 --- backend/app/services/news_service.py | 51 ++++++++++++++++++++++++++-- backend/app/services/scheduler.py | 25 +++++++++++--- 2 files changed, 68 insertions(+), 8 deletions(-) diff --git a/backend/app/services/news_service.py b/backend/app/services/news_service.py index 0c216f1..bba85b0 100755 --- a/backend/app/services/news_service.py +++ b/backend/app/services/news_service.py @@ -330,6 +330,50 @@ class NewsPlatformService: return False # ─── 新闻列表 ────────────────────────────────────────────── + async def count_today_articles(self, db, user) -> int: + """ + 获取今日广场新文章数量(用于调度器计算配额)。 + 使用轻量请求:只取第1页,统计今日文章数。 + 无会话时返回 0。 + """ + if user is None: + return 0 + from datetime import datetime as _dt + sess = await get_session(user.id) + if not sess: + return 0 + biz = await self._biz_url(db) + cfg = await self._client(db) + token = sess["token"] + try: + params = self._build_form({"pageNum": 1, "pageSize": 50, "type": "1"}, cfg) + async with httpx.AsyncClient(timeout=8) as c: + r = await c.get( + f"{biz}/business/square/list", + headers=self._bearer(token), + params=params, + ) + if r.status_code != 200: + return 0 + d = r.json() + if d.get("code") not in [0, 200]: + return 0 + nd = d.get("data", {}) + items = nd.get("data") or nd.get("list") or nd.get("records") or [] + today = 0 + for a in items: + t = a.get("publishTime") or a.get("createTime") or "" + if not t: + continue + try: + if _dt.strptime(t[:10], "%Y-%m-%d").date() == _dt.now().date(): + today += 1 + except Exception: + pass + return today + except Exception: + return 0 + async def validate_article(self, db, user, article_id: str) -> bool: """ 验证文章是否可用: @@ -376,7 +420,7 @@ class NewsPlatformService: logger.warning(f"[文章校验] {article_id} 请求异常: {e}") return False - async def get_news_list(self, db, user, count=5, interest_tags=None) -> list: + async def get_news_list(self, db, user, count=5, interest_tags=None, force_history=False) -> list: """ 获取文章列表,优先返回今日新发布的文章(从新到旧轮询), 无今日新文章时才随机翻历史页。 @@ -490,9 +534,10 @@ class NewsPlatformService: aid = str(a.get("recordId") or a.get("id", "")) if await self.validate_article(db, user, aid): valid.append(a) - if valid: + if valid and not force_history: return valid - logger.info(f"[广场新闻] {user.account} 今日文章校验后全部无效,转历史") + if not valid: + logger.info(f"[广场新闻] {user.account} 今日文章校验后全部无效,转历史") # ── Phase 2: 无今日新文章 → 从最新(第1页)开始往旧顺序遍历 ──── # 规则:始终从第1页(最新)开始,按页顺序 1→2→3...往旧方向走 diff --git a/backend/app/services/scheduler.py b/backend/app/services/scheduler.py index d083a45..24bc5f6 100755 --- a/backend/app/services/scheduler.py +++ b/backend/app/services/scheduler.py @@ -171,13 +171,28 @@ class SchedulerService: random.shuffle(rest_users) selected = priority_users + rest_users[:max(0, batch_size - priority_size)] + # ── 今日文章配额计算 ────────────────────────────────────── + # 获取今日文章数量,决定本轮有多少用户应互动今日文章 + today_count = 0 + try: + from app.services.news_service import news_service as _ns + today_count = await _ns.count_today_articles(db, selected[0] if selected else None) + except Exception: + pass + + # 配额规则:每篇今日文章最多吸引 3 个虚拟用户,超出部分走历史 + today_quota = min(today_count * 3, len(selected)) + logger.info( f"[调度] 共 {len(all_users)} 个用户,{len(eligible)} 个满足间隔," - f"本轮选取 {len(selected)} 个执行互动" + f"本轮选取 {len(selected)} 个,今日文章 {today_count} 篇," + f"配额 {today_quota} 人互动今日/{len(selected)-today_quota} 人走历史" ) - for user in selected: - asyncio.create_task(self._execute_user_interaction(user.id)) + for i, user in enumerate(selected): + # 超出今日配额的用户强制走历史文章 + force_history = (i >= today_quota) + asyncio.create_task(self._execute_user_interaction(user.id, force_history=force_history)) async def _try_login_users(self, db): """尝试登录未登录的用户""" @@ -196,7 +211,7 @@ class SchedulerService: except Exception as e: logger.error(f"自动登录失败 {user.account}: {e}") - async def _execute_user_interaction(self, user_id: int): + async def _execute_user_interaction(self, user_id: int, force_history: bool = False): """执行单用户互动 - 基于真实接口""" from app.services.news_service import news_service from app.services.ai_service import ai_service @@ -224,7 +239,7 @@ class SchedulerService: # 获取新闻列表(基于接口 GET /news/list) articles = await news_service.get_news_list( - db, user, count=5, interest_tags=interest_tags + db, user, count=5, interest_tags=interest_tags, force_history=force_history ) if not articles: # 尝试从 session 获取 org_id 再试一次