fix: 第30轮搜索审计修复 — 否定组NULL安全/嵌套括号_has_or/传递子组/DP标记列精度等8项
CI / backend (push) Canceled after 0s
CI / frontend (push) Canceled after 0s

This commit is contained in:
34047007@qq.com
2026-07-29 07:30:36 +08:00
parent 475d6bb80c
commit fc53a5b255
5 changed files with 129 additions and 32 deletions
+71 -2
View File
@@ -2,7 +2,7 @@
> 本文档按修复轮次详细记录所有搜索功能合规性修复的背景、根因分析和修改内容。
>
> **累计**24 轮,290+ 项修复,80+ 字段标签注册,1000+ 项测试覆盖
> **累计**25 轮,298+ 项修复,80+ 字段标签注册,1000+ 项测试覆盖
> **时间跨度**2026-07-24 ~ 2026-07-29
> **核心文件**`pubmed_query_parser.py`~1100 行)→ `search_engine.py`~1960 行)
@@ -24,7 +24,8 @@
12. [第十二轮:第 12 轮深度审计修复(21 项)](#第十二轮第-12-轮深度审计修复)
13. [第十三轮:第 13 轮深度审计修复(21 项)](#第十三轮第-13-轮深度审计修复)
14. [第十五轮(第 24 次审计修复)](#round-24第-24-次全面审计修复)
15. [遗留限制](#遗留限制)
15. [R30(第 5 轮并行审计修复)](#r30-2026-07-29-第五轮并行审计修复)
16. [遗留限制](#遗留限制)
---
@@ -2237,3 +2238,71 @@ if _PARTIAL_DATE_RE.match(end_val):
- ✅ **1007 tests passed, 0 failed**
- 全部搜索测试通过
- 无回归
## R30 (2026-07-29): 第五轮并行审计修复
> **变更类型**:6 项修复(1 HIGH 已在前序会话修复 + 5 项新增)
### R30-1 (HIGH): 否定组 NULL 安全包裹整个组合条件
**文件**[search_engine.py:1385-1409](backend/app/services/search_engine.py#L1385)
**根因**:R28 为否定组添加的 NULL 安全 NOT 包裹过于宽泛:`or_(not_(date_cond + keyword_combined), col1.is_(None), col2.is_(None))`。当日期列为 NULL 时,关键词匹配也通过 NULL 安全逃逸,导致否定组中的关键词条件失效。
**修复**:完全移除此 NULL 安全包裹层。否定组现在使用纯 `not_(combined)`,不附加列 NULL 检查。日期列的 NULL 安全由列精确的 `dp_negated` 路径单独处理。
### R30-2 (MEDIUM): `dp_negated` 双列 NULL 安全不精确
**文件**[search_engine.py:1525-1531](backend/app/services/search_engine.py#L1525)
**根因**:顶层 `dp_negated` 路径始终同时添加 `pub_year.is_(None)` 和 `pub_date.is_(None)`,即使条件仅使用其中一列(如纯年范围 `2024:2025[DP]` 只使用 `pub_year`)。
**修复**:检查条件使用了哪些列(`year_from/to` → `pub_year``date_from/to` → `pub_date`),仅对实际使用的列添加 NULL 安全。
### R30-3 (MEDIUM): 嵌套括号 `_has_or` 跨越作用域边界
**文件**[pubmed_query_parser.py:911](backend/app/services/pubmed_query_parser.py#L911)
**根因**`_has_or` 通过扫描 `start_pos:end_pos` 范围内所有 token 检测 OR 运算符。嵌套括号如 `((A OR B AND C) AND D)` 中,外层括号的 `_has_or` 扫描到内层 OR,导致外层分组操作符被设为 "or" 而非 "and"。
**修复**:在扫描过程中跟踪括号深度,仅计数当前深度(`_or_depth == 0`)的 OR token。
### R30-4 (MEDIUM): `_child_gids` 包含传递性子组
**文件**[pubmed_query_parser.py:897](backend/app/services/pubmed_query_parser.py#L897)
**根因**`_child_gids` 从所有返回的 Term 中收集 group_id,包括嵌套括号创建的传递性父组。这些传递性组不应作为当前组的子组引用。
**修复**:过滤掉 (a) 已在现有 `sub_group_refs` 中的 gid(传递性子组)和 (b) 自身就是父组的 gid(`sub_group_refs` 非空)。
### R30-5 (MEDIUM): 非否定组 DP 标记 NULL 安全列不精确
**文件**[search_engine.py:1289-1292](backend/app/services/search_engine.py#L1289), [search_engine.py:1337-1338](backend/app/services/search_engine.py#L1337)
**根因**:非否定组中的 DP 标记 `is_not=True`(如 `NOT 2024:2025[DP]` 在非否定组内)和子组路径中的 DP 标记始终同时添加 `pub_year.is_(None)` 和 `pub_date.is_(None)`。
**修复**:根据标记文本中的范围值类型(4 位年份 → `pub_year`,日期格式 → `pub_date`)选择性地添加 NULL 安全列。
### R30-6 (LOW): `query=None` 崩溃
**文件**[search_engine.py:256](backend/app/services/search_engine.py#L256)
**根因**`query.strip()` 在 `query` 为 `None` 时抛出 `AttributeError`。
**修复**:改为 `if query and query.strip():`。
### R30-7 (LOW): 搜索错误详情丢失
**文件**[SearchView.vue](frontend/src/views/app/SearchView.vue)
**根因**400 错误的 catch 块使用硬编码的 '搜索参数有误' 提示,未展示服务器的错误详情。
**修复**:对 400 错误优先展示 `e?.response?.data?.detail`。
### R30-8 (LOW): 日期范围正则遗漏 YYYY-MM-DD
**文件**[AdvancedPubSearchView.vue](frontend/src/views/public/AdvancedPubSearchView.vue)
**根因**:日期范围正则只匹配 `YYYY:YYYY` 和 `YYYY/MM/DD:YYYY/MM/DD`,遗漏 `YYYY-MM-DD:YYYY-MM-DD`ISO 格式)。
**修复**:添加 `-` 作为可选日期分隔符。`(\d{4}(?:[-\/]\d{2}[-\/]\d{2})?)`。