fix(camera): add 30-second scan waiting watchdog

This commit is contained in:
xsl
2026-10-16 16:07:53 +08:00
parent 5e07985b19
commit b19811f974
4 changed files with 96 additions and 24 deletions
+41 -15
View File
@@ -173,6 +173,14 @@
/* ─── 弹窗管理 ─── */
var SCAN_CYCLE_MS = 1000, scanCycle = null, scanMask = null;
var SCAN_WAIT_TIMEOUT_MS = 30000, analysisWait = null;
function stopAnalysisWait() {
var wait = analysisWait;
analysisWait = null;
if (!wait) return;
clearTimeout(wait.timer);
if (typeof wait.cancel === 'function') wait.cancel();
}
function restartScanCycle() {
if (!scanCycle || scanCycle.done) return;
var overlay = $('#p-analyzing'), cycle = scanCycle;
@@ -236,7 +244,7 @@
}
function closeOverlay(id) {
$('#' + id).classList.remove('show');
if (id === 'p-analyzing') stopScanCycle();
if (id === 'p-analyzing') { stopAnalysisWait(); stopScanCycle(); }
}
// 点遮罩空白处关闭(与各自关闭按钮同行为:隐私→回启动页、拍照引导→留在档案页)
@@ -1137,6 +1145,20 @@
camStep = 0;
photos = photosByView(photos);
openOverlay('p-analyzing');
var wait = { deadline: Date.now() + SCAN_WAIT_TIMEOUT_MS, timer: null, cancel: null };
analysisWait = wait;
function expireWait() {
if (analysisWait !== wait || seq !== camSeq) return;
trackCapture('failed');
camSeq++; // Invalidate request/animation callbacks BEFORE resolving/aborting them.
closeOverlay('p-analyzing');
stopCamera(); voice.stop(); shutterLock = false;
photos = []; captureDiagnostics = []; camStep = 0;
toast('检测服务开小差了,请重试');
showScreen('s-profile');
popHistorySilently();
}
wait.timer = setTimeout(expireWait, SCAN_WAIT_TIMEOUT_MS);
voice.play(VOICE.analyzing); // 「正在生成您的肌肤报告」
// 档案与检测一起上报(后台记录用):年龄非法值服务端按 null 处理
var profile = {
@@ -1146,13 +1168,15 @@
skinType: SKINS[skinIndex].name
};
var animationComplete = scanCycle.promise;
requestAnalyze(photos, seq, profile, function (errMsg, payload) {
wait.cancel = requestAnalyze(photos, seq, profile, function (errMsg, payload) {
if (seq !== camSeq) return; // 流程已被重置:丢弃
if (Date.now() >= wait.deadline) { expireWait(); return; }
trackCapture(errMsg ? 'failed' : 'response');
// A fast successful response waits for one actual visible animation cycle;
// cancellation/errors never get held hostage by the presentation animation.
(errMsg ? Promise.resolve() : animationComplete).then(function () {
if (seq !== camSeq) return;
if (Date.now() >= wait.deadline) { expireWait(); return; }
closeOverlay('p-analyzing');
stopCamera();
voice.stop(); // 结果已出:排队中的「分析中」等不再补播
@@ -1208,11 +1232,11 @@
}
/* ─── 分析请求(mock=1 直返内置样本;有 MOCK_FIXTURE 时用报告页可视化 fixture,便于离线/自动化验证) ─── */
// 后端最多请求两次(各 60s),中间等待 4s;另留上传、落盘和响应传输余量。
var ANALYZE_TIMEOUT_MS = 150000;
// Request timeout complements the independent 30s presentation watchdog.
var ANALYZE_TIMEOUT_MS = 30000;
function requestAnalyze(ph, seq, profile, done) {
if (IS_MOCK && typeof SkinScore !== 'undefined') {
setTimeout(function () {
var mockTimer = setTimeout(function () {
if (seq !== camSeq) return; // mock 同样尊重取消/重置
var fx = window.MOCK_FIXTURE; // assets/mock/report-fixture.js(仅 mock 模式注入)
if (fx) {
@@ -1223,10 +1247,15 @@
done(null, { result: SkinScore.MOCK_RESULT, side: SkinScore.MOCK_SIDE });
}
}, 900);
return;
return function () { clearTimeout(mockTimer); };
}
var ctrl = typeof AbortController !== 'undefined' ? new AbortController() : null;
var timer = setTimeout(function () { if (ctrl) ctrl.abort(); }, ANALYZE_TIMEOUT_MS);
var completed = false;
function finish(error, payload) { if (completed) return; completed = true; clearTimeout(timer); if (seq === camSeq) done(error, payload); }
var timer = setTimeout(function () {
finish('检测服务开小差了,请重试');
if (ctrl) ctrl.abort();
}, ANALYZE_TIMEOUT_MS);
fetch('api/skin-analyze', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -1245,17 +1274,14 @@
signal: ctrl ? ctrl.signal : undefined // 老内核无 AbortController:仅靠令牌丢弃迟到响应
}).then(function (res) { return res.json(); })
.then(function (data) {
clearTimeout(timer);
if (seq !== camSeq) return; // 流程已被重置,丢弃过期响应
if (!data.ok) done(data.message || '检测失败,请重试');
else done(null, data);
if (!data.ok) finish(data.message || '检测失败,请重试');
else finish(null, data);
})
.catch(function (err) {
clearTimeout(timer);
if (seq !== camSeq) return;
if (err && err.name === 'AbortError') done('检测服务开小差了,请稍候重试');
else done('网络异常,请检查网络后重试');
if (err && err.name === 'AbortError') finish('检测服务开小差了,请重试');
else finish('网络异常,请检查网络后重试');
});
return function () { completed = true; clearTimeout(timer); if (ctrl) ctrl.abort(); };
}
/* ─── SkinScore viewModel → 报告渲染(评分公式见 js/skin-score.js,不直接向用户展示 P 值) ─── */