4 Commits

Author SHA1 Message Date
459f1adf6d 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)
2026-10-20 11:06:29 +08:00
e8a5b34d42 fix(backend): 补齐单次上传端点的并发护栏(BACKLOG C7)
- middleware/concurrency_guard.js: HEAVY_PATTERNS 增加单段上传正则
  /^\/api\/upload\/[^/]+$/i —— 单段锚定([^/]+$)保证不误伤"两段"的轻路由:
  GET /status、GET /submissions、DELETE /:id(放宽成前缀匹配会让「查状态也 429」)
- controller/upload.js: upload() 补 await acquireFileIo() +
  finally { if (ioAcquired) releaseFileIo(); }
  ⚠️ 不用 uploadChunk 的无条件 release:本方法 429 分支是 acquire 失败后 return,
     无条件释放会把"别人的"配额放掉 => 计数下溢 => 护栏静默失效

背景:修复前该端点是**唯一在用**的作品上传路径(Competition.vue:477),却既不在
      concurrencyGuard.HEAVY_PATTERNS(表内 chunk/merge 已成死路由)、也不调 file_io_guard
      => 零护栏。因服务器入带宽仅 ≈1.25MB/s 天然节流,当前风险有限,但按 C3 升入带宽
      后即变实质风险 => 与 C3 绑定,先补护栏。

验证:
- verify-guard-r43.mjs 33/33(单元:路径判别力 + 满载 429 + 配额隔离 + 释放语义 + 变异自证)
- verify-guard-r43-live.mjs 8/9(真接口:30 路 5MB 并发上传 => 20 成功 / 10 个 429 +
  Retry-After: 2;15 个轻路由探针在并发中全 200;唯一失败为清理步骤,根因是
  saveSubmissionRecord 的 upsert 语义,非缺陷)
- 重启后 master PID 23684;[AppBoot][pool] 40 <= 151
2026-09-29 13:17:53 +08:00
23604f2718 feat(exam/auth): 登录闸门 + 交卷稳定性加固(R4-R8)
R4 登录/交卷背压闸门
- app/middleware/concurrency_guard.js: 新增。超限即 429 + Retry-After(立即拒绝式,
  不排队;实测 429 P50=34ms)。RETRY_AFTER 收敛为单一有依据常量 = '2'
  (≈1.8 个满载窗口;实测 bcryptjs 1129ms / 原生 265ms 每窗口)。
  配额:LOGIN_MAX=16 / SUBMIT_MAX=24 / HEAVY_REQ_MAX=20,各 *REQ_MAX 覆盖。
- config/config.default.js: 闸门配额与连接池容量配置;DB dialect 锁 mysql。

R7 顺序红线 + R8 Retry-After 同源 + 原生 bcrypt
- 3 处硬编码 '2' 收敛为单一常量;补"勿因换实现而删闸门"红线注释。
- bcryptjs -> bcrypt@6(N-API,Windows/Linux 均免编译;drop-in,存量哈希 $2b$10$ 兼容)。
- app/model/account.js / app/service/auth.js: 同步哈希实现注释(D4 全族 5 处陈旧
  SHA256 注释修正)。

答题链路与综合大题
- app/controller/exam.js: 中职/高职综合大题接口(GET big-question / POST big-answer,
  逐问 2000 字上限校验);uploadWord 对中职/高职封堵(400)。
- app/service/exam.js: 抽题快照 question_snapshot 落库、答案归集键修正。
- app/controller/auth.js / app/service/auth.js: toClientUser() 唯一出口,
  未发布态按组剔除 score,exam_score/word_score/ppt_score 任何情况下不下发。
- app/router.js: 新增大题与管理端路由。
- app/extend/helper.js: 下载文件名 RFC5987 编码(禁双重编码)。
- app.js: 池容量断言按 env 区分单/多 worker(防 dev 假日志)。

验证:ci-gates.sh 9/9 全绿;verify_login_guard.py 28/28(含 D 组变异 9/9);
真实登录/抽题/交卷端到端 code=0;前端构建 BUILD_RC=0。
2026-09-22 09:16:55 +08:00
WorkBuddy
a303cf2d53 chore: 后端首次基线提交
运行必须源码:app/(controller/model/service/middleware/lib/utils/validate/router)、config/、database/(migrations/seeders/sql)、启动脚本、app/public/templates 模板。
已忽略运行非必须:node_modules/、logs/、run/、app/public 运行时上传数据、.env*、构建/缓存产物。
2026-08-17 12:25:42 +08:00