fix: 第8轮搜索深度审计修复 — 缓存失效、Redis重试、中文标签、翻页稳定性等12项
CRITICAL:
- invalidate_search_cache 清理 atm:* 缓存(MeSH ATM扩展不再用过期结果)
- Pro 方案 api_quota_per_day 1000→10000(修复低于 Free 的数据错误)
- CacheService/RateLimitMiddleware Redis 连接失败60秒自动重试(原永久降级)
- 普通搜索中文输入自动匹配 GlobalTag.name_zh(如"肺癌"通过MeSH标签关联文献)
- AdvancedPubSearchView resolveQuery 添加 seen Set 检测交替 #N 循环引用
HIGH:
- 限速器 _burst_windows 每500请求清理过期条目(防止内存泄漏)
- cron daily_ftp_update 末尾调用 invalidate_search_cache()(自动管道不再用过期缓存)
MEDIUM:
- _apply_order_by ASC 排序加 id tiebreaker(title/journal/first_author翻页跳行/重复)
- _keyset_condition 所有 is_(None) 加 id tiebreaker + __NULL__ 哨兵值
- _field_condition("all") 默认tsvector路径加 journal/journal_iso ILIKE 兜底
- SearchView restoreFromQuery date_preset/year_from/year_to 优先顺序修复
docs: 更新 12/13 搜索文档,移除 CLAUDE.md 陈旧 SQLite 提及
This commit is contained in:
@@ -5,6 +5,12 @@ interface UsePaginationOptions {
|
||||
fetchFn: (page: number) => Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* Offset-based pagination composable.
|
||||
*
|
||||
* 注意:keyset 分页模式(date/cited/title/journal/first_author 排序)由 SearchView
|
||||
* 独立管理 keysetCursors/keysetHasMore/keysetPage。此 composable 的 totalPages/hasMore
|
||||
* 在 keyset 模式下语义不准确(keyset 用 has_more 标志而非总页数判断),由调用方覆盖使用。 */
|
||||
export function usePagination(opts: UsePaginationOptions) {
|
||||
const { fetchFn, pageSize: initialPageSize = 20 } = opts
|
||||
const page = ref(1)
|
||||
|
||||
@@ -32,18 +32,35 @@ export function resolveQuery(query: string, entries: HistoryEntry[]): string {
|
||||
})
|
||||
}
|
||||
|
||||
/** 递归展开所有 #N 引用为纯查询 */
|
||||
/** 递归展开所有 #N 引用为纯查询,带循环引用检测 */
|
||||
export function expandQuery(query: string, entries: HistoryEntry[]): string {
|
||||
const seen = new Set<string>()
|
||||
let prev = ''
|
||||
let current = query
|
||||
for (let i = 0; i < 10; i++) {
|
||||
if (current === prev) break
|
||||
const refs = current.match(/#(\d+)/g)
|
||||
if (refs) {
|
||||
for (const ref of refs) {
|
||||
if (seen.has(ref)) return prev // 循环引用 → 返回上次安全结果
|
||||
seen.add(ref)
|
||||
}
|
||||
}
|
||||
prev = current
|
||||
current = resolveQuery(current, entries)
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
/** 重新计算所有条目的 expanded_query,在删除/淘汰后保证一致性 */
|
||||
function recomputeExpanded(entries: HistoryEntry[]) {
|
||||
const ids = new Set(entries.map(e => e.id))
|
||||
for (const entry of entries) {
|
||||
entry.expanded_query = expandQuery(entry.query, entries)
|
||||
.replace(/#(\d+)/g, (m) => ids.has(m) ? m : '(deleted)')
|
||||
}
|
||||
}
|
||||
|
||||
export function useSearchHistory() {
|
||||
const entries = ref<HistoryEntry[]>(loadAll())
|
||||
|
||||
@@ -69,6 +86,7 @@ export function useSearchHistory() {
|
||||
if (all.length >= MAX_ENTRIES) {
|
||||
all.sort((a, b) => a.timestamp.localeCompare(b.timestamp))
|
||||
all.shift()
|
||||
recomputeExpanded(all) // 清除悬空 #N 引用
|
||||
}
|
||||
|
||||
all.push(entry)
|
||||
@@ -79,6 +97,7 @@ export function useSearchHistory() {
|
||||
|
||||
function remove(id: string) {
|
||||
const all = loadAll().filter(e => e.id !== id)
|
||||
recomputeExpanded(all) // 重新展开,清除悬空 #N 引用
|
||||
saveAll(all)
|
||||
entries.value = all
|
||||
}
|
||||
@@ -90,8 +109,10 @@ export function useSearchHistory() {
|
||||
|
||||
function download() {
|
||||
const all = loadAll()
|
||||
/** 转义 TSV 特殊字符:制表符/换行/回车 → 空格 */
|
||||
const escapeTsv = (s: string) => s.replace(/[\t\n\r]/g, ' ')
|
||||
const lines = all.map(e =>
|
||||
`${e.id}\t${e.result_count ?? ''}\t${e.timestamp}\t${e.query}`
|
||||
`${e.id}\t${e.result_count ?? ''}\t${e.timestamp}\t${escapeTsv(e.query)}`
|
||||
)
|
||||
const blob = new Blob([lines.join('\n')], { type: 'text/plain;charset=utf-8' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
@@ -99,7 +120,8 @@ export function useSearchHistory() {
|
||||
a.href = url
|
||||
a.download = `pubmed-search-history-${new Date().toISOString().slice(0, 10)}.tsv`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
// 延迟回收 blob URL,确保浏览器已开始下载
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000)
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user