feat(monitor): 作品标签 —— 词表在设置里维护,作品上贴
和「作品备注」刻意并存,不是一回事:备注是一句话的自由文字,标签是**从一份固定
词表里选的分类**。分类必须封闭,否则「重点 / 重要 / 优先」各写各的,筛就没法用了。
* `monitor_tag`:词表,全局一套(不按平台分)—— 「重点」是给人自己用的心智,不该在
小红书和抖音各定义一遍。颜色存的是**调色板里的名字**,不是色值:Tailwind 的类名是
静态提取的,拼出来的 `text-${color}` 它看不见,线上会静默变无色。
* `monitor_note_tag`:作品↔标签,**多对多**。一条作品可以既是重点又是竞品;只能贴一个
的话人就会跑去备注里写自由文字,这份词表就白建了。全部替换式提交,不是逐个增删。
* 删除标签时**显式删关联**,不靠外键级联 —— 测试跑 SQLite,它默认不开外键约束。
* 组头上的筛选做成平铺开关片,多选是「或」。
顺带:/monitor/notes 现在支持按 tag_id 筛。
This commit is contained in:
+60
-1
@@ -40,6 +40,8 @@ from ..schemas.monitor import (
|
||||
CookiePayload,
|
||||
CreatorAliasPayload,
|
||||
NoteAliasPayload,
|
||||
NoteTagsPayload,
|
||||
TagPayload,
|
||||
MonitorTaskCreate,
|
||||
MonitorTaskUpdate,
|
||||
WebhookPayload,
|
||||
@@ -130,13 +132,16 @@ async def list_notes(
|
||||
only_new: bool = False,
|
||||
limit: int = Query(default=200, ge=1, le=2000),
|
||||
platform: Optional[str] = None,
|
||||
tag_id: Optional[List[int]] = Query(
|
||||
default=None, description="可重复传,命中其中任意一个标签即算"
|
||||
),
|
||||
):
|
||||
async with get_session() as session:
|
||||
return {
|
||||
# windowed=True:作品栏是「正在盯的窗口」,每个博主只画最新 max_notes_count 条。
|
||||
# 库里的全量还在,趋势图/报表/导出读的都是它。
|
||||
"notes": await service.list_notes(
|
||||
session, task_id, only_new, limit, platform, windowed=True
|
||||
session, task_id, only_new, limit, platform, windowed=True, tag_ids=tag_id
|
||||
),
|
||||
# 博主**单独给一份**,而不是让前端从作品里推。作品推不出「一条作品都没有的
|
||||
# 博主」—— 那正是最该显示的一类(还在涨粉,只是最近没发)。
|
||||
@@ -299,6 +304,22 @@ async def set_note_alias_endpoint(
|
||||
return {"note_id": note_id, "alias": payload.alias.strip()}
|
||||
|
||||
|
||||
@router.put("/notes/{note_id}/tags")
|
||||
async def set_note_tags_endpoint(
|
||||
note_id: str,
|
||||
payload: NoteTagsPayload,
|
||||
platform: str = Query(default=PLATFORM_XHS),
|
||||
):
|
||||
"""把一条作品的标签**整体替换**成这一组(空列表 = 全摘掉)。
|
||||
|
||||
传的是「这条作品现在该有哪些标签」,而不是「加上/去掉哪一个」—— 界面上是一个
|
||||
复选框对话框,点保存就是一次全量提交。
|
||||
"""
|
||||
async with get_session() as session:
|
||||
tags = await service.set_note_tags(session, platform, note_id, payload.tag_ids)
|
||||
return {"note_id": note_id, "tags": tags}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# QR login
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -416,6 +437,44 @@ async def _resolve_scope(
|
||||
return [task for task in task_ids if task in platform_ids]
|
||||
|
||||
|
||||
@router.get("/tags")
|
||||
async def list_tags():
|
||||
"""标签词表 + 各自被用在多少条作品上(删之前能看清代价)。"""
|
||||
async with get_session() as session:
|
||||
return {"tags": await service.list_tags(session)}
|
||||
|
||||
|
||||
@router.post("/tags")
|
||||
async def create_tag(payload: TagPayload):
|
||||
async with get_session() as session:
|
||||
try:
|
||||
tag = await service.create_tag(session, payload.name, payload.color)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
return {"id": tag.id, "name": tag.name, "color": tag.color, "note_count": 0}
|
||||
|
||||
|
||||
@router.put("/tags/{tag_id}")
|
||||
async def update_tag(tag_id: int, payload: TagPayload):
|
||||
async with get_session() as session:
|
||||
try:
|
||||
tag = await service.update_tag(session, tag_id, payload.name, payload.color)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc))
|
||||
return {"id": tag.id, "name": tag.name, "color": tag.color}
|
||||
|
||||
|
||||
@router.delete("/tags/{tag_id}")
|
||||
async def delete_tag(tag_id: int):
|
||||
"""删标签,连同它在所有作品上的贴纸。"""
|
||||
async with get_session() as session:
|
||||
try:
|
||||
removed = await service.delete_tag(session, tag_id)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc))
|
||||
return {"removed": removed}
|
||||
|
||||
|
||||
@router.get("/export")
|
||||
async def export_data(
|
||||
kind: str = Query(..., description="notes | comments | report"),
|
||||
|
||||
Reference in New Issue
Block a user