fix(monitor): 作品栏只显示每个博主最新的 N 条(库里一条不删)
Deploy VitePress site to Pages / build (push) Canceled after 0s
Deploy VitePress site to Pages / Deploy (push) Canceled after 0s

原来 max_notes_count **只**被当作每轮拉取的条数,落库时从不删任何东西,列表也不按
博主截断。于是超出的作品会一直堆着(21、22、23…),而掉出采集窗口的那些**再也采不到**
—— 指标冻在最后一次,看上去却和正在跟踪的作品一模一样。界面上那句「仅显示每个博主
最新的 N 条作品」是假的,后端根本没做这个截断。

现在界面按窗口显示:库是账本,界面是窗口。趋势图、报表、**导出**读的仍然是全量 ——
导出是数据,少几行就是在丢东西,所以 `windowed` 默认关,只有 /monitor/notes 打开。

窗口按**发布时间**排序,不是「最后一次见到」:降级路径(作品列表被风控挡住)会把所有
已知作品都刷一遍,那样每个人的 last_seen_at 都一样,按它开窗等于没开。解析不出日期的
作品**永不隐藏** —— 因为读不出日期就让一条作品消失,比多显示一条糟得多。

只对博主模式开窗;笔记模式的目标本身就是一件作品。
This commit is contained in:
2026-10-11 08:30:10 +08:00
parent dbfbf32225
commit d52b6942ac
3 changed files with 264 additions and 3 deletions
+69 -2
View File
@@ -460,15 +460,75 @@ async def _latest_creator_stats(
return latest
def _window_order(note: MonitorNote) -> tuple:
"""开窗排序:按**发布时间**倒序。
采集端 ``aweme/post`` 拿的就是最新 N 条,界面的窗口必须和它口径一致 —— 否则会出现
「显示着的这条其实已经采不到了」。
**时间不知道的排最前(永不隐藏)**:因为解析不出日期就让一条作品从列表里消失,比
多显示一条糟得多。
"""
return (note.published_at is None, note.published_at or 0)
async def _within_creator_window(
session: AsyncSession, notes: List[MonitorNote]
) -> List[MonitorNote]:
"""每个博主只留**最新 N 条**,N 取任务上的 ``max_notes_count``。
**库里一条都不删。** 作品栏是「我正在盯的窗口」,库是账本:掉出窗口的作品采集端就
再也取不到了(它取的是最新 N 条),指标会冻在最后一次而**它看上去和正在跟踪的
作品一模一样** —— 那才是真正会骗人的地方。趋势图、报表、导出读的仍然是全量。
只对**博主模式**开窗:笔记模式的 ``max_notes_count`` 管的是「每条目标拉多少」,
而它的目标本身就是一件作品,不存在「一个博主的第 21 条」。
"""
task_ids = {note.task_id for note in notes}
tasks = {
row.id: row
for row in (
await session.scalars(
select(MonitorTask).where(MonitorTask.id.in_(list(task_ids)))
)
).all()
}
by_creator: Dict[tuple, List[MonitorNote]] = {}
for note in notes:
by_creator.setdefault((note.task_id, note.creator_hash), []).append(note)
kept: List[MonitorNote] = []
for (note_task_id, _creator_hash), group in by_creator.items():
task = tasks.get(note_task_id)
if task is None or task.mode != MODE_CREATOR:
kept.extend(group)
continue
group.sort(key=_window_order, reverse=True)
kept.extend(group[: task.max_notes_count])
return kept
async def list_notes(
session: AsyncSession,
task_id: Optional[int] = None,
only_new: bool = False,
limit: int = 200,
platform: Optional[str] = None,
*,
windowed: bool = False,
) -> List[Dict[str, Any]]:
"""Tracked notes with their latest metrics and change vs the previous run."""
query = select(MonitorNote).order_by(MonitorNote.last_seen_at.desc()).limit(limit)
"""Tracked notes with their latest metrics and change vs the previous run.
``windowed`` 打开后只返回每个博主最新 ``max_notes_count`` 条(见
``_within_creator_window``)。**默认关闭**:导出要的是全量账本,而默认开启的话,
忘了传这个参数的那条路会静默少数据。
"""
query = select(MonitorNote).order_by(MonitorNote.last_seen_at.desc())
# 开窗时**不能**在这里 limit:得先把每个博主的窗开出来,顺序反了的话,全局上限会把
# 某个博主最新的那几条挤掉,剩下的还都是别人的。
if not windowed:
query = query.limit(limit)
if task_id is not None:
query = query.where(MonitorNote.task_id == task_id)
if platform is not None:
@@ -481,6 +541,13 @@ async def list_notes(
if not notes:
return []
if windowed:
notes = await _within_creator_window(session, notes)
# 开完窗再截全局上限。窗口内的作品本来就不多,这里的 200 只是兜底。
notes = notes[:limit]
if not notes:
return []
note_ids = [note.note_id for note in notes]
# Fetch every snapshot for these notes in one go and gather the two most