Build and publish Docker images / Test, build and publish (push) Successful in 13m51s
329 lines
21 KiB
Markdown
329 lines
21 KiB
Markdown
# 衡准 · 考试信息管理系统
|
||
|
||
一个完整可运行的分级权限考试信息管理系统,使用 Node.js 后端;本地开发采用 SQLite,生产环境支持 MySQL 8.4。
|
||
|
||
## 已实现功能
|
||
|
||
### 公开服务首页
|
||
|
||
- 通知公告首页展示与详情阅读
|
||
- 已发布考试、报名时间、考试时间和科目展示
|
||
- 考试服务办理流程说明
|
||
- 考生注册与双角色登录入口
|
||
|
||
### 考生中心
|
||
|
||
- 报名号即考生账户,同一考生参加不同考试始终使用同一个号码
|
||
- 学校管理员按一个或多个班级填写人数并提交批量申领(单人也按 1 人批次走审批)
|
||
- 首次登录强制修改初始密码,完成后才能补全个人信息
|
||
- 完整维护姓名、性别、证件号码、籍贯、出生日期、民族、家庭住址、手机号、邮箱、学校、班级、监护人和紧急联系人
|
||
- 自主注册可由超级管理员随时开启或关闭;开启后系统直接生成固定报名号
|
||
- 资料审核状态与管理员审核意见
|
||
- 查看开放考试并自主选择多个报考科目
|
||
- 查看报名审核、应缴金额、缴费状态及班级负责人确认记录
|
||
- 准考证生成状态、开放时间与下载
|
||
- 已发布成绩查询,并可按科目提交成绩复议、查看审批进度与结论
|
||
- 通知公告中心
|
||
|
||
### 管理后台
|
||
|
||
- 超级、校级、班级三级管理员,同一级支持多个账号
|
||
- 超级管理员管理全局事务,并可监督、修改、退回全部审批流程
|
||
- 校级管理员管理本校班级、班级管理员、考生和报名流程,按班级批量申领报名号,并提交本校考点、考场档案变更
|
||
- 班级管理员可查看并审批本班考生、报名与成绩复议流程,并维护本班考生缴费状态;成绩录入仍仅限超级管理员
|
||
- 报名终审与缴费确认相互独立,系统不接入支付 SDK;超级、校级、班级管理员均可在各自数据范围内修改缴费状态,确认缴费时记录办理人和时间
|
||
- 超级、校级、班级管理员均可按各自数据范围筛选、查看及导出 Excel 缴费名单
|
||
- 考生信息修改、考试报名、成绩复议、批量报名号申领、考点考场变更使用可配置的多步骤审批流程
|
||
- 班级和校级审批自动限定到考生所属班级、学校;同范围多名管理员按当前待办与历史分配量自动均分
|
||
- 当前处理人可将流程转交给同范围的同级管理员
|
||
- 自定义报名号生成规则,可组合年份、学校代码、性别、固定值和流水号
|
||
- 报名号在创建考生账户时只生成一次,后续考试报名自动复用
|
||
- 超级管理员只维护号码规则;校级批量申请最终批准后,系统原子生成固定报名号、随机初始密码和待补录账户
|
||
- 批次结果按班级返回校级管理员,并可导出 Excel 安全下发
|
||
- 结构化考点与考场档案,包含代码、负责人、应急电话、开放时间、交通、楼栋、楼层、容量、座位编排说明、类型和状态
|
||
- 考点新增及考点/考场修改先形成申请快照,审批通过后才整体更新正式档案
|
||
- 班级、班级管理员、报名号班级配额、考生资料、考点考场和成绩均提供 Excel 模板、导入与当前数据导出;只读角色保留对应导出能力
|
||
- Excel 导入逐行校验并返回具体行号;考生资料和考点考场的批量修改仍必须经过配置好的审批流程
|
||
- 考务指标与审计日志
|
||
- 考生资料审核、通过或退回修改
|
||
- 考试报名及科目审核
|
||
- 创建考试并结构化配置科目日期、时间、费用和满分;每科可独立选择固定分、排名前百分比或不设单科线,并汇总总分
|
||
- 支持固定总分线、总成绩排名前百分比、单科均达线及不判定四类整场合格策略
|
||
- 通知发布、草稿、撤回及首页置顶
|
||
- 按整场考试预检并批量编排准考证,支持班内、校内、县区内、市内和省内五级混编
|
||
- 预置“县区编号+考场号+座位号”“县区号+考场号+流水号”“考点学校代码+考场号+座位号”“考生学校代码+考场号+座位号”四种号码规则
|
||
- 多科目考生固定在同一考点,各科独立分配考场和座位;同科目组合优先相邻编排
|
||
- 编排前校验科目时间冲突、考点容量、档案完整性和号码唯一性,默认保留备用考场并支持稳定种子复现
|
||
- 每科可独立设置固定及格分、排名前百分比或不设单科线;成绩等级按同场同科排名百分位自动计算
|
||
- 默认排名等级区间为前 10% A+、前 25% A、前 50% B+、前 70% B、前 90% C、其余 D;同分共享名次
|
||
- 成绩管理中心按多场考试切换,展示录入/发布进度、成绩出齐人数、整场合格率、缺失科次与复议数量,并提供可筛选成绩台账
|
||
- 成绩复议终审表单展示考试、科目、原分、当前排名和达线规则;批准后在同一事务内更新成绩并只重算该考生的排名区间结论
|
||
- 成绩 Excel 采用“上传解析与逐行校验—页面暂存预览—确认后原子批量写库”的两阶段流程,预览不会修改数据库
|
||
- 超级管理员可将整场考试不可逆归档;归档后手工录入、Excel 导入、复议改分和考试配置全部锁定,历史报名、准考证与成绩默认折叠展示
|
||
- 管理员与考生接口权限隔离
|
||
|
||
### 系统能力
|
||
|
||
- PBKDF2 加盐密码哈希
|
||
- 可选 TOTP 二次验证,支持验证器扫码绑定、一次性恢复码与登录防重放
|
||
- TOTP 密钥使用 AES-256-GCM 加密存储,恢复码仅保存带服务端密钥的哈希
|
||
- HttpOnly、SameSite 登录 Cookie
|
||
- 服务端角色权限校验
|
||
- SQLite / MySQL 8.4 双数据库持久化
|
||
- 可选 Redis 公开接口缓存,支持写后版本失效、热点请求合并和故障回源
|
||
- 规范关系模型、外键、唯一约束和业务索引
|
||
- 业务写入与审计日志使用原子事务提交
|
||
- 关键管理操作审计日志
|
||
- 组织、学校、班级三级数据范围在服务端强制过滤
|
||
- 审批实例、当前责任人、转交和监督操作全程留痕
|
||
- 桌面端与移动端响应式布局
|
||
- Excel 文件使用 `exceljs` 生成和解析,并限制上传文件大小
|
||
|
||
## 运行
|
||
|
||
需要 Node.js 22.5 或更高版本(SQLite 使用 Node.js 内置驱动)。
|
||
|
||
```powershell
|
||
npm install
|
||
npm start
|
||
```
|
||
|
||
打开 <http://127.0.0.1:4173>。
|
||
|
||
账户可在“账户安全”中启用 TOTP 二次验证。生产环境必须设置至少 32 个字符的 `TOTP_ENCRYPTION_KEY`;该值用于加密 TOTP 密钥并保护恢复码哈希,部署后必须稳定保存,不能随意更换。本地开发未设置时会使用仅适合开发的稳定派生值。
|
||
|
||
本地开发无需额外配置,首次运行会自动创建 `data/exam.sqlite` 和完整关系型数据库结构,但不会导入学校、考生、考试或报名测试数据。首次建库只写入系统基础配置和一个超级管理员;账号、密码和显示名可通过 `INITIAL_ADMIN_USERNAME`、`INITIAL_ADMIN_PASSWORD`、`INITIAL_ADMIN_DISPLAY_NAME` 设置。当前数据库结构版本为 v17;v16 数据库会自动增加 TOTP 字段,低于 v15 的开发库会提示重建。
|
||
|
||
### Docker
|
||
|
||
项目根目录包含生产镜像和 Docker Compose 配置。默认使用 SQLite,数据库保存在命名卷 `exam-information-data` 中,因此重建容器不会丢失数据。
|
||
|
||
先创建容器环境文件,并将其中的 TOTP 主密钥和初始管理员密码替换为安全随机值:
|
||
|
||
```powershell
|
||
Copy-Item .env.docker.example .env.docker
|
||
```
|
||
|
||
然后构建并启动:
|
||
|
||
```powershell
|
||
docker compose up --build --detach
|
||
```
|
||
|
||
启动完成后访问 <http://127.0.0.1:4173>。查看状态和日志可运行:
|
||
|
||
```powershell
|
||
docker compose ps
|
||
docker compose logs --follow app
|
||
```
|
||
|
||
停止服务使用 `docker compose down`;该命令会保留数据库卷。只有明确需要删除全部 SQLite 数据时才使用 `docker compose down --volumes`。
|
||
|
||
也可以只构建镜像:
|
||
|
||
```powershell
|
||
docker build --tag hengzhun-exam-system:local .
|
||
```
|
||
|
||
镜像默认监听 `0.0.0.0:4173`,以非 root 用户运行,并通过 `/api/public/home` 执行健康检查。需要连接 MySQL 或 Redis 时,用运行环境变量覆盖 `DATABASE_CLIENT`、`DATABASE_URL`/`MYSQL_*`、`REDIS_URL`;此时 SQLite 数据卷可以移除。
|
||
|
||
#### Gitea Actions 自动发布到 Docker Hub 与 Gitea 软件包
|
||
|
||
工作流位于 `.gitea/workflows/docker-publish.yml`。它会先安装依赖并运行测试,然后构建 `linux/amd64`、`linux/arm64` 双架构镜像,并同时推送到 Docker Hub 与 `git.biss.click/biss/exam-information-system`。
|
||
|
||
使用前需要完成以下配置:
|
||
|
||
1. 在 Docker Hub 创建目标仓库,并创建具有该仓库 Read & Write 权限的访问令牌。
|
||
2. 在 Gitea 仓库的 Actions Variables 中添加 `DOCKERHUB_IMAGE`,值为不带 registry 和 tag 的完整镜像名,例如 `yourname/exam-information-system`。
|
||
3. 在 Gitea 仓库的 Actions Secrets 中添加 `DOCKERHUB_USERNAME` 和 `DOCKERHUB_TOKEN`。前者填写 Docker Hub 用户名,后者填写访问令牌,不要填写账户密码。
|
||
4. 使用对 `biss` 组织拥有软件包写权限的 Gitea 账号,在“设置 → 应用 → 生成新令牌”中创建具有 Package Read & Write 权限的个人访问令牌。在仓库 Actions Variables 中添加 `REGISTRY_USERNAME`,值为该令牌所属的用户名;在 Actions Secrets 中添加 `REGISTRY_TOKEN`,值为个人访问令牌。自定义 Secret 不能使用 Gitea 保留的 `GITEA_` 前缀,也不能用工作流内置的 `GITEA_TOKEN` 代替该软件包令牌。
|
||
5. 确保 Gitea Actions 与仓库的软件包注册表已启用,并且 `ubuntu-latest` Runner 能访问 Docker daemon、GitHub、Docker Hub 和 `git.biss.click`。双架构构建还需要 Runner 允许 QEMU 注册步骤运行。
|
||
|
||
推送到 `master` 后会发布 `latest` 和 `sha-<短提交号>`;推送形如 `v1.2.3` 的 Git 标签后会发布 `1.2.3`、`1.2` 和对应的提交标签,并在镜像推送成功后自动创建同名 Gitea Release。例如:
|
||
|
||
```powershell
|
||
git tag v1.2.3
|
||
git push origin v1.2.3
|
||
```
|
||
|
||
工作流也支持从 Gitea Actions 页面手动运行。构建缓存保存为同一 Docker Hub 仓库中的 `buildcache` 标签,以加快后续构建。Release 使用工作流内置的 `GITEA_TOKEN` 创建,无需添加额外 Secret;仓库或组织“Actions → General”中的任务令牌最大权限必须允许 Releases Write。
|
||
|
||
首次成功推送后,容器镜像会出现在 `biss` 所有者的软件包列表。Gitea 的软件包归属于用户或组织,不会天然归属于某个仓库;打开该软件包的设置页面,将它关联到 `Exam-Information-System`,即可让它显示在此仓库的“软件包”页。之后可使用 `docker pull git.biss.click/biss/exam-information-system:latest` 拉取。
|
||
|
||
需要清空并重建空业务库时运行 `npm run reset-db`。需要测试数据时再手动运行 `npm run seed-test-data`;导入脚本会读取项目根目录的 `.env`,并根据 `DATABASE_CLIENT` 选择 SQLite 或 MySQL。它会生成 4 所学校、360 名批量考生及 360 条不同状态的报名数据,并明确不生成考场编排计划和准考证。省市区县下拉数据位于 `src/data/china-regions.mjs`,当前版本为国家地名信息库截至 2025-12-31 的三级快照,并补入和康县(653228)与和安县(653229);从新版 CSV 更新时可运行 `node scripts/build-regions.mjs <CSV路径> src/data/china-regions.mjs`。
|
||
|
||
## 数据库配置
|
||
|
||
应用启动时会自动读取项目根目录的 `.env`,可先运行 `Copy-Item .env.example .env` 创建配置文件。命令行或部署平台已经注入的进程环境变量优先于 `.env`。应用根据 `DATABASE_CLIENT` 使用不同数据库;未设置时,开发/测试环境默认 `sqlite`,`NODE_ENV=production` 默认 `mysql`。
|
||
|
||
公开首页的机构名称、机构代码、电话、地址、邮箱、主标语和页脚提示分别由 `PUBLIC_SITE_NAME`、`PUBLIC_SITE_CODE`、`PUBLIC_SITE_PHONE`、`PUBLIC_SITE_ADDRESS`、`PUBLIC_SITE_EMAIL`、`PUBLIC_SITE_HERO_*`、`PUBLIC_SITE_FOOTER_NOTICE` 配置。修改 `.env` 后需要重启应用;这些配置会覆盖数据库中的演示机构信息,且只通过公开首页接口返回非敏感展示字段。
|
||
|
||
### 本地 SQLite
|
||
|
||
```powershell
|
||
$env:DATABASE_CLIENT = 'sqlite'
|
||
$env:SQLITE_PATH = './data/exam.sqlite'
|
||
npm start
|
||
```
|
||
|
||
`SQLITE_PATH` 可省略,默认路径就是 `./data/exam.sqlite`。
|
||
|
||
### 生产 MySQL 8.4
|
||
|
||
先在 MySQL 8.4 中创建数据库和最小权限账号:
|
||
|
||
```sql
|
||
CREATE DATABASE exam_information CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
|
||
CREATE USER 'exam_app'@'%' IDENTIFIED BY 'replace-with-a-strong-password';
|
||
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER ON exam_information.* TO 'exam_app'@'%';
|
||
```
|
||
|
||
启动应用时设置连接信息,应用会自动创建以下关系表和系统基础配置,但不会自动写入测试业务数据:
|
||
|
||
- `schools`、`school_classes`、`users`、`candidate_profiles`
|
||
- `school_student_partitions`(学校学生专属表登记)
|
||
- `exams`、`exam_subjects`
|
||
- `exam_data_partitions`(考试专属表登记)
|
||
- `registrations`、`registration_subjects`
|
||
- `admission_number_rules`、`exam_arrangement_plans`、`admit_cards`、`admit_card_subjects`
|
||
- `results`、`notices`、`audit_logs`
|
||
- `test_centers`、`test_rooms`、`center_change_requests`、`center_change_rooms`
|
||
- `number_rules`、`number_rule_segments`
|
||
- `candidate_account_batches`、`candidate_account_batch_items`
|
||
- `workflow_definitions`、`workflow_steps`、`workflow_instances`、`workflow_actions`
|
||
- `organization`、`schema_metadata`
|
||
|
||
所有关联均有外键约束,账号、证件号、考试代码、报名关系、准考证号和单科成绩均有对应唯一约束。
|
||
|
||
系统采用“总表索引 + 独立物理分表”存储:创建每场考试时,会立即创建该场考试专用的
|
||
`exam_<分区键>_candidates`、`exam_<分区键>_admissions`、`exam_<分区键>_results`、
|
||
`exam_<分区键>_centers` 四张表;创建每所学校时,会创建 `school_<分区键>_students`
|
||
学生专属表。分区键由业务 ID 的 SHA-256 摘要生成,不直接拼接用户输入。报名、缴费、准考证编排、
|
||
成绩和学生资料发生变化后,专属表会自动同步;总表继续承担跨考试、跨学校查询和外键完整性约束。
|
||
|
||
```powershell
|
||
$env:NODE_ENV = 'production'
|
||
$env:DATABASE_CLIENT = 'mysql'
|
||
$env:MYSQL_HOST = '127.0.0.1'
|
||
$env:MYSQL_PORT = '3306'
|
||
$env:MYSQL_USER = 'exam_app'
|
||
$env:MYSQL_PASSWORD = 'replace-with-a-strong-password'
|
||
$env:MYSQL_DATABASE = 'exam_information'
|
||
$env:HOST = '0.0.0.0'
|
||
npm start
|
||
```
|
||
|
||
也可以只设置标准连接地址 `DATABASE_URL=mysql://user:password@host:3306/database`。完整模板见 `.env.example`;将模板复制为 `.env` 后取消 MySQL 配置项的注释并填写实际连接信息即可。生产部署仍建议由部署平台注入环境变量,避免在服务器文件中保存密码。
|
||
|
||
### Redis 缓存(可选)
|
||
|
||
配置 `REDIS_URL` 后,应用会缓存公开首页、已发布公告详情和每名考生的已发布成绩查询。公开数据默认 TTL 为 60 秒,成绩默认 TTL 为 24 小时;成绩录入/发布、批量导入、考试归档和成绩复议会自动使成绩缓存失效,超级管理员也可以在成绩管理中心手动刷新全部成绩缓存。Redis 在启动或运行期间不可用时,接口会自动回源数据库,不影响登录、报名和管理功能。
|
||
|
||
```powershell
|
||
$env:REDIS_URL = 'redis://127.0.0.1:6379/0'
|
||
$env:REDIS_CACHE_PREFIX = 'exam-information'
|
||
$env:REDIS_CACHE_TTL_SECONDS = '60'
|
||
$env:REDIS_RESULTS_CACHE_TTL_SECONDS = '86400'
|
||
npm start
|
||
```
|
||
|
||
生产环境可使用 `redis://` 或启用 TLS 的 `rediss://` 连接地址,并通过 `REDIS_CONNECT_TIMEOUT_MS` 调整启动连接超时。
|
||
|
||
### 导入服务器 MySQL 测试数据
|
||
|
||
先停止正在运行的应用进程,确认服务器 `.env` 中已经设置 `DATABASE_CLIENT=mysql` 及完整 MySQL 连接参数,然后执行:
|
||
|
||
```powershell
|
||
npm run seed-test-data:mysql
|
||
```
|
||
|
||
脚本会读取 `.env`,校验当前连接的数据库名称、v15 表结构和已有数据。目标是新数据库时会自动建表并导入;目标只有首次启动生成的空业务结构时会在事务中替换为样例数据。若检测到学校、考生、考试、报名等业务数据,脚本默认拒绝覆盖。
|
||
|
||
仅在确认目标是可以完全覆盖的测试库时使用:
|
||
|
||
```powershell
|
||
npm run seed-test-data:mysql -- --force
|
||
```
|
||
|
||
强制模式会删除该 MySQL 数据库内现有应用数据并在同一事务中写入样例数据,但不会删除数据库或数据表。导入完成后再重新启动应用,避免导入期间出现并发写入或保留旧登录会话。不要对生产业务库执行此命令。本地需要明确使用 SQLite 时可运行 `npm run seed-test-data:sqlite`。
|
||
|
||
## 手动测试数据账号
|
||
|
||
测试数据脚本会提供以下账号;其中初始超级管理员也可能由正常首次建库创建,并可通过环境变量改名、改密,其余校级、班级和考生账号不会在正常启动时创建:
|
||
|
||
为了便于临时联调,所有预置样例账号统一使用密码 `12345678`,且预置考生不会在首次登录时被要求改密。通过系统业务流程后续新建的账号仍按正式规则生成随机初始密码。
|
||
|
||
| 角色 | 账号 | 密码 |
|
||
| --- | --- | --- |
|
||
| 超级管理员 | `admin` | `12345678` |
|
||
| 超级管理员(监督演示) | `supervisor` | `12345678` |
|
||
| 校级管理员 | `school_admin` | `12345678` |
|
||
| 同校校级管理员(转交演示) | `school_admin_2` | `12345678` |
|
||
| 班级管理员 | `class_admin` | `12345678` |
|
||
| 同班班级管理员(均分演示) | `class_admin_2` | `12345678` |
|
||
| 考生 | `2026-HZ01-F-0001` | `12345678` |
|
||
|
||
## 测试结束后初始化系统
|
||
|
||
先停止应用,然后运行以下命令。命令会读取 `.env` 并自动选择 SQLite 或 MySQL,删除样例学校、考生、考试、报名等业务数据,恢复系统基础配置和一个初始超级管理员:
|
||
|
||
```powershell
|
||
npm run initialize-system
|
||
```
|
||
|
||
也可以明确指定数据库类型:
|
||
|
||
```powershell
|
||
npm run initialize-system:sqlite
|
||
npm run initialize-system:mysql
|
||
```
|
||
|
||
MySQL 模式会自动识别由本项目生成的批量样例数据并清理。若目标包含无法识别为样例数据的业务记录,命令会拒绝执行;只有明确确认目标可完全清空时才可运行 `npm run initialize-system:mysql -- --force`。初始化完成后,初始管理员账号由 `.env` 中的 `INITIAL_ADMIN_USERNAME`、`INITIAL_ADMIN_PASSWORD`、`INITIAL_ADMIN_DISPLAY_NAME` 决定,然后再重新启动应用。
|
||
|
||
## 自动化测试
|
||
|
||
```powershell
|
||
npm test
|
||
```
|
||
|
||
测试使用独立临时 SQLite 数据库,覆盖固定报名号跨考试复用、首次登录强制改密、完整资料补录、自主注册开关、三级管理员数据范围、本校班级与班级管理员管理、多级审批、同级转交、校级按班级批量申领与终审原子建号、三级管理员范围内缴费状态修改与名单导出、结构化考点考场及变更审批、多资源 Excel 导入导出、多科目报名、独立科目及格规则、成绩 Excel 预览后原子提交、五级准考证混编、四种号码规则、多科目同考点、成绩复议、校班严格匹配和多人均分。
|
||
|
||
## 项目结构
|
||
|
||
项目采用模块化单体架构:仍由一个 Node.js 进程部署,但 HTTP、权限、业务路由、数据库适配和前端页面按职责分开。
|
||
|
||
```text
|
||
index.html 页面入口
|
||
styles.css 公共首页、考生端、管理端响应式样式
|
||
app.js 前端路由、事件与表单控制器
|
||
server.mjs HTTP 服务启动、模块装配与静态文件服务
|
||
database.mjs 数据仓储与数据库模块装配
|
||
excel.mjs Excel 模板、导入解析与导出工作簿
|
||
|
||
src/data/base.mjs 空业务库与系统基础配置
|
||
src/data/seed.mjs 手动测试数据生成器
|
||
scripts/import-test-data.mjs 独立测试数据导入脚本
|
||
src/http/responses.mjs JSON、文件与请求体处理
|
||
src/security/session.mjs Cookie 会话与当前用户
|
||
src/security/authorization.mjs 管理层级、权限和数据范围
|
||
src/routes/public.routes.mjs 公开 API
|
||
src/routes/auth.routes.mjs 登录、注册与改密 API
|
||
src/routes/candidate.routes.mjs 考生业务 API
|
||
src/routes/admin.routes.mjs 管理业务 API
|
||
|
||
src/database/schema.mjs SQLite / MySQL 关系模型
|
||
src/database/sqlite-adapter.mjs SQLite 初始化、迁移与事务适配
|
||
src/database/mysql-adapter.mjs MySQL 初始化、迁移与事务适配
|
||
|
||
src/client/state.mjs 前端共享状态
|
||
src/client/api.mjs 浏览器 API 请求封装
|
||
src/client/ui.mjs 格式化、图标与通用 UI 工具
|
||
src/client/public-views.mjs 公共首页与登录注册视图
|
||
src/client/candidate-views.mjs 考生中心视图
|
||
src/client/admin-views.mjs 管理后台视图
|
||
|
||
tests/system.test.mjs 端到端系统测试
|
||
data/exam.sqlite 本地运行后生成的 SQLite 数据库
|
||
.env.example 开发与生产环境变量模板
|
||
```
|