Files
backend/CLAUDE.md
T
34047007@qq.com 036494ae35
CI / backend (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
chore: add Gitea service to docker-compose, fix gitignore
- Gitea (postgres-backed) on ports 3000/2222
- Exclude .claude/ dir and *.db from git
- Update CLAUDE.md with Gitea remote info
2026-07-27 08:13:53 +08:00

10 KiB
Raw Blame History

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Development Commands

# Start dev environment (backend + frontend)
cd backend && python scripts/seed_data.py && uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
cd frontend && npm install && npm run dev

# Run tests (611 tests, ~3min)
cd backend && python -m pytest tests/ -v --no-cov

# Build frontend (type-check + prod bundle, ~5s)
cd frontend && npm run build

# Database migrations (Alembic)
cd backend && python -m alembic -c alembic/alembic.ini upgrade head    # 应用迁移
cd backend && python -m alembic -c alembic/alembic.ini downgrade -1   # 回滚一步
cd backend && python -m alembic -c alembic/alembic.ini history         # 查看历史
cd backend && python -m alembic -c alembic/alembic.ini current         # 查看当前版本
# 自动生成迁移(修改模型后执行):
cd backend && python -m alembic -c alembic/alembic.ini revision --autogenerate -m "变更说明"

# Docker deployment
docker compose up -d                    # development(自动运行迁移)
docker compose -f docker-compose.prod.yml up -d  # production(自动运行迁移)

# PubMed pipeline (real data)
curl -X POST localhost:8000/api/v1/admin/pipeline/run -H "Authorization: Bearer $(admin_token)"
# FTP baseline import (first-time full data)
cd backend && python scripts/pubmed_baseline.py --dir /path/to/pubmed/baseline/ --demo
# 批量刷新被引次数(PubMed elink,免费,3 req/s。全库约N分钟/N秒)
curl -X POST localhost:8000/api/v1/admin/pipeline/refresh-citations -H "Authorization: Bearer $(admin_token)"
curl -X POST "localhost:8000/api/v1/admin/pipeline/refresh-citations?limit=500" -H "Authorization: Bearer $(admin_token)"

Architecture Overview

Database Migrations (Alembic)

Alembic 管理数据库 schema 版本。初始迁移 alembic/versions/e56e408b2208_initial_schema.py 包含所有 32 个模型的建表语句。

工作流:

  1. 修改 Python 模型(增/删/改字段)
  2. 运行 alembic revision --autogenerate -m "描述" 自动生成迁移脚本
  3. 检查生成的脚本,确认无误
  4. 运行 alembic upgrade head 应用到数据库

部署: Docker Compose 启动时会自动执行 alembic upgrade head

模型修改后务必同步文档: 每次通过 migration 增/删/改字段后,必须同步更新 docs/03-数据库设计.md 中对应表的 CREATE TABLE 定义和索引,保持与模型代码一致。

Multi-Tenant Isolation

Two levels: ContextVar (application) + PostgreSQL RLS (database, production only). Every tenant-scoped request sets tenant_ctx via get_current_user (not middleware — avoids connection pool races). The JWT access token carries tid (tenant_id) and is_superuser. Admin routes (/admin/*) enforce require_superuser via router-level dependency — demo users get 403.

Middleware Stack (inside→out)

CORS → RateLimit → PerformanceMonitor → SecurityHeaders → CSRFProtection → CSRFCookie → Trace

Rate limiter caches plan quotas 5 minutes. CSRF exempts Bearer tokens and /auth/*, /public/*, /captcha/*.

Backend Layer Pattern

api/v1/  →  schemas/  →  services/  →  models/  (DB)
                ↑
          core/ (security, permissions, tenant_ctx, middleware)

Services never import from api/. Models never import from services/. core/plans.py defines Free/Pro/Team/Enterprise feature matrix — use get_tenant_plan() to check feature access.

Database

39 SQLAlchemy 2.0 models. All TIMESTAMPTZ columns use server_default=func.now(). Pure date fields (pub_date, approval_date) use DATE.

user_feed is the only table needing partitioning — PARTITION BY RANGE (pushed_at) monthly.

Feed Engine

generate_feeds_for_literature(): new article → match tags against active subscriptions → compute priority (must_read/recommended/related) → insert user_feed. Called after every PubMed pipeline run.

Frontend Route Architecture

Three layouts: PublicLayout (no auth, no sidebar), AuthLayout (centered card), AppLayout (sidebar + header). Admin uses its own AdminLayout. All pages lazy-loaded via () => import(...). Auth guard in router.beforeEach() checks auth.isAuthenticated.

PubMed Pipeline

FTP 每日更新文件为主数据源2026-07-25 决策)。取代旧 E-utilities API 多路搜索策略。

日常运行:

  1. FTP 下载 pubmed25updateNNNN.xml.gz(每日 ~10MB,含全部新增/修改/删除记录)
  2. 解析_extract_article()(复用 pubmed_baseline.py 的 lxml 解析器)
  3. 过滤_is_oncology() 按 MeSH 肿瘤科筛选(复用 pubmed_baseline.py
  4. 处理<PubmedArticle>_update_lit_from_article() upsert<DeleteCitation> → 标记 retracted=True
  5. 打标: MeSH UI → global_tags lookup → global_literature_tags INSERT
  6. 检查点pipeline_runs.processed_date 记录已处理的 EDAT 日期

降级:FTP 不可用时,回退到 pubmed_api.pyNCBI E-utilities3 req/s)的 reldate=1&datetype=edat 查询。

ARQ Scheduled Tasks (backend/app/tasks/worker.py)

定时任务使用 ARQ (Redis-backed),所有时间均为 UTC

任务 函数 Cron (UTC) 北京时间 说明
每日 FTP 增量 daily_ftp_update 03:07 每天 11:07 FTP 下载更新文件,处理新增/修改/删除(取代精搜+宽搜+retagger
引用更新 daily_citation_update 05:13 每天 13:13 刷新最近文献被引次数
摘要邮件 daily_digest_task 22:30 每天 06:30 (次日) 每日摘要推送

旧任务已移除: daily_pubmed_pipelineMAJR 精搜)、weekly_broad_pipelineTitle/Abstract 宽搜)和 mesh_retagger.py(回查)已被 daily_ftp_update 完全取代。FTP 每日更新文件包含所有新/改/删记录,覆盖 MeSH in-process→medline 过渡,不再需要多路搜索和回查。

手动触发:

# FTP 增量更新
curl -X POST localhost:8000/api/v1/admin/pipeline/run -H "Authorization: Bearer $(admin_token)"
# 批量刷新被引次数
curl -X POST localhost:8000/api/v1/admin/pipeline/refresh-citations -H "Authorization: Bearer $(admin_token)"
curl -X POST "localhost:8000/api/v1/admin/pipeline/refresh-citations?limit=500" -H "Authorization: Bearer $(admin_token)"

每条文献入库时自动查询被引次数pubmed_api.py:fetch_citedby_counts(),通过 NCBI elink 免费接口,200 篇/req)。每日定时任务 daily_citation_update 刷新最近文献的被引。管理后台也支持手动全量刷新。

技术要点:

  • 接口:elink.fcgi?dbfrom=pubmed&linkname=pubmed_pubmed_citedin&id=PMID1,PMID2,...
  • 限速:无 API Key 3 req/s,有 key 10 req/s
  • 批量 200 篇/次,全库 1 万篇约需 15-20 分钟免费模式
  • 更新 global_literature.cited_by_count + updated_at(所以 🔄 徽标会联动)
  • 新文献引用数可能为 0(刚发表,随后每次定时刷新自动更新)

NLM 行为要点:

  • DateRevised 不可靠——NLM 明确说不应依赖它判断 revision(如 MeSH 年更大批改记录但不一定赋值)。判 new/revised 的唯一稳妥方式是 PMID 撞库
  • _update_lit_from_article() 对已存在 PMID 直接覆盖所有字段,不做"是否变了"的检查
  • 每年 11 月中–12 月中是 MeSH 年更期,indexed 记录暂停灌入,只发 in-process/publisher。此时宽搜补充尤为重要

PMC OA TDM (Full Text)

为未来深度学习 NLP(药物-靶点关系抽取、临床试验结构化提取)做数据准备。

PMC OA 覆盖: 约 15% 的 PubMed 文献有 PMC 全文,但高影响力期刊 OA 比例远高于此,对 oncology 领域数据挖掘足够。

数据流: PubMed efetch 已返回 pmc_id + is_oapmc_oa.py 用 OA Service API 下载 XML → jats_parser.py 解析成结构化 sections。

存储策略: 存 parsed sections JSONfull_text_sections 字段),不存原始 XML。sections 结构:

{"sections": [{"heading": "Introduction", "text": "..."}, {"heading": "Methods", "text": "..."}],
 "chemicals": [{"name": "Trastuzumab", "registry_number": "..."}],
 "references": [{"pmid": 12345, "title": "..."}],
 "tables": [{"caption": "Table 1", "data": "..."}]}

实现(Phase 1): app/services/pmc_oa.py — OA Service API 客户端;app/services/jats_parser.py — JATS XML 解析;GlobalLiterature.full_text_sections JSON 字段(Alembic 迁移);在 _run_pipeline() 中新增 OA 全文抓取阶段。Phase 2 回填现有 OA 文献。Phase 3 按需上 NLP 模型。

技术要点:

  • APIhttps://www.ncbi.nlm.nih.gov/pmc/utils/oa/oa.fcgi?id=PMCID → XML 响应
  • PMCID ↔ PMID 映射已在 PubMed efetch 阶段完成(pmc_id 字段)
  • JATS XML 按 <sec> 标签组织,<sec-title> + 段落文本即可提取结构化 sections
  • <chemical> 标签含 Registry Number,可对接 PubChem
  • 限速:共用 NCBI E-utilities 池(3 req/s 无 key10 req/s 有 key
  • _run_pipeline() 集成:在打标 + Feed 生成后,对 is_oa=True 且无 full_text_sections 的文献执行

Key Design Decisions

  • No multi-specialty in one deployment. One codebase, one YAML config, one Docker stack per specialty. Oncology is the first.
  • Personal → Team upgrade is zero-data-migration. Personal user's tenant_id stays the same; only is_personal flips and plan_type changes.
  • SQLite in dev, PostgreSQL in prod. SQLAlchemy generic types enable this.
  • Dev mode password reset returns the reset link directly in API response (no SMTP needed).
  • user["sub"] is a string. Always convert to uuid.UUID() before passing to SQLAlchemy queries.
  • 搜索功能必须与 PubMed 完全一致。 这是硬性要求,不是"未来优化"。所有 PubMed 字段标签必须全量支持,已存储数据的立即接通搜索路径,缺失数据的补充 XML 抽取和存储。不允许任何字段退化到纯文本搜索。