CREATE TABLE。库与进程的归属、库间引用关系、为什么不能跨库 JOIN,先看数据库结构;业务语义看领域模型与数据归属。
可执行的 DDL 事实来源始终是各库自己的 schema SQL:
rakull_server/data_server/app/db/schema.sql(PostgreSQL 库rakull_data,58 张表)rakull_server/data_server/app/db/collection_schema.sql(PostgreSQL 库rakull_collection,2 张表)rakull_server/user_manager/app/db/schema.sql(SQLite 库local_auth.db,5 张表)
类型约定
rakull_data / rakull_collection(PostgreSQL)下表不逐条重复的通用约定:
- 自增主键统一为
BIGINT GENERATED ALWAYS AS IDENTITY;ID 外键列也是BIGINT。 - 时间戳存为 UTC 的
TEXT:DEFAULT now_compact()复刻旧 SQLite 的datetime('now')(YYYY-MM-DD HH:MM:SS),高精度时间用DEFAULT now_iso_ms()。 - JSON 仍存
TEXT列(没有 jsonb 列);0/1 布尔用SMALLINT;浮点用DOUBLE PRECISION;媒体字节用BYTEA。 - schema 里还有 12 个从旧 SQLite 触发器移植的用户触发器(plpgsql)。
local_auth.db(SQLite)按 SQLite 的动态类型与亲和性标注;连接由 app/local_auth/db.py 统一执行 PRAGMA journal_mode=WAL 与 PRAGMA foreign_keys=ON。
rakull_data 核心 6 表
articles
| 字段 | PostgreSQL 类型 | 默认值与约束 |
|---|---|---|
id | BIGINT GENERATED ALWAYS AS IDENTITY | 主键,自增 |
user_id | BIGINT | NOT NULL;逻辑指向 user_manager.users.id |
title | TEXT | NOT NULL DEFAULT '' |
author | TEXT | NOT NULL DEFAULT '' |
intro | TEXT | NOT NULL DEFAULT '' |
summary | TEXT | NOT NULL DEFAULT '' |
raw_content | TEXT | NOT NULL DEFAULT '' |
genre | TEXT | NOT NULL DEFAULT 'article';仅 article、interview、song |
output_language | TEXT | NOT NULL DEFAULT 'zh';应用约定 zh、en、zh-Hant,数据库未设 CHECK |
explanation_level | TEXT | NOT NULL DEFAULT 'N3';CHECK 限制为 N1、N2、N3、N4、N5、es_low、es_high、jh、hs |
primary_binding_id | TEXT | NOT NULL DEFAULT '';兼容/诊断用的精确 ModelBinding UUID;新文章客户端留空 |
primary_model | TEXT | NOT NULL DEFAULT '';文章选择的去重模型名;留空表示使用有效默认模型 |
fallback_model | TEXT | NOT NULL DEFAULT '' |
status | TEXT | NOT NULL DEFAULT 'completed';仅 completed、partially_completed、processing、failed、cancelled |
progress | INTEGER | NOT NULL DEFAULT 100;CHECK 范围 0–100 |
metadata | TEXT | NOT NULL DEFAULT '{}';JSON 文本 |
is_public | SMALLINT | NOT NULL DEFAULT 0;CHECK 仅 0/1 |
guest_visible | SMALLINT | NOT NULL DEFAULT 0;CHECK 仅 0/1 |
created_at | TEXT | NOT NULL DEFAULT now_compact()(UTC 文本) |
idx_articles_user(user_id)、idx_articles_genre(genre)、idx_articles_visibility(is_public, guest_visible)。
weekly_recommendations
| 字段 | PostgreSQL 类型 | 默认值与约束 |
|---|---|---|
article_id | BIGINT | 主键;物理外键 → articles.id ON DELETE CASCADE |
sort_order | INTEGER | NOT NULL DEFAULT 0 |
created_at | TEXT | NOT NULL DEFAULT now_compact()(UTC 文本) |
idx_weekly_recs_order(sort_order)。一篇文章最多出现一次;列表按 sort_order,再按 created_at 读取。
article_favorites
| 字段 | PostgreSQL 类型 | 默认值与约束 |
|---|---|---|
user_id | BIGINT | 联合主键;逻辑指向 user_manager.users.id |
article_id | BIGINT | 联合主键;物理外键 → articles.id ON DELETE CASCADE |
created_at | TEXT | NOT NULL DEFAULT now_iso_ms();UTC 高精度时间文本 |
idx_article_favorites_user_created(user_id, created_at DESC, article_id DESC)。收藏写入和删除均幂等,列表按最新收藏优先并重新套用文章可见性。
media_objects
| 字段 | PostgreSQL 类型 | 默认值与约束 |
|---|---|---|
id | BIGINT GENERATED ALWAYS AS IDENTITY | 主键,自增 |
kind | TEXT | NOT NULL;仅 sentence_audio、scenario_audio、upload_audio、upload_video、upload_image |
content_type | TEXT | NOT NULL DEFAULT 'application/octet-stream' |
byte_size | INTEGER | NOT NULL DEFAULT 0 |
data | BYTEA | 可空;OBJECT_STORE=local 时句子/情景音频字节内联于此,cos 时为 NULL |
storage_key | TEXT | 可空;OBJECT_STORE=cos 时指向腾讯云 COS 对象 key,local 时为 NULL |
cache_hash | TEXT | 可空;内容寻址缓存哈希(文本 + 声优 + 内容类型) |
created_at | TEXT | NOT NULL DEFAULT now_compact()(UTC 文本) |
idx_media_objects_cache_hash(cache_hash) WHERE cache_hash IS NOT NULL。数据库没有强制 data 与 storage_key 至少一个非空。这张表是关系库与对象存储的交汇点,两种字节落点(local 内联 BYTEA / cos 只存 key)的组合关系见数据库结构的「关系库与对象存储如何配合」一节。
sentences
| 字段 | PostgreSQL 类型 | 默认值与约束 |
|---|---|---|
id | BIGINT GENERATED ALWAYS AS IDENTITY | 主键,自增 |
article_id | BIGINT | NOT NULL;物理外键 → articles.id ON DELETE CASCADE |
sentence_index | INTEGER | NOT NULL;与 article_id 联合唯一 |
sentence | TEXT | NOT NULL DEFAULT '' |
full_prompt | TEXT | NOT NULL DEFAULT '[]';JSON 数组文本 |
translation | TEXT | NOT NULL DEFAULT '' |
explanation | TEXT | NOT NULL DEFAULT '' |
furigana | TEXT | NOT NULL DEFAULT '[]';JSON 数组文本 |
analysis_status | TEXT | NOT NULL DEFAULT 'pending';仅 pending、success、failed、skip |
explain_status | TEXT | 可空;非空时仅 explaining、explained、failed |
audio_object_id | BIGINT | 可空;物理外键 → media_objects.id ON DELETE SET NULL |
audio_voice | TEXT | NOT NULL DEFAULT '' |
tts_error | TEXT | NOT NULL DEFAULT '';最近一次经过清理和截断的 TTS 错误 |
note | TEXT | NOT NULL DEFAULT '' |
metadata | TEXT | NOT NULL DEFAULT '{}';JSON 文本 |
created_at | TEXT | NOT NULL DEFAULT now_compact()(UTC 文本) |
UNIQUE(article_id, sentence_index);命名索引 idx_sentences_article(article_id)。
feedback
| 字段 | PostgreSQL 类型 | 默认值与约束 |
|---|---|---|
id | BIGINT GENERATED ALWAYS AS IDENTITY | 主键,自增 |
user_id | BIGINT | 可空;逻辑指向 user_manager.users.id,空值允许匿名 |
article_id | BIGINT | 可空;同库上下文 ID,但没有物理外键 |
sentence_index | INTEGER | 可空;与 article_id 一起标识可选句子上下文,没有物理约束 |
category | TEXT | NOT NULL DEFAULT 'other';仅 bug、suggestion、content、other、publish_request |
message | TEXT | NOT NULL |
created_at | TEXT | NOT NULL DEFAULT now_compact()(UTC 文本) |
publish_request 是“申请公开”复用的反馈类别,不是单独的表。
local_auth.db(SQLite)5 表
users
| 字段 | SQLite 类型 | 默认值与约束 |
|---|---|---|
id | INTEGER | 主键,自增 |
username | TEXT | NOT NULL UNIQUE |
password_hash | TEXT | NOT NULL |
role | TEXT | NOT NULL DEFAULT 'user';仅 user、root |
created_at | TEXT | NOT NULL DEFAULT datetime('now') |
last_donation_at | TEXT | 可空;最近一次未被驳回的赞助时间 |
last_donation_amount | REAL | NOT NULL DEFAULT 0;单位是 last_donation_currency,不是 CNY |
last_donation_currency | TEXT | NOT NULL DEFAULT '' |
last_donation_method | TEXT | NOT NULL DEFAULT '';wechat、kofi、bmc、other |
donation_total_cny | REAL | NOT NULL DEFAULT 0;累计金额,按静态汇率折算为 CNY |
donation_count | INTEGER | NOT NULL DEFAULT 0 |
donation_prompt_muted_until | TEXT | 可空;在此时间之前客户端隐藏「赞助」入口 |
username 的 UNIQUE 约束会创建 SQLite 自动索引。
后 7 个字段是 donations 的汇总缓存:app/donations/service.py 的 refresh_rollup() 每次写入都从 donations 重新计算(而不是累加),因此不会与明细不一致。驳回某条赞助会连带清掉静默期。
invite_codes
| 字段 | SQLite 类型 | 默认值与约束 |
|---|---|---|
id | INTEGER | 主键,自增 |
code | TEXT | NOT NULL UNIQUE |
is_active | INTEGER | NOT NULL DEFAULT 1;仅 0/1 |
created_by | INTEGER | 可空;物理外键 → users.id ON DELETE SET NULL |
created_at | TEXT | NOT NULL DEFAULT datetime('now') |
code 的 UNIQUE 约束会创建 SQLite 自动索引。
registration_requests
| 字段 | SQLite 类型 | 默认值与约束 |
|---|---|---|
id | INTEGER | 主键,自增 |
username | TEXT | NOT NULL |
password_hash | TEXT | NOT NULL |
invite_code | TEXT | NOT NULL;保存提交值,没有物理外键 |
remarks | TEXT | NOT NULL DEFAULT '' |
status | TEXT | NOT NULL DEFAULT 'pending';仅 pending、approved、rejected |
reviewed_by | INTEGER | 可空;物理外键 → users.id ON DELETE SET NULL |
reviewed_at | TEXT | 可空 |
created_at | TEXT | NOT NULL DEFAULT datetime('now') |
idx_regreq_status(status)。用户名是否已有待审核申请由应用查询检查,不是 UNIQUE 约束。
usage_events
| 字段 | SQLite 类型 | 默认值与约束 |
|---|---|---|
id | INTEGER | 主键,自增 |
user_id | INTEGER | NOT NULL;物理外键 → users.id ON DELETE CASCADE |
kind | TEXT | NOT NULL |
ref_id | TEXT | 可空 |
occurred_at | TEXT | NOT NULL DEFAULT datetime('now') |
idx_usage_user_time(user_id, occurred_at)。
donations
| 字段 | SQLite 类型 | 默认值与约束 |
|---|---|---|
id | INTEGER | 主键,自增 |
user_id | INTEGER | NOT NULL;物理外键 → users.id ON DELETE CASCADE |
method | TEXT | NOT NULL;仅 wechat、kofi、bmc、other |
amount | REAL | NOT NULL CHECK (amount > 0);单位是 currency |
currency | TEXT | NOT NULL DEFAULT 'CNY' |
amount_cny | REAL | NOT NULL DEFAULT 0;按 DONATION_FX_TO_CNY 折算,写入时算好 |
status | TEXT | NOT NULL DEFAULT 'pending';仅 pending、confirmed、rejected |
is_anonymous | INTEGER | NOT NULL DEFAULT 0;仅 0/1;为 1 时不进致谢名单 |
note | TEXT | NOT NULL DEFAULT '' |
reviewed_by | INTEGER | 可空;物理外键 → users.id ON DELETE SET NULL |
reviewed_at | TEXT | 可空 |
donated_at | TEXT | NOT NULL DEFAULT datetime('now') |
idx_donations_user_time(user_id, donated_at)、idx_donations_status(status)。
支付发生在 RakuLL 之外,目前拿不到可信的支付结果,因此每条记录都是用户自行登记的,初始为 pending;rejected 的行不计入任何汇总,也不出现在致谢名单里。status 是为将来自动核验留的钩子(Ko-fi 服务端有 webhook,微信需要商户号),细节见赞助。bmc(Buy Me a Coffee)已在 CHECK 里放开,以后启用不需要迁移。
rakull_collection(PostgreSQL)2 表
collections
| 字段 | PostgreSQL 类型 | 默认值与约束 |
|---|---|---|
id | BIGINT GENERATED ALWAYS AS IDENTITY | 主键,自增 |
user_id | BIGINT | NOT NULL;逻辑指向 user_manager.users.id |
name | TEXT | NOT NULL |
description | TEXT | NOT NULL DEFAULT '' |
created_at | TEXT | NOT NULL DEFAULT now_compact()(UTC 文本) |
idx_collections_user(user_id)。
collection_items
| 字段 | PostgreSQL 类型 | 默认值与约束 |
|---|---|---|
id | BIGINT GENERATED ALWAYS AS IDENTITY | 主键,自增 |
collection_id | BIGINT | NOT NULL;同库物理外键 → collections.id ON DELETE CASCADE |
article_id | BIGINT | NOT NULL;逻辑指向 rakull_data.articles.id(跨库,无 FK) |
sentence_index | INTEGER | NOT NULL;与 article_id 一起逻辑指向一条句子 |
front | TEXT | NOT NULL DEFAULT '';收藏时快照 |
back | TEXT | NOT NULL DEFAULT '';收藏时快照 |
note | TEXT | NOT NULL DEFAULT '' |
grammar | TEXT | NOT NULL DEFAULT '';分组与排序标签 |
due_at | TEXT | NOT NULL DEFAULT '';SM-2 到期时刻;空串表示新卡,立即到期 |
last_reviewed_at | TEXT | NOT NULL DEFAULT '' |
interval_minutes | INTEGER | NOT NULL DEFAULT 0;0 表示仍在学习步阶 |
ease | DOUBLE PRECISION | NOT NULL DEFAULT 2.5;SM-2 难度系数,范围 1.3–3.0 由应用保证 |
reps | INTEGER | NOT NULL DEFAULT 0 |
lapses | INTEGER | NOT NULL DEFAULT 0 |
card_version | INTEGER | NOT NULL DEFAULT 0;乐观锁版本,提交复习时版本不符返回 409 |
created_at | TEXT | NOT NULL DEFAULT now_compact()(UTC 文本) |
UNIQUE(collection_id, article_id, sentence_index);命名索引 idx_collitems_collection(collection_id)、idx_collitems_grammar(collection_id, grammar)、idx_collitems_due(due_at)。三个索引与全部 SM-2 列都直接建在 collection_schema.sql 中,bootstrap 时随表一起幂等创建。