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:
2026-09-22 09:17:06 +08:00
parent 1bfe007149
commit c2551b64e8
5 changed files with 523 additions and 35 deletions

View File

@@ -31,13 +31,14 @@ export function activate(data) {
}
/**
* 参赛者登录(身份证号 + 密码)
* @param {string} idNumber - 身份证号
* 参赛者登录(手机号 + 密码)
* @param {string} phone - 手机号
* @param {string} password - 密码
* @param {object} [config] - 透传给 axios 的配置(如 { silent: true } 抑制拦截器逐次错误提示)
* @returns {Promise} { token, user }
*/
export function login(phone, password) {
return request.post('/auth/login', { phone, password })
export function login(phone, password, config) {
return request.post('/auth/login', { phone, password }, config)
}
/**

View File

@@ -175,6 +175,16 @@ request.interceptors.response.use(
return res
},
async (error) => {
// silent 模式(2026-09-21 新增):调用方可在请求 config 上传 silent: true
// 来抑制**逐次**错误提示。供自带走重试的调用方(登录 / 交卷 / 逐题保存)使用,
// 避免重试期间连弹多条相同报错、让考生误以为系统崩了。
// ⛔ 不传 silent 的既有调用方行为**完全不变**(silent 为 undefined → falsy)。
// 注:401 分支的提示由 forceLogout 负责(token 真失效必须让用户看到),不受此影响。
const silent = !!(error.config && error.config.silent)
const notifyError = (msg) => {
if (!silent) ElMessage.error(msg)
}
// 处理 HTTP 状态码错误
if (error.response) {
const { status } = error.response
@@ -240,26 +250,26 @@ request.interceptors.response.use(
}
case 403:
ElMessage.error('您没有权限执行此操作')
notifyError('您没有权限执行此操作')
break
case 404:
ElMessage.error('请求的资源不存在')
notifyError('请求的资源不存在')
break
case 500:
ElMessage.error('服务器内部错误,请稍后重试')
notifyError('服务器内部错误,请稍后重试')
break
default:
ElMessage.error(error.response.data?.message || `请求失败 (${status})`)
notifyError(error.response.data?.message || `请求失败 (${status})`)
}
} else if (error.code === 'ECONNABORTED') {
// 请求超时
ElMessage.error('请求超时,请检查网络连接')
notifyError('请求超时,请检查网络连接')
} else {
// 网络错误或其他错误
ElMessage.error('网络连接异常,请检查网络')
notifyError('网络连接异常,请检查网络')
}
return Promise.reject(error)