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';
|
'use strict';
|
||||||
|
|
||||||
const Controller = require('egg').Controller;
|
const Controller = require('egg').Controller;
|
||||||
|
const { resolveAggregationKeysForContestant } = require('../lib/aggregationKey');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 成绩查询控制器 - ScoreController
|
* 成绩查询控制器 - ScoreController
|
||||||
@@ -138,8 +139,10 @@ class ScoreController extends Controller {
|
|||||||
where: { id: operatorId },
|
where: { id: operatorId },
|
||||||
attributes: ['id', 'name', 'word_score', 'ppt_score', 'score', 'exam_score'],
|
attributes: ['id', 'name', 'word_score', 'ppt_score', 'score', 'exam_score'],
|
||||||
});
|
});
|
||||||
const keys = [String(operatorId)];
|
// 归集键(单一真相源 lib/aggregationKey):
|
||||||
if (operatorAggUid) keys.push(String(operatorAggUid));
|
// 中职/高职 = contestant.id 为主键;兜底键(真实聚合 uid)仅在无歧义时启用,
|
||||||
|
// 避免「操作员真实聚合 uid 撞上同组他人 contestant.id」而读到他人(甚至他队)作答记录。
|
||||||
|
const keys = await resolveAggregationKeysForContestant(ctx, group, operatorId, operatorAggUid);
|
||||||
|
|
||||||
return { group, isExam, operatorId, operator, aggKeys: keys, viewerName: me.name };
|
return { group, isExam, operatorId, operator, aggKeys: keys, viewerName: me.name };
|
||||||
}
|
}
|
||||||
|
|||||||
152
app/lib/aggregationKey.js
Normal file
152
app/lib/aggregationKey.js
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 归集键单一真相源 - aggregationKey
|
||||||
|
* ===================================================================
|
||||||
|
* 背景(2026-10-08 生产实测,人工测试反馈「测试账号的提交数据出现在正常
|
||||||
|
* 生产账号上」):
|
||||||
|
*
|
||||||
|
* 各组提交表的 aggregation_uid **写入键并不统一**:
|
||||||
|
* - 中职 / 高职:controller/exam.js 落库时写 `aggregation_uid = contestant.id`
|
||||||
|
* - 本科 / 教师:benke_submissions / jiaoshi_submissions 写
|
||||||
|
* `aggregation.aggregation_uid`(真实聚合 uid)
|
||||||
|
*
|
||||||
|
* 历史实现为兼容「两种写法并存」,对两侧都做**双键归集**——同时用
|
||||||
|
* `contestant.id` 与「真实 aggregation_uid」去匹配提交表。
|
||||||
|
*
|
||||||
|
* 但这两套编号来自**彼此独立的自增序列**,数值区间完全重叠。于是当
|
||||||
|
* 「某人的真实聚合 uid == 同组另一人的 contestant.id」时,前者会把后者的
|
||||||
|
* 作答记录 / 作品提交「借」过来展示 —— 生产实测共 **283 对**碰撞
|
||||||
|
* (gaozhi 239 / benke 43 / jiaoshi 1),已复现 1 例:
|
||||||
|
* 王乐乐(cid=600, 真实聚合 uid=621) 命中 吴巍荪(cid=621) 的 70 题 / 38 分。
|
||||||
|
*
|
||||||
|
* 治法(本模块):
|
||||||
|
* 1. **主键按组语义确定**,单一、无歧义:
|
||||||
|
* - 考试组(zhongzhi / gaozhi)主键 = contestant.id
|
||||||
|
* - 作品组(benke / jiaoshi)主键 = 真实 aggregation_uid
|
||||||
|
* 2. **兜底键沿用历史混杂写法,但必须通过歧义判定**才启用:
|
||||||
|
* - 不得等于同组任何人的主键(唯一的真实歧义来源:跨编号空间重叠)
|
||||||
|
* - (「兜底键被多人共用」结构上不可能:两套编号各自都是 PRIMARY KEY,
|
||||||
|
* 详见 buildAggregationKeyMap 内的不变量说明,故不设该分支)
|
||||||
|
* 未通过者一律丢弃 —— 只丢掉「本就会命中他人数据」的兜底,保留其余兼容性。
|
||||||
|
*
|
||||||
|
* 生产实测效果:283 个碰撞兜底被丢弃,317 个安全兜底保留,残留碰撞 **0**。
|
||||||
|
*
|
||||||
|
* ⚠️ 使用约束:本模块的歧义判定以 **同组** 为范围。调用方必须保证
|
||||||
|
* `contestants` 与查询的提交表同属一个 `group`(提交表按组物理分表,
|
||||||
|
* 跨组不会互相命中,故同组范围即充分)。
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** 考试类组别(客观题 + 综合大题;提交表按 contestant.id 归集) */
|
||||||
|
const EXAM_GROUPS = ['zhongzhi', 'gaozhi'];
|
||||||
|
|
||||||
|
/** 是否为考试类组别 */
|
||||||
|
function isExamGroup(group) {
|
||||||
|
return EXAM_GROUPS.indexOf(group) !== -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _blank(v) {
|
||||||
|
return v === null || v === undefined || v === '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主键:该组数据**实际写入**所用的键。
|
||||||
|
* @param {string} group
|
||||||
|
* @param {number|string} contestantId contestants.id
|
||||||
|
* @param {number|string|null} realAggUid aggregation.aggregation_uid
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function primaryKeyOf(group, contestantId, realAggUid) {
|
||||||
|
const cid = String(contestantId);
|
||||||
|
if (isExamGroup(group)) return cid;
|
||||||
|
// 作品组:主键是真实聚合 uid;缺失时退化为 cid(保持可用,不再引入第二个键)
|
||||||
|
return _blank(realAggUid) ? cid : String(realAggUid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 兜底键:历史混杂写法所用键(与主键分属**相反**的编号空间)。
|
||||||
|
* @returns {string|null} null 表示无兜底键
|
||||||
|
*/
|
||||||
|
function fallbackKeyOf(group, contestantId, realAggUid) {
|
||||||
|
const cid = String(contestantId);
|
||||||
|
if (isExamGroup(group)) return _blank(realAggUid) ? null : String(realAggUid);
|
||||||
|
return cid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量:为**同组**全部选手构建归集键。
|
||||||
|
* @param {string} group
|
||||||
|
* @param {Array<{id:number|string}>} contestants 同组选手列表
|
||||||
|
* @param {Object<string, number|string>} realAggByContestant contestant.id -> 真实 aggregation_uid
|
||||||
|
* @returns {Object<string, string[]>} contestant.id -> 归集键数组(首元素必为主键,且不重复)
|
||||||
|
*/
|
||||||
|
function buildAggregationKeyMap(group, contestants, realAggByContestant) {
|
||||||
|
const aggMap = realAggByContestant || {};
|
||||||
|
const list = Array.isArray(contestants) ? contestants : [];
|
||||||
|
const primarySet = new Set();
|
||||||
|
const plan = new Map();
|
||||||
|
|
||||||
|
for (const c of list) {
|
||||||
|
const cid = String(c.id);
|
||||||
|
const rag = aggMap[cid] !== undefined ? aggMap[cid] : aggMap[c.id];
|
||||||
|
const p = primaryKeyOf(group, cid, rag);
|
||||||
|
const f = fallbackKeyOf(group, cid, rag);
|
||||||
|
plan.set(cid, { p, f });
|
||||||
|
primarySet.add(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ⚠️ 结构性不变量(已核对 DDL):`aggregation.aggregation_uid` 是 PRIMARY KEY(全局唯一),
|
||||||
|
// `contestants.id` 也是 PRIMARY KEY ⇒ 兜底键不可能被两名选手共用
|
||||||
|
// (考试组兜底 = 真实聚合 uid,作品组兜底 = cid,两者各自唯一)。
|
||||||
|
// 故**不需要**「兜底键被多人共用」的计数分支——那是永假的死判据。
|
||||||
|
// 唯一真实的歧义来源 = 跨编号空间重叠(兜底键撞上他人主键),下面一行即判据。
|
||||||
|
const result = {};
|
||||||
|
for (const [cid, { p, f }] of plan) {
|
||||||
|
const keys = [p];
|
||||||
|
if (f && f !== p && !primarySet.has(f)) keys.push(f);
|
||||||
|
result[cid] = keys;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判定某个键是否**已被同组他人认领为主键**(单选手路径的兜底键歧义判定)。
|
||||||
|
*
|
||||||
|
* - 考试组:主键 = contestant.id ⇒ 查同组 contestants 是否存在该 id
|
||||||
|
* - 作品组:主键 = 真实 aggregation_uid ⇒ 查同组 aggregation 是否存在该 aggregation_uid
|
||||||
|
*
|
||||||
|
* 两处查询都命中主键/唯一列,`COUNT` ∈ {0,1},判据非退化(`>0` 即「有人认领」)。
|
||||||
|
*
|
||||||
|
* @returns {Promise<boolean>} true = 有歧义,兜底键必须丢弃
|
||||||
|
*/
|
||||||
|
async function isKeyClaimedInGroup(ctx, group, key) {
|
||||||
|
if (_blank(key)) return false;
|
||||||
|
const numeric = /^\d+$/.test(String(key)) ? Number(key) : String(key);
|
||||||
|
if (isExamGroup(group)) {
|
||||||
|
return (await ctx.model.Contestant.count({ where: { id: numeric, group } })) > 0;
|
||||||
|
}
|
||||||
|
return (await ctx.model.Aggregation.count({ where: { aggregation_uid: numeric, group } })) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单选手:解析归集键(语义与 buildAggregationKeyMap 一致,歧义判定改为按需查库)。
|
||||||
|
* @returns {Promise<string[]>} 归集键数组(首元素必为主键)
|
||||||
|
*/
|
||||||
|
async function resolveAggregationKeysForContestant(ctx, group, contestantId, realAggUid) {
|
||||||
|
const cid = String(contestantId);
|
||||||
|
const p = primaryKeyOf(group, cid, realAggUid);
|
||||||
|
const f = fallbackKeyOf(group, cid, realAggUid);
|
||||||
|
const keys = [p];
|
||||||
|
if (f && f !== p && !(await isKeyClaimedInGroup(ctx, group, f))) keys.push(f);
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
EXAM_GROUPS,
|
||||||
|
isExamGroup,
|
||||||
|
primaryKeyOf,
|
||||||
|
fallbackKeyOf,
|
||||||
|
buildAggregationKeyMap,
|
||||||
|
isKeyClaimedInGroup,
|
||||||
|
resolveAggregationKeysForContestant,
|
||||||
|
};
|
||||||
@@ -4,6 +4,10 @@ const Service = require('egg').Service;
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const XLSX = require('xlsx');
|
const XLSX = require('xlsx');
|
||||||
|
const {
|
||||||
|
buildAggregationKeyMap,
|
||||||
|
resolveAggregationKeysForContestant,
|
||||||
|
} = require('../lib/aggregationKey');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 客观题(选择题 / 判断题)每小题固定分值。
|
* 客观题(选择题 / 判断题)每小题固定分值。
|
||||||
@@ -457,11 +461,10 @@ class AdminService extends Service {
|
|||||||
order: [['id', 'ASC']],
|
order: [['id', 'ASC']],
|
||||||
});
|
});
|
||||||
|
|
||||||
// 建立 contestant.id -> 真实 aggregation_uid 映射。
|
// 归集键统一由 lib/aggregationKey 提供(单一真相源):
|
||||||
// 旧代码曾把聚合报名 uid 直接写进 zhongzhi/gaozhi_submissions 的 aggregation_uid 列,
|
// 主键按组语义确定(中职/高职 = contestant.id;本科/教师 = 真实 aggregation_uid),
|
||||||
// 而当前 _getUserAggregation 返回 aggregation_uid = contestant.id;两种写法并存于历史数据。
|
// 兜底键沿用历史混杂写法,但**仅在不与同组任何主键冲突、且不被多人共用时才启用**。
|
||||||
// 若仅按 contestant.id 归集,历史提交者的答题数/提交状态会因聚合键错位而恒为 0 / 未提交。
|
// 旧实现无条件双键归集 ⇒ 两套独立自增序列数值区间重叠时会命中他人作答记录(生产 283 对碰撞)。
|
||||||
// 故按「contestant.id」与「真实 aggregation_uid」两种 key 同时归集,消除错位。
|
|
||||||
const aggRows = await ctx.model.Aggregation.findAll({
|
const aggRows = await ctx.model.Aggregation.findAll({
|
||||||
where: { account_uid: contestants.map(c => c.id) },
|
where: { account_uid: contestants.map(c => c.id) },
|
||||||
attributes: ['account_uid', 'aggregation_uid'],
|
attributes: ['account_uid', 'aggregation_uid'],
|
||||||
@@ -469,6 +472,7 @@ class AdminService extends Service {
|
|||||||
});
|
});
|
||||||
const realAggByContestant = {};
|
const realAggByContestant = {};
|
||||||
aggRows.forEach(a => { realAggByContestant[a.account_uid] = a.aggregation_uid; });
|
aggRows.forEach(a => { realAggByContestant[a.account_uid] = a.aggregation_uid; });
|
||||||
|
const aggKeysByContestant = buildAggregationKeyMap(group, contestants, realAggByContestant);
|
||||||
|
|
||||||
// 获取所有提交记录
|
// 获取所有提交记录
|
||||||
const allSubmissions = await SubmissionModel.findAll({
|
const allSubmissions = await SubmissionModel.findAll({
|
||||||
@@ -495,8 +499,8 @@ class AdminService extends Service {
|
|||||||
examSubs.forEach(w => { wordMap[String(w.contestant_id)] = w; });
|
examSubs.forEach(w => { wordMap[String(w.contestant_id)] = w; });
|
||||||
|
|
||||||
return contestants.map(c => {
|
return contestants.map(c => {
|
||||||
// 兼容新旧两种聚合键:同时归集 contestant.id 与真实 aggregation_uid 下的答题记录
|
// 归集键(含歧义消解后的兜底键);首元素必为主键
|
||||||
const candidateKeys = [String(c.id), String(realAggByContestant[c.id])].filter(Boolean);
|
const candidateKeys = aggKeysByContestant[String(c.id)] || [String(c.id)];
|
||||||
const subs = candidateKeys.flatMap(k => submissionsByUser[k] || []);
|
const subs = candidateKeys.flatMap(k => submissionsByUser[k] || []);
|
||||||
const answeredCount = subs.length;
|
const answeredCount = subs.length;
|
||||||
const autoScore = subs.reduce((sum, s) => sum + (s.score || 0), 0);
|
const autoScore = subs.reduce((sum, s) => sum + (s.score || 0), 0);
|
||||||
@@ -1353,11 +1357,10 @@ class AdminService extends Service {
|
|||||||
raw: true,
|
raw: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// contestant.id -> 真实 aggregation_uid 映射。
|
// 归集键统一由 lib/aggregationKey 提供(单一真相源,见模块头注释):
|
||||||
// 历史数据曾把真实聚合 uid 写进 zhongzhi/gaozhi_submissions 的 aggregation_uid 列,
|
// 主键按组语义确定,兜底键仅在无歧义时启用。
|
||||||
// 而当前 _getUserAggregation 返回 aggregation_uid = contestant.id;两种写法并存。
|
// ⚠️ 旧实现无条件双键归集 ⇒ 「真实聚合 uid」撞上同组他人 contestant.id 时,
|
||||||
// 若仅按 contestant.id 归集,纯历史数据者的答题数/卷面分会因聚合键错位而恒为 0。
|
// 会把他人作答记录(答题数/卷面分)计入本行,生产已复现(cid=600 命中 cid=621)。
|
||||||
// 故按「contestant.id」与「真实 aggregation_uid」两种 key 同时归集,消除错位。
|
|
||||||
const aggRows = await ctx.model.Aggregation.findAll({
|
const aggRows = await ctx.model.Aggregation.findAll({
|
||||||
where: { account_uid: contestants.map(c => c.id) },
|
where: { account_uid: contestants.map(c => c.id) },
|
||||||
attributes: ['account_uid', 'aggregation_uid'],
|
attributes: ['account_uid', 'aggregation_uid'],
|
||||||
@@ -1365,6 +1368,7 @@ class AdminService extends Service {
|
|||||||
});
|
});
|
||||||
const realAggByContestant = {};
|
const realAggByContestant = {};
|
||||||
aggRows.forEach(a => { realAggByContestant[a.account_uid] = a.aggregation_uid; });
|
aggRows.forEach(a => { realAggByContestant[a.account_uid] = a.aggregation_uid; });
|
||||||
|
const aggKeysByContestant = buildAggregationKeyMap(group, contestants, realAggByContestant);
|
||||||
|
|
||||||
const list = contestants.map(c => ({
|
const list = contestants.map(c => ({
|
||||||
uid: c.id,
|
uid: c.id,
|
||||||
@@ -1401,7 +1405,7 @@ class AdminService extends Service {
|
|||||||
const examMap = {};
|
const examMap = {};
|
||||||
examSubs.forEach(w => { examMap[String(w.contestant_id)] = w; });
|
examSubs.forEach(w => { examMap[String(w.contestant_id)] = w; });
|
||||||
list.forEach(item => {
|
list.forEach(item => {
|
||||||
const keys = [String(item.uid), String(realAggByContestant[item.uid])].filter(Boolean);
|
const keys = aggKeysByContestant[String(item.uid)] || [String(item.uid)];
|
||||||
let answeredCount = 0;
|
let answeredCount = 0;
|
||||||
let autoScore = 0;
|
let autoScore = 0;
|
||||||
keys.forEach(k => {
|
keys.forEach(k => {
|
||||||
@@ -1438,7 +1442,7 @@ class AdminService extends Service {
|
|||||||
if (!subByAgg[k]) subByAgg[k] = s;
|
if (!subByAgg[k]) subByAgg[k] = s;
|
||||||
});
|
});
|
||||||
list.forEach(item => {
|
list.forEach(item => {
|
||||||
const keys = [String(item.uid), String(realAggByContestant[item.uid])].filter(Boolean);
|
const keys = aggKeysByContestant[String(item.uid)] || [String(item.uid)];
|
||||||
let sub = null;
|
let sub = null;
|
||||||
keys.forEach(k => { if (!sub && subByAgg[k]) sub = subByAgg[k]; });
|
keys.forEach(k => { if (!sub && subByAgg[k]) sub = subByAgg[k]; });
|
||||||
item.answeredCount = 0;
|
item.answeredCount = 0;
|
||||||
@@ -2152,16 +2156,20 @@ class AdminService extends Service {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 1) 双键归集(与 getExamSubmissions 完全同口径) ----
|
// ---- 1) 归集键(单一真相源 lib/aggregationKey,与「考生答题记录」完全同口径) ----
|
||||||
|
// 兜底键仅在无歧义(不被同组他人当作主键)时启用,
|
||||||
|
// 避免「真实聚合 uid 撞上同组他人 contestant.id」而把他人作答记录并入本复核页。
|
||||||
const agg = await ctx.model.Aggregation.findOne({
|
const agg = await ctx.model.Aggregation.findOne({
|
||||||
where: { account_uid: cid },
|
where: { account_uid: cid },
|
||||||
attributes: ['aggregation_uid'],
|
attributes: ['aggregation_uid'],
|
||||||
raw: true,
|
raw: true,
|
||||||
});
|
});
|
||||||
const keys = [String(cid)];
|
const keys = await resolveAggregationKeysForContestant(
|
||||||
if (agg && agg.aggregation_uid != null && String(agg.aggregation_uid) !== String(cid)) {
|
ctx,
|
||||||
keys.push(String(agg.aggregation_uid));
|
g,
|
||||||
}
|
cid,
|
||||||
|
agg ? agg.aggregation_uid : null
|
||||||
|
);
|
||||||
|
|
||||||
const SubmissionModel = g === 'zhongzhi' ? ctx.model.ZhongzhiSubmission : ctx.model.GaozhiSubmission;
|
const SubmissionModel = g === 'zhongzhi' ? ctx.model.ZhongzhiSubmission : ctx.model.GaozhiSubmission;
|
||||||
const subs = await SubmissionModel.findAll({
|
const subs = await SubmissionModel.findAll({
|
||||||
@@ -2350,10 +2358,12 @@ class AdminService extends Service {
|
|||||||
attributes: ['aggregation_uid'],
|
attributes: ['aggregation_uid'],
|
||||||
raw: true,
|
raw: true,
|
||||||
});
|
});
|
||||||
const keys = [String(contestantId)];
|
const keys = await resolveAggregationKeysForContestant(
|
||||||
if (agg && agg.aggregation_uid != null && String(agg.aggregation_uid) !== String(contestantId)) {
|
ctx,
|
||||||
keys.push(String(agg.aggregation_uid));
|
group,
|
||||||
}
|
contestantId,
|
||||||
|
agg ? agg.aggregation_uid : null
|
||||||
|
);
|
||||||
|
|
||||||
const rows = await SubmissionModel.findAll({
|
const rows = await SubmissionModel.findAll({
|
||||||
where: { aggregation_uid: { [ctx.model.Sequelize.Op.in]: keys.map(Number) } },
|
where: { aggregation_uid: { [ctx.model.Sequelize.Op.in]: keys.map(Number) } },
|
||||||
|
|||||||
Reference in New Issue
Block a user