两件都是「作品栏里把这东西认出来」的延伸: * **账号级指标**:作品列表只会说「这条涨了多少赞」,说不了「这个人整个 账号的粉丝在涨还是在掉」。抖音的资料接口本来就有粉丝数/总获赞/作品数, 每轮顺手记一条快照(`monitor_creator_stat`,粒度 = 任务×博主×轮次, 和作品指标同形)。组头显示最近一条。 快照在「一条作品都没采到」的早退**之前**落:作品列表被风控挡住的那一轮, 正是「粉丝还在涨、但新作品没在发现」最该被看见的时刻。 * **作品备注**:博主备注回答「这个账号是谁」,这条回答「这条我要盯着」。 一个博主底下常常只有一两件值得盯的作品,所以不能合并成一条。键取 (platform, note_id),跨任务共用一份。 两边都守住同一条口径:**不知道就是 null,不写成 0** —— 0 在趋势图上是一条 砸到底的线,和「还没采到」是两回事。
147 lines
5.3 KiB
Python
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"
|