Files
Cyrene_v2/internal/db/migrations/0021_channel_events.up.sql
T
AskaEth ec3f6e810a feat(events): 渠道事件存储地基(Event/Store + SQLite 实现 + 有界计数聚合 + 保留期 GC + 迁移 0021)
- 新包 internal/runtime/events:Tier/Category/Platform/ChannelType/ActorRole 具名常量 + Event/Filter/Counter + Store 六方法
- 留存口径:T2/T3 逐条落行;T1 只计数;unknown / decode_error / link 三例外逐条落行(链路是否活着是事故定案唯一证据)
- 计数:内存聚合 + 上限 512 的有界缓冲 + 满即整批 upsert,落盘失败回填而不丢;GC:保留期 720h/间隔 1h,≤0 = 关闭
- 迁移 0021_channel_events(两表 + 6 索引,含计数 upsert 唯一键);Event 无 flag/正文/URL 字段(design/26 §4.2 红线)
2026-10-05 13:47:25 +08:00

72 lines
5.3 KiB
SQL
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.
-- 0021_channel_events: 渠道非消息事件的**留存地基**(design/26 §4.1;切片 E1)。
--
-- 动因(design/26 §0.2,已确证事故):qq_obv11 的入站总闸曾把 post_type != message
-- 的帧零日志、零计数地丢掉,于是「上游推过没有 / 是不是被我们过滤了」在数据里
-- 再也答不上来。本表就是那块地基:让到达适配器的每一帧至少留下可查的结构级痕迹。
--
-- 两张表**刻意不合并**(design/26 §4.1 已论证):
-- ① 写放大 —— 心跳 / client_status / 在线状态是高频遥测,逐帧一行会把库写胖;
-- 计数表按「平台+账号+平台原生类型」upsert,行数上界是「平台数 × 事件类型数」。
-- ② 保留期不同 —— channel_events 有审计价值、按保留期清扫;
-- 计数表本身就是「历史已浓缩成一行」的形态,**删了就再也答不上「推过没有」**,
-- 因此它保留 first_seen_at / last_seen_at,**不参与 GC**。
-- ③ 两张表不同构(一张逐条、一张聚合),不是「同一份东西抄了两遍」。
-- T3 与 T2 的列**完全相同**(只差 tier 取值),故合在同一张表里:拆开会让「单一
-- 查询面 / 单一保留期 / 单一 GC」变成三份实现(design/26 §4.1)。
--
-- 命名口径:表名复数、主键 id、时间戳 created_at/updated_at、索引 idx_{table}_{col}
-- (援引 AGENTS §10 已登记例外,与既有迁移一致)。时间戳沿用 0001 起的
-- `strftime('%Y-%m-%dT%H:%M:%fZ','now')`(UTC ISO-8601 毫秒),定宽 ⇒ 字典序比较
-- 等价于时间序,时间窗筛选因此能直接吃上 idx_channel_events_created_at。
--
-- 脱敏(design/26 §4.2,**表结构层面也留了痕**):本表**没有**承载平台原文的列 ——
-- summary 只存适配器按事件类型生成的**固定文案**,OneBot request.flag(可执行凭据)
-- 与平台正文**绝不入库**(故本迁移里连一个 flag/comment/raw/content 列都不存在)。
CREATE TABLE IF NOT EXISTS channel_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
platform TEXT NOT NULL, -- qq-obv11 / kook / telegram / weixin-ilink / webhook
account_id TEXT NOT NULL DEFAULT '', -- 多账号(CHANNELS_JSON 的账号 id);空 = 单账号/未知
tier TEXT NOT NULL DEFAULT '', -- t1 / t2 / t3(design/26 §2 的留存档)
category TEXT NOT NULL DEFAULT '', -- 归一化类别(join/leave/recall/ban/admin/friend_request/
-- group_request/poke/reaction/button/item/vote/link/unknown)
platform_type TEXT NOT NULL DEFAULT '', -- 平台原生类型码(notice_type / meta_event_type / request_type /
-- KOOK extra.type / Telegram update 键名),原样保留便于排障
channel_id TEXT NOT NULL DEFAULT '',
channel_type TEXT NOT NULL DEFAULT '', -- direct / group
group_id TEXT NOT NULL DEFAULT '',
actor_id TEXT NOT NULL DEFAULT '', -- 结构化字段,**绝不来自文本**
actor_role TEXT NOT NULL DEFAULT '', -- owner/member/system/unknown(结构化解析所得)
target_id TEXT NOT NULL DEFAULT '', -- 被作用者(poke / 禁言 / 运气王…)
self_related INTEGER NOT NULL DEFAULT 0, -- 是否命中 self_id / 她自己
message_id TEXT NOT NULL DEFAULT '', -- 关联消息 id(撤回 / 回应 / 按钮 / 置顶)
summary TEXT NOT NULL DEFAULT '', -- 脱敏结构级摘要(固定文案,不含平台原文)
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
);
-- 查询面与 GC 的**唯一依赖**:列表倒序、时间窗筛选、过期行清扫都靠它。
CREATE INDEX IF NOT EXISTS idx_channel_events_created_at ON channel_events (created_at);
-- 面板/端点的「平台 + 类别」筛选(design/26 §4.5 的查询参数)。
CREATE INDEX IF NOT EXISTS idx_channel_events_platform_category ON channel_events (platform, category);
CREATE INDEX IF NOT EXISTS idx_channel_events_channel_id ON channel_events (channel_id);
CREATE INDEX IF NOT EXISTS idx_channel_events_tier ON channel_events (tier);
CREATE TABLE IF NOT EXISTS channel_event_counters (
id INTEGER PRIMARY KEY AUTOINCREMENT,
platform TEXT NOT NULL,
account_id TEXT NOT NULL DEFAULT '',
tier TEXT NOT NULL DEFAULT '',
platform_type TEXT NOT NULL DEFAULT '',
category TEXT NOT NULL DEFAULT '',
count INTEGER NOT NULL DEFAULT 0,
first_seen_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
last_seen_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
);
-- 唯一约束承载 upsert(`ON CONFLICT(platform,account_id,platform_type) DO UPDATE`):
-- 一次 flush 因此是同键累加,而不是每次心跳插一行。
CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_event_counters_key
ON channel_event_counters (platform, account_id, platform_type);
CREATE INDEX IF NOT EXISTS idx_channel_event_counters_category ON channel_event_counters (category);