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

348 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 监控层数据库表结构
`api/monitor/**` 这一层用到的全部表。库名 `mediacrawler_prod`(MySQL 5.7,utf8mb4)。
配套的原始 DDL 在 [`db-schema.sql`](./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)存进来但**从不原样返回**给
前端,只回「有没有设置」和长度。