feat(auth/exam): 登录重试同源 + 静默刷新透传 + 交卷链路加固(R5/R8)
前端重试与后端闸门同源(须成对上线) - src/api/request.js: 新增 parseRetryAfterMs / computeLoginRetryDelayMs, 退避取 max(指数退避, Retry-After) + ±20% 抖动 + 服务端值封顶; silent 透传(doSilentRefresh 不再吞掉 silent 标记)。 - src/api/auth.js / src/stores/auth.js: 登录 429 走同源重试;错误分类区分 "TCP 未握手(可重试)"与"业务码失败(不可重试)"。 - src/stores/exam.js: 交卷链路顺序红线(取卷 -> 作答 -> submit-all 严格顺序), 答案不丢 / 重复不脏 / 窗口不锁死。 - src/views/Competition.vue: 中职/高职综合大题区块(3 x textarea,2000 字上限 与右下角计数);本科/教师组保留 Word 上传入口。 验证:源码抽取测试 8/8(含判别力自证:不读 header 的对照实现必 FAIL); npm run build BUILD_RC=0(12.82s / 1711 modules);真实交卷端到端 code=0。
This commit is contained in:
@@ -723,7 +723,14 @@ async function handleSubmitAll() {
|
||||
await examStore.submitAll({})
|
||||
ElMessage.success('提交成功!')
|
||||
} catch (err) {
|
||||
if (err?.message) ElMessage.error(err.message)
|
||||
// 交卷失败:答案已保留在本地草稿中,须明确告知可重试,
|
||||
// 避免考生误以为「已经交上了」而离开页面。
|
||||
const msg = (err && err.message) || ''
|
||||
if (msg) {
|
||||
ElMessage.error(msg)
|
||||
} else {
|
||||
ElMessage.error('提交未完成,请检查网络后重试(您的答案已在本机保留)')
|
||||
}
|
||||
return
|
||||
} finally {
|
||||
submitting.value = false
|
||||
@@ -752,25 +759,35 @@ async function autoSubmit() {
|
||||
router.push('/dashboard')
|
||||
return
|
||||
}
|
||||
ElMessage.warning('考试时间已到,系统将自动保存并提交您的答案')
|
||||
ElMessage.warning('考试时间已到,系统将自动保存并提交您的答案,请勿关闭页面')
|
||||
try {
|
||||
// forceSubmit:倒计时归零即合法交卷,绕过「未答满全部题目 / 窗口已关闭」闸门,
|
||||
// forceSubmit:倒计时归零即合法交卷,豁免「未答满全部题目」闸门,
|
||||
// 确保最后输入的大题答案与 submit_time 都能落库(管理后台显示真实交卷时间)。
|
||||
// ⚠️ 注意 forceSubmit **不再**能够绕过「考试窗口已关闭」——窗口关闭后由服务端
|
||||
// 走结算通道(以库内已落库作答为准补写 submit_time),这正是本流程的兜底。
|
||||
const saved = await ensureBigAnswersSaved({ force: true })
|
||||
if (!saved) {
|
||||
// 落库失败不退化为静默丢答案:提示考生手动重试
|
||||
// 大题落库失败不退化为静默丢答案:提示考生手动重试
|
||||
ElMessage.error('综合大题答案未能保存,请点击「提交全部」重试')
|
||||
autoSubmitted.value = false
|
||||
return
|
||||
}
|
||||
// submitAll 内部会逐题保存客观题答案并写入提交时间(大题已在上面落库)
|
||||
// submitAll 内含自动重试(网络层失败 / 429 / 5xx 会指数退避重试 6 次),
|
||||
// 每次重试都带上内存中的全部客观题答案做一次全量落库。
|
||||
await examStore.submitAll({ forceSubmit: true })
|
||||
ElMessage.success('已自动保存并提交')
|
||||
router.push('/dashboard')
|
||||
} catch (err) {
|
||||
autoSubmitted.value = false
|
||||
console.error('自动提交失败', err)
|
||||
ElMessage.error('自动提交失败,请手动点击「提交全部」')
|
||||
// 🔴 不再只提示「请手动点击提交全部」:重试若干次仍失败说明网络或服务端持续不可用,
|
||||
// 此时答案已保留在本地草稿中(见 stores/exam.js),须明确告知考生可重试,
|
||||
// 否则考生会误以为已经交上卷。窗口关闭后服务端仍接受结算补交,故重试始终有效。
|
||||
ElMessageBox.alert(
|
||||
'自动提交未能完成(网络异常或服务器繁忙)。您的答案已在本机保留,请点击「提交全部」重试;若仍失败,请立即联系监考老师。',
|
||||
'提交未完成',
|
||||
{ confirmButtonText: '我知道了', type: 'error' }
|
||||
).catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1012,21 +1029,36 @@ onMounted(async () => {
|
||||
|
||||
// 恢复已有答案:考试组在「已提交 / 意外退出后重进」时始终尝试恢复,
|
||||
// 避免断网、断电或误操作退出导致答题进度丢失(仅重载答案,不返回评分与对错)。
|
||||
// 🔴 2026-09-21 加固:服务端答案恢复后**必须**再用本地草稿补齐 ——
|
||||
// 逐题保存失败的作答只存在于本机 localStorage,若不合并草稿,
|
||||
// 「断网期间作答后刷新」会静默丢失这批内容。两条路径都要尝试草稿恢复,
|
||||
// 且草稿恢复失败绝不能中断进入考试。
|
||||
try {
|
||||
const answersData = await examApi.getAnswers(group)
|
||||
const restored = {}
|
||||
if (answersData && answersData.answers && answersData.answers.length > 0) {
|
||||
const restored = {}
|
||||
answersData.answers.forEach(a => {
|
||||
if (a.answer_data) {
|
||||
restored[a.question_uid] = a.answer_data
|
||||
}
|
||||
})
|
||||
if (Object.keys(restored).length > 0) {
|
||||
examStore.restoreAnswers(restored)
|
||||
}
|
||||
}
|
||||
examStore.restoreAnswers(restored)
|
||||
// 草稿只补「服务端没有」的题,绝不覆盖服务端已有作答
|
||||
const merged = examStore.restoreDraft()
|
||||
if (merged && merged.merged > 0) {
|
||||
ElMessage.warning(`已从本机恢复 ${merged.merged} 道未同步到服务器的作答,请核对后再交卷`)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('恢复已有答案失败', e)
|
||||
console.warn('恢复已有答案失败,尝试仅用本地草稿恢复', e)
|
||||
try {
|
||||
const merged = examStore.restoreDraft()
|
||||
if (merged && merged.merged > 0) {
|
||||
ElMessage.warning(`服务器暂时不可用,已从本机恢复 ${merged.merged} 道作答`)
|
||||
}
|
||||
} catch (e2) {
|
||||
console.warn('本地草稿恢复失败', e2)
|
||||
}
|
||||
}
|
||||
window.addEventListener('beforeunload', handleBeforeUnload)
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user