本页回答三个问题:系统里有哪些业务数据、哪个服务拥有它们、服务之间如何引用。具体处理过程见文章处理的持久化流程,字段默认值与索引见数据库表字段参考

阅读地图

要理解的业务场景数据所有者主要数据
登录、注册、权限、用量user_manager用户、邀请码、注册申请、用量事件
上传、阅读、编辑、翻译、讲解、音频、反馈、文章收藏、每周推荐data_server文章、句子、媒体对象、反馈、用户文章收藏、推荐排序
收藏与复习卡片collection_server收藏夹、卡片快照与语法分组
“拥有”表示该服务是数据的事实来源并负责写入规则。Controller 可以编排流程或组合响应,但不因此成为数据所有者。

三种需要区分的结构

层次含义当前实现
领域对象业务代码使用的稳定概念,不要求与表一一对应data_server 定义 ArticleSentenceMediaObjectFeedback dataclass
数据库行关系库中的物理记录(data_server 是 PostgreSQL 行,user_manager 是 SQLite 行),包含文本编码、默认值和约束例如 sentences.furigana 是 JSON 文本,读取为领域对象后是数组
逻辑外键跨服务或未声明物理约束的 ID 引用例如 articles.user_id 指向 user_manager.users.id,不能跨库 SQL join
weekly_recommendationsarticle_favorites 都是关系行:前者表达文章与推荐顺序,后者表达用户与整篇文章的收藏关系;它们不另设领域 dataclass,仓储直接返回 Articleuser_manager 当前仍主要把 SQLite 行转换为字典;收藏域的行转换由 data_serverPgCollectionRepository 完成,collection_server 已不再访问任何数据库。

user_manager:身份、权限与用量

领域概念关键数据说明
Userid, username, password_hash, role, created_atroleuserroot;JWT 中携带用户 ID、用户名和角色
InviteCodeid, code, is_active, created_by, created_at控制注册入口,可由管理员创建和停用
RegistrationRequestid, username, password_hash, invite_code, remarks, status, reviewed_by, reviewed_at, created_at审核通过后才创建用户;状态为 pending/approved/rejected
UsageEventid, user_id, kind, ref_id, occurred_at每次事件一行;上传类型、LLM 调用等按 kind 聚合
管理员账号和预置邀请码属于初始化数据,不是运行时“第一个注册者升级”规则。管理员由 app/db/seed.py 幂等写入,邀请码 zywaizx 由 schema 幂等写入。

data_server:文章处理与阅读数据

领域概念关键数据说明
Articleid, user_id, title, author, intro, summary, raw_content, genre, output_language, explanation_level, primary_binding_id, primary_model, fallback_model, status, progress, metadata, is_public, guest_visible, created_atsummary 是尽力生成的文章总结;output_language 记录后续翻译和讲解应沿用的语言;explanation_level 记录讲解门槛与深度,默认 N3
Sentenceid, article_id, sentence_index, sentence, full_prompt, translation, explanation, furigana, analysis_status, explain_status, audio_object_id, audio_voice, note, metadata, created_atfurigana 在领域中是有序映射数组;空白句也可作为布局分隔符存在
MediaObjectid, kind, content_type, byte_size, data, storage_key, created_atlocal 模式句子音频把字节放在 data(PostgreSQL BYTEA);cos 模式 data 为空,由 storage_key 指向腾讯云 COS 对象
Feedbackid, user_id, article_id, sentence_index, category, message, created_atpublish_request 复用反馈行表达“申请公开”,没有单独的申请表
每周推荐关系article_id, sort_order, created_at单一的管理员排序列表;入选本身构成公开阅读入口,不受文章其他可见性标志限制
文章收藏关系user_id, article_id, created_at当前用户收藏的整篇文章;读取时重新套用文章可见性,文章删除时级联清理
Agent 的内部 prompt、模型调用和重试不属于数据层;数据层只保存其产出:切句后的原句、翻译、讲解、假名、音频、总结和状态。full_prompt 是会长期保留的辅助数据,但 Model 不执行它。

data_server:收藏与复习快照(rakull_collection)

收藏域是 data_server 内部的独立限界上下文:物理上是单独的 PostgreSQL 数据库 rakull_collection(独立 schema SQL、独立连接工厂),与内容库 rakull_data 不共享连接、不能跨库 JOIN。collection_server 只是客户端直连的无状态薄网关,经”用户 Bearer + 服务断言”调用 data_server 的内部端点;每次请求都按断言中的 actor_user_id(必须与 Bearer 用户一致)限定所有权。
领域概念关键数据说明
Collectionid, user_id, name, description, created_at每个请求都按 actor_user_id 限定所有权
CollectionItemid, collection_id, article_id, sentence_index, front, back, note, grammar, created_atfront/back 是加入时的反归一化快照;grammar 用于卡片分组与排序
ReviewState(SM-2)due_at, last_reviewed_at, interval_minutes, ease, reps, lapses, card_version与卡片同表;调度算法与排队/提交全部在 data_server 完成,card_version 做乐观锁(过期提交返回 409)
当客户端未提供 front/back 时,由 data_server同进程内直接读 sentences 仓储取原句和翻译后保存快照(失败降级为空串);不再经过跨服务 HTTP。源句之后被编辑或删除,不会自动改写已经收藏的卡片。

服务之间如何引用

  • articles.user_idcollections.user_id 是指向 users.id 的跨库逻辑外键。
  • collection_items.(article_id, sentence_index) 逻辑指向一条 data_serversentences。注意收藏表在 rakull_collection、句子在 rakull_data:即使归同一个 data_server 进程持有,二者仍是两个物理隔离的 PostgreSQL 数据库,不能跨库 SQL JOIN,article_id 只是逻辑引用;PostgreSQL 只约束它与同库内 collections 的关系。
  • feedback.user_id 是可空的跨库引用;article_id/sentence_index 也没有物理约束,用作可选上下文。
  • sentences.article_idsentences.audio_object_idweekly_recommendations.article_id 等同库关系由 PostgreSQL 外键约束。
  • article_favorites.user_id 是跨库逻辑引用;article_id 是同库物理外键并使用级联删除。

当前存储实现

服务数据访问边界当前落盘方式
data_server明确定义 ArticleRepositoryArticleFavoriteRepositorySentenceRepositoryWeeklyRecommendationRepositoryObjectStore 等 Protocol;路由通过 Storage 使用实现;收藏域挂 PgCollectionRepository唯一的关系存储实现是 PostgreSQL(app/storage/pg/):句子音频在 local 模式使用 media_objects.data BYTEA;收藏域在独立的 rakull_collection
user_manager路由和用量服务通过 get_conn() 直接执行参数化 SQL仍为 SQLite:本地账号库 local_auth.db
collection_server无数据访问层;只做鉴权、请求校验与错误透传,通过 CollectionDataClient(Bearer + 服务断言)转发到 data_server 内部接口不落盘(无状态)
关系存储只有一套实现:data_server 的仓储全部位于 app/storage/pg/PgArticleRepositoryPgCollectionRepository 等),运行时没有 SQLite fallback,也没有 STORAGE_BACKEND 之类的后端开关。仍然可切换的只有 OBJECT_STORElocal / cos)这一组对象/文件存储实现,二者只是媒体字节的存放位置不同。
二进制数据还有两条不同路径:
  • 句子 TTS 音频由 MediaObjectStore 写入 PostgreSQL:OBJECT_STORE=local 时字节落在 media_objects.data 的 BYTEA 列,cos 时字节上传 COS、行内只留 storage_key;句子只保存 audio_object_idaudio_voice
  • /v1/data/files 的一般上传由 LocalFileStore 写入 data_serverUPLOAD_DIR;视频入口 /api/videos/file 也会写入 immersive_study_server 自己的 UPLOAD_DIR,再把本地路径交给转写流程。这些本地文件不是 media_objects 字节。
下一步可按问题继续阅读:文章处理的持久化流程说明数据怎样变化,数据库表字段参考列出全部物理字段和约束。