修复资源搜索结果在大样本下超时后重复追加,导致重复渲染与计数膨胀的问题

- 前端:引入搜索 “会话号 + validating” 双重校验,超时立即取消当前会话,并在批处理/渲染前校验,阻断超时后的继续写入;保留稳定 v-for key 确保渲染一致性
- 后端:`get_detail` 增强容错,避免无 `code`/网络异常引发 KeyError;`/get_share_detail` 统一错误返回结构,前端稳定处理
This commit is contained in:
x1ao4 2025-08-27 23:33:53 +08:00
parent 3ccaeeae15
commit 5216fa981d
3 changed files with 62 additions and 13 deletions

View File

@ -1200,6 +1200,10 @@ def get_share_detail():
if not is_sharing: if not is_sharing:
return jsonify({"success": False, "data": {"error": stoken}}) return jsonify({"success": False, "data": {"error": stoken}})
share_detail = account.get_detail(pwd_id, stoken, pdir_fid, _fetch_share=1) share_detail = account.get_detail(pwd_id, stoken, pdir_fid, _fetch_share=1)
# 统一错误返回,避免前端崩溃
if isinstance(share_detail, dict) and share_detail.get("error"):
return jsonify({"success": False, "data": {"error": share_detail.get("error")}})
share_detail["paths"] = paths share_detail["paths"] = paths
share_detail["stoken"] = stoken share_detail["stoken"] = stoken

View File

@ -1032,7 +1032,7 @@
<div class="dropdown-item text-muted" v-else style="font-size:14px; padding-left: 8px; text-align: left;"> <div class="dropdown-item text-muted" v-else style="font-size:14px; padding-left: 8px; text-align: left;">
{{ smart_param.taskSuggestions.message ? smart_param.taskSuggestions.message : smart_param.taskSuggestions.data && smart_param.taskSuggestions.data.length ? `以下资源由 ${(smart_param.taskSuggestions.source || '').replace(/,\s*/g, '、')} 搜索提供(仅显示有效链接,共 ${(smart_param.taskSuggestions.data || []).length} 个),如有侵权请联系资源发布方` : "未搜索到有效资源" }} {{ smart_param.taskSuggestions.message ? smart_param.taskSuggestions.message : smart_param.taskSuggestions.data && smart_param.taskSuggestions.data.length ? `以下资源由 ${(smart_param.taskSuggestions.source || '').replace(/,\s*/g, '、')} 搜索提供(仅显示有效链接,共 ${(smart_param.taskSuggestions.data || []).length} 个),如有侵权请联系资源发布方` : "未搜索到有效资源" }}
</div> </div>
<div v-for="suggestion in smart_param.taskSuggestions.data || []" :key="suggestion.taskname" class="dropdown-item cursor-pointer" @click.prevent="selectSuggestion(index, suggestion)" style="font-size: 14px;" :title="getSuggestionHoverTitle(suggestion)"> <div v-for="suggestion in smart_param.taskSuggestions.data || []" :key="(suggestion.shareurl || '') + '_' + (suggestion.taskname || '') + '_' + (suggestion.publish_date || '')" class="dropdown-item cursor-pointer" @click.prevent="selectSuggestion(index, suggestion)" style="font-size: 14px;" :title="getSuggestionHoverTitle(suggestion)">
<span v-html="suggestion.verify ? '✅': ''"></span> {{ suggestion.taskname }} <span v-html="suggestion.verify ? '✅': ''"></span> {{ suggestion.taskname }}
<small class="text-muted"> <small class="text-muted">
<a :href="suggestion.shareurl" target="_blank" @click.stop> · {{ suggestion.shareurl.replace(/^https?:\/\/pan\.quark\.cn\/s\//, '') }}</a> <a :href="suggestion.shareurl" target="_blank" @click.stop> · {{ suggestion.shareurl.replace(/^https?:\/\/pan\.quark\.cn\/s\//, '') }}</a>
@ -1960,7 +1960,7 @@
<div class="dropdown-item text-muted" v-else style="font-size:14px; padding-left: 8px; text-align: left;"> <div class="dropdown-item text-muted" v-else style="font-size:14px; padding-left: 8px; text-align: left;">
{{ smart_param.taskSuggestions.message ? smart_param.taskSuggestions.message : smart_param.taskSuggestions.data && smart_param.taskSuggestions.data.length ? `以下资源由 ${(smart_param.taskSuggestions.source || '').replace(/,\s*/g, '、')} 搜索提供(仅显示有效链接,共 ${(smart_param.taskSuggestions.data || []).length} 个),如有侵权请联系资源发布方` : "未搜索到有效资源" }} {{ smart_param.taskSuggestions.message ? smart_param.taskSuggestions.message : smart_param.taskSuggestions.data && smart_param.taskSuggestions.data.length ? `以下资源由 ${(smart_param.taskSuggestions.source || '').replace(/,\s*/g, '、')} 搜索提供(仅显示有效链接,共 ${(smart_param.taskSuggestions.data || []).length} 个),如有侵权请联系资源发布方` : "未搜索到有效资源" }}
</div> </div>
<div v-for="suggestion in smart_param.taskSuggestions.data || []" :key="suggestion.taskname" class="dropdown-item cursor-pointer" @click.prevent="selectSuggestion(-1, suggestion)" style="font-size: 14px;" :title="getSuggestionHoverTitle(suggestion)"> <div v-for="suggestion in smart_param.taskSuggestions.data || []" :key="(suggestion.shareurl || '') + '_' + (suggestion.taskname || '') + '_' + (suggestion.publish_date || '')" class="dropdown-item cursor-pointer" @click.prevent="selectSuggestion(-1, suggestion)" style="font-size: 14px;" :title="getSuggestionHoverTitle(suggestion)">
<span v-html="suggestion.verify ? '✅': ''"></span> {{ suggestion.taskname }} <span v-html="suggestion.verify ? '✅': ''"></span> {{ suggestion.taskname }}
<small class="text-muted"> <small class="text-muted">
<a :href="suggestion.shareurl" target="_blank" @click.stop> · {{ suggestion.shareurl.replace(/^https?:\/\/pan\.quark\.cn\/s\//, '') }}</a> <a :href="suggestion.shareurl" target="_blank" @click.stop> · {{ suggestion.shareurl.replace(/^https?:\/\/pan\.quark\.cn\/s\//, '') }}</a>
@ -2251,6 +2251,8 @@
valid: 0 valid: 0
}, },
searchTimer: null, searchTimer: null,
// 新增:搜索会话号用于取消上一次验证/渲染,避免卡死和重复
searchSessionId: 0
}, },
activeTab: 'config', activeTab: 'config',
configModified: false, configModified: false,
@ -4274,6 +4276,8 @@
// 确保显示下拉菜单 // 确保显示下拉菜单
this.smart_param.showSuggestions = true; this.smart_param.showSuggestions = true;
try { try {
// 启动新的搜索会话,后续增量结果仅在会话一致时才渲染
const sessionId = ++this.smart_param.searchSessionId;
axios.get('/task_suggestions', { axios.get('/task_suggestions', {
params: { params: {
q: taskname, q: taskname,
@ -4281,9 +4285,10 @@
} }
}).then(response => { }).then(response => {
// 接收到数据后,过滤无效链接 // 接收到数据后,过滤无效链接
if (sessionId !== this.smart_param.searchSessionId) return; // 旧会话结果忽略
if (response.data.success && response.data.data && response.data.data.length > 0) { if (response.data.success && response.data.data && response.data.data.length > 0) {
// 使用新增的方法验证链接有效性 // 使用新增的方法验证链接有效性
this.validateSearchResults(response.data); this.validateSearchResults(response.data, sessionId);
} else { } else {
this.smart_param.taskSuggestions = response.data; this.smart_param.taskSuggestions = response.data;
// 重新确认设置为true // 重新确认设置为true
@ -4294,6 +4299,7 @@
}).catch(error => { }).catch(error => {
this.smart_param.isSearching = false; this.smart_param.isSearching = false;
this.smart_param.validating = false; // 重置验证状态 this.smart_param.validating = false; // 重置验证状态
}); });
} catch (e) { } catch (e) {
this.smart_param.taskSuggestions = { this.smart_param.taskSuggestions = {
@ -4301,10 +4307,11 @@
}; };
this.smart_param.isSearching = false; this.smart_param.isSearching = false;
this.smart_param.validating = false; // 重置验证状态 this.smart_param.validating = false; // 重置验证状态
} }
}, },
// 添加新方法来验证搜索结果 // 添加新方法来验证搜索结果
validateSearchResults(searchData) { validateSearchResults(searchData, sessionId) {
const invalidTerms = [ const invalidTerms = [
"分享者用户封禁链接查看受限", "分享者用户封禁链接查看受限",
"好友已取消了分享", "好友已取消了分享",
@ -4347,6 +4354,7 @@
return new Promise((resolve) => { return new Promise((resolve) => {
if (!link.shareurl) { if (!link.shareurl) {
// 没有分享链接,直接跳过 // 没有分享链接,直接跳过
resolve(null); resolve(null);
return; return;
} }
@ -4358,6 +4366,10 @@
.then(response => { .then(response => {
// 更新进度 // 更新进度
this.smart_param.validateProgress.current++; this.smart_param.validateProgress.current++;
if (sessionId !== this.smart_param.searchSessionId) {
resolve(null);
return;
}
if (response.data.success) { if (response.data.success) {
// 检查文件列表是否为空 // 检查文件列表是否为空
@ -4365,6 +4377,7 @@
if (shareDetail.list && shareDetail.list.length > 0) { if (shareDetail.list && shareDetail.list.length > 0) {
// 链接有效,添加到有效结果列表 // 链接有效,添加到有效结果列表
this.smart_param.validateProgress.valid++; this.smart_param.validateProgress.valid++;
resolve(link); resolve(link);
return; return;
} }
@ -4390,18 +4403,21 @@
// 如果不是已知的失效原因,保留该结果 // 如果不是已知的失效原因,保留该结果
if (!isInvalid) { if (!isInvalid) {
this.smart_param.validateProgress.valid++; this.smart_param.validateProgress.valid++;
resolve(link); resolve(link);
return; return;
} }
} }
// 链接无效 // 链接无效
resolve(null); resolve(null);
}) })
.catch(error => { .catch(error => {
// 验证出错,保守处理为有效 // 验证出错,保守处理为有效
this.smart_param.validateProgress.current++; this.smart_param.validateProgress.current++;
this.smart_param.validateProgress.valid++; this.smart_param.validateProgress.valid++;
resolve(link); resolve(link);
}); });
}); });
@ -4409,6 +4425,8 @@
// 修改processBatch函数增加快速显示功能 // 修改processBatch函数增加快速显示功能
const processBatch = async () => { const processBatch = async () => {
// 新会话已开始或已被取消validating=false则停止当前批次
if (sessionId !== this.smart_param.searchSessionId || !this.smart_param.validating) return;
// 取下一批处理 // 取下一批处理
const batch = toProcess.splice(0, batchSize); const batch = toProcess.splice(0, batchSize);
if (batch.length === 0) { if (batch.length === 0) {
@ -4431,6 +4449,7 @@
validResults.sort((a, b) => getItemTs(b) - getItemTs(a)); validResults.sort((a, b) => getItemTs(b) - getItemTs(a));
// 每批次都增量更新到界面,显示当前有效数量并保持正在验证状态 // 每批次都增量更新到界面,显示当前有效数量并保持正在验证状态
if (sessionId !== this.smart_param.searchSessionId || !this.smart_param.validating) return; // 渲染前再次检查会话
this.smart_param.taskSuggestions = { this.smart_param.taskSuggestions = {
success: searchData.success, success: searchData.success,
source: searchData.source, source: searchData.source,
@ -4449,7 +4468,10 @@
// 设置超时,避免永久等待 // 设置超时,避免永久等待
setTimeout(() => { setTimeout(() => {
// 如果验证还在进行中,强制完成 // 如果验证还在进行中,强制完成
if (this.smart_param.validating) { if (this.smart_param.validating && sessionId === this.smart_param.searchSessionId) {
// 在收尾前立即取消会话,避免后续批次继续追加导致重复
this.smart_param.validating = false;
this.smart_param.searchSessionId++;
// 将剩余未验证的链接添加到结果中 // 将剩余未验证的链接添加到结果中
const remaining = toProcess.filter(item => item.shareurl); const remaining = toProcess.filter(item => item.shareurl);
validResults.push(...remaining); validResults.push(...remaining);
@ -4460,6 +4482,7 @@
// 完成验证 // 完成验证
this.finishValidation(searchData, validResults); this.finishValidation(searchData, validResults);
} }
}, 30000); // 30秒超时 }, 30000); // 30秒超时
}, },
@ -4480,6 +4503,7 @@
message: validResults.length === 0 ? "未找到有效的分享链接" : searchData.message message: validResults.length === 0 ? "未找到有效的分享链接" : searchData.message
}; };
this.smart_param.taskSuggestions = result; this.smart_param.taskSuggestions = result;
this.smart_param.isSearching = false; this.smart_param.isSearching = false;
this.smart_param.validating = false; this.smart_param.validating = false;

View File

@ -1225,18 +1225,39 @@ class Quark:
"_fetch_total": "1", "_fetch_total": "1",
"_sort": "file_type:asc,updated_at:desc", "_sort": "file_type:asc,updated_at:desc",
} }
response = self._send_request("GET", url, params=querystring).json() # 兼容网络错误或服务端异常
if response["code"] != 0: try:
return {"error": response["message"]} response = self._send_request("GET", url, params=querystring).json()
if response["data"]["list"]: except Exception:
list_merge += response["data"]["list"] return {"error": "request error"}
# 统一判错:某些情况下返回没有 code 字段
code = response.get("code")
status = response.get("status")
if code not in (0, None):
return {"error": response.get("message", "unknown error")}
if status not in (None, 200):
return {"error": response.get("message", "request error")}
data = response.get("data") or {}
metadata = response.get("metadata") or {}
if data.get("list"):
list_merge += data["list"]
page += 1 page += 1
else: else:
break break
if len(list_merge) >= response["metadata"]["_total"]: # 防御性metadata 或 _total 缺失时不再访问嵌套键
total = metadata.get("_total") if isinstance(metadata, dict) else None
if isinstance(total, int) and len(list_merge) >= total:
break break
response["data"]["list"] = list_merge # 统一输出结构,缺失字段时提供默认值
return response["data"] if not isinstance(data, dict):
return {"error": response.get("message", "request error")}
data["list"] = list_merge
if "paths" not in data:
data["paths"] = []
return data
def get_fids(self, file_paths): def get_fids(self, file_paths):
fids = [] fids = []