fix: 第27轮搜索审计修复 — 跨域日期污染/部分日期范围/标记组路径等7项
This commit is contained in:
@@ -2014,3 +2014,84 @@ def _is_negated_cond(c):
|
||||
| Bug 3 | LOW | Facet cache 从未写入 p2+(`total=0` 回退) |
|
||||
| Bug 4 | LOW | 无效日期字段标签静默降级为全字段搜索 |
|
||||
| Bug 5 | LOW | 否定组内冗余日期条件(单个年份 + 全范围) |
|
||||
|
||||
---
|
||||
|
||||
# 第27轮审计修复 (R27)
|
||||
|
||||
## 背景
|
||||
|
||||
第 27 轮由 3 个并行审计 agent 覆盖:解析器、引擎、前端/集成。发现 2 个 CRITICAL 引擎-解析器交互 bug、1 个 MEDIUM 解析器 bug,以及若干 LOW 问题。
|
||||
|
||||
## 修复清单
|
||||
|
||||
### R27-1 (CRITICAL): 日期范围属性跨作用域污染
|
||||
|
||||
**文件**:[pubmed_query_parser.py:1024-1088](backend/app/services/pubmed_query_parser.py#L1024) + [search_engine.py:1300-1306](backend/app/services/search_engine.py#L1300)
|
||||
|
||||
**根因**:`_parse_range` 对所有非否定日期范围都用 `max/min` intersect 更新全局 `edat_from/edat_to` 属性。当同一日期字段同时出现在顶层和括号组内(如 `2000:2010[EDAT] NOT (cancer AND 2005:2006[EDAT])`),顶层范围被组内范围错误缩小:`edat_from` 从 `2000-01-01` 变成 `2005-01-01`,`edat_to` 从 `2010-12-31` 变成 `2006-12-31`。加上 `_top_level_date_fields` 未追踪 `__RANGE_*` 标记,导致 `_neg_only` 判定跳过整个日期节。
|
||||
|
||||
**修复**:
|
||||
- `_parse_range` 增加 `if self._depth == 0` 守卫——仅顶层范围更新全局属性,组内范围不再污染
|
||||
- `parse()` 循环 `terms` 追踪 `__RANGE_*` 标记的 `_top_level_date_fields`
|
||||
- 新增 `_build_marker_condition()` 静态方法——从标记内嵌的 `start_val:end_val` 文本重建 SQL 条件
|
||||
- 组循环中处理标记:非否定组 → `g_pos.append`,否定组 → `g_neg.append`
|
||||
- 移除 `_build_date_cond_from_pp` 在否定组中的调用(已被标记直接处理取代)
|
||||
|
||||
### R27-2 (MEDIUM): 范围语法中的部分日期 YYYY-MM 静默降级
|
||||
|
||||
**文件**:[pubmed_query_parser.py:1015-1017](backend/app/services/pubmed_query_parser.py#L1015)
|
||||
|
||||
**根因**:`_validate_date_str` 只接受 `YYYY` 或 `YYYY-MM-DD`。`2024-01:2024-06[DP]` 中两个端点无法通过验证,整个范围降级为纯文本。单值部分日期(`2024-01[DP]`)因 R26 的 `_expand_partial_date` 调用路径已正确处理,但 `_parse_range` 缺少相同调用。
|
||||
|
||||
**修复**:在 `_parse_range` 的验证前添加 `_PARTIAL_DATE_RE` 匹配 + `_expand_partial_date` 展开:
|
||||
|
||||
```python
|
||||
if _PARTIAL_DATE_RE.match(start_val):
|
||||
start_val, _ = _expand_partial_date(start_val)
|
||||
if _PARTIAL_DATE_RE.match(end_val):
|
||||
_, end_val = _expand_partial_date(end_val)
|
||||
```
|
||||
|
||||
### R27-3 (MEDIUM): `_is_negated_cond` NULL-safe 模式验证不完整
|
||||
|
||||
**文件**:[search_engine.py:1345-1351](backend/app/services/search_engine.py#L1345)
|
||||
|
||||
**根因**:NULL-safe NOT 检测只检查 `or_` 的第一个子句是 `not_(...)`,没有验证第二个子句是 `is_(None)`。潜在误报:`or_(not_(X), Y)` 会被错误识别为否定。
|
||||
|
||||
**修复**:增加对第二子句的 `is_(None)` / `isnot(None)` 验证。
|
||||
|
||||
### R27-4 (LOW): `_neg_single_dates` NOT 条件缺少 NULL 安全
|
||||
|
||||
**文件**:[search_engine.py:1421, 1477](backend/app/services/search_engine.py#L1421)
|
||||
|
||||
**根因**:`not_(and_(col >= from, col <= to))` — 当 `col` 为 NULL 时,`NULL >= date` → NULL,`NOT(NULL)` → NULL(WHERE 中为假)。NULL 日期行的记录被错误排除。
|
||||
|
||||
**修复**:使用 `or_(not_(_range_cond), col.is_(None))`。
|
||||
|
||||
### R27-5 (LOW): 空白字符引号文本不跳过
|
||||
|
||||
**文件**:[pubmed_query_parser.py:939](backend/app/services/pubmed_query_parser.py#L939)
|
||||
|
||||
**根因**:`" "[TI]` — `text = " "`(两个空格),`if not text` 为 False,创建搜索条件包含双空格。
|
||||
|
||||
**修复**:改为 `if not text or not text.strip(): return []`。
|
||||
|
||||
## 前端审计发现(R27 未修复)
|
||||
|
||||
| 编号 | 严重度 | 描述 | 文件 |
|
||||
|------|--------|------|------|
|
||||
| C-1 | CRITICAL | 非 keyset 排序(best_match/relevance)第 2 页起总分页消失 | SearchView.vue:278 |
|
||||
| C-2 | CRITICAL | `#N` 引用在 SearchView 中不解析 | SearchView.vue:865 |
|
||||
| H-1 | HIGH | offset 分页第 2 页起 total 显示为 0 | SearchView.vue:278,353 |
|
||||
| H-2 | HIGH | 错误时 goToPage 不回退页码 | SearchView.vue:369-380 |
|
||||
| M-1 | MEDIUM | 空查询时年份直方图与"未找到"同时显示 | SearchView.vue:625 |
|
||||
| M-3 | MEDIUM | URL 同时存储 `date_preset` 和绝对日期 | SearchView.vue:512-521 |
|
||||
| L-3 | LOW | 空查询无筛选时返回全库年份分布 | search_engine.py:622-641 |
|
||||
|
||||
## 验证
|
||||
|
||||
- ✅ **110 search tests passed**(parser + search engine + integration)
|
||||
- 解析器 36 测试全部通过
|
||||
- 5 个新增 R27 正确性检查通过
|
||||
- 全量套件中仅外部服务连接失败(httpx.ConnectError),与改动无关
|
||||
|
||||
Reference in New Issue
Block a user