Files
MediaCrawler/tests/test_monitor_note_alias.py
T
butubb 20e672834c
Deploy VitePress site to Pages / build (push) Canceled after 0s
Deploy VitePress site to Pages / Deploy (push) Canceled after 0s
feat(monitor): 博主的粉丝数,以及给作品起备注
两件都是「作品栏里把这东西认出来」的延伸:

* **账号级指标**:作品列表只会说「这条涨了多少赞」,说不了「这个人整个
  账号的粉丝在涨还是在掉」。抖音的资料接口本来就有粉丝数/总获赞/作品数,
  每轮顺手记一条快照(`monitor_creator_stat`,粒度 = 任务×博主×轮次,
  和作品指标同形)。组头显示最近一条。

  快照在「一条作品都没采到」的早退**之前**落:作品列表被风控挡住的那一轮,
  正是「粉丝还在涨、但新作品没在发现」最该被看见的时刻。

* **作品备注**:博主备注回答「这个账号是谁」,这条回答「这条我要盯着」。
  一个博主底下常常只有一两件值得盯的作品,所以不能合并成一条。键取
  (platform, note_id),跨任务共用一份。

两边都守住同一条口径:**不知道就是 null,不写成 0** —— 0 在趋势图上是一条
砸到底的线,和「还没采到」是两回事。
2026-10-10 18:09:03 +08:00

147 lines
5.3 KiB
Python

# -*- coding: utf-8 -*-
"""作品备注 —— 一个博主底下,哪几条是真正要盯的。
和博主备注(test_monitor_creators.py)是一对,但回答的不是同一个问题:博主备注回答
「这个账号是谁」,作品备注回答「这条作品我要盯着」。一个博主底下常常只有一两件值得
盯的作品,所以不能合成一条。
"""
import httpx
import pytest
import pytest_asyncio
from api.main import app
from api.monitor import db as monitor_db
from api.monitor.models import MODE_CREATOR, MonitorNote, MonitorTask
NOTE_ID = "note-a"
TITLE = "中秋哪儿都堵"
@pytest_asyncio.fixture
async def client(tmp_path):
monitor_db.set_sqlite_path(tmp_path / "monitor.db")
await monitor_db.init_db()
transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as http_client:
yield http_client
await monitor_db.dispose_engine()
async def _seed(platform: str = "xhs", task_name: str = "任务", note_id: str = NOTE_ID) -> int:
async with monitor_db.get_session() as session:
task = MonitorTask(
name=task_name, platform=platform, mode=MODE_CREATOR, enabled=True,
interval_minutes=60, max_notes_count=20, enable_comments=False,
max_comments_count=50, run_timeout_seconds=3600,
notify_enabled=False, created_at=0, updated_at=0,
)
session.add(task)
await session.flush()
session.add(
MonitorNote(
task_id=task.id, note_id=note_id, title=TITLE,
note_url="", cover="", creator_hash="hash-a",
creator_name="张三", source_kind="", published_at=None,
first_seen_run_id=1, first_seen_at=0,
last_seen_run_id=1, last_seen_at=0,
)
)
return task.id
async def _notes(client, platform: str = "xhs"):
return (await client.get("/api/monitor/notes", params={"platform": platform})).json()["notes"]
async def _set_alias(client, alias: str, platform: str = "xhs", note_id: str = NOTE_ID):
return await client.put(
f"/api/monitor/notes/{note_id}",
params={"platform": platform},
json={"alias": alias},
)
class TestNoteAlias:
@pytest.mark.asyncio
async def test_notes_start_without_a_remark(self, client):
await _seed()
assert (await _notes(client))[0]["note_alias"] == ""
@pytest.mark.asyncio
async def test_a_remark_comes_back_with_the_notes(self, client):
await _seed()
response = await _set_alias(client, "重点")
assert response.status_code == 200
note = (await _notes(client))[0]
assert note["note_alias"] == "重点"
# 备注是**叠加**在标题之上的,不是替换 —— 标题仍然是这条作品本身。
assert note["title"] == TITLE
@pytest.mark.asyncio
async def test_a_remark_is_shared_across_tasks(self, client):
"""同一件作品被两个任务都监控时,备注只该填一次。"""
await _seed(task_name="任务甲")
await _seed(task_name="任务乙")
await _set_alias(client, "重点")
for note in await _notes(client):
assert note["note_alias"] == "重点"
@pytest.mark.asyncio
async def test_a_remark_does_not_leak_to_another_platform(self, client):
"""作品的 id 是平台各自的编号体系 —— 抖音的 123 和小红书的 123 是两条作品。"""
await _seed(platform="xhs")
await _seed(platform="dy")
await _set_alias(client, "小红书那边的", platform="xhs")
assert (await _notes(client, "xhs"))[0]["note_alias"] == "小红书那边的"
assert (await _notes(client, "dy"))[0]["note_alias"] == ""
@pytest.mark.asyncio
async def test_a_remark_does_not_leak_to_another_work(self, client):
"""钉住这里的**作用域**:键是 note_id。写错成按任务存的话,给一条起了备注,
同一个博主底下的其它作品会跟着一起变 —— 那这个功能就没用了。"""
await _seed(note_id="note-a")
await _seed(task_name="另一个任务", note_id="note-b")
await _set_alias(client, "重点", note_id="note-a")
by_id = {note["note_id"]: note["note_alias"] for note in await _notes(client)}
assert by_id == {"note-a": "重点", "note-b": ""}
@pytest.mark.asyncio
async def test_an_empty_remark_clears_it(self, client):
await _seed()
await _set_alias(client, "重点")
await _set_alias(client, "")
assert (await _notes(client))[0]["note_alias"] == ""
@pytest.mark.asyncio
async def test_a_remark_is_trimmed(self, client):
await _seed()
await _set_alias(client, " 重点 ")
assert (await _notes(client))[0]["note_alias"] == "重点"
@pytest.mark.asyncio
async def test_the_two_kinds_of_remark_stay_apart(self, client):
"""博主备注和作品备注是两张表、两个键 —— 一个不该把另一个盖掉。"""
await _seed()
await _set_alias(client, "重点")
await client.put(
"/api/monitor/creators/hash-a", params={"platform": "xhs"}, json={"alias": "竞品A"}
)
note = (await _notes(client))[0]
assert note["note_alias"] == "重点"
assert note["creator_alias"] == "竞品A"