feat: 登录门认证 + 每套试卷独立满分等增强
- 登录门:进入页面先输入系统网址/API路由/账号/密码,测试连接通过后才显示登录按钮,登录后才进入主界面;密码每次需输入,防止依赖一次配置直接查看数据 - 每套试卷独立满分配置(设置中按当前试卷单独设置,合格率按 ≥60% 满分计算) - 班级联动统计与面板计数(成绩明细/缺考名单标题计数随班级过滤) - 移除最高/最低分获得者显示 - 平均用时不换行(避免“秒”字换行) - 卷王地址默认 https://exam.nxxy335.top,默认勾选校验 HTTPS 证书 - 统计卡片含合格率(≥60% 满分) - .gitignore 排除 data/(账号密码/令牌/学生名单,禁止入库)
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
<?php
|
||||
/**
|
||||
* NY 问卷考试系统数据平台 - 后端接口
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
require __DIR__ . '/bootstrap.php';
|
||||
|
||||
$action = (string)($_GET['action'] ?? $_POST['action'] ?? '');
|
||||
|
||||
function cfg(): array
|
||||
{
|
||||
return ny_load_config();
|
||||
}
|
||||
|
||||
try {
|
||||
switch ($action) {
|
||||
// ---------------- 配置 ----------------
|
||||
case 'config_get':
|
||||
$c = cfg();
|
||||
$c['has_password'] = $c['password'] !== '';
|
||||
ny_ok($c);
|
||||
|
||||
case 'config_save':
|
||||
$raw = (string)file_get_contents('php://input');
|
||||
$in = json_decode($raw, true);
|
||||
if (!is_array($in)) {
|
||||
$in = $_POST;
|
||||
}
|
||||
$old = cfg();
|
||||
$allowed = ['base_url', 'api_prefix', 'username', 'password', 'timeout', 'refresh_interval', 'page_size', 'name_keywords'];
|
||||
$new = $old;
|
||||
foreach ($allowed as $k) {
|
||||
if (array_key_exists($k, $in)) {
|
||||
$new[$k] = is_string($in[$k]) ? trim((string)$in[$k]) : $in[$k];
|
||||
}
|
||||
}
|
||||
foreach (['verify_ssl', 'include_temp_save', 'dedupe'] as $k) {
|
||||
if (array_key_exists($k, $in)) {
|
||||
$new[$k] = filter_var($in[$k], FILTER_VALIDATE_BOOL);
|
||||
}
|
||||
}
|
||||
$pwdIn = $in['password'] ?? null;
|
||||
if ($pwdIn === null || $pwdIn === '' || $pwdIn === '******') {
|
||||
$new['password'] = $old['password'];
|
||||
}
|
||||
$new['base_url'] = rtrim($new['base_url'], '/');
|
||||
$new['api_prefix'] = '/' . trim((string)$new['api_prefix'], '/');
|
||||
$new['timeout'] = (int)$new['timeout'];
|
||||
$new['refresh_interval'] = (int)$new['refresh_interval'];
|
||||
if ($new['refresh_interval'] < 3) {
|
||||
$new['refresh_interval'] = 3;
|
||||
}
|
||||
// 每套试卷独立满分:选中试卷时写入 full_scores[projectId],否则更新全局默认
|
||||
$fs = (int)($in['full_score'] ?? $new['full_score'] ?? 100);
|
||||
if ($fs < 1) {
|
||||
$fs = 100;
|
||||
}
|
||||
$pid = trim((string)($in['project_id'] ?? ''));
|
||||
if ($pid !== '') {
|
||||
$scores = is_array($new['full_scores'] ?? null) ? $new['full_scores'] : [];
|
||||
$scores[$pid] = $fs;
|
||||
$new['full_scores'] = $scores;
|
||||
} else {
|
||||
$new['full_score'] = $fs;
|
||||
}
|
||||
ny_save_config($new);
|
||||
@unlink(NY_DATA . '/cache/token.json'); // 清掉旧令牌
|
||||
ny_ok(['saved' => true, 'config' => ny_load_config()]);
|
||||
|
||||
// ---------------- 连接测试 ----------------
|
||||
case 'ping':
|
||||
$client = new SurveyKingClient(cfg());
|
||||
$res = $client->ping();
|
||||
$res['session'] = $client->sessionInfo();
|
||||
ny_ok($res);
|
||||
|
||||
case 'diag':
|
||||
$client = new SurveyKingClient(cfg());
|
||||
ny_ok($client->diagnose());
|
||||
|
||||
// ---------------- 问卷列表 ----------------
|
||||
case 'projects':
|
||||
$client = new SurveyKingClient(cfg());
|
||||
$list = $client->listProjects();
|
||||
$out = [];
|
||||
foreach ($list as $p) {
|
||||
if (!is_array($p)) {
|
||||
continue;
|
||||
}
|
||||
$out[] = [
|
||||
'id' => ny_str($p['id'] ?? ''),
|
||||
'name' => ny_str($p['name'] ?? '') ?: '未命名',
|
||||
'mode' => ny_str($p['mode'] ?? ''),
|
||||
'total' => (int)($p['total'] ?? 0),
|
||||
'status'=> (int)($p['status'] ?? 0),
|
||||
];
|
||||
}
|
||||
ny_ok($out);
|
||||
|
||||
// ---------------- 数据 ----------------
|
||||
case 'data':
|
||||
$projectId = (string)($_GET['projectId'] ?? '');
|
||||
if ($projectId === '') {
|
||||
ny_fail('请先选择问卷/考试');
|
||||
}
|
||||
$nameQid = (string)($_GET['nameQid'] ?? '');
|
||||
$c = cfg();
|
||||
$result = DataService::build($c, $projectId, $nameQid);
|
||||
$fsMap = is_array($c['full_scores'] ?? null) ? $c['full_scores'] : [];
|
||||
$fs = isset($fsMap[$projectId]) ? (int)$fsMap[$projectId] : (int)($c['full_score'] ?? 100);
|
||||
if ($fs < 1) {
|
||||
$fs = 100;
|
||||
}
|
||||
$result['config'] = [
|
||||
'refresh_interval' => (int)$c['refresh_interval'],
|
||||
'base_url' => $c['base_url'],
|
||||
'full_score' => $fs,
|
||||
];
|
||||
ny_ok($result);
|
||||
|
||||
// ---------------- 名单 ----------------
|
||||
case 'roster':
|
||||
ny_ok(['students' => Roster::all(), 'meta' => Roster::meta()]);
|
||||
|
||||
case 'roster_import':
|
||||
if (empty($_FILES['file']) || !is_uploaded_file($_FILES['file']['tmp_name'])) {
|
||||
ny_fail('请选择要导入的文件');
|
||||
}
|
||||
$f = $_FILES['file'];
|
||||
$ext = strtolower(pathinfo((string)$f['name'], PATHINFO_EXTENSION));
|
||||
$dest = NY_DATA . '/uploads/roster_' . date('Ymd_His') . '.' . ($ext !== '' ? $ext : 'dat');
|
||||
if (!@move_uploaded_file($f['tmp_name'], $dest)) {
|
||||
ny_fail('文件保存失败,请检查 data/uploads 目录权限');
|
||||
}
|
||||
try {
|
||||
$res = Roster::importFile($dest, $ext);
|
||||
} catch (Throwable $e) {
|
||||
@unlink($dest);
|
||||
ny_fail($e->getMessage());
|
||||
}
|
||||
$mode = (string)($_POST['mode'] ?? 'replace');
|
||||
if ($mode === 'append') {
|
||||
$merged = array_merge(Roster::all(), $res['students']);
|
||||
Roster::save($merged);
|
||||
} else {
|
||||
Roster::save($res['students']);
|
||||
}
|
||||
ny_ok(['students' => Roster::all(), 'meta' => Roster::meta(), 'imported' => $res['count']]);
|
||||
|
||||
case 'roster_save':
|
||||
$raw = (string)file_get_contents('php://input');
|
||||
$in = json_decode($raw, true);
|
||||
if (!is_array($in) || !isset($in['students']) || !is_array($in['students'])) {
|
||||
ny_fail('参数错误');
|
||||
}
|
||||
$students = [];
|
||||
foreach ($in['students'] as $s) {
|
||||
if (!is_array($s)) {
|
||||
continue;
|
||||
}
|
||||
$students[] = [
|
||||
'sid' => (string)($s['sid'] ?? ''),
|
||||
'name' => (string)($s['name'] ?? ''),
|
||||
'cls' => trim((string)($s['cls'] ?? '')),
|
||||
];
|
||||
}
|
||||
Roster::save($students);
|
||||
ny_ok(['students' => Roster::all(), 'meta' => Roster::meta()]);
|
||||
|
||||
case 'roster_clear':
|
||||
Roster::clear();
|
||||
ny_ok(['students' => [], 'meta' => Roster::meta()]);
|
||||
|
||||
// ---------------- 导出 ----------------
|
||||
case 'export':
|
||||
case 'export_absent':
|
||||
$projectId = (string)($_GET['projectId'] ?? '');
|
||||
if ($projectId === '') {
|
||||
ny_fail('请先选择问卷/考试');
|
||||
}
|
||||
$nameQid = (string)($_GET['nameQid'] ?? '');
|
||||
$c = cfg();
|
||||
$result = DataService::build($c, $projectId, $nameQid);
|
||||
$filename = ($action === 'export_absent' ? '缺考名单' : '成绩明细')
|
||||
. '_' . preg_replace('/[\\\\\/:*?"<>|]/u', '_', (string)$result['project']['name'])
|
||||
. '_' . date('Ymd_His') . '.csv';
|
||||
|
||||
header('Content-Type: text/csv; charset=utf-8');
|
||||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||||
header('Cache-Control: no-store');
|
||||
echo "\xEF\xBB\xBF";
|
||||
$out = fopen('php://output', 'w');
|
||||
|
||||
if ($action === 'export_absent') {
|
||||
fputcsv($out, ['学号', '姓名', '班级']);
|
||||
foreach ($result['absent'] as $r) {
|
||||
fputcsv($out, [$r['sid'], $r['name'], $r['cls'] ?? '']);
|
||||
}
|
||||
} else {
|
||||
fputcsv($out, ['学号', '姓名', '班级', '成绩', '提交时间', '考试时长', '考试时长(秒)']);
|
||||
foreach ($result['rows'] as $r) {
|
||||
fputcsv($out, [
|
||||
$r['sid'],
|
||||
$r['name'],
|
||||
$r['cls'] ?? '',
|
||||
$r['score'] === null ? '' : $r['score'],
|
||||
$r['submit_at'],
|
||||
$r['duration_txt'],
|
||||
$r['duration'] === null ? '' : $r['duration'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
fclose($out);
|
||||
exit;
|
||||
|
||||
default:
|
||||
ny_fail('未知操作:' . $action);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
ny_fail($e->getMessage(), [
|
||||
'trace' => defined('NY_DEBUG') && NY_DEBUG ? $e->getTraceAsString() : null,
|
||||
]);
|
||||
}
|
||||
Reference in New Issue
Block a user