13 张表逐字段写清楚类型、可空、含义,外加四条必须先知道的约定: 毫秒时间戳、NULL 不是 0、平台 id 是 xhs/dy 而抖音落盘目录叫 douyin、作品从不删除。 和 db-schema.sql(原始 DDL)配套:那份给机器看,这份给人看。
18 KiB
监控层数据库表结构
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。
读之前要知道的四条约定
- 时间戳一律是毫秒(
bigint),由tools/time_util.get_current_timestamp()产生。 平台给秒的(抖音是秒)在adapters.PlatformAdapter.time_scale里换算 —— 差 1000 倍, 不换算的话 2026 年的作品会显示成 1970 年。 - NULL 和 0 是两回事。 指标、粉丝数这些「读不出来」的值一律存 NULL,不存 0: 0 在趋势图上是一条砸到底的线,和「不知道」完全不同。界面上 NULL 显示成「—」。
- 平台 id 是
xhs/dy(api/monitor/adapters.py)。注意dy是监控层的 id, 爬虫落盘目录叫douyin—— 两者不一致,靠adapters.artifact_dir()换算。 - 作品删除是从来不做的。 掉出采集窗口的作品仍然留在库里(趋势图、报表、导出要用), 只是界面不再显示。
表之间的关系
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)存进来但从不原样返回给
前端,只回「有没有设置」和长度。