# 肿瘤科标签体系设计

## 设计原则

- 基于 MeSH (Medical Subject Headings)，从 NLM XML 导入
- MeSH C04 (Neoplasms) 为核心，交叉 E02(治疗) + D27(药物)
- 补充 MeSH 未覆盖的临床维度：驱动基因/靶点、临床终点、临床场景
- 三级来源体系：手工维护（manual）→ MeSH 导入（mesh）→ 管道懒创建（auto）
- 用户端只展示已激活（is_active=true）的标签

---

## 一、标签分类（tag_category）

| 分类 | 枚举值 | 来源 | 示例 |
|------|--------|------|------|
| 癌种部位 | `cancer` | MeSH C + 手工 | 肺癌、乳腺癌、结直肠癌 |
| 组织分型 | `histology` | MeSH C04.557 | 腺癌、鳞癌、淋巴瘤 |
| 驱动基因/靶点 | `gene` | 手工维护（无 MeSH UI，但种子标签有 name_en） | EGFR、ALK、KRAS、PD-L1 |
| 治疗方式 | `treatment` | MeSH E + 手工 | 靶向治疗、免疫治疗、放疗 |
| 研究类型 | `study_type` | MeSH Publication Type | 指南、RCT、Meta分析 |
| 临床终点 | `endpoint` | 手工维护 | OS、PFS、ORR |
| 临床场景 | `scenario` | 手工维护 | 新辅助、一线治疗、维持治疗 |
| 其他 MeSH | `mesh_other` | 管道懒创建 | 无法归类的 MeSH Descriptor |

`mesh_other` 是管道路径的兜底分类，按 mesh_ui 前缀映射（见 §3.5）。

---

## 二、三级来源体系

### 2.1 来源定义

| 来源 | 数量（生产） | 含义 | is_active 默认值 |
|------|-------------|------|-----------------|
| `manual` | 99 | 手工维护的种子标签，有中文名、有层级 | `true` |
| `mesh` | ~670 | C04 + E02 + D27 批量导入标签（NLM 权威，全英文） | `true` |
| `auto` | ~20,122 | 管道懒创建，meeting 未匹配 MeSH 术语自动创建 | `false` |

### 2.2 manual 标签详情（99 条）

```
category    total  active  articles  has_mesh_ui
cancer       33     33      484        27
treatment    18     18      126         7
gene         18     18       46         0
endpoint     10     10      153         4
study_type    9      9      103         4
scenario     11     11       85         3
```

- 45/99 有 mesh_ui 能直接匹配 PubMed MeSH
- 17 个关键基因标签（EGFR、ALK、KRAS、BRAF 等）通过 `tmp_fix_tag_mesh_ui.py` 脚本映射到正确 mesh_ui
- 28 个标签 article_count=0（主要为 grouping 节点和部分罕见癌种），is_selectable=false 的 grouping 节点不参与打标
- 33 个非 selectable 的 grouping 节点（如"肺癌→非小细胞肺癌"下的层级节点）

### 2.3 mesh 标签详情（~670 条）

从 `scripts/import_mesh_tags.py` 导入，import_mesh_tags.py 先按 name_en 匹配，匹配不到的创 building 建有 mesh_ui 的新标签。

- **导入策略**：先匹配已有 manual 标签的 name_en，命中则关联不新建；未命中则创建新行
- **匹配问题**：约 15 个标签因命名不一致（如 seed "Lung Cancer" vs MeSH "Lung Neoplasms"）未匹配到，创建了重复标签

### 2.4 auto 标签详情（~20,122 条）

自动创建，无中文名，无层级，is_active=false。主要用于：
1. 保证打标覆盖（即使没有 manual/mesh 标签匹配）
2. 后台可见，供管理员审核后提升为 manual 标签

---

## 三、MeSH → 标签匹配策略（三阶段）

### 3.1 总体流程

```
mesh_headings[]（来自 PubMed 文章）
    │
    ├─ 阶段 1：mesh_ui 精确匹配 ──→ 查询 global_tags WHERE mesh_ui IN (...)
    │    命中 → 使用 manual/mesh 标签
    │    未命中 → 进入阶段 2
    │
    ├─ 阶段 2：name_en 回退匹配 ──→ 查询 global_tags WHERE name_en IN (...)
    │    命中 → 使用 manual/mesh 标签（解决无 mesh_ui 的种子标签）
    │    未命中 → 进入阶段 3
    │
    └─ 阶段 3：懒创建 ──→ GlobalTag(source="auto", is_active=false)
          name_zh=null，仅存英文名
          等待后台审核后激活
```

### 3.2 阶段 1：mesh_ui 精确匹配

```python
# tag_service.py:42-46
result = await db.execute(
    select(GlobalTag).where(GlobalTag.mesh_ui.in_(mesh_uis))
)
matched: list[GlobalTag] = list(result.scalars().all())
```

- 每条 PubMed 文章的 MeSH Headings 包含 `{descriptor, ui, major}`
- 用 `ui`（如 `D008175`）精准匹配 `global_tags.mesh_ui`
- 命中率高，但要求标签预先设置 mesh_ui

### 3.3 阶段 2：name_en 回退匹配

```python
# tag_service.py:54-62 — 阶段 2 回退
result = await db.execute(
    select(GlobalTag).where(GlobalTag.name_en.in_(unmatched_names))
)
```

- 为没有 mesh_ui 的种子标签提供匹配机会（如大部分 gene 标签）
- 依赖 MeSH heading 的 `descriptor` 字段与标签 `name_en` 的精确匹配
- **局限性**：MeSH 官方名称（如 "ErbB Receptors"）与 seed 标签名称（"EGFR"）不一致时无法匹配

### 3.4 阶段 3：懒创建

```python
# tag_service.py:70-86 — 懒创建
tag = GlobalTag(
    mesh_ui=ui,
    name_en=mh.get("descriptor", ""),
    name_zh=None,
    source="auto",
    is_active=False,
)
```

- 前面两阶段均未命中时才触发
- 创建 source=auto, is_active=false 的标签
- 无中文名、无层级
- 确保每篇文章的每个 MeSH heading 都能映射到标签

### 3.5 分类推断规则

懒创建时按 mesh_ui 前缀决定 tag_category：

```python
_CATEGORY_MAP = {
    "C": "cancer", "D": "gene", "E": "treatment",
    "V": "study_type",
    # F, G, H, I, J, K, L, M, N, Z → "mesh_other"
}
```

---

## 四、关键词回填策略

### 4.1 为无 mesh_ui 的 manual 标签补充文章关联

部分 manual 标签（如 scenario/endpoint 类别）没有 MeSH 对等词，无法通过 mesh_ui 或 name_en 匹配。改用标题/摘要关键词 ILIKE 匹配：

```python
# tmp_keyword_tag_backfill.py（一次执行）
UPDATE global_tags SET is_active = false
WHERE source = 'manual' AND article_count = 0 AND is_selectable = true;
# → 临时停用 32 个零文章 manual 标签

# 对每个零文章标签，按 keywords[] 做 title ILIKE 查询
INSERT INTO global_literature_tags (literature_id, tag_id, is_major)
SELECT lit.id, :tag_id, false
FROM global_literature lit
WHERE ... AND NOT EXISTS (...)
LIMIT 200;
```

### 4.2 回填结果

| 标签 | 关键词 | 新增关联数 |
|------|--------|-----------|
| Case Report | case report, case series | ~35,686 |
| Adjuvant | adjuvant | ~20,603 |
| Real World Study | real.world, retrospective | ~6,002 |
| Overall Survival | overall survival | ~4,968 |
| 其他 12 个 | 各场景关键词 | ~16,683 |
| **合计** | | **~83,942** |

**局限**：ILIKE 匹配假阳性高（如 "adjuvant" 可能匹配到非临床场景），但作为初始覆盖足够。

---

## 五、article_count 维护

### 5.1 增量更新

```python
# tag_service.py:114 — 每次新增关联时更新
tag.article_count += 1
```

- `tag_article()` 中每次新增 `GlobalLiteratureTag` 记录时 +1
- 不需要定期重算，实时准确
- 删除标签关联时不自动减（当前平台无删除操作入口）

### 5.2 手动全量刷新

```sql
UPDATE global_tags gt
SET article_count = (SELECT count(*) FROM global_literature_tags lt WHERE lt.tag_id = gt.id)
WHERE source = 'manual';
```

---

## 六、标签激活体系

### 6.1 激活规则

| 场景 | 操作 |
|------|------|
| source=manual, article_count=0, is_selectable=true | is_active=false（临时隐藏，待回填后激活） |
| source=manual, article_count>0, is_active=false | is_active=true（回填完成后激活） |
| source=mesh, article_count=0 | is_active=false（无文献的过分精细节点） |
| source=auto | is_active=false（待审核） |

### 6.2 生产环境执行

```sql
-- 停用零文章 mesh 标签
UPDATE global_tags SET is_active = false WHERE source = 'mesh' AND article_count = 0;
-- 165 个零文章 mesh 标签 → 停用

-- 停用零文章且非 selectable 的 manual 标签
UPDATE global_tags SET is_selectable = false WHERE source = 'manual' AND article_count = 0
AND EXISTS (SELECT 1 FROM global_tags t2 WHERE t2.source = 'manual' AND t2.path LIKE global_tags.path || '::%');
-- 8 个 grouping 节点 → 不可选

-- 停用零文章 manual 叶子标签
UPDATE global_tags SET is_active = false WHERE source = 'manual' AND article_count = 0 AND is_selectable = true;
-- 32 个 → 停用。mesh_ui 修复 + 关键词回填后重新激活
```

---

## 七、标签树结构

### 7.1 level 的含义

| level | 含义 | 示例 | selectable |
|-------|------|------|------------|
| 1 | 大类（按部位/类型分组） | 肺癌、乳腺癌 | false |
| 2 | 具体癌种 | 非小细胞肺癌、三阴性乳腺癌 | true |
| 3+ | 亚型/细分 | 肺腺癌、19del | true |

**注意**：level 不代表层级深度，而是按语义粒度划分。某些 level=2 的节点也是 grouping 节点（如"妇科肿瘤"下有子节点但 level=2），通过 is_selectable=false 标记。

### 7.2 grouping 节点识别

```sql
-- 判断某节点是否为 grouping 节点
-- 存在其他 manual 标签的 path 以此为前缀
SELECT EXISTS (
  SELECT 1 FROM global_tags t2
  WHERE t2.source = 'manual'
    AND t2.path LIKE '当前标签.path' || '::%'
    AND t2.id != '当前标签.id'
);
```

### 7.3 完整标签树

#### 📍 癌种部位（cancer, 33 条）

```
肿瘤科标签体系
├── 肺癌 (level 1, selectable=false)
│   ├── 非小细胞肺癌 NSCLC (level 2)
│   │   ├── 肺腺癌 (level 3)
│   │   ├── 肺鳞癌 (level 3)
│   │   └── 大细胞肺癌 (level 3)
│   └── 小细胞肺癌 SCLC (level 2)
├── 乳腺癌 (level 1, selectable=false)
│   ├── HR+/HER2- Luminal型 (level 2)
│   ├── HER2+ (level 2)
│   └── 三阴性乳腺癌 TNBC (level 2)
├── 结直肠癌 (level 1, selectable=false)
│   ├── 结肠癌 (level 2)
│   ├── 直肠癌 (level 2)
│   └── MSI-H/dMMR 结直肠癌 (level 2)
├── 胃癌 (level 1, selectable=false)
│   ├── 胃腺癌 (level 2)
│   └── 胃食管结合部癌 (level 2)
├── 肝癌 (level 1, selectable=false)
│   ├── 肝细胞癌 HCC (level 2)
│   ├── 胆管癌 (level 2)
│   └── 肝转移癌 (level 2)
├── 食管癌 (level 1, selectable=false)
├── 胰腺癌 (level 1, selectable=false)
├── 前列腺癌 (level 1, selectable=false)
├── 膀胱癌 / 肾癌 (level 1, selectable=false)
├── 头颈癌 (level 1, selectable=false)
│   ├── 鼻咽癌 (level 2)
│   ├── 口腔癌 (level 2)
│   ├── 喉癌 (level 2)
│   └── 甲状腺癌 (level 2)
├── 妇科肿瘤 (level 2, selectable=false)
│   ├── 卵巢癌 (level 3)
│   ├── 宫颈癌 (level 3)
│   └── 子宫内膜癌 (level 3)
├── 黑色素瘤 / 皮肤癌 (level 1, selectable=false)
├── 脑肿瘤 (level 1, selectable=false)
│   ├── 胶质母细胞瘤 GBM (level 2)
│   ├── 脑膜瘤 (level 2)
│   └── 脑转移瘤 (level 2)
├── 血液肿瘤 (level 1, selectable=false)
│   ├── 白血病 (level 2, selectable=false)
│   │   ├── AML (level 3)
│   │   ├── ALL (level 3)
│   │   ├── CML (level 3)
│   │   └── CLL (level 3)
│   ├── 淋巴瘤 (level 2, selectable=false)
│   │   ├── 霍奇金淋巴瘤 HL (level 3)
│   │   └── 非霍奇金淋巴瘤 NHL (level 3)
│   │       ├── DLBCL (level 4)
│   │       ├── 滤泡性淋巴瘤 (level 4)
│   │       ├── 套细胞淋巴瘤 (level 4)
│   │       └── T细胞淋巴瘤 (level 4)
│   └── 多发性骨髓瘤 (level 2)
├── 肉瘤 (level 1, selectable=false)
├── 原发不明肿瘤 CUP (level 1)
└── 儿童肿瘤 (level 1)
```

#### 🧬 驱动基因/靶点（gene, 18 条）

```
├── EGFR → 19del / L858R / T790M / C797S / exon20ins
├── ALK → EML4-ALK / 耐药突变
├── ROS1
├── BRAF V600E
├── KRAS → G12C / G12D
├── HER2 (ERBB2) → 扩增 / 突变
├── MET → exon14跳读 / 扩增
├── RET
├── NTRK1-3
├── FGFR1-4
├── IDH1/2
├── FLT3
├── KIT / PDGFRA
├── BRCA1/2 / HRD
├── MSI-H / dMMR
├── TMB-H
├── PIK3CA / PTEN / AKT
├── TP53 / RB1 / MYC
└── PD-L1
```

所有 gene 标签均无 mesh_ui，依靠 stage 2 name_en 回退匹配或关键词回填。

#### 💊 治疗方式（treatment, 18 条）

```
├── 靶向治疗 → TKI / 单克隆抗体 / ADC / 双特异性抗体
├── 免疫治疗 → PD-1抑制剂 / PD-L1抑制剂 / CTLA-4抑制剂 / LAG-3抑制剂 / CAR-T
├── 化疗 → 铂类 / 紫杉类 / 抗代谢 / 拓扑异构酶抑制剂
├── 放疗 → 常规分割 / SBRT/SRS / 质子 / 重离子
├── 手术 → 微创 / 机器人 / 器官保留
├── 内分泌治疗 → 他莫昔芬 / 芳香化酶抑制剂 / 抗雄激素
├── 抗血管生成 → 贝伐珠单抗 / 安罗替尼 / 阿帕替尼
├── 骨髓移植 → 自体 / 异基因
└── 支持治疗 → 骨转移 / 止吐 / 疼痛 / 营养
```

7/18 有 mesh_ui。

#### 📊 研究类型（study_type, 9 条）

```
├── 临床实践指南 → NCCN / CSCO / ESMO
├── 随机对照试验 RCT → 3期 / 2期
├── 系统综述/Meta分析
├── 真实世界研究 RWS
├── 病例报告/病例系列
├── 转化研究
├── 基础研究
└── 卫生经济学/HTA
```

4/9 有 mesh_ui。

#### 📈 临床终点（endpoint, 10 条）

```
├── OS (总生存期)
├── PFS (无进展生存期)
├── DFS (无病生存期)
├── ORR (客观缓解率) / DCR (疾病控制率)
├── 安全性/AE
├── 生物标志物 → ctDNA / CTC / MRD
├── 生活质量 QoL / PRO
└── 耐药机制
```

#### 🏥 临床场景（scenario, 11 条）

```
├── 早期/可手术 → 新辅助 / 辅助
├── 局部晚期 → 同步放化疗 / 转化治疗
├── 晚期/转移性 → 一线 / 二线+ / 后线
├── 维持治疗
├── 姑息/支持治疗
├── 寡转移 / 寡进展
├── 老年肿瘤
├── 儿童肿瘤
└── 遗传性肿瘤 / 家系
```

---

## 八、API 端点

### 8.1 公共标签

```
GET /public/tags → 全量标签树（Redis 缓存，1h TTL，key="public:tags"）
GET /public/cancer-tags → 仅癌种标签树
```

返回字段：

```json
{
  "id": "uuid",
  "name_zh": "肺癌",
  "name_en": "Lung Cancer",
  "path": "肿瘤科标签体系::肺癌",
  "tag_category": "cancer",
  "level": 1,
  "article_count": 62,
  "children": [...]
}
```

### 8.2 管理后台标签

```
GET /admin/tags?page=1&page_size=20&search=&source=&category=&active=
  → 分页列表，支持 source/mesh/manual/auto + category + active 筛选
  → 默认按 article_count DESC 排序
POST /admin/tags/refresh-counts → 全量刷新 article_count
```

### 8.3 首页热门标签

```vue
<!-- HomeView.vue — 侧栏热门标签 -->
const cancerTags = computed(() => allTags.value
  .filter((t: TagOption) =>
    t.tag_category === 'cancer' && t.level === 2 && t.name_zh)
  .sort((a, b) => ((b.article_count as number) || 0) - ((a.article_count as number) || 0))
)
// 限制显示 7 条 + "更多"
```

- 只选 level=2 的具体癌种（排除 grouping 节点）
- 过滤掉无中文名的标签
- 按 article_count 降序排列

---

## 九、前端标签显示规则

### 9.1 名称显示

```vue
<!-- 所有标签组件的统一规则 -->
{{ tag.name_zh || tag.name_en }}
```

如果 `name_zh` 为空（auto 标签常见），退而显示 `name_en`。此规则在以下组件统一使用：
- [LiteratureCard.vue](frontend/src/components/literature/LiteratureCard.vue)
- [LiteratureDetailView.vue](frontend/src/views/app/LiteratureDetailView.vue)（登录态）
- [LiteratureDetailView.vue](frontend/src/views/public/LiteratureDetailView.vue)（公开态）
- [CancerBrowseView.vue](frontend/src/views/public/CancerBrowseView.vue)
- [InterestSettingsView.vue](frontend/src/views/app/InterestSettingsView.vue)

### 9.2 文献卡片标签加载

```python
# tag_loader.py — 共享标签加载工具
async def load_tags_for_literature(db, literature_ids):
    select(GlobalLiteratureTag.literature_id, GlobalTag.id,
           GlobalTag.name_zh, GlobalTag.name_en,  # ← 必须同时加载
           GlobalTag.path, GlobalTag.tag_category,
           GlobalLiteratureTag.is_major)
    ...
```

**历史修复**：曾因 SELECT 遗漏 `name_en` 导致 name_zh=null 的标签显示为空白。

### 9.3 暗色模式

```vue
// PublicLayout.vue + AppLayout.vue — 用户头像背景色适配暗色模式
const avatarStyle = computed(() => ({
  background: ui.isDark ? '#1a3a5c' : '#e6f4ff',
  color: 'var(--text-primary)',
}))
```

使用 computed `:style` 绑定而非 CSS class，因为 NAvatar 有内联样式优先级高于外部 CSS。

---

## 十、缓存策略

| 缓存 | Key | TTL | 刷新机制 |
|------|-----|-----|---------|
| 公共标签树 | `public:tags` | 3600s | 被动过期，首次请求回退 DB |
| 首页 Feed | `homepage:feed` | 1800s | ARQ 定时每 30min 刷新 |
| 热门文章 | `hot_articles` | 1800s | ARQ 定时每 30min 刷新 |

---

## 十一、数据维护

### 11.1 标签导入

```bash
# 种子标签导入（dev 环境）
cd backend && python scripts/seed_data.py

# C04 MeSH 批量导入
python scripts/import_mesh_tags.py \
    --mesh-xml desc2025.xml.gz \
    --categories C04 \
    --cross-categories E02 E04 D27 \
    --specialty oncology
```

### 11.2 一次性数据清洗

```sql
-- 查看标签分布
SELECT source, tag_category, count(*), sum(article_count) FROM global_tags
GROUP BY source, tag_category ORDER BY source, tag_category;

-- 查看零文章标签
SELECT name_zh, name_en, source, tag_category, is_active
FROM global_tags WHERE (article_count = 0 OR article_count IS NULL)
ORDER BY source, tag_category;

-- 刷新 article_count
UPDATE global_tags gt
SET article_count = (SELECT count(*) FROM global_literature_tags lt WHERE lt.tag_id = gt.id);

-- 合并重复标签（name_en 近似但不同 ID）
-- 先将旧标签的 lit association 迁移到新标签
INSERT INTO global_literature_tags (literature_id, tag_id, is_major)
SELECT lt.literature_id, :target_tag_id, lt.is_major
FROM global_literature_tags lt
WHERE lt.tag_id = :source_tag_id
  AND NOT EXISTS (SELECT 1 FROM global_literature_tags lt2
                  WHERE lt2.literature_id = lt.literature_id AND lt2.tag_id = :target_tag_id);
-- 然后删除旧标签
DELETE FROM global_literature_tags WHERE tag_id = :source_tag_id;
DELETE FROM global_tags WHERE id = :source_tag_id;
```

### 11.3 生产部署注意事项

```bash
# 部署前确保标签变更已提交
cd backend && python -m alembic -c alembic/alembic.ini upgrade head

# 重建前端
cd frontend && rm -rf dist node_modules/.vite && npm run build

# 重启 worker（使标签缓存刷新）
docker compose restart worker
```

---

## 十二、已知问题与改进方向

### 12.1 约 26% 的文献无标签

- **原因**：`mesh_headings=[]` 的文献（in-process/publisher 记录，NLM 尚未标引）
- **影响范围**：约 322,507 篇（生产数据）
- **改进方向**：标题/摘要关键词匹配（已对 scenario/endpoint 标签实现，可推广到癌种）

### 12.2 name_en 匹配精度

- MeSH 官方术语名称与种子标签名称不一致（如 "ErbB Receptors" ≠ "EGFR"）
- 当前方案：为关键基因标签手动设置正确的 mesh_ui 绕过此问题

### 12.3 不完善的自动分类

- `_guess_category()` 仅按 mesh_ui 首字母分类，粒度过粗
- Phase 2 计划：基于 LLM 的精细分类

### 12.4 删除标签关联不减 article_count

- 当前 article_count 只增不减
- 如需删除标签关联（如同一 auto 标签合并到 manual 标签），需手动刷新计数

### 12.5 重复标签

- 约 15 个 seed 标签因 name_en 命名差异未匹配到 C04 导入标签，导致重复
- 可通过 `tmp_fix_tag_mesh_ui.py` 类似的合并脚本处理

---

## 十三、关键维护脚本

| 脚本 | 用途 | 执行环境 |
|------|------|---------|
| `seed_data.py` | 导入 99 条种子标签 + 期刊 | dev |
| `import_mesh_tags.py` | 导入 C04 MeSH 标签 | 一次性 |
| `tmp_fix_tag_mesh_ui.py` | 修复 17 个 gene/treatment 标签的 mesh_ui 映射 | 生产（已执行） |
| `tmp_keyword_tag_backfill.py` | 关键词回填 16 个 scenario/endpoint 标签 | 生产（已执行） |
| `tmp_audit_tags.py` | 审计标签分布 | 生产 |
