NY 问卷考试系统数据平台
基于 PHP 8 + 卷王问卷系统(SurveyKing)社区版开放 API 的实时数据统计平台。 选择某个问卷 / 考试后,自动拉取答卷 → 用「姓名」关联学号名单 → 按 学号、姓名、成绩、提交时间、考试时长 依次排列展示,并支持定时自动刷新。
功能
| 功能 | 说明 |
|---|---|
| 问卷/考试切换 | 下拉选择,实时显示该问卷的答卷数据 |
| 学号自动匹配 | 用答卷中的「姓名」题在学号名单中查找对应学号(自动识别姓名题,也可手动指定) |
| 实时刷新 | 5 秒 / 10 秒 / 15 秒 / 30 秒 / 60 秒 / 2 分钟可选,数据变化的行会高亮闪烁;每次刷新后新增的提交以绿色底纹标出 |
| 统计卡片 | 名单总人数、已提交、缺考、提交率、平均分、合格率(≥60% 满分)、最高分、最低分、平均用时;选择班级后,全部卡片只统计该班级 |
| 缺考名单 | 名单中有、但未提交的人 |
| 未匹配答卷 | 提交了但姓名不在名单中的答卷,方便排查 |
| 名单导入 | 上传 .xlsx / .xlsm / .csv / .txt,自动识别「学号」「姓名」「班级」列(班级可选);支持覆盖 / 追加、手动增删改 |
| 导出 | 成绩明细 / 缺考名单 .xlsx(零依赖生成,支持多工作表、按最新成绩、按班级分表) |
| 排序 / 班级 | 点击表头排序,成绩明细默认按提交时间降序;支持学号/姓名/班级/成绩/提交时间/时长排序;按班级下拉筛选 |
| 全屏 / 自适应 | 右上角「全屏」按钮进入全屏(布局自动铺满、表格更高);页面适配手机 / 平板 / 桌面三端 |
环境要求
- PHP ≥ 8.0(需启用
curl、json、mbstring、openssl(可选,用于 RSA 加密密码);zip可选,无 zip 扩展时会用内置解析器读 xlsx) - 推荐启用
pdo_sqlite(用于 SQLite 存储);未启用时自动降级为 JSON 文件存储 - 无需数据库服务、无需 Composer 依赖
文档
完整文档见 docs/(VitePress):
cd docs
npm install
npm run docs:dev # 本地预览
npm run docs:build # 构建静态站到 docs/.vitepress/dist
安全
- 登录凭据即卷王后台账密;连续 5 次失败锁定 10 分钟
- 所有写接口强制校验会话绑定的 CSRF 令牌
- 上传文件:扩展名白名单(
.xlsx/.xlsm/.csv/.txt)+ 随机文件名 + 解析后立即删除 - 部署时必须屏蔽
data/目录,否则配置、名单、卷王账密可被直接下载:
# nginx 不读 .htaccess,必须显式配置
location ^~ /data/ {
deny all;
return 404;
}
详见 docs/security/hardening.md。
部署
方式一:PHP 内置服务器(最快体验)
在本目录执行:
php -S 0.0.0.0:8080
浏览器打开 http://localhost:8080。
Windows 也可双击 启动平台.bat。
方式二:放到 PHP 站点目录
把整个 ny-exam-platform 目录复制到 Apache / Nginx + PHP-FPM 的站点目录下,
确保进程对 data/ 目录有写权限即可。
首次配置
打开页面 → 右上角 设置:
| 配置项 | 示例 | 说明 |
|---|---|---|
| 卷王系统地址 | https://exam.nxxy335.top |
卷王后台地址(默认值),结尾不要带 / |
| 接口前缀 | /api |
社区版默认 /api;企业版可填 /admin-api |
| 登录账号 | admin |
卷王后台管理员账号 |
| 登录密码 | 123456 |
卷王后台密码 |
| 姓名题关键词 | 姓名,名字,学生姓名 |
用于自动识别哪道题填的是姓名 |
| 满分 | 100 |
用于合格率;每套试卷可单独设置——保存时作用于当前选中的试卷,未选试卷时作为所有试卷的默认值 |
| 校验 HTTPS 证书 | 默认勾选 | 访问 https 卷王时校验证书;自签名证书可取消勾选 |
| 默认刷新间隔 | 15 |
秒 |
填完点 测试连接,显示「连接成功」后点 保存。
登录流程(社区版 v1.13):
GET /api/system取 RSA 公钥 → 公钥加密密码 →POST /api/public/login换取 JWT。 注意:v1.13 服务端的JwtTokenFilter只从 Cookiesk-token读取令牌(不读Authorization头), 所以登录后程序会把Set-Cookie里的sk-token原样带在后续每个请求的Cookie头里(同时也带Authorization头以兼容旧版)。 程序已内置该流程与令牌缓存,并向下兼容/system/auth/login、/admin-api/oauth2/token两种登录方式。
关于 401 "Full authentication is required to access this resource"
若你用自己写的客户端对接卷王社区版 v1.13,即使登录成功、拿到了 JWT,调用 /project/list 等接口仍可能返回 401。
原因是 v1.13 服务端只认 Cookie 里的 sk-token,不认 Authorization 头。务必在登录响应里取 Set-Cookie: sk-token=...,
并在后续每个请求带上 Cookie: sk-token=<token>。本平台已处理此细节。
学号名单
- 平台启动时不预设任何名单,请通过右上角 名单管理 上传你自己的名单(
.xlsx/.csv)。 - 名单文件可包含「学号」「姓名」两列,「班级」为可选列;含班级时,成绩明细会显示班级列并支持按班级筛选(一份考试涉及多个班级时很实用)。
- 上传方式:名单管理 → 点击或拖拽文件 → 选择覆盖 / 追加 → 保存名单;也可手动增 / 删 / 改。
- 上传文件仅用于解析,落盘后立即删除,不会残留在
data/uploads/。 - 名单数据保存在 SQLite
data/ny.db(无pdo_sqlite时回落到data/roster.json)。
目录结构
ny-exam-platform/
├─ index.php 主页面
├─ api.php 后端接口(代理卷王 API)
├─ bootstrap.php 配置 / 公共函数 / 自动加载
├─ lib/
│ ├─ SurveyKingClient.php 卷王 API 客户端(登录、令牌缓存、项目、答卷)
│ ├─ DataService.php 数据组装(姓名→学号匹配、统计、排序)
│ ├─ Roster.php 学号名单管理(xlsx/csv 导入与持久化)
│ └─ XlsxReader.php 零依赖 XLSX 读取器
├─ assets/ 前端样式与脚本
├─ data/ 配置、名单、SQLite 库、令牌缓存(需可写,且必须禁止 Web 直接访问)
├─ docs/ VitePress 文档站
├─ tests/ PHP 自测 + 一键运行脚本
└─ tools/ 开发辅助(mock 服务、e2e 测试)
统计口径与显示说明
- 班级联动统计:顶部「班级」下拉选择某一班级后,统计卡片(总人数 / 已提交 / 缺考 / 提交率 / 平均分 / 合格率 / 最高分 / 最低分 / 平均用时)与成绩明细、缺考名单都只统计该班级;选「全部班级」则统计整体。切换班级在本地即时重算,不额外请求服务器。
- 面板计数随班级:选择班级后,成绩明细标题右侧的「共 X 人已提交 · 名单 X 人」、缺考名单标题右侧的「共 X 人」也都只统计该班级(不再用全部班级的数据)。
- 每套试卷独立满分 / 合格率 / 免考:在主页面「试卷设置」里为当前选中的试卷单独设置,试卷之间互相隔离;未设置时回落到「设置」里的全局默认值。
- 合格率:成绩 ≥ 满分 × 合格率阈值 视为合格;阈值全局默认 60%,可在「试卷设置」里按卷覆盖,卡片标题会实时显示当前阈值。
- 平均用时不换行:平均用时(如「9 分 55 秒」)强制单行显示,避免「秒」字换行。
- 成绩明细不再显示每人的「状态」列:缺考人员统一在「缺考名单」面板查看,成绩明细只保留学号 / 姓名 / 班级 / 成绩 / 提交时间 / 考试时长。
- 新增数据标记:每次刷新后,相较上一次新出现的提交(按学号判断)整行显示绿色底纹,方便一眼看出谁刚交卷;首次加载不标记。
常见问题
1. 页面提示"无法连接卷王系统"
检查:地址端口是否正确、服务器防火墙是否放行、接口前缀是否为 /api、账号密码是否正确。
可直接在服务器本机执行 curl http://127.0.0.1:1991/api/system 验证。
2. 读到了答卷但学号为空 说明答卷里的姓名与名单不一致。处理:
- 在「姓名字段」下拉里手动指定正确的题目;
- 或到「设置」里补充姓名题关键词;
- 未匹配的答卷会显示在「未匹配答卷」面板,可据此核对姓名是否写错(如多了空格)。
3. 成绩列全是 -
说明该问卷不是考试模式或没有开启自动评分,卷王未返回 examScore。
4. 考试时长显示 -
答卷的 metaInfo.answerInfo 中没有开始/结束时间(例如是后台手动录入的答卷)。
5. 想让同一学号只保留最新提交 设置里勾选「同一学号只保留最新一份答卷」(默认开启);取消勾选则保留全部提交记录。