feat(admin): R176-R180 公示表与新闻撤回 + 管理端多管理员权限(C 层)+ 交卷冻结

- R176 成绩公示表引擎(lib/publicitySheet.js + lib/assets/publicity-template.xlsx 官方模板基底)
  与指导教师回填(lib/teacherBackfill.js);赛项名称/编号改为配置项
- R177 新闻「发布 ↔ 撤回」互斥:撤回只置 is_draft=1,不删除数据
- R178 C 层 RBAC:lib/adminPermissions.js(13 权限点 / 70 条路由规则,唯一 SoT)
  + middleware/auth.js 三段判定(默认拒绝 · is_super 单点 · token_version)
  + controller/service/adminUser.js + lib/auditLog.js + model/adminAuditLog.js
- R178 B1 交卷冻结:service.exam.assertNotSubmitted(bizCode 4006 单一判据源)
- R178 B④ 提交口径拆分:formalSubmitted / submitted / answeredCount 三字段
- R178 成绩总览补小组编号:resolveTeamLabels(与选手管理同源,含兜底与三态)
- R181 生产迁移配套:admins +7 列、admin_audit_logs(scripts/migrate_r178_admin_rbac.py)
This commit is contained in:
2026-10-20 11:06:29 +08:00
parent 5392345078
commit 459f1adf6d
21 changed files with 2886 additions and 76 deletions

View File

@@ -389,6 +389,12 @@ class ExamController extends Controller {
return;
}
// 🔴 交卷冻结(R178·B①):已正式交卷者不得再改写答案。
// 位置刻意在判分之前 —— 已冻结的答卷不该再做一次无意义的 autoScore。
// 与 service.saveAnswers 内的兜底调用是**同一个函数**(单一判据源),
// 不是两套条件;异常由下方 catch 按 bizCode 翻译成 4006。
await service.exam.assertNotSubmitted(aggregation.aggregation_uid);
// 参数校验
const rule = {
answers: { type: 'array', required: true, min: 1 },
@@ -452,6 +458,15 @@ class ExamController extends Controller {
savedCount: savedRecords.length,
}, '答案提交成功');
} catch (err) {
// 业务性拒绝(如「已交卷,答卷已冻结」)不该被伪装成 500:
// service 层用 bizCode 标记期望的错误码,这里原样翻译给前端。
if (err && err.bizCode) {
ctx.logger.warn(
`[submitAnswers] 业务拒绝 code=${err.bizCode} msg=${err.bizMessage || err.message}`
);
ctx.body = ctx.helper.error(err.bizCode, err.bizMessage || err.message);
return;
}
ctx.logger.error('提交答案失败:', err);
ctx.body = ctx.helper.error(500, '提交答案失败');
}