完成批次 L-R:限流移除/白名单驱动/打印增强/全屏/表格对齐/主题变量/滚动闪烁修复
- 移除登录失败限流;fix-config.php 修正 e2e 串味的 127.0.0.1 连接 - 白名单驱动缺/待考名单(DataService + SurveyKingClient::getWhitelist,复用已取 survey) - 打印成绩明细增强(范围/合格列默认关/去页脚);看板全屏占满;表格 vertical-align:middle;隐藏登陆账号;导入默认追加 - 主题变量收口(:root 新增 brand/primary-2/ok-2 等,替换硬编码渐变);删死规则 - 表头/数据列对齐修复(index 未匹配表、dashboard 成员榜、draw 结果/历史/排除表);看板占位卡片配色一致 - 修复滚动闪烁:row-drop-in 动画经 _dropAnim 门控,仅强制重绘时播放 - 测试:新增 test-whitelist.js;run-all.sh 全绿 132
This commit is contained in:
@@ -87,6 +87,8 @@ function ny_build_data(array $c, string $projectId, string $nameQid, bool $force
|
||||
(string)($c['name_keywords'] ?? ''),
|
||||
(int)($c['page_size'] ?? 100),
|
||||
Roster::fingerprint(),
|
||||
// 本卷独立配置(免考名单/满分/合格率)变化也应让缓存失效,否则改免考后可能短暂看到旧数据
|
||||
json_encode(DataService::projectConfig($projectId) ?? []),
|
||||
]);
|
||||
|
||||
if ($ttl > 0 && !$force) {
|
||||
@@ -110,11 +112,16 @@ function ny_build_data(array $c, string $projectId, string $nameQid, bool $force
|
||||
// 等不到(构建很慢或锁不被支持)就自己构建,保证可用性
|
||||
}
|
||||
|
||||
$result = DataService::build($c, $projectId, $nameQid);
|
||||
if ($ttl > 0) {
|
||||
DataCache::write($key, $result);
|
||||
// try/finally:无论 build 是否抛异常都释放构建锁,避免异常时锁句柄挂到进程退出造成惊群
|
||||
$result = null;
|
||||
try {
|
||||
$result = DataService::build($c, $projectId, $nameQid);
|
||||
if ($ttl > 0) {
|
||||
DataCache::write($key, $result);
|
||||
}
|
||||
} finally {
|
||||
DataCache::releaseLock($lock);
|
||||
}
|
||||
DataCache::releaseLock($lock);
|
||||
|
||||
return ['result' => $result, 'cached' => false, 'at' => time()];
|
||||
}
|
||||
@@ -152,11 +159,13 @@ try {
|
||||
$c['db_driver'] = class_exists('Db') ? Db::driver() : '';
|
||||
$c['db_configured'] = class_exists('Db') && Db::isConfigured();
|
||||
$c['db_connected'] = class_exists('Db') && Db::isConnected();
|
||||
$c['db_error'] = class_exists('Db') ? Db::lastError() : '';
|
||||
// 仅在未连接时回传错误详情(供前端诊断连接失败原因);
|
||||
// 已连接时一律不回传 lastError,避免把 SQL/文件路径等内部信息泄露给客户端。
|
||||
$c['db_error'] = (class_exists('Db') && !Db::isConnected()) ? Db::lastError() : '';
|
||||
ny_ok($c);
|
||||
|
||||
case 'config_save':
|
||||
$raw = (string)file_get_contents('php://input');
|
||||
$raw = ny_request_body();
|
||||
$in = json_decode($raw, true);
|
||||
if (!is_array($in)) {
|
||||
$in = $_POST;
|
||||
@@ -250,7 +259,7 @@ try {
|
||||
|
||||
// ---------------- 用弹窗内(未保存)的凭据测试连接 ----------------
|
||||
case 'config_test':
|
||||
$raw = (string)file_get_contents('php://input');
|
||||
$raw = ny_request_body();
|
||||
$in = json_decode($raw, true);
|
||||
if (!is_array($in)) {
|
||||
$in = $_POST;
|
||||
@@ -502,12 +511,49 @@ try {
|
||||
}
|
||||
// 解析完立即删除:Web 目录下不再留存任何上传文件
|
||||
@unlink($dest);
|
||||
$mode = (string)($_POST['mode'] ?? 'replace');
|
||||
// 默认追加:与「导入名单默认为追加」一致(学号名单管理导入)
|
||||
$mode = (string)($_POST['mode'] ?? 'append');
|
||||
if ($mode === 'append') {
|
||||
$merged = array_merge(Roster::all(), $res['students']);
|
||||
Roster::save($merged);
|
||||
// 追加:仅加入名单中尚不存在的成员(无学号时按姓名判重),避免重复行。
|
||||
// 学号与姓名分两套判重键:带学号者以 sid 判重,无学号者以姓名判重,
|
||||
// 避免「已存在带学号 A 的行」被「同名但空学号的导入行」误判为不重复而重复插入。
|
||||
$existing = Roster::all();
|
||||
$seenSid = [];
|
||||
$seenName = [];
|
||||
foreach ($existing as $s) {
|
||||
$sid = strtolower(trim((string)($s['sid'] ?? '')));
|
||||
$name = strtolower(trim((string)($s['name'] ?? '')));
|
||||
if ($sid !== '') {
|
||||
$seenSid[$sid] = true;
|
||||
} elseif ($name !== '') {
|
||||
$seenName[$name] = true;
|
||||
}
|
||||
}
|
||||
$added = [];
|
||||
foreach ($res['students'] as $s) {
|
||||
$sid = strtolower(trim((string)($s['sid'] ?? '')));
|
||||
$name = strtolower(trim((string)($s['name'] ?? '')));
|
||||
$dup = false;
|
||||
if ($sid !== '') {
|
||||
if (isset($seenSid[$sid])) { $dup = true; }
|
||||
else { $seenSid[$sid] = true; }
|
||||
} elseif ($name !== '') {
|
||||
if (isset($seenName[$name])) { $dup = true; }
|
||||
else { $seenName[$name] = true; }
|
||||
}
|
||||
if ($dup) {
|
||||
continue;
|
||||
}
|
||||
$added[] = $s;
|
||||
}
|
||||
if (!Roster::save(array_merge($existing, $added))) {
|
||||
ny_fail('名单追加失败:' . (class_exists('Db') ? Db::lastError() : '未知错误'));
|
||||
}
|
||||
$res['count'] = count($added); // 仅计入真正新增的人数
|
||||
} else {
|
||||
Roster::save($res['students']);
|
||||
if (!Roster::save($res['students'])) {
|
||||
ny_fail('名单导入失败:' . (class_exists('Db') ? Db::lastError() : '未知错误'));
|
||||
}
|
||||
}
|
||||
ny_ok([
|
||||
'students' => Roster::all(),
|
||||
@@ -517,8 +563,13 @@ try {
|
||||
'duplicates' => $res['duplicates'] ?? 0,
|
||||
]);
|
||||
|
||||
// ---------------- 从卷王白名单导入待考名单(已移除)----------------
|
||||
// 现改为:DataService::build 自动读取每套试卷卷内设置的「答题白名单」,
|
||||
// 白名单成员即「应考名单」,未设置时回退到全名单(按现在的方案显示)。
|
||||
// 名单管理里的「⬇ 从白名单导入」按钮已删除。
|
||||
|
||||
case 'roster_save':
|
||||
$raw = (string)file_get_contents('php://input');
|
||||
$raw = ny_request_body();
|
||||
$in = json_decode($raw, true);
|
||||
if (!is_array($in) || !isset($in['students']) || !is_array($in['students'])) {
|
||||
ny_fail('参数错误');
|
||||
@@ -536,7 +587,9 @@ try {
|
||||
'login' => trim((string)($s['login'] ?? '')),
|
||||
];
|
||||
}
|
||||
Roster::save($students);
|
||||
if (!Roster::save($students)) {
|
||||
ny_fail('名单保存失败:' . (class_exists('Db') ? Db::lastError() : '未知错误'));
|
||||
}
|
||||
ny_ok(['students' => Roster::all(), 'meta' => Roster::meta()]);
|
||||
|
||||
case 'roster_clear':
|
||||
@@ -545,7 +598,7 @@ try {
|
||||
|
||||
// ---------------- 未匹配答卷 → 加入名单 ----------------
|
||||
case 'unmatched_map':
|
||||
$raw = (string)file_get_contents('php://input');
|
||||
$raw = ny_request_body();
|
||||
$in = json_decode($raw, true);
|
||||
if (!is_array($in)) {
|
||||
$in = $_POST;
|
||||
@@ -575,7 +628,9 @@ try {
|
||||
if (!$found) {
|
||||
$roster[] = ['sid' => $sid, 'name' => $name, 'cls' => $cls, 'grp' => $grp, 'login' => $login];
|
||||
}
|
||||
Roster::save($roster);
|
||||
if (!Roster::save($roster)) {
|
||||
ny_fail('映射保存失败:' . (class_exists('Db') ? Db::lastError() : '未知错误'));
|
||||
}
|
||||
ny_ok(['students' => Roster::all(), 'meta' => Roster::meta(), 'mapped' => $name]);
|
||||
|
||||
// ---------------- 试卷独立配置(满分 / 合格率 / 免考名单)——仅对当前试卷生效 ----------------
|
||||
@@ -603,7 +658,7 @@ try {
|
||||
if ($projectId === '') {
|
||||
ny_fail('请先选择问卷/考试');
|
||||
}
|
||||
$raw = (string)file_get_contents('php://input');
|
||||
$raw = ny_request_body();
|
||||
$in = json_decode($raw, true);
|
||||
if (!is_array($in)) {
|
||||
$in = $_POST;
|
||||
@@ -741,11 +796,22 @@ try {
|
||||
// ---------------- 数据行 ----------------
|
||||
$sortMode = (string)($_GET['sort'] ?? '');
|
||||
if ($isLatest) {
|
||||
// 每人一行:成绩取「最近一次提交」的分数(不受 dedupe 影响),并附最高分与考试次数
|
||||
// 每人一行:成绩取「最近一次真实提交」的分数(不受 dedupe 影响),并附最高分与考试次数。
|
||||
// 注意:include_temp_save 开启时 attempts_list 可能含暂存草稿,取最新成绩应跳过 temp_save,
|
||||
// 否则导出的"最新成绩"可能是一份草稿分。
|
||||
$all = [];
|
||||
foreach ($rows as $r) {
|
||||
$list = array_values((array)($r['attempts_list'] ?? []));
|
||||
$last = $list !== [] ? $list[count($list) - 1] : null;
|
||||
$last = null;
|
||||
for ($i = count($list) - 1; $i >= 0; $i--) {
|
||||
if (empty($list[$i]['temp_save'])) {
|
||||
$last = $list[$i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($last === null && $list !== []) {
|
||||
$last = $list[count($list) - 1]; // 全是暂存时退回最后一份
|
||||
}
|
||||
$all[] = [
|
||||
'sid' => (string)($r['sid'] ?? ''),
|
||||
'name' => (string)($r['name'] ?? ''),
|
||||
@@ -846,7 +912,7 @@ try {
|
||||
|
||||
// ---------------- 随机抽取:导出白名单(xlsx,列:姓名 / 登陆账号) ----------------
|
||||
case 'draw_export':
|
||||
$raw = (string)file_get_contents('php://input');
|
||||
$raw = ny_request_body();
|
||||
$in = json_decode($raw, true);
|
||||
if (!is_array($in)) {
|
||||
$in = $_POST + $_GET;
|
||||
|
||||
Reference in New Issue
Block a user