Files
backend/docs/03-数据库设计.md
T
34047007@qq.com 62ca8fa6b8
CI / backend (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
chore: batch commit remaining changes
Includes search engine improvements, Alembic migrations,
new services (pubmed_daily_update, query_expansion),
frontend updates, and documentation sync.
2026-07-27 08:35:12 +08:00

1042 lines
66 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.
# 数据库设计
## 日期时间类型规范
**TIMESTAMPTZ 为主**。纯日期字段(出生日期、发表日期、批准日期、排期日期、PubMed 纯日期字段)用 `DATE`
```sql
-- ✅ 正确
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
pub_date DATE -- 文献发表日期不精确到时间
-- ❌ 禁止
created_at TIMESTAMP -- 不知道时区
created_at INTEGER -- Unix时间戳,人不可读,SQL 中日期运算困难
```
---
## ER 关系总图
```
tenants ──1:N── user_tenants ──N:1── users
│ │
├── teams ──N:M── team_members ├── user_subscriptions ── global_tags
├── invitations ├── user_journal_subscriptions ── global_journals
├── export_jobs ├── user_feed ── global_literature
├── shared_folders ├── user_literature ── global_literature
├── journal_club_queue ├── user_notes ── global_literature
├── approval_workflows ├── user_pdf_highlights
├── api_keys ├── user_folders
├── tenant_subscriptions ├── user_feedback
├── subscription_events └── systematic_reviews ── review_literatures
├── payment_orders
└── ai_provider_config
global_literature ──N:M── global_literature_tags ──N:M── global_tags
│ │
├── drug_approvals(pmid) ├── drug_approvals(cancer_type_tag)
├── guideline_evidence ├── guideline_versions(cancer_type_tag)
├── review_literatures └── user_subscriptions(tag)
└── global_journals(issn)
user_journal_subscriptions ──N:1── global_journals
mdt_cases ──1:N── mdt_decisions ──1:N── mdt_evidence
└── global_literature
```
---
## 一、基础架构表
### 1.1 tenants — 租户
```sql
CREATE TABLE tenants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 租户ID
name VARCHAR(200) NOT NULL, -- 租户名称
slug VARCHAR(100) UNIQUE NOT NULL, -- 唯一标识符(用于URL
hospital_name VARCHAR(200), -- 医院名称
department_name VARCHAR(200), -- 科室名称
plan_type VARCHAR(30) NOT NULL DEFAULT 'free', -- 套餐类型:free/pro/team/enterprise
is_personal BOOLEAN NOT NULL DEFAULT TRUE, -- 是否个人租户
status VARCHAR(20) NOT NULL DEFAULT 'active', -- 状态:active/inactive/suspended
logo_url VARCHAR(500), -- Logo 图片 URL
primary_color VARCHAR(7) DEFAULT '#1a5276', -- 主题色(十六进制)
settings JSONB NOT NULL DEFAULT '{}', -- 租户级 JSON 配置
max_members INTEGER NOT NULL DEFAULT 1, -- 最大成员数
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 创建时间
updated_at TIMESTAMPTZ NOT NULL DEFAULT now() -- 更新时间
);
```
### 1.2 users — 用户
```sql
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 用户ID
email VARCHAR(320) NOT NULL UNIQUE, -- 邮箱(登录账号)
hashed_password VARCHAR(128), -- 密码哈希(SSO用户可为NULL)
display_name VARCHAR(150) NOT NULL, -- 显示名称
title VARCHAR(100), -- 职称/头衔
avatar_url VARCHAR(500), -- 头像 URL
phone VARCHAR(30), -- 手机号
is_active BOOLEAN NOT NULL DEFAULT TRUE, -- 是否激活
is_superuser BOOLEAN NOT NULL DEFAULT FALSE, -- 是否超级管理员
sso_provider VARCHAR(20), -- SSO 提供商:wechat/oauth
sso_subject_id VARCHAR(255), -- SSO 主体 ID
email_verified BOOLEAN NOT NULL DEFAULT FALSE, -- 邮箱是否已验证
phone_verified BOOLEAN NOT NULL DEFAULT FALSE, -- 手机是否已验证
notify_email BOOLEAN NOT NULL DEFAULT TRUE, -- 是否接收邮件通知
digest_frequency VARCHAR(10) NOT NULL DEFAULT 'daily', -- 摘要频率:daily/weekly/never
last_login_at TIMESTAMPTZ, -- 最后登录时间
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 创建时间
updated_at TIMESTAMPTZ NOT NULL DEFAULT now() -- 更新时间
);
CREATE INDEX idx_users_email ON users(email);
```
用户通知偏好(`notify_email`, `digest_frequency`)和验证状态(`email_verified`, `phone_verified`)存储在 users 表内,不单独建通知表。
### 1.3 user_tenants — 用户-租户关联
```sql
CREATE TABLE user_tenants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 关联ID
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- 用户ID
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, -- 租户ID
role VARCHAR(20) NOT NULL DEFAULT 'viewer', -- 角色:viewer/member/admin/owner
is_default BOOLEAN NOT NULL DEFAULT FALSE, -- 是否为该用户的默认租户
joined_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 加入时间
UNIQUE (user_id, tenant_id)
);
CREATE INDEX idx_ut_user ON user_tenants(user_id);
CREATE INDEX idx_ut_user_tenant ON user_tenants(user_id, tenant_id);
```
### 1.4 login_logs — 登录日志
```sql
CREATE TABLE login_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 日志ID
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- 用户ID
tenant_id UUID REFERENCES tenants(id) ON DELETE SET NULL, -- 租户ID(可能为空)
ip_address VARCHAR(45), -- 登录IP(支持IPv6
user_agent VARCHAR(500), -- 用户代理字符串
success BOOLEAN NOT NULL DEFAULT TRUE, -- 是否登录成功
fail_reason VARCHAR(200), -- 失败原因
created_at TIMESTAMPTZ NOT NULL DEFAULT now() -- 登录时间
);
CREATE INDEX idx_ll_user_created ON login_logs(user_id, created_at);
```
---
## 二、文献核心表(全局库,所有租户共享)
### 2.1 global_literature — 全局文献库
```sql
CREATE TABLE global_literature (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 文献ID(内部)
pmid INTEGER UNIQUE NOT NULL, -- PubMed ID 【PubMed: MedlineCitation/PMID】
title TEXT NOT NULL, -- 标题 【PubMed: Article/ArticleTitle】
abstract TEXT, -- 摘要 【PubMed: Article/Abstract/AbstractText】
authors JSONB NOT NULL DEFAULT '[]', -- 作者列表 [{family,given,affiliations}] 【PubMed: Article/AuthorList/Author】
doi VARCHAR(500), -- DOI 标识符 【PubMed: Article/ELocationID[@EIdType="doi"] 或 ArticleId[@IdType="doi"]】
journal VARCHAR(500), -- 期刊全名 【PubMed: Article/Journal/Title】
journal_issn VARCHAR(20), -- 期刊 ISSN 【PubMed: Article/Journal/ISSN】
journal_iso VARCHAR(100), -- NLM ISO 缩写(如 "N Engl J Med")【PubMed: Article/Journal/ISOAbbreviation】
volume VARCHAR(50), -- 卷号 【PubMed: Article/Journal/JournalIssue/Volume】
issue VARCHAR(50), -- 期号 【PubMed: Article/Journal/JournalIssue/Issue】
pages VARCHAR(100), -- 页码 【PubMed: Article/Pagination/MedlinePgn】
pub_date DATE, -- 纸质出版日期(期刊卷期日期,若纯电子刊则为电子日期)【PubMed: Article/Journal/JournalIssue/PubDate 组合 Year+Month+Day】
print_date DATE, -- 纯纸质出版见刊日期(仅纸质版有值,从 PubDate 解析并经 history ppdat 校验)【PubMed: PubmedData/History/PubMedPubDate[@PubStatus="ppdat"]】
pub_year INTEGER, -- 出版年份 【PubMed: 从 PubDate/Year 提取】
pub_types JSONB NOT NULL DEFAULT '[]', -- 出版类型列表 【PubMed: Article/PublicationTypeList/PublicationType】
mesh_headings JSONB NOT NULL DEFAULT '[]', -- MeSH 标引词列表(含 DescriptorName+QualifierName)【PubMed: MeshHeadingList/MeshHeading】
language VARCHAR(10) DEFAULT 'en', -- 语言 【PubMed: Article/Language】
raw_xml_hash VARCHAR(64), -- 原始 XML SHA-256 哈希(去重与增量更新用,非 PubMed 字段)
pmc_id VARCHAR(20), -- PMC IDPMC1234567)【PubMed: ArticleId[@IdType="pmc"]】
is_oa BOOLEAN NOT NULL DEFAULT FALSE, -- 是否开放获取【根据 pmc_id 或 PMC OA API 判定,非 PubMed 原始字段】
keywords JSONB NOT NULL DEFAULT '[]', -- 作者关键词 【PubMed: Article/KeywordList/Keyword】
pubmed_revised DATE, -- PubMed 数据库修订日期(NLM 说明不可靠)【PubMed: MedlineCitation/DateRevised】
grants JSONB NOT NULL DEFAULT '[]', -- 基金资助信息 【PubMed: Article/GrantList/Grant】
cited_by_count INTEGER NOT NULL DEFAULT 0, -- 被引次数 【来自 NCBI elink 接口 pubmed_pubmed_citedin,非 PMID 固定字段】
full_text_sections JSONB, -- PMC OA 全文结构化段落【来自 PMC OA Service API JATS XML,非 PubMed EFetch】
full_text_path VARCHAR(255), -- COS 对象存储键(内部)
trial_reg JSONB, -- 试验注册号 {nct, eudract, chictr} 【PubMed: Article/DataBankList/DataBank,注册号在 ArticleId[@IdType="ClinicalTrials.gov"] 中也有】
study_design JSONB, -- 研究设计分类 {primary: "RCT", sub: "Phase 3", design: "parallel"}AI 推断,非 PubMed 原始字段)
pico JSONB, -- PICO 结构化抽取(AI 推断,非 PubMed 原始字段)
citation_status VARCHAR(20), -- MedlineCitation 状态 【PubMed: MedlineCitation/@Status 属性,如 "MEDLINE"/"PubMed-not-MEDLINE"/"In-Data-Review"】
date_completed DATE, -- NLM 整条编目记录完成日(含 MeSH 标引在内的全部处理完成)
meshed_date DATE, -- MeSH 数据版本日期。通常=date_completed(MeSH 是编目最后一步,两者同天)。年更刷新后=新的 date_completed。NULL=无MeSH(in-process/publisher)。NOT datetime.now()
retracted BOOLEAN NOT NULL DEFAULT FALSE, -- 是否已撤稿【依据 PubMed: Article/PublicationTypeList/PublicationType[.="Retracted Publication"]】
retraction_details JSONB, -- 撤稿详情(关联撤稿文献等)【PubMed: CommentsCorrectionsList/CommentsCorrections[@RefType="retractionof"/"retractionin"]】
license_info JSONB, -- 许可证信息(来自 PMC OA XML 或出版商元数据,非 PubMed 原始字段)
pre_extracted_data JSONB, -- Table 1 预提取数据(AI 抽取,非 PubMed 原始字段)
is_negative_result BOOLEAN NOT NULL DEFAULT FALSE, -- 是否为阴性结果(AI 判定,非 PubMed 原始字段)
rct_detection JSONB, -- RCT 自动检测结果 {is_rct, confidence, source}AI,非 PubMed 原始字段)
search_tsv TSVECTOR, -- 全文检索向量(内部 PG tsvector,非 PubMed 字段)
negative_result_details JSONB, -- 阴性结果详情(AI 抽取,非 PubMed 原始字段)
ai_summary JSONB, -- AI 摘要(内部,非 PubMed 字段)
chemical_list JSONB NOT NULL DEFAULT '[]', -- 化学物质列表 【PubMed: ChemicalList/Chemical】
investigators JSONB NOT NULL DEFAULT '[]', -- 研究者列表 [{family,given,affiliation,identifiers}] 【PubMed: InvestigatorList/Investigator】
personal_name_subjects JSONB NOT NULL DEFAULT '[]', -- 作为主题的人名 [{family,given}] 【PubMed: PersonalNameSubjectList/PersonalNameSubject】
publication_notes JSONB NOT NULL DEFAULT '[]', -- 出版注释列表 ["note1","note2"] 【PubMed: PubmedData/PublicationNote】
gene_symbols JSONB NOT NULL DEFAULT '[]', -- 基因符号列表 【PubMed: SupplMeshList/SupplMeshName;部分来自 Gene Symbol 或 ChemicalList】
num_refs INTEGER, -- 参考文献数量(来自 PMC 全文或解析,非 PubMed EFetch 原始字段)
publication_status VARCHAR(30), -- 出版状态 【PubMed: PubmedData/PublicationStatus,取值 epublish/ppublish/aheadofprint】
article_date DATE, -- 电子出版日期(先于印刷版)【PubMed: Article/ArticleDate】
create_date DATE, -- PubMed 记录创建日期(纯日期,无需时间)【PubMed: PubmedData/History/PubMedPubDate[@PubStatus="pubmed"] / MedlineCitation/DateCreated】
entrez_date DATE, -- PubMed 收录日期(纯日期,无需时间)【PubMed: PubmedData/History/PubMedPubDate[@PubStatus="entrez"]】
databank_list JSONB NOT NULL DEFAULT '[]', -- 数据库引用列表(如 ClinicalTrials.gov)【PubMed: Article/DataBankList/DataBank】
suppl_mesh_list JSONB NOT NULL DEFAULT '[]', -- 补充 MeSH 词列表(含物质名)【PubMed: SupplMeshList/SupplMeshName】
source VARCHAR(30) NOT NULL DEFAULT 'pubmed_ftp', -- 数据来源(内部追踪用:pubmed_api/pubmed_ftp,非 PubMed 字段)
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 创建时间
updated_at TIMESTAMPTZ NOT NULL DEFAULT now() -- 更新时间
);
CREATE INDEX idx_gl_pub_date ON global_literature(pub_date);
CREATE INDEX idx_gl_pub_year ON global_literature(pub_year);
CREATE INDEX idx_gl_journal_issn ON global_literature(journal_issn);
CREATE INDEX idx_gl_journal_iso ON global_literature(journal_iso);
CREATE INDEX idx_gl_created ON global_literature(created_at);
CREATE INDEX idx_gl_pmc_id ON global_literature(pmc_id);
CREATE INDEX idx_gl_search_tsv ON global_literature USING gin(search_tsv);
CREATE INDEX ix_gl_doi ON global_literature(doi);
CREATE INDEX ix_gl_language ON global_literature(language);
CREATE INDEX ix_gl_retracted ON global_literature(retracted);
CREATE INDEX ix_gl_pub_types_gin ON global_literature USING gin(pub_types);
CREATE INDEX ix_gl_study_design_gin ON global_literature USING gin(study_design);
CREATE INDEX ix_gl_grants_gin ON global_literature USING gin(grants);
CREATE INDEX ix_gl_chemical_list_gin ON global_literature USING gin(chemical_list);
CREATE INDEX ix_gl_citation_status ON global_literature(citation_status); -- 筛选:MEDLINE only
CREATE INDEX ix_gl_is_oa ON global_literature(is_oa); -- 筛选:Free full text
CREATE INDEX ix_gl_is_negative ON global_literature(is_negative_result); -- 筛选:阴性结果
CREATE INDEX ix_gl_is_preprint ON global_literature(is_preprint); -- 筛选:Exclude preprints
CREATE INDEX ix_gl_mesh_headings ON global_literature USING gin(mesh_headings); -- 筛选:Species/Sex/Age JSONB @> 查询
CREATE INDEX ix_global_literature_journal_iso_trgm ON global_literature USING gin(journal_iso gin_trgm_ops); -- PubMed [TA] ILIKE 兜底
```
> 预估值:约 600 万条(PubMed 中肿瘤相关的历史累积),单表不需要分区
**全文搜索:**
> `search_tsv``TSVECTOR` 类型,GIN 索引 `ix_gl_search_tsv`)由 PG 触发器 `trg_global_literature_tsv` 自动维护。使用 `setweight()` 区分字段权重:**title=A 权重、abstract=B 权重、authors.family=A 权重**。所有搜索(普通搜索 `field="all"` 和高级搜索字段选择中的作者/机构)均走 tsvector `@@` `plainto_tsquery()`ILIKE 仅作为兜底(NULL tsvector 记录)。触发器在 INSERT 或 UPDATE title/abstract/authors 时触发重建。千万级无压力。
>
> > **GIN 索引重建说明:** 迁移 `f80dca5baa02`add_pico_column_to_review_literatures)的 `upgrade()` 误将 `ix_gl_search_tsv` 删除后未重建,导致所有 tsvector 搜索走全表扫描。迁移 `0314f4d28728`rebuild_search_tsv_gin_index)已修复,紧跟在 `e341edea85e2` 之后。
**日期字段说明:** `pub_date`[DP] 出版日期 = LEAST(EPDAT, PPDAT))由 `article_date`[EPDAT] 电子出版日期)+ `print_date`([PPDAT] 纸质出版日期)的最小值计算得出。`create_date`[CRDT] PubMed 记录创建日期)+ `entrez_date`[EDAT] PubMed 入库日期)+ `date_completed`(NLM 整条编目记录完成日,含 MeSH 在内的全部处理完成,实践中作为 [MHDA] 近似值)。`meshed_date` 是系统内部字段,记录 MeSH 数据的版本日期:通常=`date_completed`(MeSH 是编目最后一步,两者同天);年更刷新后设为新的 `date_completed`。NULL=无 MeSH。`pubmed_revised`NLM 定义为不可靠)。`publication_status`epublish / ppublish / aheadofprint 出版阶段标识)。
> **注意:** `create_date`、`entrez_date`、`date_completed`、`meshed_date`、`pubmed_revised` 五个字段原为 TIMESTAMPTZ,现统一为 DATE 类型。原因是 NLM 所有日期来源(DateCompleted、History/PubMedPubDate、DateRevised、ArticleDate、DateCreated)仅提供 Y/M/D,无时间分量。DATE 类型更精确地反映语义,减少存储开销。
**PubMed 对应 XML 结构速查:**
| 表字段 | PubMed XML Path | 说明 |
|---|---|---|
| `pmid` | `MedlineCitation/PMID` | Pubmed ID |
| `title` | `Article/ArticleTitle` | 文章标题 |
| `abstract` | `Article/Abstract/AbstractText` | 多个 AbstractText 拼接,按 Label 区分 |
| `authors` | `Article/AuthorList/Author` | LastName + ForeName + AffiliationInfo |
| `doi` | `ArticleId[@IdType="doi"]` | 优先取 PubmedData 下的 ArticleIdList |
| `journal` | `Article/Journal/Title` | 期刊全名 |
| `journal_issn` | `Article/Journal/ISSN` | 注意区分 Print / Electronic |
| `journal_iso` | `Article/Journal/ISOAbbreviation` | NLM 标准缩写 |
| `volume` | `Article/Journal/JournalIssue/Volume` | 卷 |
| `issue` | `Article/Journal/JournalIssue/Issue` | 期 |
| `pages` | `Article/Pagination/MedlinePgn` | 页码范围 |
| `pub_date` | `Article/Journal/JournalIssue/PubDate` 取 Year/Month/Day 组合,与 print_date/articledate LEAST 计算 | [DP] 出版日期 = COALESCE(LEAST(EPDAT, PPDAT), 任一可用值) |
| `print_date` | `PubmedData/History/PubMedPubDate[@PubStatus="ppdat"]``Journal/JournalIssue/PubDate` | [PPDAT] 纸质出版日期 |
| `pub_year` | `PubDate/Year` | 单独提取年份 |
| `pub_types` | `Article/PublicationTypeList/PublicationType` | 可多个,含 "Retracted Publication" |
| `mesh_headings` | `MeshHeadingList/MeshHeading` | DescriptorName(UI) + QualifierName(UI) |
| `language` | `Article/Language` | 2-3 字母 ISO 语言码 |
| `pmc_id` | `ArticleId[@IdType="pmc"]` | 带 "PMC" 前缀 |
| `keywords` | `Article/KeywordList/Keyword` | 作者关键词(非 MeSH |
| `pubmed_revised` | `MedlineCitation/DateRevised` | 不可靠,不应视为修订依据。仅 Year/Month/DayDATE 类型 |
| `grants` | `Article/GrantList/Grant` | GrantID + Agency + Country |
| `chemical_list` | `ChemicalList/Chemical` | RegistryNumber + NameOfSubstance(UI) |
| `suppl_mesh_list` | `SupplMeshList/SupplMeshName` | 补充概念词 |
| `databank_list` | `Article/DataBankList/DataBank` | DataBankName + AccessionNumberList |
| `trial_reg` | `Article/DataBankList/DataBank` | 从中提取 NCT/EudraCT/ChiCTR |
| `citation_status` | `MedlineCitation/@Status` | "MEDLINE","PubMed-not-MEDLINE","In-Data-Review","PubMed" |
| `date_completed` | `MedlineCitation/DateCompleted` | NLM 整条编目记录完成日(含 MeSH 标引在内的全部处理完成)。实践中作为 [MHDA] 近似值使用。仅 Year/Month/DayDATE 类型 |
| `meshed_date` | 系统内部字段 | MeSH 数据版本日期。通常=date_completed(MeSH 是编目最后一步,两者同天)。年更后=新的 date_completed。NULL=无MeSH。DATE 类型 |
| `publication_status` | `PubmedData/PublicationStatus` | "epublish" / "ppublish" / "aheadofprint" |
| `article_date` | `Article/ArticleDate` | [EPDAT] 电子出版日期(印刷版之前的日期)|
| `create_date` | `PubmedData/History/PubMedPubDate[@PubStatus="pubmed"]``MedlineCitation/DateCreated` | [CRDT] PubMed 记录创建日期。仅 Year/Month/DayDATE 类型 |
| `entrez_date` | `PubmedData/History/PubMedPubDate[@PubStatus="entrez"]` | [EDAT] PubMed 收录日期。仅 Year/Month/DayDATE 类型 |
| `pharmacological_actions` | `MedlineCitation/ChemicalList/PharmacologicalAction/NameOfSubstance` | [PA] 药理作用 [{name, ui}] |
| `cois_statement` | `Article/CoiStatement` | [COIS] 利益冲突声明 |
| `vernacular_title` | `Article/VernacularTitle` | [TT] 非英文标题的拉丁转写 |
| `is_preprint` | DOI prefix + journal name 检测 | 是否为预印本(非 PubMed 原始字段) |
| `auid_data` | `Author/Identifier[Source="ORCID"]` | [AUID] 作者标识符 [{type, value, author_index}] |
| `investigators` | `InvestigatorList/Investigator` | [IR] 多中心试验研究者 [{family, given, affiliation, identifiers}] |
| `personal_name_subjects` | `PersonalNameSubjectList/PersonalNameSubject` | [PS] 作为主题的人名 [{family, given}] |
| `publication_notes` | `PubmedData/PublicationNote` | [PUBN] 出版注释列表 |
### 2.2 global_tags — 肿瘤科标签树
```sql
CREATE TABLE global_tags (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 标签ID
mesh_ui VARCHAR(20), -- MeSH 唯一标识符(UI
name_zh VARCHAR(200) NOT NULL, -- 中文名称
name_en VARCHAR(200), -- 英文名称
path VARCHAR(500) NOT NULL, -- 标签路径(如 /癌种/肺癌/非小细胞肺癌)
level INTEGER NOT NULL DEFAULT 1, -- 层级深度
parent_id UUID REFERENCES global_tags(id) ON DELETE CASCADE, -- 父标签ID
tree_number VARCHAR(100), -- MeSH 树编号
entry_terms JSONB, -- MeSH 入口词列表(~200kNLM desc2025.asc 导入,用于 ATM 自动术语映射)
source VARCHAR(20) DEFAULT 'auto', -- 数据来源:manual(手工)/mesh(C04批量)/auto(管道懒创建)
is_active BOOLEAN DEFAULT FALSE, -- 是否已确认(True=用户可见,False=仅后台可见)
tag_category VARCHAR(30) NOT NULL DEFAULT 'cancer', -- 标签分类
sort_order INTEGER DEFAULT 0, -- 排序序号
icon VARCHAR(50), -- 图标名
is_selectable BOOLEAN NOT NULL DEFAULT TRUE, -- 是否可选(叶子节点)
article_count INTEGER DEFAULT 0, -- 关联文献数
updated_at TIMESTAMPTZ NOT NULL DEFAULT now() -- 更新时间
);
CREATE INDEX idx_tags_path ON global_tags(path);
CREATE INDEX idx_tags_parent ON global_tags(parent_id);
CREATE INDEX idx_tags_category ON global_tags(tag_category);
CREATE INDEX idx_tags_mesh_ui ON global_tags(mesh_ui) WHERE mesh_ui IS NOT NULL;
```
**tag_category 枚举**`cancer`(癌种) | `histology`(组织分型) | `gene`(驱动基因) | `treatment`(治疗方式) | `study_type`(研究类型) | `endpoint`(临床终点) | `scenario`(临床场景) | `journal_tier`(期刊等级)
### 2.3 global_literature_tags — 文献标签关联
```sql
CREATE TABLE global_literature_tags (
literature_id UUID NOT NULL REFERENCES global_literature(id) ON DELETE CASCADE, -- 文献ID
tag_id UUID NOT NULL REFERENCES global_tags(id) ON DELETE CASCADE, -- 标签ID
is_major BOOLEAN NOT NULL DEFAULT FALSE, -- 是否为主要主题(MeSH Major Topic
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 创建时间
PRIMARY KEY (literature_id, tag_id)
);
CREATE INDEX idx_glt_tag ON global_literature_tags(tag_id);
```
> 预估值:约 6000 万行(600万×10标签),B-tree 索引足够,不需要分区。两种查询都命中索引。
### 2.4 global_journals — 期刊库
```sql
CREATE TABLE global_journals (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 期刊ID
name VARCHAR(500) NOT NULL, -- 期刊名称
issn VARCHAR(20), -- 印刷版 ISSN
eissn VARCHAR(20), -- 电子版 ISSN
tier VARCHAR(5) NOT NULL DEFAULT '4', -- 期刊等级:1/2/3/4
impact_factor DOUBLE PRECISION, -- 影响因子
publisher VARCHAR(300), -- 出版商
article_count INTEGER DEFAULT 0, -- 收录文献数
is_active BOOLEAN DEFAULT TRUE, -- 是否活跃
priority_score REAL, -- 综合优先级评分 0-100
priority_source VARCHAR(20) DEFAULT 'auto', -- 优先级来源:auto/manual
specialty VARCHAR(50), -- 专科领域:oncology
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 创建时间
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 更新时间
UNIQUE (issn),
UNIQUE (name)
);
CREATE INDEX idx_gj_tier ON global_journals(tier);
CREATE INDEX idx_gj_priority ON global_journals(priority_score DESC);
```
**权重评分规则(priority_score):**
| 维度 | 规则 | 最高加分 |
|---|---|---|
| Tier 基础分 | 1→80, 2→65, 3→50, 4→20 | 80 |
| IF 加成分 | ≥20→+15, ≥10→+10, ≥5→+5, ≥2→+2 | 15 |
| 收录量加成分 | ≥5000→+10, ≥1000→+5, ≥100→+2 | 10 |
| 专科加成分 | specialty='oncology'→+10 | 10 |
封顶 100 分。`priority_source='manual'` 时自动任务跳过该行。用户关注的期刊查询时 +20。
**specialty 自动判定:** Tier 1-3 → `oncology`name 包含 oncol/cancer/tumor/neoplas → `oncology`;其余 NULL。
---
## 三、用户交互表
### 3.1 user_subscriptions — 关注领域
```sql
CREATE TABLE user_subscriptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 订阅ID
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- 用户ID
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, -- 租户ID
tag_id UUID NOT NULL REFERENCES global_tags(id) ON DELETE CASCADE, -- 标签ID
match_mode VARCHAR(10) NOT NULL DEFAULT 'standard', -- 匹配模式:loose/standard/strict
is_active BOOLEAN NOT NULL DEFAULT TRUE, -- 是否激活
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 创建时间
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 更新时间
UNIQUE (user_id, tag_id)
);
CREATE INDEX idx_us_user_active ON user_subscriptions(user_id, is_active);
CREATE INDEX idx_us_tag_active ON user_subscriptions(tag_id, is_active);
```
**match_mode**`loose`(任一命中) | `standard`(主标签命中,默认) | `strict`(全部命中)
### 3.2 user_feed — 每日推送 ⚠️ 需要分区
```sql
CREATE TABLE user_feed (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- FeedID
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- 用户ID
literature_id UUID NOT NULL REFERENCES global_literature(id) ON DELETE CASCADE, -- 文献ID
matched_tags JSONB NOT NULL DEFAULT '[]', -- 命中的标签列表
priority VARCHAR(20) NOT NULL DEFAULT 'related', -- 优先级:must_read/recommended/related
is_read BOOLEAN NOT NULL DEFAULT FALSE, -- 是否已读
read_at TIMESTAMPTZ, -- 阅读时间
is_dismissed BOOLEAN NOT NULL DEFAULT FALSE, -- 是否已忽略
pushed_at TIMESTAMPTZ NOT NULL DEFAULT now() -- 推送时间
);
CREATE INDEX idx_uf_user_push ON user_feed(user_id, pushed_at);
CREATE INDEX idx_uf_user_priority ON user_feed(user_id, is_dismissed, priority);
CREATE INDEX idx_uf_user_lit ON user_feed(user_id, literature_id);
```
> 预估值:年增数千万行。当前模型未分区,**生产需改为按月分区 + 自动删除旧分区**(保留最近3个月)。查询永远只走最新分区。
### 3.3 user_literature — 个人收藏
```sql
CREATE TABLE user_literature (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 收藏ID
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- 用户ID
literature_id UUID NOT NULL REFERENCES global_literature(id) ON DELETE CASCADE, -- 文献ID
folder_id UUID REFERENCES user_folders(id) ON DELETE SET NULL, -- 所属收藏夹
reading_status VARCHAR(20) NOT NULL DEFAULT 'unread', -- 阅读状态:unread/reading/read
rating INTEGER CHECK (rating >= 1 AND rating <= 5), -- 评分 1-5
personal_tags JSONB DEFAULT '[]', -- 个人标签
saved_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 收藏时间
read_started_at TIMESTAMPTZ, -- 开始阅读时间
read_finished_at TIMESTAMPTZ, -- 读完时间
UNIQUE (user_id, literature_id)
);
CREATE INDEX idx_ul_user_saved ON user_literature(user_id, saved_at);
CREATE INDEX idx_ul_literature ON user_literature(literature_id);
```
### 3.4 user_folders — 收藏夹
```sql
CREATE TABLE user_folders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 收藏夹ID
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- 用户ID
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, -- 租户ID
name VARCHAR(200) NOT NULL, -- 收藏夹名称
description TEXT, -- 描述
color VARCHAR(7) DEFAULT '#808080', -- 颜色(十六进制)
is_shared BOOLEAN NOT NULL DEFAULT FALSE, -- 是否共享
sort_order INTEGER DEFAULT 0, -- 排序序号
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 创建时间
updated_at TIMESTAMPTZ NOT NULL DEFAULT now() -- 更新时间
);
CREATE INDEX idx_folders_user ON user_folders(user_id);
CREATE INDEX idx_folders_shared ON user_folders(tenant_id, is_shared) WHERE is_shared = TRUE;
```
### 3.5 user_notes — 阅读笔记
```sql
CREATE TABLE user_notes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 笔记ID
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- 用户ID
literature_id UUID NOT NULL REFERENCES global_literature(id) ON DELETE CASCADE, -- 文献ID
content TEXT NOT NULL, -- 笔记内容
quote_text TEXT, -- 引用的原文
is_private BOOLEAN NOT NULL DEFAULT TRUE, -- 是否私密
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 创建时间
updated_at TIMESTAMPTZ NOT NULL DEFAULT now() -- 更新时间
);
CREATE INDEX idx_un_user ON user_notes(user_id, created_at);
CREATE INDEX idx_un_literature ON user_notes(literature_id);
```
### 3.6 user_pdf_highlights — PDF批注
```sql
CREATE TABLE user_pdf_highlights (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 批注ID
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- 用户ID
literature_id UUID NOT NULL REFERENCES global_literature(id) ON DELETE CASCADE, -- 文献ID
highlight_type VARCHAR(20) NOT NULL, -- 批注类型:highlight/underline/strikeout
page_number INTEGER NOT NULL, -- 页码
position JSONB NOT NULL, -- 位置坐标
text TEXT, -- 批注文本
note TEXT, -- 批注笔记
color VARCHAR(7) DEFAULT '#ffff00', -- 高亮颜色
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 创建时间
updated_at TIMESTAMPTZ NOT NULL DEFAULT now() -- 更新时间
);
CREATE INDEX idx_hl_user_lit ON user_pdf_highlights(user_id, literature_id);
```
### 3.7 user_feedback — 文献反馈
```sql
CREATE TABLE user_feedback (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 反馈ID
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- 用户ID
literature_id UUID REFERENCES global_literature(id) ON DELETE CASCADE, -- 文献ID(可为NULL
feedback_type VARCHAR(30) NOT NULL, -- 反馈类型
detail TEXT, -- 反馈详情
is_resolved BOOLEAN DEFAULT FALSE, -- 是否已处理
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 创建时间
UNIQUE (user_id, feedback_type)
);
```
> `literature_id` 可为 NULL(反馈不一定针对具体文献,也可以是针对搜索结果或系统功能)。`UNIQUE (user_id, feedback_type)` 确保同一用户不会重复提交同类型反馈。
### 3.8 user_journal_subscriptions — 期刊关注
```sql
CREATE TABLE user_journal_subscriptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 订阅ID
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- 用户ID
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, -- 租户ID
journal_id UUID NOT NULL REFERENCES global_journals(id) ON DELETE CASCADE, -- 期刊ID
is_active BOOLEAN DEFAULT TRUE, -- 是否激活
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 创建时间
updated_at TIMESTAMPTZ NOT NULL DEFAULT now() -- 更新时间
);
CREATE INDEX ix_ujs_user_active ON user_journal_subscriptions(user_id, is_active);
CREATE INDEX ix_ujs_journal ON user_journal_subscriptions(journal_id);
```
> 关注/取消关注采用软删除(`is_active`),与 `user_subscriptions` 模式一致。关注后,该期刊的新文献会自动推送到用户 Feed。
### 3.9 user_activity_logs — 用户活动日志
```sql
CREATE TABLE user_activity_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 日志ID
user_id UUID REFERENCES users(id) ON DELETE SET NULL, -- 用户ID
tenant_id UUID REFERENCES tenants(id) ON DELETE SET NULL, -- 租户ID
activity_type VARCHAR(50) NOT NULL, -- 活动类型
target_type VARCHAR(30), -- 目标类型(literature/search等)
target_id VARCHAR(200), -- 目标ID
detail JSONB, -- 详情 JSON
ip_address VARCHAR(45), -- 客户端IP
user_agent VARCHAR(500), -- 用户代理
session_id VARCHAR(100), -- 会话ID
duration_seconds INTEGER, -- 停留时长(秒)
created_at TIMESTAMPTZ NOT NULL DEFAULT now() -- 创建时间
);
CREATE INDEX ix_ual_user_created ON user_activity_logs(user_id, created_at);
CREATE INDEX ix_ual_type_created ON user_activity_logs(activity_type, created_at);
CREATE INDEX ix_ual_target ON user_activity_logs(target_type, target_id);
```
**activity_type 枚举:** `page_view`(页面访问) | `search`(搜索) | `view_literature`(查看文献) | `save_literature`(收藏) | `export`(导出) | `browse_journals`(浏览期刊) | `click_tag`(点击标签)
> 前端通过 `router.afterEach` 自动追踪 `page_view`;关键交互(搜索、收藏、导出等)通过 `trackAction()` 手动埋点。未登录用户不记录。配合 login_logs(登录安全)和 api_usage_logs(API 调用)构成完整的四维分析体系。
### 3.10 user_dismissed_tags — 用户负偏好标签(不感兴趣学习)
```sql
CREATE TABLE user_dismissed_tags (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 记录ID
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- 用户ID
tag_id UUID NOT NULL REFERENCES global_tags(id) ON DELETE CASCADE, -- 标签ID
dismiss_count INTEGER NOT NULL DEFAULT 1, -- 不感兴趣次数
last_dismissed_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 最后不感兴趣时间
UNIQUE(user_id, tag_id)
);
CREATE INDEX ix_udt_user_count ON user_dismissed_tags(user_id, dismiss_count);
```
**说明:** 用户点"不感兴趣"后,系统提取该文章的标签累计到 `dismiss_count`。当某标签的 `dismiss_count >= 3` 时,Feed 引擎不再为用户推送该标签下的文章。可在管理后台或用户设置中重置。`user_id` + `tag_id` 唯一约束确保每个用户对每个标签只有一条累计记录。
---
## 四、团队协作表
### 4.1 teams + team_members — 团队与成员
```sql
CREATE TABLE teams (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 团队ID
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, -- 租户ID
name VARCHAR(200) NOT NULL, -- 团队名称
description TEXT, -- 描述
created_by UUID REFERENCES users(id) ON DELETE SET NULL, -- 创建者
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 创建时间
updated_at TIMESTAMPTZ NOT NULL DEFAULT now() -- 更新时间
);
CREATE INDEX idx_teams_tenant ON teams(tenant_id);
CREATE TABLE team_members (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 成员ID
team_id UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE, -- 团队ID
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- 用户ID
role VARCHAR(20) NOT NULL DEFAULT 'member', -- 角色:member/lead
joined_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 加入时间
UNIQUE (user_id, team_id)
);
CREATE INDEX idx_tm_user_team ON team_members(user_id, team_id);
CREATE INDEX idx_tm_team ON team_members(team_id);
```
### 4.2 invitations + shared_folders — 邀请与共享
```sql
CREATE TABLE invitations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 邀请ID
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, -- 租户ID
inviter_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- 邀请人
email VARCHAR(320) NOT NULL, -- 被邀请人邮箱
role VARCHAR(20) NOT NULL DEFAULT 'viewer', -- 被邀请角色
token VARCHAR(200) UNIQUE NOT NULL, -- 邀请令牌
status VARCHAR(20) NOT NULL DEFAULT 'pending', -- 状态:pending/accepted/expired
expires_at TIMESTAMPTZ NOT NULL, -- 过期时间
accepted_at TIMESTAMPTZ, -- 接受时间
created_at TIMESTAMPTZ NOT NULL DEFAULT now() -- 创建时间
);
CREATE TABLE shared_folders (
folder_id UUID NOT NULL REFERENCES user_folders(id) ON DELETE CASCADE, -- 收藏夹ID
target_type VARCHAR(20) NOT NULL, -- 共享目标类型:team/user
target_id UUID NOT NULL, -- 共享目标ID
permission VARCHAR(20) NOT NULL DEFAULT 'view', -- 权限:view/edit
created_by UUID REFERENCES users(id) ON DELETE SET NULL, -- 共享发起人
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 创建时间
PRIMARY KEY (folder_id, target_type, target_id)
);
```
### 4.3 journal_club_queue — Journal Club 文献排队
```sql
CREATE TABLE journal_club_queue (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 排队ID
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, -- 租户ID
literature_id UUID NOT NULL REFERENCES global_literature(id) ON DELETE CASCADE, -- 文献ID
suggested_by UUID REFERENCES users(id) ON DELETE SET NULL, -- 建议人
title VARCHAR(500), -- 讨论标题
scheduled_date DATE, -- 排期日期
presenter_id UUID REFERENCES users(id) ON DELETE SET NULL, -- 主讲人
status VARCHAR(20) NOT NULL DEFAULT 'queued', -- 状态:queued/completed/cancelled
discussion_notes TEXT, -- 讨论纪要
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 创建时间
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 更新时间
UNIQUE (tenant_id, literature_id, scheduled_date)
);
CREATE INDEX idx_jc_tenant ON journal_club_queue(tenant_id, status);
```
---
## 五、业务扩展表
### 5.1 drug_approvals — FDA/NMPA/EMA 药品审批
```sql
CREATE TABLE drug_approvals (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 审批ID
drug_name VARCHAR(500) NOT NULL, -- 药品商品名
generic_name VARCHAR(500) NOT NULL, -- 通用名/活性成分
target VARCHAR(200), -- 靶点/作用机制
indication TEXT NOT NULL, -- 获批适应症
cancer_type_tag_id UUID REFERENCES global_tags(id) ON DELETE SET NULL, -- 关联癌种标签
approval_agency VARCHAR(10) NOT NULL, -- 审批机构:FDA/NMPA/EMA
approval_type VARCHAR(30), -- 审批类型:regular/accelerated/breakthrough
approval_date DATE NOT NULL, -- 批准日期
pivotal_trial_nct VARCHAR(20), -- 关键试验 NCT 号
pivotal_trial_pmid INTEGER, -- 关键试验 PMID(非外键)
source_url VARCHAR(1000), -- 数据来源 URL
created_at TIMESTAMPTZ NOT NULL DEFAULT now() -- 创建时间
);
CREATE INDEX idx_da_agency_date ON drug_approvals(approval_agency, approval_date);
```
> `pivotal_trial_pmid` 是普通 INTEGER 而非外键:因为试验可能在文献入库前就已记录,删除文献也不应级联删除审批记录。
### 5.2 guideline_versions + guideline_evidence — 指南版本与证据
```sql
CREATE TABLE guideline_versions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 指南版本ID
guideline_source VARCHAR(10) NOT NULL, -- 指南来源:NCCN/ASCO/CSCO
cancer_type_tag_id UUID NOT NULL REFERENCES global_tags(id) ON DELETE RESTRICT, -- 关联癌种标签
version VARCHAR(30) NOT NULL, -- 版本号
publish_date DATE NOT NULL, -- 发布日期
change_summary_zh TEXT, -- 中文变更摘要
change_summary_en TEXT, -- 英文变更摘要
pdf_url VARCHAR(1000), -- PDF 下载 URL
source_url VARCHAR(1000), -- 来源 URL
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 创建时间
UNIQUE (guideline_source, cancer_type_tag_id, version)
);
CREATE TABLE guideline_evidence (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 证据ID
guideline_version_id UUID NOT NULL REFERENCES guideline_versions(id) ON DELETE CASCADE, -- 指南版本ID
literature_id UUID NOT NULL REFERENCES global_literature(id) ON DELETE CASCADE, -- 文献ID
recommendation TEXT, -- 推荐意见
recommendation_level VARCHAR(10), -- 推荐级别:1A/1B/2A/2B/3
treatment_line VARCHAR(20), -- 治疗线数:first/second/third
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 创建时间
UNIQUE (guideline_version_id, literature_id)
);
```
> `guideline_versions.cancer_type_tag_id` 使用 `ON DELETE RESTRICT`:指南指向的癌种标签禁止被删除。
---
## 六、审批表(企业版)
### 6.1 approval_workflows — 审批工作流
```sql
CREATE TABLE approval_workflows (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 工作流ID
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, -- 租户ID
literature_id UUID NOT NULL REFERENCES global_literature(id) ON DELETE CASCADE, -- 文献ID
submitted_by UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- 提交人
status VARCHAR(20) NOT NULL DEFAULT 'pending', -- 状态:pending/approved/rejected
current_step INTEGER NOT NULL DEFAULT 1, -- 当前步骤
total_steps INTEGER NOT NULL DEFAULT 1, -- 总步骤数
comment TEXT, -- 备注
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 创建时间
updated_at TIMESTAMPTZ NOT NULL DEFAULT now() -- 更新时间
);
CREATE TABLE approval_steps (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 步骤ID
workflow_id UUID NOT NULL REFERENCES approval_workflows(id) ON DELETE CASCADE, -- 工作流ID
step_order INTEGER NOT NULL, -- 步骤序号
approver_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- 审批人
status VARCHAR(20) NOT NULL DEFAULT 'pending', -- 状态:pending/approved/rejected
comment TEXT, -- 审批意见
decided_at TIMESTAMPTZ, -- 审批时间
created_at TIMESTAMPTZ NOT NULL DEFAULT now() -- 创建时间
);
```
### 6.2 ai_provider_config — AI 供应商配置
```sql
CREATE TABLE ai_provider_config (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 配置ID
provider VARCHAR(50) NOT NULL, -- AI 供应商:deepseek/openai/claude
api_key VARCHAR(500) NOT NULL, -- API Key(加密存储)
base_url VARCHAR(500) NOT NULL, -- API 基础 URL
model_name VARCHAR(100) NOT NULL, -- 模型名称
temperature INTEGER NOT NULL DEFAULT 3, -- 温度参数(0-10,展示为0.0-1.0
is_active BOOLEAN NOT NULL DEFAULT TRUE, -- 是否激活
updated_by UUID REFERENCES users(id) ON DELETE SET NULL, -- 最后修改人
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 创建时间
updated_at TIMESTAMPTZ NOT NULL DEFAULT now() -- 更新时间
);
```
---
## 七、运营与基础设施表
### 7.1 pipeline_runs — 数据管道日志
```sql
CREATE TABLE pipeline_runs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 运行ID
run_type VARCHAR(20) NOT NULL, -- 运行类型:daily_ftp_update / baseline_import / daily_citation_update / manual_full / etc.
status VARCHAR(20) NOT NULL DEFAULT 'running', -- 状态:running/completed/failed
files_processed INTEGER DEFAULT 0, -- 处理文件数
articles_total INTEGER DEFAULT 0, -- 总文献数
articles_new INTEGER DEFAULT 0, -- 新增文献数
articles_updated INTEGER DEFAULT 0, -- 更新文献数
articles_deleted INTEGER DEFAULT 0, -- 删除文献数
articles_filtered INTEGER DEFAULT 0, -- 过滤文献数
users_matched INTEGER DEFAULT 0, -- 匹配用户数
feeds_generated INTEGER DEFAULT 0, -- 生成 Feed 数
error_log TEXT, -- 错误日志
processed_date DATE, -- FTP 增量检查点:已处理的 EDAT 日期(2026-07-25 新增)
metadata JSON, -- 扩展元数据(基线年份、文件序号范围等)(2026-07-25 新增)
started_at TIMESTAMPTZ, -- 开始时间
completed_at TIMESTAMPTZ, -- 完成时间
created_at TIMESTAMPTZ NOT NULL DEFAULT now() -- 创建时间
);
CREATE INDEX idx_pr_type ON pipeline_runs(run_type, created_at);
```
### 7.2 tenant_subscriptions — Stripe 订阅
```sql
CREATE TABLE tenant_subscriptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 订阅ID
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE UNIQUE, -- 租户ID
stripe_customer_id VARCHAR(100), -- Stripe 客户ID
stripe_subscription_id VARCHAR(100), -- Stripe 订阅ID
plan_id VARCHAR(50), -- 套餐ID
status VARCHAR(30) NOT NULL DEFAULT 'active', -- 状态:active/past_due/canceled
current_period_start TIMESTAMPTZ NOT NULL, -- 当前计费周期开始
current_period_end TIMESTAMPTZ NOT NULL, -- 当前计费周期结束
cancel_at_period_end BOOLEAN DEFAULT FALSE, -- 是否到期取消
trial_end TIMESTAMPTZ, -- 试用结束时间
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 创建时间
updated_at TIMESTAMPTZ NOT NULL DEFAULT now() -- 更新时间
);
CREATE INDEX idx_ts_stripe_sub ON tenant_subscriptions(stripe_subscription_id);
CREATE TABLE subscription_events ( -- 订阅事件日志
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 事件ID
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, -- 租户ID
event_type VARCHAR(30) NOT NULL, -- 事件类型
from_plan VARCHAR(30), -- 原套餐
to_plan VARCHAR(30), -- 目标套餐
stripe_event_id VARCHAR(100), -- Stripe 事件ID
metadata JSONB, -- 附加元数据
created_at TIMESTAMPTZ NOT NULL DEFAULT now() -- 创建时间
);
CREATE INDEX idx_se_tenant ON subscription_events(tenant_id);
```
### 7.3 payment_orders — 支付订单
```sql
CREATE TABLE payment_orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 订单ID
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, -- 租户ID
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- 用户ID
plan VARCHAR(30) NOT NULL, -- 套餐:pro/team/enterprise
amount INTEGER NOT NULL, -- 金额(分)
status VARCHAR(20) NOT NULL DEFAULT 'pending', -- 状态:pending/paid/expired/cancelled
out_trade_no VARCHAR(64) UNIQUE NOT NULL, -- 商户订单号
prepay_id VARCHAR(128), -- 预支付ID(微信)
code_url VARCHAR(512), -- 支付二维码 URL
pay_time TIMESTAMPTZ, -- 支付时间
transaction_id VARCHAR(64), -- 支付平台交易ID
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 创建时间
updated_at TIMESTAMPTZ NOT NULL DEFAULT now() -- 更新时间
);
CREATE INDEX idx_po_tenant ON payment_orders(tenant_id);
```
### 7.4 api_keys + api_usage_logs — API密钥与使用日志
```sql
CREATE TABLE api_keys (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 密钥ID
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, -- 租户ID
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- 创建者
name VARCHAR(200) NOT NULL, -- 密钥名称
key_prefix VARCHAR(20) NOT NULL, -- 密钥前缀(便于识别)
key_hash VARCHAR(128) NOT NULL, -- 密钥哈希
scopes JSONB DEFAULT '[]', -- 权限范围
last_used_at TIMESTAMPTZ, -- 最后使用时间
expires_at TIMESTAMPTZ, -- 过期时间
is_active BOOLEAN DEFAULT TRUE, -- 是否激活
created_at TIMESTAMPTZ NOT NULL DEFAULT now() -- 创建时间
);
CREATE INDEX idx_ak_tenant_active ON api_keys(tenant_id, is_active);
CREATE TABLE api_usage_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 日志ID
api_key_id UUID REFERENCES api_keys(id) ON DELETE SET NULL, -- API密钥ID
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, -- 租户ID
endpoint VARCHAR(300) NOT NULL, -- 请求端点
method VARCHAR(10) NOT NULL, -- HTTP 方法
status_code INTEGER, -- HTTP 状态码
response_ms INTEGER, -- 响应时间(毫秒)
ip_address VARCHAR(45), -- 请求IP
created_at TIMESTAMPTZ NOT NULL DEFAULT now() -- 创建时间
);
CREATE INDEX idx_aul_tenant ON api_usage_logs(tenant_id);
CREATE INDEX idx_aul_tenant_created ON api_usage_logs(tenant_id, created_at);
```
### 7.5 export_jobs — 导出任务
```sql
CREATE TABLE export_jobs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 任务ID
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- 用户ID
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, -- 租户ID
export_format VARCHAR(10) NOT NULL, -- 导出格式:ris/bibtex/csv/pdf
citation_style VARCHAR(50), -- 引文格式
filter_params JSONB, -- 筛选参数
status VARCHAR(20) NOT NULL DEFAULT 'pending', -- 状态:pending/processing/done/failed
file_url VARCHAR(1000), -- 文件下载 URL
file_size_bytes INTEGER, -- 文件大小(字节)
error_message TEXT, -- 错误信息
total_items INTEGER, -- 导出条目数
completed_at TIMESTAMPTZ, -- 完成时间
expires_at TIMESTAMPTZ, -- 文件过期时间
created_at TIMESTAMPTZ NOT NULL DEFAULT now() -- 创建时间
);
CREATE INDEX idx_ej_user_status ON export_jobs(user_id, status);
```
### 7.6 system_notifications — 系统通知
```sql
CREATE TABLE system_notifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 通知ID
title VARCHAR(300) NOT NULL, -- 通知标题
content TEXT, -- 通知内容
target_type VARCHAR(20) NOT NULL DEFAULT 'all', -- 目标类型:all/tenant/user
target_id UUID, -- 目标ID(对应租户或用户)
is_read BOOLEAN DEFAULT FALSE, -- 是否已读
read_at TIMESTAMPTZ, -- 阅读时间
created_at TIMESTAMPTZ NOT NULL DEFAULT now() -- 创建时间
);
```
---
## 八、系统评价 / Meta 分析
### 8.1 systematic_reviews — 系统评价项目
```sql
CREATE TABLE systematic_reviews (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 评价项目ID
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- 创建者
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, -- 租户ID
title VARCHAR(500) NOT NULL, -- 评价标题
objective TEXT, -- 研究目的
protocol TEXT, -- 研究方案
inclusion_criteria JSONB DEFAULT '[]', -- 纳入标准 [str]
exclusion_criteria JSONB DEFAULT '[]', -- 排除标准 [str]
databases_searched JSONB DEFAULT '[]', -- 检索数据库 [str]
search_strategy JSONB DEFAULT '{}', -- 检索策略 {pubmed: "..."}
prisma_stage VARCHAR(20) DEFAULT 'identification', -- PRISMA 阶段
status VARCHAR(20) DEFAULT 'draft', -- 状态:draft/active/completed/archived
notes TEXT, -- 备注
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 创建时间
updated_at TIMESTAMPTZ NOT NULL DEFAULT now() -- 更新时间
);
```
**prisma_stage 流转**`identification → screening → eligibility → included`
**status**`draft / active / completed / archived`
### 8.2 review_literatures — 系统评价文献
```sql
CREATE TABLE review_literatures (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 记录ID
review_id UUID NOT NULL REFERENCES systematic_reviews(id) ON DELETE CASCADE, -- 系统评价ID
literature_id UUID NOT NULL REFERENCES global_literature(id) ON DELETE CASCADE, -- 文献ID
stage VARCHAR(20) NOT NULL DEFAULT 'screening', -- 当前阶段
exclusion_reason TEXT, -- 排除原因
notes TEXT, -- 备注
quality_score VARCHAR(10), -- 质量评分:high/moderate/low/unclear
quality_detail JSONB DEFAULT '{}', -- 质量评估细节 {domain: "low", ...}
pico JSONB, -- 评价级 PICO(与文献库隔离)
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 创建时间
updated_at TIMESTAMPTZ NOT NULL DEFAULT now() -- 更新时间
);
```
> `pico` 字段与 `global_literature.pico` 隔离:同一篇文献被多个系统评价引用时,各评价可独立编辑 PICO。
---
## 九、MDT 多学科协作
### 9.1 mdt_cases — MDT 病例
```sql
CREATE TABLE mdt_cases (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 病例ID
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, -- 租户ID
title VARCHAR(300) NOT NULL, -- 病例标题
patient_summary TEXT NOT NULL, -- 患者摘要
cancer_type VARCHAR(100), -- 癌种(冗余字段,用于快速筛选)
status VARCHAR(20) DEFAULT 'open', -- 状态:open/closed
created_by UUID REFERENCES users(id) ON DELETE SET NULL, -- 创建者
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- 创建时间
updated_at TIMESTAMPTZ NOT NULL DEFAULT now() -- 更新时间
);
```
### 9.2 mdt_decisions — MDT 决策
```sql
CREATE TABLE mdt_decisions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 决策ID
case_id UUID NOT NULL REFERENCES mdt_cases(id) ON DELETE CASCADE, -- 关联病例
conclusion TEXT NOT NULL, -- 决策结论
meeting_date TIMESTAMPTZ, -- 会议时间
participants JSONB DEFAULT '[]', -- 参会人员 [{user_id, display_name, role}]
decided_by UUID REFERENCES users(id) ON DELETE SET NULL, -- 决策者
decided_at TIMESTAMPTZ NOT NULL DEFAULT now() -- 决策时间
);
```
### 9.3 mdt_evidence — MDT 引用证据
```sql
CREATE TABLE mdt_evidence (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- 证据ID
case_id UUID NOT NULL REFERENCES mdt_cases(id) ON DELETE CASCADE, -- 关联病例
decision_id UUID REFERENCES mdt_decisions(id) ON DELETE SET NULL, -- 关联决策
literature_id UUID REFERENCES global_literature(id) ON DELETE SET NULL, -- 关联文献(可为NULL
pmid INTEGER, -- PubMed ID(备用)
citation TEXT, -- 引文文本
relevance_note TEXT, -- 相关性说明
added_by UUID REFERENCES users(id) ON DELETE SET NULL, -- 添加人
created_at TIMESTAMPTZ NOT NULL DEFAULT now() -- 创建时间
);
```
> `literature_id` 和 `pmid` 双字段:文献可能尚未入库(只有 PMID),此时先存 PMID,待 pipeline 灌入后关联。
---
## 十、全表统计数据
| # | 表名 | 预估数据量 | 说明 |
|---|------|:---:|---|
| 1 | tenants | 百~千 | |
| 2 | users | 千~万 | |
| 3 | user_tenants | 千~万 | |
| 4 | login_logs | 万~百万 | |
| 5 | global_literature | **~600万** | B-tree 索引足够 |
| 6 | global_tags | ~800 | |
| 7 | global_literature_tags | **~6000万** | B-tree 索引足够 |
| 8 | global_journals | ~9000 | priority_score DESC 索引 |
| 9 | user_subscriptions | 千~万 | |
| 10 | user_journal_subscriptions | 千~万 | |
| 11 | user_feed | **年增数千万** | ⚠️ 生产需按月分区,保留3个月 |
| 12 | user_literature | 万~百万 | |
| 13 | user_folders | 千~万 | |
| 14 | user_notes | 万~十万 | |
| 15 | user_pdf_highlights | 万~十万 | |
| 16 | user_feedback | 千 | |
| 17 | teams | 千 | |
| 18 | team_members | 千~万 | |
| 19 | invitations | 千 | |
| 20 | shared_folders | 千 | |
| 21 | journal_club_queue | 千 | |
| 22 | drug_approvals | ~500/年 | |
| 23 | guideline_versions | ~200/年 | |
| 24 | guideline_evidence | 万 | |
| 25 | approval_workflows | 千 | |
| 26 | approval_steps | 千 | |
| 27 | ai_provider_config | ~5 | |
| 28 | pipeline_runs | ~365/年 | |
| 29 | tenant_subscriptions | 百~千 | |
| 29 | subscription_events | 千 | |
| 30 | payment_orders | 千 | |
| 31 | api_keys | 百 | |
| 32 | api_usage_logs | 百万 | 按月分区(可选) |
| 33 | system_notifications | 千 | |
| 34 | systematic_reviews | 百~千 | |
| 35 | review_literatures | 千~万 | |
| 36 | mdt_cases | 百~千 | |
| 37 | mdt_decisions | 百~千 | |
| 38 | mdt_evidence | 千 | |
| 39 | export_jobs | 千 | |