From e797d188ee521f7e44820a2b0f473914f276bbc8 Mon Sep 17 00:00:00 2001 From: dekun Date: Sun, 28 Jun 2026 11:52:20 +0800 Subject: [PATCH] Switch production deployment from Docker to PM2 on Ubuntu. Add Express gateway, ecosystem config, and one-click install with native PostgreSQL, Node, and Python venv on port 23566. Co-authored-by: Cursor --- .env.example | 17 +- .gitattributes | 3 + README.md | 96 +-- backend/.env.example | 5 +- backend/Dockerfile | 19 - backend/app/core/config.py | 2 +- deploy/backup.sh | 47 +- deploy/install.sh | 466 +++++++------- deploy/pm2/.gitignore | 1 + deploy/pm2/ecosystem.config.cjs | 61 ++ deploy/pm2/package-lock.json | 1052 +++++++++++++++++++++++++++++++ deploy/pm2/package.json | 11 + deploy/pm2/server.js | 31 + deploy/uninstall.sh | 39 +- deploy/update.sh | 68 +- docker-compose.yml | 55 -- docs/DEPLOY.md | 289 ++------- docs/USAGE.md | 2 +- frontend/Dockerfile | 11 - frontend/nginx.conf | 17 - 20 files changed, 1578 insertions(+), 714 deletions(-) create mode 100644 .gitattributes delete mode 100644 backend/Dockerfile create mode 100644 deploy/pm2/.gitignore create mode 100644 deploy/pm2/ecosystem.config.cjs create mode 100644 deploy/pm2/package-lock.json create mode 100644 deploy/pm2/package.json create mode 100644 deploy/pm2/server.js delete mode 100644 docker-compose.yml delete mode 100644 frontend/Dockerfile delete mode 100644 frontend/nginx.conf diff --git a/.env.example b/.env.example index e21da20..7fe7d2f 100644 --- a/.env.example +++ b/.env.example @@ -1,23 +1,20 @@ # 复制为 .env 后修改(一键部署脚本会自动生成) # 部署目录默认:/opt/secondary-school-grade-archive -# Web 对外端口(默认 23566) WEB_PORT=23566 +API_PORT=8000 +API_TARGET=http://127.0.0.1:8000 -# 生产环境务必修改 SECRET_KEY=请替换为随机字符串 -POSTGRES_USER=postgres +POSTGRES_USER=gradeapp POSTGRES_PASSWORD=请替换为强密码 POSTGRES_DB=student_archive +DATABASE_URL=postgresql://gradeapp:请替换为强密码@127.0.0.1:5432/student_archive + +UPLOAD_DIR=/opt/secondary-school-grade-archive/uploads -# 允许跨域的前端地址(部署完成后改为实际访问地址) -# 若通过反向代理访问,请自行在反向代理层处理,此处填写直连地址即可 CORS_ORIGINS=http://127.0.0.1:23566,http://localhost:23566 -# Ollama(错题 AI 解法,可选) -# Docker 容器内通过 host.docker.internal 访问宿主机 Ollama -OLLAMA_BASE_URL=http://host.docker.internal:11434 +OLLAMA_BASE_URL=http://127.0.0.1:11434 OLLAMA_MODEL=qwen2.5:7b - -# 成绩波动高亮阈值(0.08 = 8%) FLUCTUATION_THRESHOLD=0.08 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f62094a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +* text=auto +*.sh text eol=lf +*.bash text eol=lf diff --git a/README.md b/README.md index a75815c..a66212c 100644 --- a/README.md +++ b/README.md @@ -4,37 +4,22 @@ Secondary School Grade Archive — 多用户 Web 系统:成绩录入、占比 **版权所有 © 马建军** · 微信 **dekun03** · 手机 **18364911125** -> 完整版权说明见 [COPYRIGHT.md](./COPYRIGHT.md) · 许可证见 [LICENSE](./LICENSE) +> [COPYRIGHT.md](./COPYRIGHT.md) · [LICENSE](./LICENSE) -**代码仓库:** [https://git.bz121.com/dekun/secondary-school-grade-archive.git](https://git.bz121.com/dekun/secondary-school-grade-archive.git) +**仓库:** [https://git.bz121.com/dekun/secondary-school-grade-archive.git](https://git.bz121.com/dekun/secondary-school-grade-archive.git) --- -## 文档索引 +## 文档 | 文档 | 说明 | |------|------| -| [docs/DEPLOY.md](./docs/DEPLOY.md) | **Ubuntu 一键 Docker 部署**(/opt、端口 23566) | -| [docs/USAGE.md](./docs/USAGE.md) | **用户使用说明** | -| [COPYRIGHT.md](./COPYRIGHT.md) | 版权与授权 | -| [LICENSE](./LICENSE) | 许可证全文 | +| [docs/DEPLOY.md](./docs/DEPLOY.md) | **Ubuntu PM2 一键部署** | +| [docs/USAGE.md](./docs/USAGE.md) | 用户使用说明 | --- -## 功能概览 - -- 用户注册/登录,数据按账号隔离 -- 学生管理(**初中 / 高中**学段、年级、班级) -- 成绩录入:周考 / 月考 / 期末(总分、得分、占比) -- 分科曲线:上升绿、下降红、大幅波动高亮 -- 错题库:上传图片 → PaddleOCR → Ollama 生成解法 -- 成绩 CSV 导出、备份脚本 - ---- - -## Ubuntu 一键部署(生产环境) - -**要求:** root 用户 · Ubuntu · Docker · 目录 `/opt/secondary-school-grade-archive` · 端口 **23566** +## Ubuntu 一键部署(PM2) ```bash git clone https://git.bz121.com/dekun/secondary-school-grade-archive.git /opt/secondary-school-grade-archive @@ -43,74 +28,53 @@ chmod +x deploy/*.sh bash deploy/install.sh ``` -部署完成后访问:`http://<服务器IP>:23566` +- 安装目录:`/opt/secondary-school-grade-archive` +- 访问地址:`http://<服务器IP>:23566` +- 进程管理:`pm2 status` / `pm2 logs` -脚本会自动:检测系统环境 → 安装 Docker(若缺失)→ 生成 `.env` → 构建并启动服务。 - -详细说明、运维命令、故障排查见 **[docs/DEPLOY.md](./docs/DEPLOY.md)**。 - -> **反向代理(HTTPS/域名)不包含在本项目中**,请自行配置 Nginx/Caddy 等,参见部署文档第 7 节。 +详见 [docs/DEPLOY.md](./docs/DEPLOY.md)。**反向代理不包含在本项目中。** --- ## 本地开发 -### Docker Compose(默认端口 23566) - ```bash -cp .env.example .env -docker compose --env-file .env up --build -``` +# PostgreSQL 本地安装后 +cp backend/.env.example backend/.env -- 前端:http://localhost:23566 -- API 健康检查:http://localhost:23566/api/health - -### 分步开发 - -```bash -# 仅数据库 -docker compose up db -d - -# 后端 -cd backend && pip install -r requirements.txt && cp .env.example .env +cd backend +python3 -m venv venv +source venv/bin/activate # Windows: venv\Scripts\activate +pip install -r requirements.txt uvicorn app.main:app --reload --port 8000 -# 前端 -cd frontend && npm install && npm run dev +cd frontend +npm install && npm run dev ``` -### Ollama(错题 AI,可选) +开发前端:http://localhost:5173(代理 `/api` 到 8000) + +生产网关本地模拟: ```bash -ollama pull qwen2.5:7b -ollama serve +cd deploy/pm2 && npm install +# 先 build 前端 +cd ../../frontend && npm run build +WEB_PORT=23566 pm2 start ../deploy/pm2/ecosystem.config.cjs ``` --- -## 运维快捷命令 +## 运维 ```bash -cd /opt/secondary-school-grade-archive - -docker compose ps # 状态 -bash deploy/update.sh # 更新 -bash deploy/backup.sh # 备份 -bash deploy/uninstall.sh # 停止服务 +bash deploy/update.sh # 更新 +bash deploy/backup.sh # 备份 +bash deploy/uninstall.sh # 停止 ``` --- -## 环境变量 - -见 [.env.example](./.env.example) - ---- - ## 技术支持 -- **作者:** 马建军 -- **微信:** dekun03 -- **手机:** 18364911125 - -未经授权不得商业使用或去除版权信息。 +微信 **dekun03** · 手机 **18364911125** diff --git a/backend/.env.example b/backend/.env.example index 0ca26ab..4957f0c 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,7 +1,8 @@ -DATABASE_URL=postgresql://postgres:postgres@localhost:5432/student_archive +DATABASE_URL=postgresql://gradeapp:postgres@127.0.0.1:5432/student_archive SECRET_KEY=dev-secret-key-change-in-production -CORS_ORIGINS=http://localhost:5173,http://localhost:3000 +CORS_ORIGINS=http://localhost:5173,http://localhost:23566 UPLOAD_DIR=uploads +API_PORT=8000 OLLAMA_BASE_URL=http://127.0.0.1:11434 OLLAMA_MODEL=qwen2.5:7b FLUCTUATION_THRESHOLD=0.08 diff --git a/backend/Dockerfile b/backend/Dockerfile deleted file mode 100644 index f0f7dde..0000000 --- a/backend/Dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -FROM python:3.11-slim - -WORKDIR /app - -RUN apt-get update && apt-get install -y --no-install-recommends \ - libgomp1 libglib2.0-0 libsm6 libxext6 libxrender-dev \ - && rm -rf /var/lib/apt/lists/* - -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt - -COPY app ./app - -ENV UPLOAD_DIR=/app/uploads -RUN mkdir -p /app/uploads - -EXPOSE 8000 - -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 0913f6e..4902311 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -9,7 +9,7 @@ class Settings(BaseSettings): ALGORITHM: str = "HS256" UPLOAD_DIR: str = "uploads" MAX_UPLOAD_SIZE: int = 10 * 1024 * 1024 - OLLAMA_BASE_URL: str = "http://host.docker.internal:11434" + OLLAMA_BASE_URL: str = "http://127.0.0.1:11434" OLLAMA_MODEL: str = "qwen2.5:7b" FLUCTUATION_THRESHOLD: float = 0.08 CORS_ORIGINS: str = "http://localhost:5173,http://localhost:3000,http://localhost" diff --git a/deploy/backup.sh b/deploy/backup.sh index 788a899..a9a064e 100644 --- a/deploy/backup.sh +++ b/deploy/backup.sh @@ -1,25 +1,22 @@ -#!/usr/bin/env bash -# -# 备份 PostgreSQL 与 uploads 目录 -# 版权所有 (c) 马建军 -# -set -euo pipefail - -INSTALL_DIR="${INSTALL_DIR:-/opt/secondary-school-grade-archive}" -BACKUP_DIR="${BACKUP_DIR:-${INSTALL_DIR}/backups}" -TIMESTAMP=$(date +%Y%m%d_%H%M%S) - -cd "${INSTALL_DIR}" -mkdir -p "${BACKUP_DIR}" - -echo "[INFO] 备份数据库…" -docker compose --env-file .env exec -T db \ - pg_dump -U "${POSTGRES_USER:-postgres}" "${POSTGRES_DB:-student_archive}" \ - > "${BACKUP_DIR}/db_${TIMESTAMP}.sql" - -echo "[INFO] 备份 uploads…" -tar -czf "${BACKUP_DIR}/uploads_${TIMESTAMP}.tar.gz" -C "${INSTALL_DIR}" uploads/ - -echo "[INFO] 备份完成:" -echo " ${BACKUP_DIR}/db_${TIMESTAMP}.sql" -echo " ${BACKUP_DIR}/uploads_${TIMESTAMP}.tar.gz" +#!/usr/bin/env bash +set -euo pipefail + +INSTALL_DIR="${INSTALL_DIR:-/opt/secondary-school-grade-archive}" +BACKUP_DIR="${BACKUP_DIR:-${INSTALL_DIR}/backups}" +TIMESTAMP=$(date +%Y%m%d_%H%M%S) + +cd "${INSTALL_DIR}" +# shellcheck disable=SC1090 +source .env +mkdir -p "${BACKUP_DIR}" + +echo "[INFO] 备份数据库…" +PGPASSWORD="${POSTGRES_PASSWORD}" pg_dump -h 127.0.0.1 -U "${POSTGRES_USER}" "${POSTGRES_DB}" \ + > "${BACKUP_DIR}/db_${TIMESTAMP}.sql" + +echo "[INFO] 备份 uploads…" +tar -czf "${BACKUP_DIR}/uploads_${TIMESTAMP}.tar.gz" -C "${INSTALL_DIR}" uploads/ + +echo "[INFO] 完成:" +echo " ${BACKUP_DIR}/db_${TIMESTAMP}.sql" +echo " ${BACKUP_DIR}/uploads_${TIMESTAMP}.tar.gz" diff --git a/deploy/install.sh b/deploy/install.sh index beb79d9..a43b055 100644 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -1,231 +1,235 @@ -#!/usr/bin/env bash -# -# 中学成绩档案系统 — Ubuntu 一键部署脚本 -# 版权所有 (c) 马建军 微信: dekun03 手机: 18364911125 -# -# 用法(root): -# curl -fsSL .../deploy/install.sh | bash -# 或 -# bash deploy/install.sh -# -set -euo pipefail - -REPO_URL="${REPO_URL:-https://git.bz121.com/dekun/secondary-school-grade-archive.git}" -INSTALL_DIR="${INSTALL_DIR:-/opt/secondary-school-grade-archive}" -WEB_PORT="${WEB_PORT:-23566}" -BRANCH="${BRANCH:-main}" - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' - -log_info() { echo -e "${GREEN}[INFO]${NC} $*"; } -log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } -log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } - -require_root() { - if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then - log_error "请使用 root 用户运行本脚本,例如: sudo bash deploy/install.sh" - exit 1 - fi -} - -check_os() { - if [[ ! -f /etc/os-release ]]; then - log_error "无法识别操作系统,本脚本仅支持 Ubuntu/Debian 系 Linux" - exit 1 - fi - # shellcheck source=/dev/null - source /etc/os-release - log_info "检测到系统: ${NAME:-Unknown} ${VERSION_ID:-}" - case "${ID:-}" in - ubuntu|debian) - ;; - *) - log_warn "当前系统为 ${ID:-unknown},未经完整测试,继续安装…" - ;; - esac -} - -check_resources() { - local mem_kb disk_kb - mem_kb=$(grep MemTotal /proc/meminfo | awk '{print $2}') - disk_kb=$(df -k "${INSTALL_DIR%/*}" 2>/dev/null | tail -1 | awk '{print $4}') - if [[ "${mem_kb:-0}" -lt 1800000 ]]; then - log_warn "内存不足 2GB,PaddleOCR 首次运行可能较慢" - fi - if [[ "${disk_kb:-0}" -lt 5242880 ]]; then - log_warn "可用磁盘空间不足 5GB,请确保有足够空间存放镜像与 OCR 模型" - fi -} - -check_port() { - if command -v ss &>/dev/null; then - if ss -tln | grep -q ":${WEB_PORT} "; then - log_error "端口 ${WEB_PORT} 已被占用,请修改 WEB_PORT 环境变量后重试" - exit 1 - fi - elif command -v netstat &>/dev/null; then - if netstat -tln | grep -q ":${WEB_PORT} "; then - log_error "端口 ${WEB_PORT} 已被占用" - exit 1 - fi - fi - log_info "端口 ${WEB_PORT} 可用" -} - -install_packages() { - log_info "安装基础依赖…" - apt-get update -qq - apt-get install -y -qq git curl ca-certificates openssl -} - -install_docker() { - if command -v docker &>/dev/null; then - log_info "Docker 已安装: $(docker --version)" - else - log_info "正在安装 Docker…" - apt-get install -y -qq apt-transport-https gnupg lsb-release - install -m 0755 -d /etc/apt/keyrings - curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc - chmod a+r /etc/apt/keyrings/docker.asc - echo \ - "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ - $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" \ - > /etc/apt/sources.list.d/docker.list - apt-get update -qq - apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-compose-plugin - systemctl enable docker - systemctl start docker - log_info "Docker 安装完成" - fi - - if ! docker compose version &>/dev/null; then - log_error "未检测到 docker compose 插件,请手动安装 docker-compose-plugin" - exit 1 - fi - log_info "Docker Compose: $(docker compose version)" -} - -clone_or_update_repo() { - if [[ -d "${INSTALL_DIR}/.git" ]]; then - log_info "更新代码: ${INSTALL_DIR}" - git -C "${INSTALL_DIR}" fetch origin - git -C "${INSTALL_DIR}" checkout "${BRANCH}" 2>/dev/null || true - git -C "${INSTALL_DIR}" pull --ff-only origin "${BRANCH}" || git -C "${INSTALL_DIR}" pull origin "${BRANCH}" - elif [[ -d "${INSTALL_DIR}" ]]; then - log_error "目录 ${INSTALL_DIR} 已存在但不是 git 仓库,请备份后删除或修改 INSTALL_DIR" - exit 1 - else - log_info "克隆仓库到 ${INSTALL_DIR}" - mkdir -p "$(dirname "${INSTALL_DIR}")" - git clone --branch "${BRANCH}" "${REPO_URL}" "${INSTALL_DIR}" || git clone "${REPO_URL}" "${INSTALL_DIR}" - fi -} - -generate_env() { - local env_file="${INSTALL_DIR}/.env" - local server_ip - server_ip=$(hostname -I 2>/dev/null | awk '{print $1}') - server_ip="${server_ip:-127.0.0.1}" - - if [[ -f "${env_file}" ]]; then - log_info "保留已有 .env 配置" - # shellcheck source=/dev/null - source "${env_file}" - WEB_PORT="${WEB_PORT:-23566}" - return - fi - - log_info "生成 .env 配置文件" - local secret pg_pass - secret=$(openssl rand -hex 32) - pg_pass=$(openssl rand -hex 16) - - cat > "${env_file}" </dev/null || true - docker compose --env-file .env up -d --build -} - -wait_healthy() { - local i max=60 - log_info "等待 API 就绪…" - for ((i=1; i<=max; i++)); do - if docker compose --env-file "${INSTALL_DIR}/.env" exec -T api \ - python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/api/health')" &>/dev/null; then - log_info "API 健康检查通过" - return 0 - fi - sleep 3 - done - log_warn "API 启动超时,请执行: cd ${INSTALL_DIR} && docker compose logs api" -} - -print_summary() { - local ip - ip=$(hostname -I 2>/dev/null | awk '{print $1}') - ip="${ip:-127.0.0.1}" - # shellcheck source=/dev/null - source "${INSTALL_DIR}/.env" - - echo "" - echo "==========================================" - echo " 中学成绩档案系统 部署完成" - echo " 版权所有 (c) 马建军" - echo "==========================================" - echo "" - echo " 访问地址: http://${ip}:${WEB_PORT}" - echo " 本地访问: http://127.0.0.1:${WEB_PORT}" - echo " 安装目录: ${INSTALL_DIR}" - echo "" - echo " 常用命令:" - echo " 查看状态: cd ${INSTALL_DIR} && docker compose ps" - echo " 查看日志: cd ${INSTALL_DIR} && docker compose logs -f" - echo " 停止服务: cd ${INSTALL_DIR} && docker compose down" - echo " 更新版本: bash ${INSTALL_DIR}/deploy/update.sh" - echo " 数据备份: bash ${INSTALL_DIR}/deploy/backup.sh" - echo "" - echo " 反向代理(Nginx/Caddy 等)请自行配置,本项目不包含。" - echo " 详见文档: ${INSTALL_DIR}/docs/DEPLOY.md" - echo "" - echo " 技术支持: 微信 dekun03 手机 18364911125" - echo "==========================================" -} - -main() { - echo "" - log_info "中学成绩档案系统 — 一键部署开始" - require_root - check_os - check_resources - check_port - install_packages - install_docker - clone_or_update_repo - generate_env - start_services - wait_healthy - print_summary -} - -main "$@" +#!/usr/bin/env bash +# +# 中学成绩档案系统 — Ubuntu PM2 一键部署 +# 版权所有 (c) 马建军 微信: dekun03 手机: 18364911125 +# +set -euo pipefail + +REPO_URL="${REPO_URL:-https://git.bz121.com/dekun/secondary-school-grade-archive.git}" +INSTALL_DIR="${INSTALL_DIR:-/opt/secondary-school-grade-archive}" +WEB_PORT="${WEB_PORT:-23566}" +API_PORT="${API_PORT:-8000}" +BRANCH="${BRANCH:-main}" +NODE_MAJOR="${NODE_MAJOR:-20}" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +log_info() { echo -e "${GREEN}[INFO]${NC} $*"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } + +require_root() { + if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then + log_error "请使用 root 用户运行: sudo bash deploy/install.sh" + exit 1 + fi +} + +check_os() { + if [[ ! -f /etc/os-release ]]; then + log_error "无法识别操作系统" + exit 1 + fi + # shellcheck source=/dev/null + source /etc/os-release + log_info "检测到系统: ${NAME:-Unknown} ${VERSION_ID:-}" +} + +check_port() { + if command -v ss &>/dev/null && ss -tln | grep -q ":${WEB_PORT} "; then + log_error "端口 ${WEB_PORT} 已被占用" + exit 1 + fi + log_info "端口 ${WEB_PORT} 可用" +} + +install_base_packages() { + log_info "安装系统依赖…" + apt-get update -qq + DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \ + git curl ca-certificates openssl \ + python3 python3-venv python3-pip python3-dev \ + build-essential libpq-dev \ + postgresql postgresql-contrib \ + libgomp1 libglib2.0-0 libsm6 libxrender1 libxext6 +} + +install_node_pm2() { + if ! command -v node &>/dev/null || [[ "$(node -v | cut -d. -f1 | tr -d v)" -lt "${NODE_MAJOR}" ]]; then + log_info "安装 Node.js ${NODE_MAJOR}.x…" + curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash - + apt-get install -y -qq nodejs + fi + log_info "Node: $(node -v) npm: $(npm -v)" + + if ! command -v pm2 &>/dev/null; then + log_info "安装 PM2…" + npm install -g pm2 + fi + log_info "PM2: $(pm2 -v)" +} + +clone_or_update_repo() { + if [[ -d "${INSTALL_DIR}/.git" ]]; then + log_info "更新代码: ${INSTALL_DIR}" + git -C "${INSTALL_DIR}" fetch origin + git -C "${INSTALL_DIR}" checkout "${BRANCH}" 2>/dev/null || true + git -C "${INSTALL_DIR}" pull origin "${BRANCH}" + else + log_info "克隆仓库到 ${INSTALL_DIR}" + mkdir -p "$(dirname "${INSTALL_DIR}")" + git clone --branch "${BRANCH}" "${REPO_URL}" "${INSTALL_DIR}" || git clone "${REPO_URL}" "${INSTALL_DIR}" + fi + find "${INSTALL_DIR}" -name "*.sh" -exec sed -i 's/\r$//' {} + +} + +generate_env() { + local env_file="${INSTALL_DIR}/.env" + local server_ip + server_ip=$(hostname -I 2>/dev/null | awk '{print $1}') + server_ip="${server_ip:-127.0.0.1}" + + if [[ -f "${env_file}" ]]; then + log_info "保留已有 .env" + # shellcheck disable=SC1090 + set -a && source "${env_file}" && set +a + return + fi + + local secret pg_pass pg_user + secret=$(openssl rand -hex 32) + pg_pass=$(openssl rand -hex 16) + pg_user="gradeapp" + + cat > "${env_file}" </dev/null || true + pm2 start deploy/pm2/ecosystem.config.cjs + pm2 save + env PATH="$PATH" pm2 startup systemd -u root --hp /root >/tmp/pm2-startup.sh 2>/dev/null || true + if [[ -f /tmp/pm2-startup.sh ]]; then + bash /tmp/pm2-startup.sh || true + fi +} + +wait_healthy() { + local i + for i in $(seq 1 40); do + if curl -sf "http://127.0.0.1:${WEB_PORT}/api/health" >/dev/null; then + log_info "健康检查通过" + return 0 + fi + sleep 3 + done + log_warn "健康检查超时,请查看: pm2 logs" +} + +print_summary() { + local ip + ip=$(hostname -I 2>/dev/null | awk '{print $1}') + ip="${ip:-127.0.0.1}" + echo "" + echo "==========================================" + echo " 中学成绩档案系统 PM2 部署完成" + echo " 版权所有 (c) 马建军" + echo "==========================================" + echo " 访问: http://${ip}:${WEB_PORT}" + echo " 目录: ${INSTALL_DIR}" + echo "" + echo " pm2 status" + echo " pm2 logs" + echo " bash ${INSTALL_DIR}/deploy/update.sh" + echo " bash ${INSTALL_DIR}/deploy/backup.sh" + echo "" + echo " 反向代理请自行配置,本项目不包含" + echo " 微信 dekun03 手机 18364911125" + echo "==========================================" +} + +main() { + log_info "PM2 一键部署开始" + require_root + check_os + check_port + install_base_packages + install_node_pm2 + clone_or_update_repo + generate_env + setup_postgresql + setup_backend + setup_frontend + setup_gateway + start_pm2 + wait_healthy + print_summary +} + +main "$@" diff --git a/deploy/pm2/.gitignore b/deploy/pm2/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/deploy/pm2/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/deploy/pm2/ecosystem.config.cjs b/deploy/pm2/ecosystem.config.cjs new file mode 100644 index 0000000..24f4244 --- /dev/null +++ b/deploy/pm2/ecosystem.config.cjs @@ -0,0 +1,61 @@ +const path = require('path') +const fs = require('fs') + +const root = path.resolve(__dirname, '../..') +const envPath = path.join(root, '.env') + +function loadEnv() { + const env = { NODE_ENV: 'production' } + if (!fs.existsSync(envPath)) return env + for (const line of fs.readFileSync(envPath, 'utf8').split('\n')) { + const trimmed = line.trim() + if (!trimmed || trimmed.startsWith('#')) continue + const idx = trimmed.indexOf('=') + if (idx === -1) continue + const key = trimmed.slice(0, idx).trim() + let value = trimmed.slice(idx + 1).trim() + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + value = value.slice(1, -1) + } + env[key] = value + } + return env +} + +const env = loadEnv() +const webPort = env.WEB_PORT || '23566' +const apiPort = env.API_PORT || '8000' +const venvPython = path.join(root, 'backend', 'venv', 'bin', 'python') + +module.exports = { + apps: [ + { + name: 'grade-api', + cwd: path.join(root, 'backend'), + script: venvPython, + args: `-m uvicorn app.main:app --host 127.0.0.1 --port ${apiPort}`, + interpreter: 'none', + env: { + ...env, + UPLOAD_DIR: env.UPLOAD_DIR || path.join(root, 'uploads'), + }, + max_restarts: 10, + restart_delay: 5000, + }, + { + name: 'grade-web', + cwd: path.join(root, 'deploy', 'pm2'), + script: 'server.js', + env: { + ...env, + WEB_PORT: webPort, + API_TARGET: env.API_TARGET || `http://127.0.0.1:${apiPort}`, + }, + max_restarts: 10, + restart_delay: 3000, + }, + ], +} diff --git a/deploy/pm2/package-lock.json b/deploy/pm2/package-lock.json new file mode 100644 index 0000000..509036e --- /dev/null +++ b/deploy/pm2/package-lock.json @@ -0,0 +1,1052 @@ +{ + "name": "grade-archive-gateway", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "grade-archive-gateway", + "version": "1.0.0", + "dependencies": { + "dotenv": "^16.4.7", + "express": "^4.21.2", + "http-proxy-middleware": "^3.0.3" + } + }, + "node_modules/@types/http-proxy": { + "version": "1.17.17", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz", + "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.1.tgz", + "integrity": "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-middleware": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.7.tgz", + "integrity": "sha512-iwbQltVlx8bCrqePUM8C+hllHvdawVhQJaLrj1X7qllkvFQdXFsr16pW/mo9+JDVjN+QO2XUx9jd8SmoFkE5qw==", + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.15", + "debug": "^4.3.6", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.3", + "is-plain-object": "^5.0.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": "^14.18.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/http-proxy-middleware/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-middleware/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + } + } +} diff --git a/deploy/pm2/package.json b/deploy/pm2/package.json new file mode 100644 index 0000000..3fa6ad4 --- /dev/null +++ b/deploy/pm2/package.json @@ -0,0 +1,11 @@ +{ + "name": "grade-archive-gateway", + "private": true, + "version": "1.0.0", + "description": "Static web + API reverse proxy for PM2 deployment", + "dependencies": { + "dotenv": "^16.4.7", + "express": "^4.21.2", + "http-proxy-middleware": "^3.0.3" + } +} diff --git a/deploy/pm2/server.js b/deploy/pm2/server.js new file mode 100644 index 0000000..5dff340 --- /dev/null +++ b/deploy/pm2/server.js @@ -0,0 +1,31 @@ +const path = require('path') +const fs = require('fs') +const express = require('express') +const { createProxyMiddleware } = require('http-proxy-middleware') + +const envPath = path.join(__dirname, '../../.env') +if (fs.existsSync(envPath)) { + require('dotenv').config({ path: envPath }) +} + +const PORT = Number(process.env.WEB_PORT || 23566) +const API_TARGET = process.env.API_TARGET || 'http://127.0.0.1:8000' +const STATIC_ROOT = path.join(__dirname, '../../frontend/dist') + +const app = express() +app.use( + '/api', + createProxyMiddleware({ + target: API_TARGET, + changeOrigin: true, + }), +) +app.use(express.static(STATIC_ROOT, { maxAge: '1h', index: false })) +app.get(/^\/(?!api).*/, (_req, res) => { + res.sendFile(path.join(STATIC_ROOT, 'index.html')) +}) + +app.listen(PORT, '0.0.0.0', () => { + console.log(`Grade archive web gateway listening on http://0.0.0.0:${PORT}`) + console.log(`API proxy target: ${API_TARGET}`) +}) diff --git a/deploy/uninstall.sh b/deploy/uninstall.sh index f2b656d..36de569 100644 --- a/deploy/uninstall.sh +++ b/deploy/uninstall.sh @@ -1,28 +1,11 @@ -#!/usr/bin/env bash -# -# 停止并移除容器(默认保留数据卷与 uploads) -# 版权所有 (c) 马建军 -# -set -euo pipefail - -INSTALL_DIR="${INSTALL_DIR:-/opt/secondary-school-grade-archive}" -REMOVE_VOLUMES="${REMOVE_VOLUMES:-0}" - -cd "${INSTALL_DIR}" || exit 1 - -echo "将停止 Docker 服务…" -if [[ "${REMOVE_VOLUMES}" == "1" ]]; then - echo "警告: 将删除数据库卷(所有成绩数据会丢失)" - read -r -p "确认删除数据卷? 输入 yes: " ans - if [[ "${ans}" == "yes" ]]; then - docker compose --env-file .env down -v - else - echo "已取消" - exit 1 - fi -else - docker compose --env-file .env down - echo "数据卷与 ${INSTALL_DIR}/uploads 已保留" -fi - -echo "卸载完成。源码目录 ${INSTALL_DIR} 未自动删除,如需删除请手动 rm -rf" +#!/usr/bin/env bash +set -euo pipefail + +INSTALL_DIR="${INSTALL_DIR:-/opt/secondary-school-grade-archive}" + +cd "${INSTALL_DIR}" || exit 1 +pm2 delete grade-api grade-web 2>/dev/null || true +pm2 save --force + +echo "PM2 服务已停止。PostgreSQL 数据与 ${INSTALL_DIR}/uploads 仍保留。" +echo "如需删除源码: rm -rf ${INSTALL_DIR}" diff --git a/deploy/update.sh b/deploy/update.sh index 2eb222d..7d38c6c 100644 --- a/deploy/update.sh +++ b/deploy/update.sh @@ -1,29 +1,39 @@ -#!/usr/bin/env bash -# -# 更新已部署实例:拉取代码并重建容器 -# 版权所有 (c) 马建军 -# -set -euo pipefail - -INSTALL_DIR="${INSTALL_DIR:-/opt/secondary-school-grade-archive}" -BRANCH="${BRANCH:-main}" - -GREEN='\033[0;32m' -NC='\033[0m' -log_info() { echo -e "${GREEN}[INFO]${NC} $*"; } - -if [[ ! -d "${INSTALL_DIR}/.git" ]]; then - echo "未找到部署目录 ${INSTALL_DIR},请先运行 deploy/install.sh" - exit 1 -fi - -cd "${INSTALL_DIR}" -log_info "拉取最新代码…" -git fetch origin -git checkout "${BRANCH}" 2>/dev/null || true -git pull origin "${BRANCH}" - -log_info "重建并重启服务…" -docker compose --env-file .env up -d --build - -log_info "更新完成。访问端口见 .env 中 WEB_PORT" +#!/usr/bin/env bash +set -euo pipefail + +INSTALL_DIR="${INSTALL_DIR:-/opt/secondary-school-grade-archive}" +BRANCH="${BRANCH:-main}" + +GREEN='\033[0;32m' +NC='\033[0m' +log_info() { echo -e "${GREEN}[INFO]${NC} $*"; } + +cd "${INSTALL_DIR}" || exit 1 +find "${INSTALL_DIR}" -name "*.sh" -exec sed -i 's/\r$//' {} + + +log_info "拉取最新代码…" +git fetch origin +git checkout "${BRANCH}" 2>/dev/null || true +git pull origin "${BRANCH}" + +log_info "更新后端依赖…" +cd backend +source venv/bin/activate +pip install -r requirements.txt -q +deactivate + +log_info "重建前端…" +cd ../frontend +npm ci --silent +npm run build + +log_info "更新网关…" +cd ../deploy/pm2 +npm ci --silent + +log_info "重启 PM2…" +cd "${INSTALL_DIR}" +pm2 reload deploy/pm2/ecosystem.config.cjs --update-env || pm2 start deploy/pm2/ecosystem.config.cjs +pm2 save + +log_info "更新完成" diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 44b748f..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,55 +0,0 @@ -services: - db: - image: postgres:16-alpine - restart: unless-stopped - environment: - POSTGRES_USER: ${POSTGRES_USER:-postgres} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} - POSTGRES_DB: ${POSTGRES_DB:-student_archive} - volumes: - - pgdata:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres}"] - interval: 5s - timeout: 5s - retries: 5 - networks: - - appnet - - api: - build: ./backend - restart: unless-stopped - environment: - DATABASE_URL: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-student_archive} - SECRET_KEY: ${SECRET_KEY:-dev-secret-key-change-in-production} - CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:23566} - UPLOAD_DIR: /app/uploads - OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://host.docker.internal:11434} - OLLAMA_MODEL: ${OLLAMA_MODEL:-qwen2.5:7b} - FLUCTUATION_THRESHOLD: ${FLUCTUATION_THRESHOLD:-0.08} - volumes: - - ./uploads:/app/uploads - depends_on: - db: - condition: service_healthy - extra_hosts: - - "host.docker.internal:host-gateway" - networks: - - appnet - - web: - build: ./frontend - restart: unless-stopped - ports: - - "${WEB_PORT:-23566}:80" - depends_on: - - api - networks: - - appnet - -volumes: - pgdata: - -networks: - appnet: - driver: bridge diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index adbe352..6b334d3 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -1,235 +1,126 @@ -# Ubuntu 部署文档 +# Ubuntu PM2 部署文档 -> **中学成绩档案系统**(Secondary School Grade Archive) -> 版权所有 © 马建军 · 微信 **dekun03** · 手机 **18364911125** -> 代码仓库:[https://git.bz121.com/dekun/secondary-school-grade-archive.git](https://git.bz121.com/dekun/secondary-school-grade-archive.git) +> **中学成绩档案系统** · 版权所有 © 马建军 · 微信 **dekun03** · 手机 **18364911125** +> 仓库:[https://git.bz121.com/dekun/secondary-school-grade-archive.git](https://git.bz121.com/dekun/secondary-school-grade-archive.git) --- -## 1. 部署概述 +## 1. 部署方式 | 项目 | 说明 | |------|------| -| 目标系统 | Ubuntu 20.04 / 22.04 / 24.04(推荐 22.04+) | -| 运行用户 | **root**(一键脚本要求) | -| 安装目录 | **`/opt/secondary-school-grade-archive`** | -| 部署方式 | Docker Compose | -| 对外端口 | **`23566`**(HTTP,可在 `.env` 修改 `WEB_PORT`) | -| 反向代理 | **不包含在本项目中**,需用户自行配置 Nginx/Caddy 等 | +| 方式 | **PM2**(非 Docker) | +| 系统 | Ubuntu 20.04 / 22.04 / 24.04 | +| 用户 | **root** | +| 目录 | `/opt/secondary-school-grade-archive` | +| 端口 | **23566**(Web + API 统一入口) | +| 反向代理 | **不包含**,请自行配置 | -### 1.1 架构说明 +### 架构 ``` -浏览器 ──► :23566 (Nginx/Web) ──► API (内部) ──► PostgreSQL (内部) - │ - └──► Ollama (宿主机,可选) - └──► uploads/ (宿主机挂载) +浏览器 → :23566 (PM2: grade-web, Express 静态 + /api 反代) + └──→ 127.0.0.1:8000 (PM2: grade-api, Uvicorn) + └──→ PostgreSQL (本机) + └──→ uploads/ + └──→ Ollama (本机可选, :11434) ``` -- **Web**:Nginx 提供前端静态文件,并将 `/api` 反向代理到后端容器 -- **API**:FastAPI,不直接暴露端口到宿主机 -- **PostgreSQL**:仅 Docker 内网访问,不映射宿主机端口 -- **Ollama**:可选,运行在宿主机,容器通过 `host.docker.internal` 访问 +PM2 进程: + +| 名称 | 说明 | +|------|------| +| `grade-api` | FastAPI / Uvicorn | +| `grade-web` | 前端静态资源 + `/api` 反向代理 | --- ## 2. 环境要求 -### 2.1 硬件(建议) - -| 资源 | 最低 | 推荐 | -|------|------|------| -| CPU | 2 核 | 4 核+ | -| 内存 | 2 GB | 4 GB+(启用 OCR 建议 8 GB) | -| 磁盘 | 10 GB | 20 GB+ | - -### 2.2 软件 - -- Ubuntu Server(64 位) -- 可访问互联网(拉取 Docker 镜像与 Git 仓库) -- 防火墙放行 **23566/TCP**(若需外网访问) - -### 2.3 端口 - -| 端口 | 用途 | 是否必须对外开放 | -|------|------|------------------| -| **23566** | Web 前端 + API(经 Nginx 统一入口) | 是 | -| 11434 | Ollama(宿主机,错题 AI 解法) | 仅本机,可不对外 | -| 5432 | PostgreSQL | **否**(已关闭对外映射) | +- CPU 2 核+,内存 4 GB+(OCR 建议 8 GB) +- 磁盘 15 GB+ +- 可访问 Git 仓库与 npm / PyPI --- -## 3. 一键部署(推荐) - -以 **root** 登录 Ubuntu 服务器后执行: +## 3. 一键部署 ```bash -# 方式 A:克隆后执行(仓库已有代码时) git clone https://git.bz121.com/dekun/secondary-school-grade-archive.git /opt/secondary-school-grade-archive cd /opt/secondary-school-grade-archive chmod +x deploy/*.sh bash deploy/install.sh ``` -```bash -# 方式 B:仅下载安装脚本(仓库已推送 install.sh 后) -export WEB_PORT=23566 -export INSTALL_DIR=/opt/secondary-school-grade-archive -curl -fsSL https://git.bz121.com/dekun/secondary-school-grade-archive/raw/branch/main/deploy/install.sh -o /tmp/install.sh -chmod +x /tmp/install.sh -bash /tmp/install.sh -``` +脚本自动完成: -### 3.1 脚本自动完成的事项 +1. 检测 root、Ubuntu、端口 23566 +2. 安装 PostgreSQL、Python3、Node.js 20、PM2 +3. 克隆/更新代码 +4. 生成 `.env`(随机密钥、数据库密码) +5. 创建 PostgreSQL 用户与数据库 +6. Python 虚拟环境 + `pip install` +7. 前端 `npm ci && npm run build` +8. `pm2 start` 并设置开机自启 -1. 检测是否为 root 用户 -2. 检测 Ubuntu/Debian 系操作系统 -3. 检测内存、磁盘(不足时警告) -4. 检测 **23566** 端口是否占用 -5. 安装 `git`、`curl`、`openssl` 等基础工具 -6. 若未安装 Docker,自动安装 **Docker CE + Compose 插件** -7. 克隆/更新代码到 `/opt/secondary-school-grade-archive` -8. 自动生成 `.env`(随机 `SECRET_KEY`、数据库密码) -9. `docker compose up -d --build` 构建并启动 -10. 等待 API 健康检查通过后输出访问地址 - -### 3.2 部署成功后的访问 - -``` -http://<服务器IP>:23566 -``` - -首次使用请在页面 **注册** 账号,然后登录添加学生。 +部署成功后访问:**`http://<服务器IP>:23566`** --- ## 4. 环境变量(`.env`) -部署后配置文件位于: +| 变量 | 默认 | 说明 | +|------|------|------| +| `WEB_PORT` | 23566 | 对外 Web 端口 | +| `API_PORT` | 8000 | 内部 API 端口 | +| `DATABASE_URL` | 自动生成 | PostgreSQL 连接 | +| `SECRET_KEY` | 自动生成 | JWT 密钥 | +| `UPLOAD_DIR` | `.../uploads` | 错题图片目录 | +| `OLLAMA_BASE_URL` | `http://127.0.0.1:11434` | 本地 Ollama | -``` -/opt/secondary-school-grade-archive/.env -``` - -| 变量 | 默认值 | 说明 | -|------|--------|------| -| `WEB_PORT` | `23566` | Web 对外端口 | -| `SECRET_KEY` | 自动生成 | JWT 密钥,生产环境请勿泄露 | -| `POSTGRES_PASSWORD` | 自动生成 | 数据库密码 | -| `CORS_ORIGINS` | 含服务器 IP | 前端跨域白名单 | -| `OLLAMA_BASE_URL` | `http://host.docker.internal:11434` | Ollama 地址 | -| `OLLAMA_MODEL` | `qwen2.5:7b` | 模型名称 | -| `FLUCTUATION_THRESHOLD` | `0.08` | 成绩波动高亮阈值(8%) | - -修改 `.env` 后重启: +修改后重启: ```bash cd /opt/secondary-school-grade-archive -docker compose --env-file .env up -d -``` - -修改 `WEB_PORT` 后需重新创建容器: - -```bash -docker compose --env-file .env up -d --force-recreate web +pm2 reload deploy/pm2/ecosystem.config.cjs --update-env ``` --- -## 5. 常用运维命令 +## 5. 常用命令 ```bash cd /opt/secondary-school-grade-archive -# 查看运行状态 -docker compose ps +pm2 status # 进程状态 +pm2 logs # 全部日志 +pm2 logs grade-api # 后端日志 +pm2 logs grade-web # 网关日志 -# 查看日志 -docker compose logs -f -docker compose logs -f api -docker compose logs -f web - -# 停止服务 -docker compose down - -# 启动服务 -docker compose --env-file .env up -d - -# 更新版本(拉代码 + 重建) -bash deploy/update.sh - -# 数据备份 -bash deploy/backup.sh - -# 卸载容器(保留数据) -bash deploy/uninstall.sh - -# 卸载并删除数据库卷(危险) -REMOVE_VOLUMES=1 bash deploy/uninstall.sh +bash deploy/update.sh # 拉代码 + 重建 + 重启 +bash deploy/backup.sh # 备份数据库与 uploads +bash deploy/uninstall.sh # 停止 PM2 服务 ``` --- -## 6. Ollama 可选配置(错题 AI 解法) - -错题上传后的「整理题目 / 生成解法」依赖宿主机 Ollama。若不安装,OCR 仍可用,AI 解法需手动填写。 +## 6. Ollama(可选) ```bash -# 安装 Ollama(参考 https://ollama.com) curl -fsSL https://ollama.com/install.sh | sh - -# 拉取模型 ollama pull qwen2.5:7b - -# 确认服务运行 -curl http://127.0.0.1:11434/api/tags -``` - -确保 `.env` 中: - -``` -OLLAMA_BASE_URL=http://host.docker.internal:11434 -OLLAMA_MODEL=qwen2.5:7b ``` --- -## 7. 反向代理(用户自行配置) +## 7. 反向代理(自行配置) -**本项目不包含 HTTPS、域名、反向代理配置。** 若需通过域名访问或启用 HTTPS,请在宿主机自行配置 Nginx/Caddy/Traefik 等。 - -### 7.1 原则 - -- 反向代理将流量转发到 **`http://127.0.0.1:23566`** -- 代理需支持 **WebSocket**(若未来扩展)及 **大文件上传**(错题图片最大 10MB) -- 代理 `/api` 与 `/` 均应转发到同一 upstream(本项目 Nginx 已统一处理) - -### 7.2 Nginx 示例(仅供参考,不包含在项目中) - -```nginx -server { - listen 443 ssl http2; - server_name grade.example.com; - - ssl_certificate /path/to/fullchain.pem; - ssl_certificate_key /path/to/privkey.pem; - client_max_body_size 10M; - - location / { - proxy_pass http://127.0.0.1:23566; - 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 反向代理后,请同步修改 `.env` 中的 `CORS_ORIGINS`,加入 `https://grade.example.com`。 +将域名/HTTPS 流量转发到 **`http://127.0.0.1:23566`** 即可。 +使用 HTTPS 后请更新 `.env` 中 `CORS_ORIGINS`。 --- -## 8. 防火墙示例(UFW) +## 8. 防火墙 ```bash ufw allow 22/tcp @@ -243,62 +134,22 @@ ufw enable | 现象 | 处理 | |------|------| -| 无法访问 23566 | `docker compose ps` 确认 web 运行;`ufw status` 检查防火墙 | -| 注册/登录 502 | `docker compose logs api` 查看后端;确认 db 健康 | -| OCR 很慢/失败 | 首次运行需下载 PaddleOCR 模型;查看 api 日志 | -| AI 解法失败 | 确认宿主机 Ollama 运行;`curl host.docker.internal:11434` 从 api 容器内测试 | -| 端口被占用 | 修改 `.env` 中 `WEB_PORT` 或释放占用进程 | +| 无法访问 | `pm2 status` · `ss -tlnp \| grep 23566` | +| 502 / API 错误 | `pm2 logs grade-api` | +| 数据库连接失败 | `systemctl status postgresql` · 检查 `.env` 中 `DATABASE_URL` | +| 前端空白 | 确认 `frontend/dist` 存在 · `pm2 logs grade-web` | -进入 API 容器调试: +--- + +## 10. 自定义参数 ```bash -docker compose exec api bash +WEB_PORT=23566 INSTALL_DIR=/opt/secondary-school-grade-archive bash deploy/install.sh ``` --- -## 10. 数据备份与恢复 +## 11. 版权 -### 备份 - -```bash -bash /opt/secondary-school-grade-archive/deploy/backup.sh -``` - -生成文件位于 `backups/`: - -- `db_YYYYMMDD_HHMMSS.sql` — 数据库 -- `uploads_YYYYMMDD_HHMMSS.tar.gz` — 错题图片 - -### 恢复数据库(示例) - -```bash -cd /opt/secondary-school-grade-archive -docker compose exec -T db psql -U postgres student_archive < backups/db_XXXXXX.sql -``` - ---- - -## 11. 版权与授权 - -本软件著作权归 **马建军** 所有。部署和使用须遵守 [LICENSE](../LICENSE) 与 [COPYRIGHT.md](../COPYRIGHT.md)。 - -- 微信:**dekun03** -- 手机:**18364911125** - -未经授权不得用于商业分发或去除版权信息。 - ---- - -## 12. 自定义安装参数 - -```bash -# 自定义端口 -WEB_PORT=23566 bash deploy/install.sh - -# 自定义目录 -INSTALL_DIR=/opt/my-grade-app bash deploy/install.sh - -# 指定分支 -BRANCH=main bash deploy/install.sh -``` +见 [LICENSE](../LICENSE) · [COPYRIGHT.md](../COPYRIGHT.md) +技术支持:微信 **dekun03** · 手机 **18364911125** diff --git a/docs/USAGE.md b/docs/USAGE.md index f461852..5902042 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -16,7 +16,7 @@ - 错题库:拍照上传 → OCR 识别 → AI 生成解法(可编辑) - 成绩 CSV 导出 -部署方式见 [DEPLOY.md](./DEPLOY.md)。 +部署方式见 [DEPLOY.md](./DEPLOY.md)(PM2,端口 23566)。 --- diff --git a/frontend/Dockerfile b/frontend/Dockerfile deleted file mode 100644 index 77ceded..0000000 --- a/frontend/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM node:20-alpine AS build -WORKDIR /app -COPY package*.json ./ -RUN npm ci -COPY . . -RUN npm run build - -FROM nginx:alpine -COPY nginx.conf /etc/nginx/conf.d/default.conf -COPY --from=build /app/dist /usr/share/nginx/html -EXPOSE 80 diff --git a/frontend/nginx.conf b/frontend/nginx.conf deleted file mode 100644 index 161227f..0000000 --- a/frontend/nginx.conf +++ /dev/null @@ -1,17 +0,0 @@ -server { - listen 80; - server_name localhost; - root /usr/share/nginx/html; - index index.html; - - location /api/ { - proxy_pass http://api:8000/api/; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - client_max_body_size 10M; - } - - location / { - try_files $uri $uri/ /index.html; - } -}