fix(agg-key): 归集键串行修复 —— 考试/作品提交记录不得跨选手串行展示
现象(人工测试反馈,已取证):
测试账号 吴巍荪(cid=621, test_flag=1) 完成一次高职考试提交后,
正常生产账号 王乐乐(cid=600, test_flag=0) 在管理端评分列表出现**完全相同**的
作答数据(70 题 / 38 分),而王乐乐本人从未作答。
根因:
各提交表的 aggregation_uid 写入键不统一 ——
- 中职/高职:controller/exam.js 写 aggregation_uid = contestant.id
- 本科/教师:*_submissions 写 aggregation.aggregation_uid(真实聚合 uid)
历史实现为兼容「两种写法并存」,对两侧都做**无条件双键归集**
(同时用 contestant.id 与真实 aggregation_uid 去匹配提交表)。
而这两套编号来自**彼此独立的自增序列**、数值区间完全重叠 ⇒
「某人的真实聚合 uid == 同组另一人的 contestant.id」时,
前者会把后者的作答/作品记录「借」过来展示。
生产实测共 283 对碰撞(gaozhi 239 / benke 43 / jiaoshi 1)。
治法:新增唯一真相源 app/lib/aggregationKey.js
- 主键按组语义确定:考试组 = contestant.id;作品组 = 真实 aggregation_uid
- 兜底键沿用历史混杂写法,但**仅在与同组任何主键不冲突时**才启用
(未通过者丢弃 —— 只丢掉「本就会命中他人数据」的兜底,保留其余兼容性)
- 不设「兜底键被多人共用」分支:两套编号各自都是 PRIMARY KEY
(aggregation_uid / contestants.id),该情形结构上不可能发生,属死判据
- 6 处归集点全部改走该模块:
service/admin.js getExamSubmissions / getScores(考试分支) / getScores(作品分支)
getContestantScore / _reviewWorks
controller/score.js _resolveTeamScoreSource(考生端,与 getContestantScore 同函数)
生产实测影响面(用**线上实际部署的模块** + 生产真实数据核算,n=600):
旧规则碰撞对 283 → 新规则残留碰撞 **0**;兜底保留 317 / 丢弃 283
守恒校验:保留 317 + 丢弃 283 = 600;丢弃数 == 旧规则碰撞对 ⇒ YES
观测值会变化的考生 = **仅 1 人**(gaozhi cid=600 王乐乐 旧=70 → 新=0,即本病例)
吴巍荪 cid=621 归集键 ["621","642"] 行数 70 保持 **未被误伤**
验证:scripts/verify_agg_key_crosstalk.py 17/17 PASS(真实 HTTP + 库内真值)
夹具刻意构造「真实聚合 uid == 同组他人 contestant.id」并含**正向对照**(兜底保留者)
变异自证 A(拆掉碰撞闸门)→ H1/H4/H5/H6/B1/B4 共 6 项变红,失败信息精确复现原现象
变异自证 B(兜底键整体删除=过度修复)→ H3/H4/H5/H8 共 4 项变红
⇒ 两条职责各有独立判别器;还原后 diff 空、md5 一致、零标记残留
部署(生产 back-end):线上 3 文件 md5 与本地逐文件一致;
MainPID 3343212 → 3344477;8023 监听正常;journalctl -p err = -- No entries --;
路由探针 health 200 / 错口令 HTTP200+code4004 / 不存在路由 404 / 未授权 401
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const Controller = require('egg').Controller;
|
||||
const { resolveAggregationKeysForContestant } = require('../lib/aggregationKey');
|
||||
|
||||
/**
|
||||
* 成绩查询控制器 - ScoreController
|
||||
@@ -138,8 +139,10 @@ class ScoreController extends Controller {
|
||||
where: { id: operatorId },
|
||||
attributes: ['id', 'name', 'word_score', 'ppt_score', 'score', 'exam_score'],
|
||||
});
|
||||
const keys = [String(operatorId)];
|
||||
if (operatorAggUid) keys.push(String(operatorAggUid));
|
||||
// 归集键(单一真相源 lib/aggregationKey):
|
||||
// 中职/高职 = contestant.id 为主键;兜底键(真实聚合 uid)仅在无歧义时启用,
|
||||
// 避免「操作员真实聚合 uid 撞上同组他人 contestant.id」而读到他人(甚至他队)作答记录。
|
||||
const keys = await resolveAggregationKeysForContestant(ctx, group, operatorId, operatorAggUid);
|
||||
|
||||
return { group, isExam, operatorId, operator, aggKeys: keys, viewerName: me.name };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user