Compare commits
@@ -0,0 +1,21 @@
|
|||||||
|
.git
|
||||||
|
.agents
|
||||||
|
.codex
|
||||||
|
node_modules
|
||||||
|
tests
|
||||||
|
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
!.env.docker.example
|
||||||
|
|
||||||
|
data/*
|
||||||
|
!data/.gitkeep
|
||||||
|
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
Dockerfile*
|
||||||
|
compose*.yml
|
||||||
|
compose*.yaml
|
||||||
|
README.md
|
||||||
|
LICENSE
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# 首次启动前请复制为 .env.docker,并替换下面两个值。
|
||||||
|
# TOTP_ENCRYPTION_KEY 必须至少 32 个字符,部署后不得随意更换。
|
||||||
|
TOTP_ENCRYPTION_KEY=replace-with-a-random-secret-of-at-least-32-characters
|
||||||
|
|
||||||
|
INITIAL_ADMIN_USERNAME=admin
|
||||||
|
INITIAL_ADMIN_PASSWORD=replace-with-a-strong-admin-password
|
||||||
|
INITIAL_ADMIN_DISPLAY_NAME=系统管理员
|
||||||
|
|
||||||
|
# 下列公开站点信息可按需修改。
|
||||||
|
PUBLIC_SITE_NAME=海州市教育考试中心
|
||||||
|
PUBLIC_SITE_CODE=HZ-EDU-032
|
||||||
|
PUBLIC_SITE_PHONE=0518-8602 3158
|
||||||
|
PUBLIC_SITE_ADDRESS=江苏省连云港市海州区文教路 18 号
|
||||||
|
PUBLIC_SITE_EMAIL=
|
||||||
|
PUBLIC_SITE_HERO_EYEBROW=HAIZHOU EXAMINATION SERVICE
|
||||||
|
PUBLIC_SITE_HERO_TITLE=一个报名号,
|
||||||
|
PUBLIC_SITE_HERO_HIGHLIGHT=贯穿每一次考试。
|
||||||
|
PUBLIC_SITE_HERO_DESCRIPTION=使用学校下发的报名号登录,完成密码更新和个人信息核验后,即可办理所有考试事项。
|
||||||
|
PUBLIC_SITE_FOOTER_NOTICE=本平台展示数据仅用于系统演示
|
||||||
|
|
||||||
|
# 可选 Redis 缓存;容器外的 Redis 不应填写 127.0.0.1。
|
||||||
|
# REDIS_URL=redis://redis-host:6379/0
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
name: Build and publish Docker images
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
name: Test, build and publish
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
releases: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out repository
|
||||||
|
uses: https://github.com/actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: https://github.com/actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: npm test
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: https://github.com/docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: https://github.com/docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: https://github.com/docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: docker.io
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Log in to Gitea Container Registry
|
||||||
|
uses: https://github.com/docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: git.biss.click
|
||||||
|
username: ${{ vars.REGISTRY_USERNAME }}
|
||||||
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
|
||||||
|
- name: Generate image tags and labels
|
||||||
|
id: metadata
|
||||||
|
uses: https://github.com/docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
docker.io/${{ vars.DOCKERHUB_IMAGE }}
|
||||||
|
git.biss.click/biss/exam-information-system
|
||||||
|
flavor: latest=false
|
||||||
|
tags: |
|
||||||
|
type=raw,value=latest,enable=${{ gitea.ref == 'refs/heads/master' }}
|
||||||
|
type=sha,format=short,prefix=sha-
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
|
||||||
|
- name: Build and push image
|
||||||
|
uses: https://github.com/docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.metadata.outputs.tags }}
|
||||||
|
labels: ${{ steps.metadata.outputs.labels }}
|
||||||
|
cache-from: type=registry,ref=docker.io/${{ vars.DOCKERHUB_IMAGE }}:buildcache
|
||||||
|
cache-to: type=registry,ref=docker.io/${{ vars.DOCKERHUB_IMAGE }}:buildcache,mode=max
|
||||||
|
|
||||||
|
- name: Create Gitea release
|
||||||
|
if: gitea.ref_type == 'tag'
|
||||||
|
uses: https://gitea.com/actions/gitea-release-action@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
tag_name: ${{ gitea.ref_name }}
|
||||||
|
name: ${{ gitea.ref_name }}
|
||||||
|
body: |
|
||||||
|
Docker 镜像已发布:
|
||||||
|
|
||||||
|
```text
|
||||||
|
docker pull docker.io/${{ vars.DOCKERHUB_IMAGE }}:${{ steps.metadata.outputs.version }}
|
||||||
|
docker pull git.biss.click/biss/exam-information-system:${{ steps.metadata.outputs.version }}
|
||||||
|
```
|
||||||
@@ -5,4 +5,5 @@ data/*.sqlite-shm
|
|||||||
data/*.sqlite-wal
|
data/*.sqlite-wal
|
||||||
node_modules/
|
node_modules/
|
||||||
.env
|
.env
|
||||||
|
.env.docker
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
FROM node:22-bookworm-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm ci --omit=dev --ignore-scripts \
|
||||||
|
&& npm cache clean --force
|
||||||
|
|
||||||
|
COPY --chown=node:node . .
|
||||||
|
|
||||||
|
ENV NODE_ENV=production \
|
||||||
|
HOST=0.0.0.0 \
|
||||||
|
PORT=4173 \
|
||||||
|
DATABASE_CLIENT=sqlite \
|
||||||
|
SQLITE_PATH=/app/data/exam.sqlite
|
||||||
|
|
||||||
|
EXPOSE 4173
|
||||||
|
VOLUME ["/app/data"]
|
||||||
|
|
||||||
|
USER node
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
||||||
|
CMD ["node", "-e", "fetch('http://127.0.0.1:' + (process.env.PORT || '4173') + '/api/public/home').then(response => { if (!response.ok) process.exit(1); }).catch(() => process.exit(1));"]
|
||||||
|
|
||||||
|
CMD ["node", "server.mjs"]
|
||||||
@@ -94,6 +94,62 @@ npm start
|
|||||||
|
|
||||||
本地开发无需额外配置,首次运行会自动创建 `data/exam.sqlite` 和完整关系型数据库结构,但不会导入学校、考生、考试或报名测试数据。首次建库只写入系统基础配置和一个超级管理员;账号、密码和显示名可通过 `INITIAL_ADMIN_USERNAME`、`INITIAL_ADMIN_PASSWORD`、`INITIAL_ADMIN_DISPLAY_NAME` 设置。当前数据库结构版本为 v17;v16 数据库会自动增加 TOTP 字段,低于 v15 的开发库会提示重建。
|
本地开发无需额外配置,首次运行会自动创建 `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`。
|
需要清空并重建空业务库时运行 `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`。
|
||||||
|
|
||||||
## 数据库配置
|
## 数据库配置
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
image: exam-information-system:local
|
||||||
|
init: true
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- .env.docker
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
HOST: 0.0.0.0
|
||||||
|
PORT: 4173
|
||||||
|
DATABASE_CLIENT: sqlite
|
||||||
|
SQLITE_PATH: /app/data/exam.sqlite
|
||||||
|
ports:
|
||||||
|
- "4173:4173"
|
||||||
|
volumes:
|
||||||
|
- exam-information-data:/app/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
exam-information-data:
|
||||||
Generated
+16
-16
@@ -1787,12 +1787,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/escape-string-regexp": {
|
"node_modules/escape-string-regexp": {
|
||||||
"version": "4.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
|
||||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
"integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
@@ -2543,18 +2543,6 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"url": "https://opencollective.com/unified"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mdast-util-from-markdown": {
|
"node_modules/mdast-util-from-markdown": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz",
|
||||||
@@ -3843,6 +3831,18 @@
|
|||||||
"node": ">=22.12.0"
|
"node": ">=22.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sanitize-html/node_modules/escape-string-regexp": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/saxes": {
|
"node_modules/saxes": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz",
|
||||||
|
|||||||
Reference in New Issue
Block a user