在线竞赛题库与套题系统,支持题目浏览、考试 / 练习模式、AI 辅助提示与解析、CPOAuth 统一认证。
Important
该文档部分内容由 AI 分析而成。
| 目录 | 说明 |
|---|---|
web/ |
Vue 3 + Vite 前端 SPA |
server/ |
Node.js + Express REST API 后端 |
docs/ |
补充文档(AI 题目配置提示词等) |
docker-compose.yml |
本地开发用 MariaDB + Redis 基础设施 |
docker-compose.prod.yml |
Linux 生产部署编排(含 api / web 容器) |
| 分类 | 技术 |
|---|---|
| 运行时 | Node.js ≥20 |
| 框架 | Express.js 4 + TypeScript 5 |
| 数据库 | MariaDB 11(mysql2 原生 SQL,无 ORM) |
| 缓存 / 会话 | Redis 7(ioredis) |
| 认证 | CPOAuth OAuth2 + Admin Token |
| Schema 迁移 | 启动时自动检测并补列(ensureUserSchema()) |
| 开发运行 | tsx watch |
| 分类 | 技术 |
|---|---|
| 框架 | Vue 3(Composition API + <script setup>) |
| 构建 | Vite 5 |
| 路由 | vue-router 4(history 模式) |
| 国际化 | vue-i18n 9(zh-CN / en-US / ja-JP) |
| 主题 | CSS Variables(auto / dark / light 三模式) |
| 样式 | 原生 CSS(按页面 / 组件拆分) |
| Markdown 渲染 | markdown-it 14 + KaTeX + highlight.js |
| 图标 | Font Awesome Free 6 |
| 表 | 用途 |
|---|---|
users |
用户账号,含 OAuth 绑定、偏好设置 |
app_settings |
键值对应用配置(OAuth、AI、站点公告) |
system_pages |
CMS 页面(用户协议、隐私政策、自定义页面) |
admin_tokens |
管理员登录令牌 |
ai_usage_daily |
每日 AI 调用量统计 |
problemsets |
题目集(四种类型:官方公开 / 个人精选 / 个人公开 / 个人私有) |
questions |
题目详情(选择题 / 填空题,支持材料组) |
submissions |
用户提交记录与评分结果 |
- 题目 DSL:自定义格式(
:::question/:::group),支持选择题(单选 / 多选)和填空题,支持材料组(共享材料的多题组合) - AI 辅助:多模型配置,每日配额,一键生成题目提示 (
hint) 和解析 (solution) - 认证体系:CPOAuth OAuth2(洛谷统一认证)为用户入口,Admin Token 为管理员后台登录入口
- 即时判分:选择题标准化答案比对,填空题精确匹配
- 管理后台:用户管理、题目集 / 题目编辑、OAuth 配置、系统页面管理、数据备份
当前仓库已经针对 Linux 部署做了几项兼容性处理:
- Docker 构建阶段固定使用
pnpm@9.15.4 - 支持通过
NPM_REGISTRY覆盖容器内的 npm registry,避免部分 Linux 服务器拉不到registry.npmjs.org - 根目录新增
.nvmrc,便于 Linux 上直接对齐 Node 20 - 生产环境端口支持通过
API_BIND_HOST/WEB_BIND_HOST控制绑定地址,方便只监听127.0.0.1
如果你只是要部署到 Linux,推荐直接看下面的"Linux 详细部署教程";如果你要本地开发,再看前面的开发章节。
- Node.js 20.x
pnpm9.x- MariaDB / MySQL
- Redis
如果你本机使用 nvm,推荐直接在仓库根目录执行:
nvm install
nvm use
corepack enable
corepack prepare pnpm@9.15.4 --activatepnpm installcp server/.env.example server/.env默认开发环境配置如下:
- API:
http://localhost:3000 - Web:
http://localhost:5173 - MariaDB:
127.0.0.1:3306 - Redis:
127.0.0.1:6379
如需指定前端请求地址,可额外执行:
cp web/.env.example web/.env.developmentpnpm dev也可以拆开启动:
pnpm run dev:api
pnpm run dev:web如果你的开发机没有本地 MariaDB / Redis,可以直接用仓库根目录的 Compose:
cp .env.example .env
pnpm run dev:infra默认端口:
- MariaDB:
127.0.0.1:3307 - Redis:
127.0.0.1:6380
此时把 server/.env 改成:
DB_HOST=127.0.0.1
DB_PORT=3307
REDIS_HOST=127.0.0.1
REDIS_PORT=6380停止基础设施:
pnpm run dev:infra:down下面这套流程是给 Ubuntu / Debian 服务器准备的,Rocky Linux / AlmaLinux / CentOS 也可以参考同样思路,只是系统包管理命令不同。
生产环境推荐使用 Docker Compose 部署,不建议把 Node、MariaDB、Redis 全部直接装到宿主机上手工跑服务。
第一件事:你准备怎么暴露站点。
- 方案 A:
ti.luogu.me和api.ti.luogu.me分域部署 - 方案 B:只用一个域名,例如
ti.luogu.me,前端走/,后端走/api
第二件事:你准备怎么绑定宿主机端口。
- 如果你前面还有宿主机 Nginx / Caddy,建议把
API_BIND_HOST和WEB_BIND_HOST设成127.0.0.1 - 如果你只是临时起服务测试,允许外部直接访问,也可以保持默认
0.0.0.0
最低建议:
- 2 vCPU
- 2 GB 内存
- 20 GB 可用磁盘
更稳妥的生产建议:
- 2 到 4 vCPU
- 4 GB 内存
- SSD 磁盘
- 能正常访问 Docker Hub、npm registry 或你自定义的镜像源
先安装 Docker、Git、Curl:
sudo apt update
sudo apt install -y ca-certificates curl git docker.io docker-compose-plugin
sudo systemctl enable --now docker把当前用户加入 docker 组,避免后续每条命令都写 sudo:
sudo usermod -aG docker "$USER"
newgrp docker如果你还想在宿主机上做本地构建、调试或排查 Node 问题,再额外安装 nvm、Node 和 pnpm:
curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
cd /path/to/ti.luogu.me
nvm install
nvm use
corepack enable
corepack prepare pnpm@9.15.4 --activate说明:
- 仓库根目录已经提供
.nvmrc,nvm install会自动安装 Node 20 - 如果你只用 Docker Compose 部署,其实不装 Node / pnpm 也可以
- 如果服务器访问
registry.npmjs.org很慢或会超时,请提前准备镜像源,后面配置NPM_REGISTRY
推荐把项目放在固定目录,例如 /opt/ti.luogu.me:
sudo mkdir -p /opt/ti.luogu.me
sudo chown -R "$USER":"$USER" /opt/ti.luogu.me
git clone <your-repo-url> /opt/ti.luogu.me
cd /opt/ti.luogu.me后续所有命令默认都在仓库根目录执行。
cp .env.example .env这一步非常重要。
docker-compose.prod.yml 里的端口、数据库口令、构建镜像源、前端 API 地址都会从根目录 .env 里读取。
下面先给一个比较通用、适合“前端一个域名、API 一个域名”的示例:
TZ=Asia/Shanghai
MARIADB_ROOT_PASSWORD=请改成强密码
MARIADB_DATABASE=luogu_ti
MARIADB_USER=app
MARIADB_PASSWORD=请改成强密码
API_BIND_HOST=127.0.0.1
API_PORT=3000
WEB_BIND_HOST=127.0.0.1
WEB_PORT=5173
NPM_REGISTRY=https://registry.npmmirror.com
VITE_API_BASE_URL=https://api.ti.luogu.me
PUBLIC_API_BASE_URL=https://api.ti.luogu.me
WEB_BASE_URL=https://ti.luogu.me
CPOAUTH_BASE_URL=https://auth.luogu.me如果你准备使用“同域名 + /api 反代”的方式,也就是浏览器最终只访问 https://ti.luogu.me,那么可以这样配:
TZ=Asia/Shanghai
MARIADB_ROOT_PASSWORD=请改成强密码
MARIADB_DATABASE=luogu_ti
MARIADB_USER=app
MARIADB_PASSWORD=请改成强密码
API_BIND_HOST=127.0.0.1
API_PORT=3000
WEB_BIND_HOST=127.0.0.1
WEB_PORT=5173
NPM_REGISTRY=https://registry.npmmirror.com
VITE_API_BASE_URL=
PUBLIC_API_BASE_URL=
WEB_BASE_URL=https://ti.luogu.me
CPOAUTH_BASE_URL=https://auth.luogu.me这几项变量的含义建议你看清楚:
| 变量 | 作用 | 建议 |
|---|---|---|
MARIADB_ROOT_PASSWORD |
MariaDB root 口令 | 必改,且要足够复杂 |
MARIADB_DATABASE |
业务数据库名 | 一般保持 luogu_ti 即可 |
MARIADB_USER |
业务数据库用户 | 一般保持 app 即可 |
MARIADB_PASSWORD |
业务数据库口令 | 必改,且不要和 root 口令相同 |
API_BIND_HOST |
宿主机 API 监听地址 | 有反向代理时设 127.0.0.1 |
API_PORT |
宿主机 API 端口 | 默认 3000 |
WEB_BIND_HOST |
宿主机前端监听地址 | 有反向代理时设 127.0.0.1 |
WEB_PORT |
宿主机前端端口 | 默认 5173 |
NPM_REGISTRY |
容器构建阶段使用的 npm registry | 海外机器可改成 https://registry.npmjs.org,网络受限环境保留镜像源 |
VITE_API_BASE_URL |
前端构建时写死的 API 基础地址 | 分域部署时写 https://api.xxx.com;同域 /api 反代时可留空 |
PUBLIC_API_BASE_URL |
后端对外公开 API 基础地址 | 分域部署时与 API 域名一致;同域 /api 反代时可留空 |
WEB_BASE_URL |
站点对外访问域名 | 建议写真实 HTTPS 域名 |
CPOAUTH_BASE_URL |
CPOAuth 服务地址 | 默认 https://auth.luogu.me |
特别说明:
NPM_REGISTRY是这次 Linux 兼容处理里的重点之一。很多服务器不是 Docker 装不上,而是构建镜像时拉pnpm或装依赖超时。- 如果你服务器能稳定访问官方源,可以把
NPM_REGISTRY改成https://registry.npmjs.org - 如果你保持
VITE_API_BASE_URL=为空,前端会走同源请求,然后由web容器内的 Nginx 把/api转发给api容器
第一次部署建议直接构建并启动:
docker compose -f docker-compose.prod.yml up -d --build查看容器状态:
docker compose -f docker-compose.prod.yml ps查看后端日志:
docker compose -f docker-compose.prod.yml logs -f api查看前端日志:
docker compose -f docker-compose.prod.yml logs -f web查看数据库日志:
docker compose -f docker-compose.prod.yml logs -f mariadb说明:
- 第一次启动时,MariaDB 初始化可能需要几十秒
api服务会等待mariadb和redis健康检查通过后再启动- 如果你改过
.env里的构建相关变量,例如NPM_REGISTRY、VITE_API_BASE_URL,记得重新执行up -d --build
建议依次检查:
docker compose -f docker-compose.prod.yml ps里所有服务都处于runningapi日志里能看到类似listening on :3000- 访问
http://127.0.0.1:3000/能返回pong - 访问
http://127.0.0.1:5173/能返回前端页面
如果你把 API_BIND_HOST / WEB_BIND_HOST 设成 127.0.0.1,上面这两条检查建议在服务器本机执行,例如:
curl http://127.0.0.1:3000/
curl -I http://127.0.0.1:5173/生产环境通常还会在宿主机再放一层 Nginx 或 Caddy,负责:
- 绑定 80 / 443
- 配置 HTTPS 证书
- 处理域名转发
建议:
ti.luogu.me->http://127.0.0.1:5173api.ti.luogu.me->http://127.0.0.1:3000
Nginx 参考配置:
server {
listen 80;
server_name ti.luogu.me;
location / {
proxy_pass http://127.0.0.1:5173;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 80;
server_name api.ti.luogu.me;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}如果你希望用户只访问一个域名,比如 https://ti.luogu.me,那可以让宿主机 Nginx 统一转发:
server {
listen 80;
server_name ti.luogu.me;
location /api/ {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
proxy_pass http://127.0.0.1:5173;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}这一模式下建议:
VITE_API_BASE_URL=留空PUBLIC_API_BASE_URL=留空WEB_BASE_URL=https://ti.luogu.me
项目当前支持通过数据库插入一条 Admin Token 的方式做首次后台登录。
先进入 MariaDB 容器:
docker compose -f docker-compose.prod.yml exec mariadb mariadb -uapp -p"$MARIADB_PASSWORD" "$MARIADB_DATABASE"插入一条 32 位字母数字 token:
INSERT INTO admin_tokens (token, created_by_uid)
VALUES ('AbCdEfGhIjKlMnOpQrStUvWxYz123456', 'root');然后访问:
https://ti.luogu.me/auth/login
在登录页下方使用这条 Admin Token 登录后台,完成 CPOAuth 相关配置。
更新代码后重新构建:
cd /opt/ti.luogu.me
git pull
docker compose -f docker-compose.prod.yml up -d --build如果你只是重启服务,不重新构建:
docker compose -f docker-compose.prod.yml restart如果要停服:
docker compose -f docker-compose.prod.yml down注意:
down不会删除命名卷,数据库和 Redis 数据默认还在- 如果你执行
down -v,命名卷会被删掉,数据库数据也会丢失,生产环境不要随便用
现象:
pnpm install超时- Docker build 里拉
pnpm@9.15.4失败 - 日志里出现访问
registry.npmjs.org超时
排查:
- 确认服务器能否访问外网
- 把
.env里的NPM_REGISTRY改成可达的镜像源 - 重新执行
docker compose -f docker-compose.prod.yml up -d --build
现象:
- Compose 启动时报端口冲突
处理:
- 用
ss -lntp | grep -E '3000|5173'查占用 - 改
.env里的API_PORT或WEB_PORT - 如果前面有宿主机 Nginx / Caddy,推荐直接把
API_BIND_HOST和WEB_BIND_HOST设成127.0.0.1
优先检查:
- 你到底是“分域部署”还是“同域
/api反代” VITE_API_BASE_URL是否配对了当前方案PUBLIC_API_BASE_URL/WEB_BASE_URL是否写成了真实对外域名- 宿主机反向代理是否正确把请求转给了
127.0.0.1:3000和127.0.0.1:5173
优先检查:
WEB_BASE_URL是否是用户真实访问的 HTTPS 域名PUBLIC_API_BASE_URL是否和 API 对外域名一致- 后台里的 CPOAuth callback URL 是否与你最终公网访问地址一致
- 反向代理是否正确透传了
Host与X-Forwarded-Proto
优先检查:
docker compose -f docker-compose.prod.yml logs -f mariadb- 宿主机磁盘空间是否够
.env中的数据库口令是否写了非法字符或被错误转义- 之前是否残留了异常卷数据
pnpm build
pnpm -C server run typecheck
pnpm -C web exec vue-tsc --noEmit
docker compose ps
docker compose logs -f mariadb
docker compose logs -f redis
docker compose -f docker-compose.prod.yml ps
docker compose -f docker-compose.prod.yml logs -f api
docker compose -f docker-compose.prod.yml logs -f web