fix(monitor): 没有作品的博主在作品栏里也要看得见
分组是从作品推出来的(按作品的 creator_hash 归组),于是没有作品的博主根本 不进列表:目标加了、资料采到了、粉丝数就躺在库里,界面上什么都看不见。 而这类博主恰恰是最该看见的 —— 还在涨粉,只是最近没发东西。藏起来正好藏反了。 线上就有一个:5 个目标里 3 个没作品,那 3 个连同已采到的粉丝数一起消失了。 改成 **账号快照 ∪ 作品** 两个来源: * 有快照没作品 → 一个 0 篇的组,备注和粉丝数照常显示,组里写「暂无作品」; * 有作品没快照 → 一个没有账号指标的组(小红书那条路不产生快照)。 /notes 因此多返回一份 `creators`,而不是让前端从作品里推 —— 作品推不出上面 第一类人。组头改读它,`MonitorNote` 上那几个字段降级成「顺着作品问作者」用。
This commit is contained in:
+116
-14
@@ -431,30 +431,31 @@ async def set_note_alias(
|
||||
|
||||
async def _latest_creator_stats(
|
||||
session: AsyncSession,
|
||||
task_ids: Sequence[int],
|
||||
creator_hashes: Sequence[str],
|
||||
task_ids: Optional[Sequence[int]] = None,
|
||||
creator_hashes: Optional[Sequence[str]] = None,
|
||||
) -> Dict[tuple, "MonitorCreatorStat"]:
|
||||
"""``(task_id, creator_hash) -> 最近一条``账号级快照。
|
||||
|
||||
两个参数都是**可选过滤**:传 None 就是不限。``list_notes`` 两个都给(只要手里
|
||||
这批作品涉及的博主),``list_creators`` 只给任务(要的是全部博主,包括一条作品
|
||||
都没有的那些)。
|
||||
|
||||
按 run_id 而不是 captured_at 取「最近」:和作品指标用的是同一个口径,两者放一起
|
||||
看才不会出现「作品数据来自第 8 轮、粉丝数来自第 9 轮」这种对不上的情况。
|
||||
"""
|
||||
if not task_ids or not creator_hashes:
|
||||
if task_ids is not None and not task_ids:
|
||||
return {}
|
||||
if creator_hashes is not None and not creator_hashes:
|
||||
return {}
|
||||
|
||||
rows = (
|
||||
await session.scalars(
|
||||
select(MonitorCreatorStat)
|
||||
.where(
|
||||
MonitorCreatorStat.task_id.in_(list(task_ids)),
|
||||
MonitorCreatorStat.creator_hash.in_(list(creator_hashes)),
|
||||
)
|
||||
.order_by(MonitorCreatorStat.run_id.desc())
|
||||
)
|
||||
).all()
|
||||
query = select(MonitorCreatorStat).order_by(MonitorCreatorStat.run_id.desc())
|
||||
if task_ids is not None:
|
||||
query = query.where(MonitorCreatorStat.task_id.in_(list(task_ids)))
|
||||
if creator_hashes is not None:
|
||||
query = query.where(MonitorCreatorStat.creator_hash.in_(list(creator_hashes)))
|
||||
|
||||
latest: Dict[tuple, MonitorCreatorStat] = {}
|
||||
for row in rows:
|
||||
for row in (await session.scalars(query)).all():
|
||||
latest.setdefault((row.task_id, row.creator_hash), row) # 已按 run_id 倒序
|
||||
return latest
|
||||
|
||||
@@ -604,6 +605,107 @@ async def list_notes(
|
||||
return result
|
||||
|
||||
|
||||
async def list_creators(
|
||||
session: AsyncSession,
|
||||
task_id: Optional[int] = None,
|
||||
platform: Optional[str] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""作品栏里要显示的**博主** —— **包括一条作品都没有的**。
|
||||
|
||||
分组原先是从作品推出来的(按作品的 creator_hash 归组),于是没有作品的博主根本
|
||||
不会出现在列表里:目标加了、资料也采到了、粉丝数就躺在库里,界面上什么都看不见。
|
||||
而「这个号在涨粉、只是最近没发作品」恰恰是最该看见的一种情况 —— 藏起来正好藏反了。
|
||||
|
||||
所以来源换成 **账号快照 ∪ 作品**:
|
||||
|
||||
* 有快照没作品 → 一个 0 篇的组,粉丝数照常显示;
|
||||
* 有作品没快照 → 一个没有账号指标的组(小红书那条路不产生快照,就是这种情况)。
|
||||
|
||||
``creator_alias`` 从作品备注那张表来;``note_count`` / ``last_activity_at`` 用来
|
||||
排序,让最近还在动的博主排在前面。
|
||||
"""
|
||||
scope: Optional[List[int]] = None
|
||||
if task_id is not None:
|
||||
scope = [task_id]
|
||||
elif platform is not None:
|
||||
scope = await platform_task_ids(session, platform)
|
||||
if not scope:
|
||||
return []
|
||||
|
||||
# 作品一侧:谁有作品、有几篇、最后一次是什么时候。
|
||||
work_query = (
|
||||
select(
|
||||
MonitorNote.task_id,
|
||||
MonitorNote.creator_hash,
|
||||
func.count().label("note_count"),
|
||||
func.max(MonitorNote.last_seen_at).label("last_seen_at"),
|
||||
func.max(MonitorNote.creator_name).label("creator_name"),
|
||||
)
|
||||
.where(MonitorNote.creator_hash != "")
|
||||
.group_by(MonitorNote.task_id, MonitorNote.creator_hash)
|
||||
)
|
||||
if scope is not None:
|
||||
work_query = work_query.where(MonitorNote.task_id.in_(scope))
|
||||
|
||||
work: Dict[tuple, Dict[str, Any]] = {}
|
||||
for row in (await session.execute(work_query)).all():
|
||||
work[(row.task_id, row.creator_hash)] = {
|
||||
"note_count": row.note_count,
|
||||
"last_seen_at": row.last_seen_at,
|
||||
"creator_name": row.creator_name or "",
|
||||
}
|
||||
|
||||
stats = await _latest_creator_stats(session, task_ids=scope)
|
||||
if not work and not stats:
|
||||
return []
|
||||
|
||||
# 备注是按 (platform, creator_hash) 存的,所以要知道每个博主属于哪个平台。
|
||||
involved = {key[0] for key in set(work) | set(stats)}
|
||||
task_platform = {
|
||||
row.id: row.platform
|
||||
for row in (
|
||||
await session.execute(
|
||||
select(MonitorTask.id, MonitorTask.platform).where(
|
||||
MonitorTask.id.in_(list(involved))
|
||||
)
|
||||
)
|
||||
).all()
|
||||
}
|
||||
aliases = await _creator_alias_map(session)
|
||||
|
||||
result: List[Dict[str, Any]] = []
|
||||
for key in set(work) | set(stats):
|
||||
row_task, creator_hash = key
|
||||
work_row = work.get(key)
|
||||
stat = stats.get(key)
|
||||
result.append(
|
||||
{
|
||||
"task_id": row_task,
|
||||
"creator_hash": creator_hash,
|
||||
# 昵称优先取作品的(那是界面上本来就在用的),快照的兜底 —— 没有作品
|
||||
# 的博主只剩快照这一个来源。
|
||||
"creator_name": (work_row or {}).get("creator_name")
|
||||
or (stat.nickname if stat else ""),
|
||||
"creator_alias": aliases.get(
|
||||
(task_platform.get(row_task, ""), creator_hash), ""
|
||||
),
|
||||
"note_count": (work_row or {}).get("note_count", 0),
|
||||
"creator_fans": stat.fans if stat else None,
|
||||
"creator_total_favorited": stat.total_favorited if stat else None,
|
||||
"creator_works": stat.works_count if stat else None,
|
||||
"creator_stats_at": stat.captured_at if stat else None,
|
||||
# 排序用:作品最近出现的时间,或者账号指标的采集时间,取晚的那个。
|
||||
"last_activity_at": max(
|
||||
(work_row or {}).get("last_seen_at") or 0,
|
||||
stat.captured_at if stat else 0,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
result.sort(key=lambda row: row["last_activity_at"], reverse=True)
|
||||
return result
|
||||
|
||||
|
||||
async def note_series(session: AsyncSession, note_id: str, task_id: Optional[int] = None) -> List[Dict[str, Any]]:
|
||||
"""Metric time series for one note."""
|
||||
query = (
|
||||
|
||||
@@ -133,7 +133,10 @@ async def list_notes(
|
||||
):
|
||||
async with get_session() as session:
|
||||
return {
|
||||
"notes": await service.list_notes(session, task_id, only_new, limit, platform)
|
||||
"notes": await service.list_notes(session, task_id, only_new, limit, platform),
|
||||
# 博主**单独给一份**,而不是让前端从作品里推。作品推不出「一条作品都没有的
|
||||
# 博主」—— 那正是最该显示的一类(还在涨粉,只是最近没发)。
|
||||
"creators": await service.list_creators(session, task_id, platform),
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user