Files
backend/docs/03-数据库设计.md
34047007@qq.com c50831d1d8
CI / backend (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
test: 修复全量测试套件 — 1004 passed,消除注册限速 429 等故障
全部 auth 类 fixture 从 HTTP register 改为 DB 直接创建,绕过 6次/小时 IP 限速
(conftest、test_literature、test_subscriptions、test_verification、test_auth、
 test_approvals、test_notifications、test_user_settings 共 8 个文件)。
同步修复 europe_pmc 解析、admin pipeline、ai_summary、email_service、
security/permissions 等共 21 个文件的断言和适配问题。
2026-07-27 22:19:27 +08:00

67 KiB
Raw Permalink Blame History

数据库设计

日期时间类型规范

TIMESTAMPTZ 为主。纯日期字段(出生日期、发表日期、批准日期、排期日期、PubMed 纯日期字段)用 DATE

-- ✅ 正确
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 — 租户

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 — 用户

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 — 用户-租户关联

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 — 登录日志

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 — 全局文献库

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】
    tag_ids               UUID[],                                      -- 反范式标签ID数组 + GIN,免 JOIN global_literature_tags【由 tag_service.tag_article() 维护,使用 PostgreSQL unnest() + ARRAY() 构造,非 PubMed 字段】
    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()           -- 更新时间
);
-- ═══ B-tree 索引(既有筛选) ═══
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 @> 查询

-- ═══ BRIN 索引(时序数据,体积小两个数量级) ═══
CREATE INDEX ix_gl_pub_date_brin ON global_literature USING brin(pub_date);
CREATE INDEX ix_gl_pub_year_brin ON global_literature USING brin(pub_year);

-- ═══ Covering Indexdate 排序 + 高频引用列,index-only scan ═══
CREATE INDEX ix_gl_pub_date_covering ON global_literature(pub_date, id) INCLUDE (journal_issn, cited_by_count, is_oa, retracted, is_negative_result, is_preprint, journal, pub_year, article_date, doi, pmc_id, language, citation_status);

-- ═══ Partial Index(稀疏布尔,体积缩小 99.9%) ═══
CREATE INDEX ix_gl_retracted_true ON global_literature(retracted) WHERE retracted = TRUE;
CREATE INDEX ix_gl_is_oa_true ON global_literature(is_oa) WHERE is_oa = TRUE;
CREATE INDEX ix_gl_is_negative_true ON global_literature(is_negative_result) WHERE is_negative_result = TRUE;
CREATE INDEX ix_gl_is_preprint_true ON global_literature(is_preprint) WHERE is_preprint = TRUE;

-- ═══ GIN 反范式标签数组索引(标签筛选免 JOIN global_literature_tags ═══
CREATE INDEX ix_gl_tag_ids_gin ON global_literature USING gin(tag_ids);
CREATE INDEX ix_global_literature_journal_iso_trgm ON global_literature USING gin(journal_iso gin_trgm_ops);  -- PubMed [TA] ILIKE 兜底

预估值:约 600 万条(PubMed 中肿瘤相关的历史累积),单表不需要分区

全文搜索:

search_tsvTSVECTOR 类型,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 索引重建说明: 迁移 f80dca5baa02add_pico_column_to_review_literatures)的 upgrade() 误将 ix_gl_search_tsv 删除后未重建,导致所有 tsvector 搜索走全表扫描。迁移 0314f4d28728rebuild_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_revisedNLM 定义为不可靠)。publication_statusepublish / ppublish / aheadofprint 出版阶段标识)。

注意: create_dateentrez_datedate_completedmeshed_datepubmed_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 — 肿瘤科标签树

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 — 文献标签关联

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 — 期刊库

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 → oncologyname 包含 oncol/cancer/tumor/neoplas → oncology;其余 NULL。


三、用户交互表

3.1 user_subscriptions — 关注领域

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_modeloose(任一命中) | standard(主标签命中,默认) | strict(全部命中)

3.2 user_feed — 每日推送 ⚠️ 需要分区

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 — 个人收藏

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 — 收藏夹

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 — 阅读笔记

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批注

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 — 文献反馈

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 — 期刊关注

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 — 用户活动日志

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 — 用户负偏好标签(不感兴趣学习)

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 — 团队与成员

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 — 邀请与共享

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 文献排队

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 药品审批

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 — 指南版本与证据

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 — 审批工作流

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 供应商配置

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 — 数据管道日志

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 订阅

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 — 支付订单

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密钥与使用日志

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 — 导出任务

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 — 系统通知

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 — 系统评价项目

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

statusdraft / active / completed / archived

8.2 review_literatures — 系统评价文献

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 病例

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 决策

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 引用证据

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_idpmid 双字段:文献可能尚未入库(只有 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