Files
MediaCrawler/docs/db-schema.md
T
butubb c83669087b
Deploy VitePress site to Pages / build (push) Canceled after 0s
Deploy VitePress site to Pages / Deploy (push) Canceled after 0s
docs: 监控层表结构说明(带字段备注)
13 张表逐字段写清楚类型、可空、含义,外加四条必须先知道的约定:
毫秒时间戳、NULL 不是 0、平台 id 是 xhs/dy 而抖音落盘目录叫 douyin、作品从不删除。

和 db-schema.sql(原始 DDL)配套:那份给机器看,这份给人看。
2026-10-11 09:21:01 +08:00

18 KiB
Raw Blame History

监控层数据库表结构

api/monitor/** 这一层用到的全部表。库名 mediacrawler_prod(MySQL 5.7,utf8mb4)。

配套的原始 DDL 在 db-schema.sql —— 那份是 SHOW CREATE TABLE 直接导的, 含引擎、字符集和索引定义;本文是带字段说明的可读版。

这两份都不是迁移脚本。 建表和补列是 api/monitor/db.py::init_db() 在每次启动时按 ORM 元数据自动做的:create_all 建缺失的表,_ensure_columns 对已存在的表逐列 ALTER TABLE ADD COLUMN(新增 NOT NULL 列会带上默认值给老行兜底)。所以加一个字段只需要改 models.py,不用手写 SQL。


读之前要知道的四条约定

  1. 时间戳一律是毫秒(bigint),由 tools/time_util.get_current_timestamp() 产生。 平台给秒的(抖音是秒)在 adapters.PlatformAdapter.time_scale 里换算 —— 差 1000 倍, 不换算的话 2026 年的作品会显示成 1970 年。
  2. NULL 和 0 是两回事。 指标、粉丝数这些「读不出来」的值一律存 NULL,不存 0: 0 在趋势图上是一条砸到底的线,和「不知道」完全不同。界面上 NULL 显示成「—」。
  3. 平台 id 是 xhs / dy(api/monitor/adapters.py)。注意 dy 是监控层的 id, 爬虫落盘目录叫 douyin —— 两者不一致,靠 adapters.artifact_dir() 换算。
  4. 作品删除是从来不做的。 掉出采集窗口的作品仍然留在库里(趋势图、报表、导出要用), 只是界面不再显示。

表之间的关系

monitor_task ─┬─< monitor_target        一个任务配若干个目标(博主 / 作品)
              ├─< monitor_run           每次运行一条
              ├─< monitor_note ──< monitor_note_metric
              └─< monitor_creator_stat

monitor_note ──┬── monitor_note_alias   (按 platform + note_id 关联,不是外键)
               └── monitor_note_tag ──< monitor_tag

monitor_creator_alias                   (按 platform + creator_hash 关联)
monitor_event                           事件流,多数挂在 task / run 上
monitor_setting                         键值设置,独立

有真正外键(ON DELETE CASCADE)的只有 task_id:monitor_target / monitor_run / monitor_note / monitor_creator_stat 都指向 monitor_task,删任务会一并带走。

其余关联刻意不用外键:它们跨的是「平台 + 业务 id」而不是主键,而且备注/标签是人为 维护的,不该跟着采集数据被级联掉。


monitor_task — 监控任务

字段 类型 空 说明
id int 否 主键
name varchar(200) 否 任务名,人起的
platform varchar(32) 否 xhs / dy
mode varchar(16) 否 creator 博主模式 / note 作品模式
enabled tinyint(1) 否 是否参与定时调度
interval_minutes int 否 间隔模式的周期(分钟)
max_notes_count int 否 每个博主每轮采多少条。作品栏也按它开窗(只画最新 N 条),但库里不删
enable_comments tinyint(1) 否 是否采评论
max_comments_count int 否 每条作品最多采多少条评论
run_timeout_seconds int 否 一轮的墙钟上限,超了判 timeout
notify_enabled tinyint(1) 否 推送「有新作品」。默认关——可能每轮都有,会刷屏
notify_failures tinyint(1) 否 推送「出异常了」(登录失效 / 运行失败 / 没抓到数据)。默认开
schedule_mode varchar(16) 否 interval / daily / weekly
schedule_hours varchar(96) 否 daily/weekly 用:几点跑,逗号分隔,如 9,18
schedule_days varchar(32) 否 weekly 用:周几,0 = 周一(对齐 Python 的 date.weekday())
schedule_minute int 否 整点后的第几分钟,三个模式共用
next_run_at bigint 是 下次该跑的时刻。调度器就按这个字段挑任务
last_run_at bigint 是 上次运行的开始时刻(不是结束)
last_status varchar(32) 否 上次运行状态,取值见 monitor_run.status
last_error text 是 上次失败的原因,给人看的一句话
last_notified_at bigint 是 上次推送的时刻,用来限流
created_at / updated_at bigint 否

schedule_hours / schedule_days 存的是逗号分隔的字符串而不是关联表 —— 内容是几个 小整数,单独建表只会让每次读取多一跳。


monitor_target — 任务的目标

一个任务可以配多个博主(或作品)。

字段 类型 空 说明
id int 否 主键
task_id int 否 → monitor_task.id,级联删除
kind varchar(16) 否 等于任务的 mode:creator / note
external_id varchar(128) 否 规范化后的 id。博主是 sec_uid(抖音)/ user_id(小红书),作品是 note_id
xsec_token varchar(512) 否 只有小红书有:分享链接里那个会过期的令牌。抖音恒为空串
xsec_source varchar(64) 否 同上,配套的 source 参数
raw_value text 否 用户当初粘进来的原文(URL 或裸 id),出问题时用来回溯
label varchar(200) 否 展示用的名字
enabled tinyint(1) 否 关掉的目标这一轮不采
created_at bigint 否

唯一键 (task_id, kind, external_id) —— 同一个目标粘两次不会变成两行。


monitor_run — 每一次运行

字段 类型 空 说明
id int 否 主键
task_id int 否 → monitor_task.id,级联删除
trigger varchar(16) 否 scheduled 定时 / manual 手动
status varchar(16) 否 pending 排队中、running 运行中、success、partial 部分成功、failed、timeout、interrupted 被中断
phase varchar(16) 否 建这条 run 时任务的 mode。任务中途改了模式,也还能知道当时跑的是什么
save_data_path text 否 这一轮产物的落盘目录
queued_at bigint 否 入队时刻
not_before bigint 否 在这之前不许开跑(错峰 / 防抖)
started_at bigint 是 真正开跑的时刻
finished_at bigint 是 结束时刻
exit_code bigint 是 退出码。抖音走进程内 HTTP,值是 0 / 1;小红书走子进程。Windows 的进程崩溃会是大整数(NTSTATUS),ingest.describe_exit_code() 会翻译
notes_fetched int 否 这一轮读到多少条作品
comments_fetched int 否 读到多少条评论
new_notes int 否 其中多少条是新的
new_comments int 否 同上
is_baseline tinyint(1) 否 是否该任务的第一轮成功运行。基线轮刻意不发增量事件 —— 没有「上一轮」可比,全报成新增只是噪音
max_comments_count int 否 跑的时候用的值快照。任务后来改了,历史记录仍然能解释得通
error_message text 是 失败原因

monitor_note — 作品(笔记 / 视频)

字段 类型 空 说明
id int 否 主键
task_id int 否 → monitor_task.id,级联删除
note_id varchar(128) 否 平台上的作品 id(抖音叫 aweme_id)
title text 否 标题 / 文案
note_url text 否 作品页地址
cover text 否 封面。优先是本地缓存地址 /api/monitor/covers/{note_id}——远程图床地址带签名、会过期(实测隔天 403),本地那份不会
creator_hash varchar(64) 否 创作者 id 的单向哈希。爬虫刻意不落原始 user_id,所以这是唯一稳定的创作者标识,作品栏按它分组
creator_name varchar(200) 否 创作者昵称(本仓库关掉了脱敏,是原文)
source_kind varchar(16) 否 平台自己的作品类型:小红书取 type,抖音取 aweme_type
published_at bigint 是 作者发布的时间。和 first_seen_at 完全不是一回事——把早就发过的作品加进监控时,两者能差好几个月
first_seen_run_id int 是 第一次看到它是在哪一轮
first_seen_at bigint 否 我们第一次看到它的时刻
last_seen_run_id int 是 最后一次采到它的轮次
last_seen_at bigint 否 最后一次采到它的时刻

唯一键 (task_id, note_id) —— 同一件作品被两个任务同时监控就是两行,各算各的增量。

掉出采集窗口的作品不会被删。界面按 max_notes_count 开窗只画最新 N 条,而库里保留 全部:趋势图、报表、导出读的都是这份全量。


monitor_note_metric — 作品指标快照(趋势的来源)

一轮一条,作品的四项互动指标。

字段 类型 空 说明
id int 否 主键
task_id int 否 属于哪个任务
note_id varchar(128) 否 哪条作品
run_id int 否 哪一轮采的
captured_at bigint 否 采集时刻
liked_count int 是 点赞。解析不出来就是 NULL,不是 0
comment_count int 是 评论
collected_count int 是 收藏
share_count int 是 分享
raw_liked_count varchar(64) 否 平台返回的原文(可能是 1.2万)
raw_comment_count varchar(64) 否 同上
raw_collected_count varchar(64) 否 同上
raw_share_count varchar(64) 否 同上

唯一键 (task_id, note_id, run_id)。

留 raw_* 四列是为了事后可审计:解析规则出问题时,原文还在,能重新算一遍。


monitor_comment — 评论

字段 类型 空 说明
id int 否 主键
task_id int 否 属于哪个任务
note_id varchar(128) 否 评论挂在哪条作品下
comment_id varchar(128) 否 平台上的评论 id
content text 否 评论正文
nickname varchar(200) 否 评论人昵称
creator_hash varchar(64) 否 评论人 id 的哈希
create_time bigint 是 评论发布时间
like_count int 是 这条评论的点赞
sub_comment_count int 否 子评论数。平台没给就是不填,不猜
parent_comment_id varchar(128) 否 父评论 id。顶层评论两边写法不同:小红书是空串,抖音是 "0"——adapters.parent_comment_id() 会把 "0" 归一成空串
first_seen_run_id int 是 第一次见到是在哪一轮
first_seen_at bigint 否 第一次见到的时刻

唯一键 (task_id, note_id, comment_id)。

评论接口没有时间排序,只能看到平台默认顺序下的前 N 条。所以第一次见到的评论分成 「上一轮之后发的」和「第一次见到的」两类事件,而不是一口咬定都是新发的。


monitor_creator_stat — 博主账号级快照

一门一个人:粉丝数 / 总获赞 / 作品数。这是作品列表给不了的东西 —— 作品说的是「这条涨了 多少赞」,这里说的是「这个人整个账号在涨还是在掉」。

字段 类型 空 说明
id int 否 主键
task_id int 否 → monitor_task.id,级联删除
run_id int 否 哪一轮采的
creator_hash varchar(64) 否 博主哈希(和 monitor_note.creator_hash 同一个口径)
nickname varchar(128) 否 昵称
creator_id varchar(128) 否 平台上的主页 id(抖音是 sec_uid)。用来拼「跳到他主页」的链接——哈希是单向的,拼不出地址
fans bigint 是 粉丝数
total_favorited bigint 是 总获赞
works_count bigint 是 主页作品总数(和我们监控到的条数不是一回事)
following bigint 是 关注数
captured_at bigint 否 采集时刻

唯一键 (task_id, creator_hash, run_id)。

目前只有抖音会写这张表。 小红书走的是爬虫子进程,而它的 save_creator() 在教学版里 是空函数,从来没落过创作者资料。所以小红书的博主没有粉丝数,也没有主页链接。


monitor_creator_alias — 博主备注

字段 类型 空 说明
id int 否 主键
platform varchar(16) 否 xhs / dy
creator_hash varchar(64) 否 哪个博主
alias varchar(128) 否 人自己起的名字(「竞品A」)。空串表示没起过
updated_at bigint 否

唯一键 (platform, creator_hash) —— 同一个博主出现在多个任务里,备注只填一次。

作品栏按 creator_hash 分组,可那是个哈希、昵称又常常认不出是谁;备注是唯一能把账号对上 人的东西。


monitor_note_alias — 作品备注

字段 类型 空 说明
id int 否 主键
platform varchar(16) 否 xhs / dy
note_id varchar(128) 否 哪条作品
alias varchar(128) 否 一句话的备注
updated_at bigint 否

唯一键 (platform, note_id)。

和博主备注是一对,但回答的不是同一个问题:博主备注回答「这个账号是谁」,作品备注回答 「这条我要盯着」。一个博主底下常常只有一两件值得盯的,所以不能合并。


monitor_tag — 标签词表

在「设置 → 作品标签」里维护。全局一套,不按平台分——「重点」是给人自己用的心智,不该在 小红书和抖音各定义一遍。

字段 类型 空 说明
id int 否 主键
name varchar(32) 否 标签名
color varchar(16) 否 调色板里的名字:cyan/pink/green/orange/muted。不是色值——Tailwind 的类名是构建时静态提取的,拼出来的动态类名它看不见,线上会静默变无色
sort_order int 否 展示顺序。新建时取 max+1 而不是 count:删过标签之后 count 会撞上已有的值
created_at bigint 否

唯一键 (name) —— 两个「重点」在筛选列表里是灾难,选哪个都像对的。


monitor_note_tag — 作品 ↔ 标签

字段 类型 空 说明
id int 否 主键
platform varchar(16) 否 xhs / dy
note_id varchar(128) 否 哪条作品
tag_id int 否 → 逻辑上指向 monitor_tag.id,但没有外键(见下)
created_at bigint 否

唯一键 (platform, note_id, tag_id) —— 多对多:一条作品可以既是「重点」又是「竞品」。

删标签时显式删关联,不依赖外键级联:测试跑的是 SQLite,而它默认不启用外键约束。 靠级联的话会出现「测试全绿、线上才对」或者反过来的情况。

标签和备注并存、不是一回事:备注是一句话的自由文字,标签是封闭词表里的分类。分类 必须封闭,否则「重点/重要/优先」各写各的,筛选就没法用了。


monitor_event — 变化事件

界面上的「变化」列表和企业微信推送读的都是它。

字段 类型 空 说明
id int 否 主键
task_id int 否 属于哪个任务
run_id int 是 由哪一轮产生
type varchar(32) 否 见下表
severity varchar(16) 否 info / warning / error
target_kind varchar(16) 否 目前只有两种值:作品级的事件写 note,任务级的事件(运行失败 / 疑似登录失效 / 没抓到数据)留空串
target_id varchar(128) 否 对象 id —— 作品级的是作品 id(note_id / aweme_id),任务级的是空串
title text 否 一句话。推送和列表里显示的就是它
payload_json text 否 结构化细节(增量数值、退出码、失败原因…)
created_at bigint 否
is_read tinyint(1) 否 未读数就是按它统计的

type 的取值:

值 含义
new_note 发现新作品
new_comment_posted 上一轮之后发布的评论
new_comment_seen 第一次见到的评论(未必是刚发的,见评论那节的说明)
metric_delta 指标有明显变化
run_failed 运行失败
suspected_auth_failure 疑似登录失效(一条都没抓到,且没有别的解释)
no_data_found 没抓到数据,但原因不是登录(比如产物写在别的目录)

monitor_setting — 键值设置

字段 类型 空 说明
key varchar(64) 否 主键。两种形状:platform.<平台>.<名字> / system.<名字>
value text 否 一律存字符串,读的时候按注册表里的类型转
updated_at bigint 否

哪些 key 合法、各是什么类型/默认值,由 api/monitor/app_settings.py 的注册表决定 —— 设置页 的表单就是照它自动生成的。密钥类的设置(webhook 地址、cookie)存进来但从不原样返回给 前端,只回「有没有设置」和长度。