# 考试平台 **Repository Path**: hexm02/examination-platform ## Basic Information - **Project Name**: 考试平台 - **Description**: No description available - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-05-20 - **Last Updated**: 2026-06-02 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 党建考试系统独立版操作说明 本项目是一个可独立部署的党建考试系统,包含前端考试页面、统计后台、Java 8 接口服务和 H2 嵌入式数据库。生产环境只需要 Java 8,不依赖 Python、NGINX、Apache、Tomcat、Maven 或互联网。 ## 一、功能特点 ### 1. 考试与练习 - 支持练习模式和考试模式。 - 支持单选、多选、判断、填空、简答题。 - 练习提交后立即判题,并记录答题明细。 - 考试按题库配置统一计时,倒计时结束后自动交卷。 - 考试次数受题库配置限制,达到上限后不能再次考试。 - 考试结果页展示姓名、机构、考试时间、实际用时、得分、正确题数、错误题数和正确率。 - 是否允许查看错题、是否允许考后回看,由题库配置控制。 ### 2. 题库维护 - 仓库仅维护 `data/question-bank-data.js`(可编辑源文件);`web/question-bank-data.js` 由服务启动或后台上传题库时自动生成(与上传相同的压缩逻辑),不纳入版本库。 - 管理员可以在统计后台上传新的题库文件,上传后覆盖当前题库。 - 上传题库前会进行格式和数据校验,校验失败不会覆盖原文件。 - 上传成功时,旧的完整题库会自动备份为 `data/question-bank-data.YYYYMMDDHHMMSS.bak.js`。 - 后台提供题库模板下载和当前完整题库 JS 下载,方便制作新题库;前端只请求压缩后的题库 JS。 ### 3. 数据记录与统计 - 记录练习和考试答题行为,包括用户、机构、题号、题型、答案、得分、是否正确、客户端 IP、题库编号和题库名称。 - 记录每次考试成绩,包括考试编号、总分、满分、正确题数、错误题数、用时、客户端 IP。 - 统计后台支持当前题库维度的数据查看。 - 支持错误率高的题目、考试分数排名、练习次数排名、部门考试平均分排名、部门平均练习次数排名。 - 答题明细支持按用户、题号、模式查询,分页每页 20 条。 - 支持导出当前题库数据为 CSV(UTF-8 BOM,可用 Excel 打开),包含答题记录与考试记录两个区块。 ### 4. 用户身份 - 前端不允许用户自行填写姓名和机构。 - 用户姓名和机构由外部系统通过 URL 参数传入。 - URL 参数使用轻量加密和签名方案,前端可解密,无需外部依赖。 ### 5. 移动端接入 移动端在专用项目中新建 H5 页面,经**移动端后台 BFF** 访问本系统内网 API(手机不直连本系统)。PC 考试页与统计后台行为不变。 - 考试系统改造说明:[docs/移动端考试接入-考试系统改造说明.md](docs/移动端考试接入-考试系统改造说明.md) - 移动端平台改造说明(含接口与 payload 约定):[docs/移动端考试接入-移动端平台改造说明.md](docs/移动端考试接入-移动端平台改造说明.md) 移动端常用接口: | 方法 | 路径 | 说明 | |------|------|------| | GET | `/api/question-bank/current` | 当前题库 JSON(`meta` + `questionBank`),无需 admin | | GET | `/api/exam-attempts` | 已考次数 | | GET | `/api/exam-results/latest` | 最近一次成绩 | | POST | `/api/exam-submissions` | 考试交卷 | ## 二、程序结构 ```text dangjian-quiz-standalone/ pom.xml start.sh stop.sh build-java-prod.sh README.md app/ java/ src/main/java/com/dangjian/quiz/App.java python/ app.py data/ question-bank-data.js question-bank-data.*.bak.js quiz.mv.db log/ dist/ scripts/ seed_stress_data.py web/ index.html favicon.ico quiz-config.js question-import-template-json.xlsx admin/ index.html admin.css admin.js docs/ 移动端考试接入-考试系统改造说明.md 移动端考试接入-移动端平台改造说明.md tests/ test_standalone.py ``` 说明: - `pom.xml`:Maven 构建配置,生成 Java 8 fat jar。 - `app/java/src/main/java/com/dangjian/quiz/App.java`:Java 后端服务,提供静态页面、API、H2 写入、统计和导出。 - `start.sh`:一键后台启动脚本,执行 `java -jar app.jar`,源码目录下也兼容 `app/java/target/app.jar`。 - `stop.sh`:停止脚本,读取 `data/server.pid` 并停止当前服务进程。 如果 PID 文件不存在,会按端口查找服务进程;默认端口为 `9000`,可通过 `DANGJIAN_QUIZ_PORT` 指定。停止前会校验进程命令,避免误停止其他 Java 进程。 - `build-java-prod.sh`:生成 Java 生产部署包。 - `app/python/app.py`:旧 Python 后端源码,仅作为历史参考,生产部署不使用。 - `data/quiz.mv.db`:H2 数据库文件,首次启动时自动创建;运行中如果文件被删除,下一次接口访问会自动重建空库。 - `dist/`:生产部署包输出目录。 - `web/index.html`:考试和练习前端页面。 - `web/admin/`:统计后台前端页面。 - `web/quiz-config.js`:系统固定配置,如接口地址、用户参数名、加密密钥、应用名。 - `data/question-bank-data.js`:唯一纳入版本库的题库源文件(可编辑、格式化 JSON)。 - `web/question-bank-data.js`:服务启动或上传题库后自动生成(压缩版),供考试页加载;首次部署需先执行 `./start.sh` 生成该文件。 - `log/`:应用日志目录(`app-0.log`、`app-1.log` 等 `*.log`);`log/console.log` 为 `start.sh` 控制台输出。 - `web/question-import-template-json.xlsx`:题库制作模板,支持动态拆分选项生成 JSON。 - `tests/test_standalone.py`:自动化测试脚本。 ## 三、运行环境 生产服务器需要: ```bash java -version ``` 版本要求: ```text Java 8 或以上 ``` 适用部署场景: ```text Linux 服务器 单位内网访问 浏览器访问考试页面和统计后台 ``` ## 四、启动服务 进入项目目录: ```bash cd dangjian-quiz-standalone ``` 启动: ```bash ./start.sh ``` `start.sh` 会在后台启动: ```text java -jar app.jar ``` 启动成功后脚本会退出,服务继续在后台运行。PID 会写入: ```text data/server.pid ``` 启动过程的控制台输出会写入: ```text log/console.log ``` 默认 JVM 最大内存为 `512M`: ```text DANGJIAN_JAVA_OPTS=-Xmx512m ``` 如需调整 JVM 参数: ```bash DANGJIAN_JAVA_OPTS='-Xms128m -Xmx512m' ./start.sh ``` 默认监听: ```text 0.0.0.0:9000 ``` 访问地址: ```text 考试页面:http://服务器IP:9000/ 统计后台:http://服务器IP:9000/admin/ 健康检查:http://服务器IP:9000/api/health ``` 停止服务: ```bash ./stop.sh ``` 如果是在前台终端直接启动,也可以在启动终端按 `Ctrl + C` 停止。 ## 五、构建 Java 生产部署包 构建机需要具备: ```text Java 8 Maven ``` 构建前检查: ```bash java -version mvn -version ``` 执行构建: ```bash ./build-java-prod.sh ``` 构建完成后会生成: ```text dist/dangjian-quiz-java-prod-YYYYMMDD-HHMMSS.tar.gz dist/dangjian-quiz-java-prod-YYYYMMDD-HHMMSS.zip dist/dangjian-quiz-java-prod-YYYYMMDD-HHMMSS.zip.sha256 ``` 每次构建会自动清理 `dist/` 下旧的 `dangjian-quiz-java-prod-*` 产物,只保留本次最新版本。 生产包为带密码的 ZIP 文件,解压密码: ```text 100101 ``` `.zip` 内部包含同名 `.tar.gz`。把 `.zip` 上传到生产服务器后,先用密码解出 `.tar.gz`,再解压运行: ```bash unzip -P 100101 dangjian-quiz-java-prod-YYYYMMDD-HHMMSS.zip mkdir dangjian-quiz cd dangjian-quiz tar -xzf dangjian-quiz-java-prod-YYYYMMDD-HHMMSS.tar.gz DANGJIAN_ADMIN_PASSWORD='你的后台密码' ./start.sh ``` 说明:`.tar.gz` 解开后就是程序文件,例如 `app.jar`、`start.sh`、`stop.sh`、`web/`、`data/`,不会再额外套一层版本号目录。建议先创建一个部署目录再解压。 ## 六、部署配置 ### 1. 修改端口 默认端口是 `9000`。如果要改为 `8080`: ```bash DANGJIAN_QUIZ_PORT=8080 ./start.sh ``` ### 2. 修改监听地址 默认监听 `0.0.0.0`,局域网其他电脑可以访问。如果只允许服务器本机访问: ```bash DANGJIAN_QUIZ_HOST=127.0.0.1 ./start.sh ``` ### 3. 修改后台密码 统计后台默认密码: ```text admin123456 ``` 正式部署建议修改: ```bash DANGJIAN_ADMIN_PASSWORD=你的新密码 ./start.sh ``` `start.sh` 会读取并导出 `DANGJIAN_ADMIN_PASSWORD`。如果未设置,会使用默认密码并在启动日志中提示。 也可以同时修改端口: ```bash DANGJIAN_QUIZ_PORT=8080 DANGJIAN_ADMIN_PASSWORD=你的新密码 ./start.sh ``` ### 4. 修改 JVM 内存 默认最大内存为 `512M`,一般内网考试场景够用。如果服务器内存较小或并发较高,可以按需调整: ```bash DANGJIAN_JAVA_OPTS='-Xms128m -Xmx512m' ./start.sh ``` ## 七、题库配置 完整题库文件: ```text data/question-bank-data.js ``` 题库文件格式: ```js window.DANGJIAN_QUESTION_BANK = { "meta": { "bankId": "dangjian-quiz-v1", "bankName": "党建知识考试", "version": "1.0.0", "examConfig": { "durationMinutes": 30, "maxAttempts": 3, "shuffleQuestions": false, "questionIds": ["DJ-001", "DJ-002", "DJ-003"], "examNotice": "请认真作答,交卷后不可修改。", "showWrongBookAfterExam": true, "allowReviewAfterExam": true } }, "questionBank": [] }; ``` 字段说明: - `bankId`:题库唯一编号,建议使用英文、数字、短横线或下划线,正式使用后不要随意修改。 - `bankName`:题库名称,也作为考试名称使用,可以是中文。 - `version`:题库版本号,显示在首页底部。 - `examConfig`:考试配置,必须存在;不存在或校验失败时不允许开始考试。 - `durationMinutes`:考试时长,单位分钟。 - `maxAttempts`:每个用户在当前题库下允许考试的最大次数。 - `shuffleQuestions`:考试题目是否乱序,`true` 表示乱序,`false` 表示按 `questionIds` 顺序。 - `questionIds`:本次考试使用的题号,必须非空,题号必须存在于 `questionBank`。 - `examNotice`:考试须知,显示在首页考试说明区域。 - `showWrongBookAfterExam`:考试结果页是否允许查看错题。 - `allowReviewAfterExam`:考试后是否允许回看答题结果。 管理员上传题库时,系统会先校验完整版本: - `meta.bankId`、`meta.bankName` 是否存在。 - `meta.examConfig` 是否存在且合法。 - `examConfig.questionIds` 是否非空、是否重复、题号是否存在。 - `shuffleQuestions`、`showWrongBookAfterExam`、`allowReviewAfterExam` 是否为布尔值。 - 题目 `id` 是否为空或重复。 - 题型、题干、答案是否完整。 - 选择题和判断题的答案是否存在于选项 `key` 中。 校验通过后,系统会自动完成两件事: - 写入可编辑源文件:`data/question-bank-data.js` - 自动生成运行版:`web/question-bank-data.js`(不提交版本库) 前端页面加载 `web/` 下运行版;后台下载“当前题库 JS”得到的是 `data/` 源文件。 题库模板 `web/question-import-template-json.xlsx` 的“试题列表”工作表固定为 8 列,不要改变列结构。选项仍然写在“选项(一行对应一个选项)”这一列中,格式示例: ```text A.选项一 B.选项二 C.选项三 ``` 新版 `.xlsx` 模板支持最多 20 个选项,对应选项 key 为 A-T。用户在选项单元格里填写多少个非空选项行,生成的 JSON 就会包含多少个 `options`;多余的空行会自动忽略。填空题如果有多个空,标准答案用英文竖线 `|` 分隔,例如 `答案1|答案2`,模板会生成 `answer: ["答案1", "答案2"]`,前端会按答案数量显示多个输入框。生成结果复制“复制JSON”工作表的「复制粘贴内容」列(当前为 BC 列)。**首题**为纯 JSON 对象;**从第二题起**行前自动带英文逗号,可多行选中 BC 列后一次性复制粘贴到 `questionBank` 数组(无需手动画逗号)。单独校验某一题时,请复制「题目JSON」列(BB 列)。 ## 八、统计后台使用 访问: ```text http://服务器IP:9000/admin/ ``` 输入后台密码后可以使用: - 选择题库:默认选中当前题库文件中的题库,同时会列出数据库中已有记录的题库。 - 更新题库:上传覆盖题库、下载题库模板、下载当前完整题库文件。 - 导出当前题库 CSV:导出所选题库的答题与考试业务数据(`GET /api/export/database.csv?questionBankId=...`)。 - 刷新:重新加载统计卡片、排名、图表和答题明细。 - 使用说明:打开业务操作手册,查看普通用户练习考试和管理员后台操作步骤。 - 锁定:清除本浏览器后台登录状态。 ## 九、数据库说明 数据库文件: ```text data/quiz.mv.db ``` 如果服务运行过程中误删 `data/quiz.mv.db`,系统会在下一次访问数据库接口时自动创建新的空数据库和业务表。注意:自动重建只能恢复表结构,不能恢复已删除的历史数据,因此生产环境仍应定期备份。 业务表: - `answer_records`:答题明细表,保存练习和考试的每题记录。 - `exam_records`:考试成绩表,保存每一次考试的汇总成绩。 两个表都会保存: - `question_bank_id`:题库编号。 - `question_bank_name`:题库名称。 - `client_ip`:客户端 IP。 建议定期备份: ```bash cp data/quiz.mv.db data/quiz.mv.db.bak ``` 迁移服务器时,保留 `data/quiz.mv.db` 即可保留历史业务数据。 运维人员如需直接操作 H2 数据库(例如清理异常考试记录),见:[docs/运维-数据库维护与清理.md](docs/运维-数据库维护与清理.md)。 ### H2 Web 控制台(独立端口) 与考试 HTTP 服务(默认 9000)**分离**,由同一 `app.jar` 在 **独立端口**(默认 **9001**)启动 H2 Web 控制台: | 环境变量 | 默认值 | 说明 | |----------|--------|------| | `DANGJIAN_H2_CONSOLE_PORT` | `9001` | 设为 `0` 禁用 | - 访问地址:`http://:9001/`(独立端口,无入口密码) - 连接数据库:JDBC URL 填 `jdbc:h2:file:<部署目录>/data/quiz`,用户名 `sa`,密码**留空** ## 十、日志功能 ### 日志文件 日志文件位置: ```text log/app-0.log log/app-1.log log/console.log # start.sh 标准输出/错误 ``` 日志文件说明: - 使用 Java 自带日志系统 (`java.util.logging`) - 自动轮转:单个文件最大 10MB,保留 5 个备份 - 文件名格式:`log/app-0.log`、`log/app-1.log` …(均为 `*.log`) - 启动时会写入 `[Startup]`、`[QuestionBank]` 等详细启动日志(Java 版本、目录、监听地址、题库同步结果等) ### 日志格式 ``` 2026-05-21 15:00:01 信息 [Startup] ======== 党建考试系统启动 ======== 2026-05-21 15:00:01 信息 [Startup] 监听地址: 0.0.0.0:9000 2026-05-21 15:00:01 信息 [QuestionBank] 由 data 源文件生成 web 运行版 2026-05-21 15:00:02 信息 [Startup] 服务启动完成,开始接收请求 2026-05-21 15:01:12 信息 [API] POST /api/answer-records [192.168.1.100] ``` ### 日志级别 - `INFO`:启动详情、题库同步、API 请求(方法、路径、客户端 IP、结果) - `FINE`:详细数据操作(答题记录写入) - `SEVERE`:错误和异常信息 ### 前端日志 前端日志输出到浏览器控制台: - `[用户信息]`:用户信息解析过程 - `[API]`:API 请求和响应 调试方法: 1. 打开浏览器开发者工具(F12) 2. 切换到 Console 标签 3. 访问考试页面,查看日志输出 ## 十一、用户参数加密协议 考试页面必须通过外部系统跳转进入。外部系统将用户信息加密后放入 URL 参数。 配置文件: ```text web/quiz-config.js ``` 配置示例: ```js window.DANGJIAN_QUIZ_CONFIG = { apiBase: "/api", enableReport: true, identityParam: "userInfo", identitySecret: "change-this-identity-secret", identityTokenMaxAgeMinutes: 120, appName: "党建考试系统", supportTeam: "信息技术支持团队" }; ``` 字段说明: - `identityParam`:URL 参数名,默认 `userInfo`。 - `identitySecret`:加密密钥,外部系统和本系统必须保持一致。 - `identityTokenMaxAgeMinutes`:本系统判定用户参数有效期,单位分钟;默认 120 分钟。设置为 `0` 表示不检查时间。该字段只配置在本系统 `quiz-config.js` 中,外部系统不需要、也不应该在用户 JSON 中传入这个参数。 用户信息 JSON: ```json { "userId": "U001", "userName": "张三", "branchId": "B001", "branchName": "综合办公室", "issuedAt": "2026-05-14T09:00:00+08:00" } ``` 本系统只读取需要的字段: - `userName`:姓名,也兼容 `username`、`name`。 - `branchName`:机构名称,也兼容 `department`、`orgName`、`organization`。 - `issuedAt`:用户参数生成时间,也兼容 `timestamp`、`createdAt`、`loginTime`、`ts`。 第三方系统可以同步传入 `userId`、`branchId` 等字段。本系统会读取姓名、机构名称和生成时间,并在浏览器控制台打印解密后的完整用户信息,便于排查跳转问题。有效期长短由本系统的 `identityTokenMaxAgeMinutes` 决定,外部系统只需要提供生成时间。 跳转地址: ```text http://服务器IP:9000/?userInfo=加密后的用户参数 ``` 协议格式: ```text v1.密文.签名 ``` 生成步骤: ```text 1. 用户 JSON 转 UTF-8 字节。 2. 使用 identitySecret 生成简单密钥流。 3. 对 UTF-8 字节进行 XOR 加密。 4. 加密结果做 base64url 编码,得到密文。 5. 使用 FNV-1a 对 “密文|identitySecret” 生成签名。 6. 拼接为 v1.密文.签名。 ``` JavaScript 生成示例: ```js function base64Url(bytes) { let binary = ""; bytes.forEach((b) => { binary += String.fromCharCode(b); }); return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, ""); } function fnv1a(value) { let hash = 2166136261; for (let i = 0; i < String(value).length; i += 1) { hash ^= String(value).charCodeAt(i); hash = Math.imul(hash, 16777619); } return (hash >>> 0).toString(36); } function makeIdentityKey(secret) { const text = String(secret || ""); const length = Math.max(text.length, 1); const key = []; for (let i = 0; i < 32; i += 1) { key.push((text.charCodeAt(i % length) + i * 17 + 31) & 255); } return key; } function xorIdentityBytes(bytes, secret) { const key = makeIdentityKey(secret); let seed = 0; key.forEach((item) => { seed = (Math.imul(seed ^ item, 1103515245) + 12345) >>> 0; }); return Uint8Array.from(bytes, (byte, index) => { seed = (Math.imul(seed ^ key[index % key.length], 1664525) + 1013904223) >>> 0; return byte ^ ((seed >>> 16) & 255); }); } function makeUserInfoToken(user, secret) { const json = JSON.stringify(user); const encrypted = xorIdentityBytes(new TextEncoder().encode(json), secret); const cipher = base64Url(encrypted); return "v1." + cipher + "." + fnv1a(cipher + "|" + secret); } const token = makeUserInfoToken({ userId: currentUser.userId, userName: "张三", branchId: currentUser.branchId || "", branchName: "综合办公室", issuedAt: new Date().toISOString() }, "change-this-identity-secret"); const url = "http://服务器IP:9000/?userInfo=" + encodeURIComponent(token); ``` 注意:该方案主要用于防止普通用户直接篡改 URL 参数。由于密钥需要放在前端配置中,它不是高强度身份认证方案。若后续需要更高安全级别,建议由外部统一认证系统签发后端可验证的令牌。 ## 十二、自动化测试 项目内置自动化测试: ```bash mvn package python3 -m unittest discover -s tests -v ``` 测试覆盖: - 健康检查和页面访问。 - 后台密码登录和接口鉴权。 - 练习答题记录写入。 - 考试提交、考试次数、最近考试结果。 - 统计接口、排名接口、部门统计。 - 答题明细分页。 - 当前题库数据导出。 - 题库上传格式校验。 - 运行中删除数据库后的自动重建。 前端脚本语法检查: ```bash node --check web/admin/admin.js node --check web/quiz-config.js node --check web/question-bank-data.js ``` ## 十三、常见问题 ### 1. 其他电脑无法访问 请检查: - 服务器防火墙是否放行端口。 - 是否使用默认监听地址 `0.0.0.0`。 - 浏览器访问的是否是服务器真实 IP。 ### 2. 统计后台提示密码错误 请确认启动服务时是否设置了 `DANGJIAN_ADMIN_PASSWORD`。如果设置过,需要使用新密码登录。 ### 3. 启动提示未找到 java 请确认服务器已经安装 Java 8,并且 `java` 命令在 `PATH` 中: ```bash java -version ``` 生产服务器不需要 Maven,Maven 只在构建机上使用。 ### 4. 考试页面提示未检测到用户信息 请确认: - 是否从外部系统统一入口跳转。 - URL 中是否包含 `userInfo` 参数。 - 外部系统使用的 `identitySecret` 是否和 `web/quiz-config.js` 一致。 - 用户参数是否包含 `issuedAt` 或兼容的生成时间字段。 - 生成时间是否超过本系统 `identityTokenMaxAgeMinutes` 有效期。 ### 5. 修改题库后页面没有变化 请尝试: - 刷新浏览器。 - 清理浏览器缓存。 - 确认修改或上传的是 `data/question-bank-data.js`;上传成功后服务会自动刷新 `web/question-bank-data.js`。 - 确认题库上传成功且未被校验拒绝。 ### 6. 导出数据不是预期题库 统计后台导出的是当前下拉框选中的题库数据。请先确认“题库”下拉框选中项,再点击导出。 ## 十四、上线前检查 正式上线前建议完成: - 修改 `DANGJIAN_ADMIN_PASSWORD`,不要使用默认后台密码。 - 修改 `web/quiz-config.js` 中的 `identitySecret`,并同步给外部系统。 - 确认完整题库中的 `bankId`、`bankName`、`examConfig` 和题目数据正确。 - 执行自动化测试并确认通过。 - 使用真实浏览器完成一次练习和一次考试。 - 备份 `data/quiz.mv.db`。