fix: 今日文章配额控制,避免全部虚拟用户集中互动同一篇
问题:今日只有1篇文章时,所有虚拟用户全部互动该文章,历史文章无人问津 修复方案(配额制): - 新增 count_today_articles():轻量统计今日广场文章数 - 配额规则:每篇今日文章最多吸引3个虚拟用户(可调) - 今日1篇 → 最多3人互动今日,其余全走历史 - 今日5篇 → 最多15人互动今日,其余走历史 - 今日10篇以上 → 批次内所有人均可互动今日文章 - get_news_list() 新增 force_history 参数,强制走 Phase 2 - 调度器在分发任务前计算配额,超出配额的用户透传 force_history=True 效果:新文章获得合理曝光,历史文章持续被互动,分布更自然
This commit is contained in:
@@ -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,8 +534,9 @@ 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
|
||||
if not valid:
|
||||
logger.info(f"[广场新闻] {user.account} 今日文章校验后全部无效,转历史")
|
||||
|
||||
# ── Phase 2: 无今日新文章 → 从最新(第1页)开始往旧顺序遍历 ────
|
||||
|
||||
@@ -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 再试一次
|
||||
|
||||
Reference in New Issue
Block a user