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 <cursoragent@cursor.com>
This commit is contained in:
+7
-10
@@ -1,23 +1,20 @@
|
|||||||
# 复制为 .env 后修改(一键部署脚本会自动生成)
|
# 复制为 .env 后修改(一键部署脚本会自动生成)
|
||||||
# 部署目录默认:/opt/secondary-school-grade-archive
|
# 部署目录默认:/opt/secondary-school-grade-archive
|
||||||
|
|
||||||
# Web 对外端口(默认 23566)
|
|
||||||
WEB_PORT=23566
|
WEB_PORT=23566
|
||||||
|
API_PORT=8000
|
||||||
|
API_TARGET=http://127.0.0.1:8000
|
||||||
|
|
||||||
# 生产环境务必修改
|
|
||||||
SECRET_KEY=请替换为随机字符串
|
SECRET_KEY=请替换为随机字符串
|
||||||
POSTGRES_USER=postgres
|
POSTGRES_USER=gradeapp
|
||||||
POSTGRES_PASSWORD=请替换为强密码
|
POSTGRES_PASSWORD=请替换为强密码
|
||||||
POSTGRES_DB=student_archive
|
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
|
CORS_ORIGINS=http://127.0.0.1:23566,http://localhost:23566
|
||||||
|
|
||||||
# Ollama(错题 AI 解法,可选)
|
OLLAMA_BASE_URL=http://127.0.0.1:11434
|
||||||
# Docker 容器内通过 host.docker.internal 访问宿主机 Ollama
|
|
||||||
OLLAMA_BASE_URL=http://host.docker.internal:11434
|
|
||||||
OLLAMA_MODEL=qwen2.5:7b
|
OLLAMA_MODEL=qwen2.5:7b
|
||||||
|
|
||||||
# 成绩波动高亮阈值(0.08 = 8%)
|
|
||||||
FLUCTUATION_THRESHOLD=0.08
|
FLUCTUATION_THRESHOLD=0.08
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
* text=auto
|
||||||
|
*.sh text eol=lf
|
||||||
|
*.bash text eol=lf
|
||||||
@@ -4,37 +4,22 @@ Secondary School Grade Archive — 多用户 Web 系统:成绩录入、占比
|
|||||||
|
|
||||||
**版权所有 © 马建军** · 微信 **dekun03** · 手机 **18364911125**
|
**版权所有 © 马建军** · 微信 **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/DEPLOY.md](./docs/DEPLOY.md) | **Ubuntu PM2 一键部署** |
|
||||||
| [docs/USAGE.md](./docs/USAGE.md) | **用户使用说明** |
|
| [docs/USAGE.md](./docs/USAGE.md) | 用户使用说明 |
|
||||||
| [COPYRIGHT.md](./COPYRIGHT.md) | 版权与授权 |
|
|
||||||
| [LICENSE](./LICENSE) | 许可证全文 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 功能概览
|
## Ubuntu 一键部署(PM2)
|
||||||
|
|
||||||
- 用户注册/登录,数据按账号隔离
|
|
||||||
- 学生管理(**初中 / 高中**学段、年级、班级)
|
|
||||||
- 成绩录入:周考 / 月考 / 期末(总分、得分、占比)
|
|
||||||
- 分科曲线:上升绿、下降红、大幅波动高亮
|
|
||||||
- 错题库:上传图片 → PaddleOCR → Ollama 生成解法
|
|
||||||
- 成绩 CSV 导出、备份脚本
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Ubuntu 一键部署(生产环境)
|
|
||||||
|
|
||||||
**要求:** root 用户 · Ubuntu · Docker · 目录 `/opt/secondary-school-grade-archive` · 端口 **23566**
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://git.bz121.com/dekun/secondary-school-grade-archive.git /opt/secondary-school-grade-archive
|
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
|
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)。**反向代理不包含在本项目中。**
|
||||||
|
|
||||||
详细说明、运维命令、故障排查见 **[docs/DEPLOY.md](./docs/DEPLOY.md)**。
|
|
||||||
|
|
||||||
> **反向代理(HTTPS/域名)不包含在本项目中**,请自行配置 Nginx/Caddy 等,参见部署文档第 7 节。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 本地开发
|
## 本地开发
|
||||||
|
|
||||||
### Docker Compose(默认端口 23566)
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
# PostgreSQL 本地安装后
|
||||||
docker compose --env-file .env up --build
|
cp backend/.env.example backend/.env
|
||||||
```
|
|
||||||
|
|
||||||
- 前端:http://localhost:23566
|
cd backend
|
||||||
- API 健康检查:http://localhost:23566/api/health
|
python3 -m venv venv
|
||||||
|
source venv/bin/activate # Windows: venv\Scripts\activate
|
||||||
### 分步开发
|
pip install -r requirements.txt
|
||||||
|
|
||||||
```bash
|
|
||||||
# 仅数据库
|
|
||||||
docker compose up db -d
|
|
||||||
|
|
||||||
# 后端
|
|
||||||
cd backend && pip install -r requirements.txt && cp .env.example .env
|
|
||||||
uvicorn app.main:app --reload --port 8000
|
uvicorn app.main:app --reload --port 8000
|
||||||
|
|
||||||
# 前端
|
cd frontend
|
||||||
cd frontend && npm install && npm run dev
|
npm install && npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### Ollama(错题 AI,可选)
|
开发前端:http://localhost:5173(代理 `/api` 到 8000)
|
||||||
|
|
||||||
|
生产网关本地模拟:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ollama pull qwen2.5:7b
|
cd deploy/pm2 && npm install
|
||||||
ollama serve
|
# 先 build 前端
|
||||||
|
cd ../../frontend && npm run build
|
||||||
|
WEB_PORT=23566 pm2 start ../deploy/pm2/ecosystem.config.cjs
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 运维快捷命令
|
## 运维
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /opt/secondary-school-grade-archive
|
bash deploy/update.sh # 更新
|
||||||
|
bash deploy/backup.sh # 备份
|
||||||
docker compose ps # 状态
|
bash deploy/uninstall.sh # 停止
|
||||||
bash deploy/update.sh # 更新
|
|
||||||
bash deploy/backup.sh # 备份
|
|
||||||
bash deploy/uninstall.sh # 停止服务
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 环境变量
|
|
||||||
|
|
||||||
见 [.env.example](./.env.example)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 技术支持
|
## 技术支持
|
||||||
|
|
||||||
- **作者:** 马建军
|
微信 **dekun03** · 手机 **18364911125**
|
||||||
- **微信:** dekun03
|
|
||||||
- **手机:** 18364911125
|
|
||||||
|
|
||||||
未经授权不得商业使用或去除版权信息。
|
|
||||||
|
|||||||
@@ -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
|
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
|
UPLOAD_DIR=uploads
|
||||||
|
API_PORT=8000
|
||||||
OLLAMA_BASE_URL=http://127.0.0.1:11434
|
OLLAMA_BASE_URL=http://127.0.0.1:11434
|
||||||
OLLAMA_MODEL=qwen2.5:7b
|
OLLAMA_MODEL=qwen2.5:7b
|
||||||
FLUCTUATION_THRESHOLD=0.08
|
FLUCTUATION_THRESHOLD=0.08
|
||||||
|
|||||||
@@ -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"]
|
|
||||||
@@ -9,7 +9,7 @@ class Settings(BaseSettings):
|
|||||||
ALGORITHM: str = "HS256"
|
ALGORITHM: str = "HS256"
|
||||||
UPLOAD_DIR: str = "uploads"
|
UPLOAD_DIR: str = "uploads"
|
||||||
MAX_UPLOAD_SIZE: int = 10 * 1024 * 1024
|
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"
|
OLLAMA_MODEL: str = "qwen2.5:7b"
|
||||||
FLUCTUATION_THRESHOLD: float = 0.08
|
FLUCTUATION_THRESHOLD: float = 0.08
|
||||||
CORS_ORIGINS: str = "http://localhost:5173,http://localhost:3000,http://localhost"
|
CORS_ORIGINS: str = "http://localhost:5173,http://localhost:3000,http://localhost"
|
||||||
|
|||||||
+22
-25
@@ -1,25 +1,22 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
#
|
set -euo pipefail
|
||||||
# 备份 PostgreSQL 与 uploads 目录
|
|
||||||
# 版权所有 (c) 马建军
|
INSTALL_DIR="${INSTALL_DIR:-/opt/secondary-school-grade-archive}"
|
||||||
#
|
BACKUP_DIR="${BACKUP_DIR:-${INSTALL_DIR}/backups}"
|
||||||
set -euo pipefail
|
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||||
|
|
||||||
INSTALL_DIR="${INSTALL_DIR:-/opt/secondary-school-grade-archive}"
|
cd "${INSTALL_DIR}"
|
||||||
BACKUP_DIR="${BACKUP_DIR:-${INSTALL_DIR}/backups}"
|
# shellcheck disable=SC1090
|
||||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
source .env
|
||||||
|
mkdir -p "${BACKUP_DIR}"
|
||||||
cd "${INSTALL_DIR}"
|
|
||||||
mkdir -p "${BACKUP_DIR}"
|
echo "[INFO] 备份数据库…"
|
||||||
|
PGPASSWORD="${POSTGRES_PASSWORD}" pg_dump -h 127.0.0.1 -U "${POSTGRES_USER}" "${POSTGRES_DB}" \
|
||||||
echo "[INFO] 备份数据库…"
|
> "${BACKUP_DIR}/db_${TIMESTAMP}.sql"
|
||||||
docker compose --env-file .env exec -T db \
|
|
||||||
pg_dump -U "${POSTGRES_USER:-postgres}" "${POSTGRES_DB:-student_archive}" \
|
echo "[INFO] 备份 uploads…"
|
||||||
> "${BACKUP_DIR}/db_${TIMESTAMP}.sql"
|
tar -czf "${BACKUP_DIR}/uploads_${TIMESTAMP}.tar.gz" -C "${INSTALL_DIR}" uploads/
|
||||||
|
|
||||||
echo "[INFO] 备份 uploads…"
|
echo "[INFO] 完成:"
|
||||||
tar -czf "${BACKUP_DIR}/uploads_${TIMESTAMP}.tar.gz" -C "${INSTALL_DIR}" uploads/
|
echo " ${BACKUP_DIR}/db_${TIMESTAMP}.sql"
|
||||||
|
echo " ${BACKUP_DIR}/uploads_${TIMESTAMP}.tar.gz"
|
||||||
echo "[INFO] 备份完成:"
|
|
||||||
echo " ${BACKUP_DIR}/db_${TIMESTAMP}.sql"
|
|
||||||
echo " ${BACKUP_DIR}/uploads_${TIMESTAMP}.tar.gz"
|
|
||||||
|
|||||||
+235
-231
@@ -1,231 +1,235 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
#
|
#
|
||||||
# 中学成绩档案系统 — Ubuntu 一键部署脚本
|
# 中学成绩档案系统 — Ubuntu PM2 一键部署
|
||||||
# 版权所有 (c) 马建军 微信: dekun03 手机: 18364911125
|
# 版权所有 (c) 马建军 微信: dekun03 手机: 18364911125
|
||||||
#
|
#
|
||||||
# 用法(root):
|
set -euo pipefail
|
||||||
# curl -fsSL .../deploy/install.sh | bash
|
|
||||||
# 或
|
REPO_URL="${REPO_URL:-https://git.bz121.com/dekun/secondary-school-grade-archive.git}"
|
||||||
# bash deploy/install.sh
|
INSTALL_DIR="${INSTALL_DIR:-/opt/secondary-school-grade-archive}"
|
||||||
#
|
WEB_PORT="${WEB_PORT:-23566}"
|
||||||
set -euo pipefail
|
API_PORT="${API_PORT:-8000}"
|
||||||
|
BRANCH="${BRANCH:-main}"
|
||||||
REPO_URL="${REPO_URL:-https://git.bz121.com/dekun/secondary-school-grade-archive.git}"
|
NODE_MAJOR="${NODE_MAJOR:-20}"
|
||||||
INSTALL_DIR="${INSTALL_DIR:-/opt/secondary-school-grade-archive}"
|
|
||||||
WEB_PORT="${WEB_PORT:-23566}"
|
RED='\033[0;31m'
|
||||||
BRANCH="${BRANCH:-main}"
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
RED='\033[0;31m'
|
NC='\033[0m'
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||||
NC='\033[0m'
|
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||||
|
log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||||
log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
|
||||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
require_root() {
|
||||||
log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
|
||||||
|
log_error "请使用 root 用户运行: sudo bash deploy/install.sh"
|
||||||
require_root() {
|
exit 1
|
||||||
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
|
fi
|
||||||
log_error "请使用 root 用户运行本脚本,例如: sudo bash deploy/install.sh"
|
}
|
||||||
exit 1
|
|
||||||
fi
|
check_os() {
|
||||||
}
|
if [[ ! -f /etc/os-release ]]; then
|
||||||
|
log_error "无法识别操作系统"
|
||||||
check_os() {
|
exit 1
|
||||||
if [[ ! -f /etc/os-release ]]; then
|
fi
|
||||||
log_error "无法识别操作系统,本脚本仅支持 Ubuntu/Debian 系 Linux"
|
# shellcheck source=/dev/null
|
||||||
exit 1
|
source /etc/os-release
|
||||||
fi
|
log_info "检测到系统: ${NAME:-Unknown} ${VERSION_ID:-}"
|
||||||
# shellcheck source=/dev/null
|
}
|
||||||
source /etc/os-release
|
|
||||||
log_info "检测到系统: ${NAME:-Unknown} ${VERSION_ID:-}"
|
check_port() {
|
||||||
case "${ID:-}" in
|
if command -v ss &>/dev/null && ss -tln | grep -q ":${WEB_PORT} "; then
|
||||||
ubuntu|debian)
|
log_error "端口 ${WEB_PORT} 已被占用"
|
||||||
;;
|
exit 1
|
||||||
*)
|
fi
|
||||||
log_warn "当前系统为 ${ID:-unknown},未经完整测试,继续安装…"
|
log_info "端口 ${WEB_PORT} 可用"
|
||||||
;;
|
}
|
||||||
esac
|
|
||||||
}
|
install_base_packages() {
|
||||||
|
log_info "安装系统依赖…"
|
||||||
check_resources() {
|
apt-get update -qq
|
||||||
local mem_kb disk_kb
|
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
|
||||||
mem_kb=$(grep MemTotal /proc/meminfo | awk '{print $2}')
|
git curl ca-certificates openssl \
|
||||||
disk_kb=$(df -k "${INSTALL_DIR%/*}" 2>/dev/null | tail -1 | awk '{print $4}')
|
python3 python3-venv python3-pip python3-dev \
|
||||||
if [[ "${mem_kb:-0}" -lt 1800000 ]]; then
|
build-essential libpq-dev \
|
||||||
log_warn "内存不足 2GB,PaddleOCR 首次运行可能较慢"
|
postgresql postgresql-contrib \
|
||||||
fi
|
libgomp1 libglib2.0-0 libsm6 libxrender1 libxext6
|
||||||
if [[ "${disk_kb:-0}" -lt 5242880 ]]; then
|
}
|
||||||
log_warn "可用磁盘空间不足 5GB,请确保有足够空间存放镜像与 OCR 模型"
|
|
||||||
fi
|
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…"
|
||||||
check_port() {
|
curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash -
|
||||||
if command -v ss &>/dev/null; then
|
apt-get install -y -qq nodejs
|
||||||
if ss -tln | grep -q ":${WEB_PORT} "; then
|
fi
|
||||||
log_error "端口 ${WEB_PORT} 已被占用,请修改 WEB_PORT 环境变量后重试"
|
log_info "Node: $(node -v) npm: $(npm -v)"
|
||||||
exit 1
|
|
||||||
fi
|
if ! command -v pm2 &>/dev/null; then
|
||||||
elif command -v netstat &>/dev/null; then
|
log_info "安装 PM2…"
|
||||||
if netstat -tln | grep -q ":${WEB_PORT} "; then
|
npm install -g pm2
|
||||||
log_error "端口 ${WEB_PORT} 已被占用"
|
fi
|
||||||
exit 1
|
log_info "PM2: $(pm2 -v)"
|
||||||
fi
|
}
|
||||||
fi
|
|
||||||
log_info "端口 ${WEB_PORT} 可用"
|
clone_or_update_repo() {
|
||||||
}
|
if [[ -d "${INSTALL_DIR}/.git" ]]; then
|
||||||
|
log_info "更新代码: ${INSTALL_DIR}"
|
||||||
install_packages() {
|
git -C "${INSTALL_DIR}" fetch origin
|
||||||
log_info "安装基础依赖…"
|
git -C "${INSTALL_DIR}" checkout "${BRANCH}" 2>/dev/null || true
|
||||||
apt-get update -qq
|
git -C "${INSTALL_DIR}" pull origin "${BRANCH}"
|
||||||
apt-get install -y -qq git curl ca-certificates openssl
|
else
|
||||||
}
|
log_info "克隆仓库到 ${INSTALL_DIR}"
|
||||||
|
mkdir -p "$(dirname "${INSTALL_DIR}")"
|
||||||
install_docker() {
|
git clone --branch "${BRANCH}" "${REPO_URL}" "${INSTALL_DIR}" || git clone "${REPO_URL}" "${INSTALL_DIR}"
|
||||||
if command -v docker &>/dev/null; then
|
fi
|
||||||
log_info "Docker 已安装: $(docker --version)"
|
find "${INSTALL_DIR}" -name "*.sh" -exec sed -i 's/\r$//' {} +
|
||||||
else
|
}
|
||||||
log_info "正在安装 Docker…"
|
|
||||||
apt-get install -y -qq apt-transport-https gnupg lsb-release
|
generate_env() {
|
||||||
install -m 0755 -d /etc/apt/keyrings
|
local env_file="${INSTALL_DIR}/.env"
|
||||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
|
local server_ip
|
||||||
chmod a+r /etc/apt/keyrings/docker.asc
|
server_ip=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||||
echo \
|
server_ip="${server_ip:-127.0.0.1}"
|
||||||
"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" \
|
if [[ -f "${env_file}" ]]; then
|
||||||
> /etc/apt/sources.list.d/docker.list
|
log_info "保留已有 .env"
|
||||||
apt-get update -qq
|
# shellcheck disable=SC1090
|
||||||
apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
set -a && source "${env_file}" && set +a
|
||||||
systemctl enable docker
|
return
|
||||||
systemctl start docker
|
fi
|
||||||
log_info "Docker 安装完成"
|
|
||||||
fi
|
local secret pg_pass pg_user
|
||||||
|
secret=$(openssl rand -hex 32)
|
||||||
if ! docker compose version &>/dev/null; then
|
pg_pass=$(openssl rand -hex 16)
|
||||||
log_error "未检测到 docker compose 插件,请手动安装 docker-compose-plugin"
|
pg_user="gradeapp"
|
||||||
exit 1
|
|
||||||
fi
|
cat > "${env_file}" <<EOF
|
||||||
log_info "Docker Compose: $(docker compose version)"
|
# generated by deploy/install.sh — $(date -Iseconds)
|
||||||
}
|
WEB_PORT=${WEB_PORT}
|
||||||
|
API_PORT=${API_PORT}
|
||||||
clone_or_update_repo() {
|
API_TARGET=http://127.0.0.1:${API_PORT}
|
||||||
if [[ -d "${INSTALL_DIR}/.git" ]]; then
|
SECRET_KEY=${secret}
|
||||||
log_info "更新代码: ${INSTALL_DIR}"
|
POSTGRES_USER=${pg_user}
|
||||||
git -C "${INSTALL_DIR}" fetch origin
|
POSTGRES_PASSWORD=${pg_pass}
|
||||||
git -C "${INSTALL_DIR}" checkout "${BRANCH}" 2>/dev/null || true
|
POSTGRES_DB=student_archive
|
||||||
git -C "${INSTALL_DIR}" pull --ff-only origin "${BRANCH}" || git -C "${INSTALL_DIR}" pull origin "${BRANCH}"
|
DATABASE_URL=postgresql://${pg_user}:${pg_pass}@127.0.0.1:5432/student_archive
|
||||||
elif [[ -d "${INSTALL_DIR}" ]]; then
|
UPLOAD_DIR=${INSTALL_DIR}/uploads
|
||||||
log_error "目录 ${INSTALL_DIR} 已存在但不是 git 仓库,请备份后删除或修改 INSTALL_DIR"
|
CORS_ORIGINS=http://${server_ip}:${WEB_PORT},http://127.0.0.1:${WEB_PORT},http://localhost:${WEB_PORT}
|
||||||
exit 1
|
OLLAMA_BASE_URL=http://127.0.0.1:11434
|
||||||
else
|
OLLAMA_MODEL=qwen2.5:7b
|
||||||
log_info "克隆仓库到 ${INSTALL_DIR}"
|
FLUCTUATION_THRESHOLD=0.08
|
||||||
mkdir -p "$(dirname "${INSTALL_DIR}")"
|
EOF
|
||||||
git clone --branch "${BRANCH}" "${REPO_URL}" "${INSTALL_DIR}" || git clone "${REPO_URL}" "${INSTALL_DIR}"
|
chmod 600 "${env_file}"
|
||||||
fi
|
log_info ".env 已生成"
|
||||||
}
|
}
|
||||||
|
|
||||||
generate_env() {
|
setup_postgresql() {
|
||||||
local env_file="${INSTALL_DIR}/.env"
|
# shellcheck disable=SC1090
|
||||||
local server_ip
|
source "${INSTALL_DIR}/.env"
|
||||||
server_ip=$(hostname -I 2>/dev/null | awk '{print $1}')
|
log_info "配置 PostgreSQL…"
|
||||||
server_ip="${server_ip:-127.0.0.1}"
|
systemctl enable postgresql
|
||||||
|
systemctl start postgresql
|
||||||
if [[ -f "${env_file}" ]]; then
|
|
||||||
log_info "保留已有 .env 配置"
|
if ! sudo -u postgres psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='${POSTGRES_USER}'" | grep -q 1; then
|
||||||
# shellcheck source=/dev/null
|
sudo -u postgres psql -c "CREATE USER ${POSTGRES_USER} WITH PASSWORD '${POSTGRES_PASSWORD}';"
|
||||||
source "${env_file}"
|
else
|
||||||
WEB_PORT="${WEB_PORT:-23566}"
|
sudo -u postgres psql -c "ALTER USER ${POSTGRES_USER} WITH PASSWORD '${POSTGRES_PASSWORD}';"
|
||||||
return
|
fi
|
||||||
fi
|
|
||||||
|
if ! sudo -u postgres psql -tAc "SELECT 1 FROM pg_database WHERE datname='${POSTGRES_DB}'" | grep -q 1; then
|
||||||
log_info "生成 .env 配置文件"
|
sudo -u postgres psql -c "CREATE DATABASE ${POSTGRES_DB} OWNER ${POSTGRES_USER};"
|
||||||
local secret pg_pass
|
fi
|
||||||
secret=$(openssl rand -hex 32)
|
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE ${POSTGRES_DB} TO ${POSTGRES_USER};"
|
||||||
pg_pass=$(openssl rand -hex 16)
|
}
|
||||||
|
|
||||||
cat > "${env_file}" <<EOF
|
setup_backend() {
|
||||||
# 由 deploy/install.sh 自动生成 — $(date -Iseconds)
|
log_info "安装 Python 依赖…"
|
||||||
WEB_PORT=${WEB_PORT}
|
cd "${INSTALL_DIR}/backend"
|
||||||
SECRET_KEY=${secret}
|
python3 -m venv venv
|
||||||
POSTGRES_USER=postgres
|
# shellcheck disable=SC1091
|
||||||
POSTGRES_PASSWORD=${pg_pass}
|
source venv/bin/activate
|
||||||
POSTGRES_DB=student_archive
|
pip install --upgrade pip -q
|
||||||
CORS_ORIGINS=http://${server_ip}:${WEB_PORT},http://127.0.0.1:${WEB_PORT},http://localhost:${WEB_PORT}
|
pip install -r requirements.txt -q
|
||||||
OLLAMA_BASE_URL=http://host.docker.internal:11434
|
deactivate
|
||||||
OLLAMA_MODEL=qwen2.5:7b
|
}
|
||||||
FLUCTUATION_THRESHOLD=0.08
|
|
||||||
EOF
|
setup_frontend() {
|
||||||
chmod 600 "${env_file}"
|
log_info "构建前端…"
|
||||||
log_info ".env 已写入 ${env_file}"
|
cd "${INSTALL_DIR}/frontend"
|
||||||
}
|
npm ci --silent
|
||||||
|
npm run build
|
||||||
start_services() {
|
}
|
||||||
log_info "构建并启动 Docker 服务(首次可能需 10–30 分钟)…"
|
|
||||||
cd "${INSTALL_DIR}"
|
setup_gateway() {
|
||||||
mkdir -p uploads backups
|
log_info "安装 Web 网关依赖…"
|
||||||
docker compose --env-file .env pull 2>/dev/null || true
|
cd "${INSTALL_DIR}/deploy/pm2"
|
||||||
docker compose --env-file .env up -d --build
|
npm ci --silent
|
||||||
}
|
}
|
||||||
|
|
||||||
wait_healthy() {
|
start_pm2() {
|
||||||
local i max=60
|
log_info "启动 PM2 服务…"
|
||||||
log_info "等待 API 就绪…"
|
cd "${INSTALL_DIR}"
|
||||||
for ((i=1; i<=max; i++)); do
|
mkdir -p uploads backups
|
||||||
if docker compose --env-file "${INSTALL_DIR}/.env" exec -T api \
|
pm2 delete grade-api grade-web 2>/dev/null || true
|
||||||
python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/api/health')" &>/dev/null; then
|
pm2 start deploy/pm2/ecosystem.config.cjs
|
||||||
log_info "API 健康检查通过"
|
pm2 save
|
||||||
return 0
|
env PATH="$PATH" pm2 startup systemd -u root --hp /root >/tmp/pm2-startup.sh 2>/dev/null || true
|
||||||
fi
|
if [[ -f /tmp/pm2-startup.sh ]]; then
|
||||||
sleep 3
|
bash /tmp/pm2-startup.sh || true
|
||||||
done
|
fi
|
||||||
log_warn "API 启动超时,请执行: cd ${INSTALL_DIR} && docker compose logs api"
|
}
|
||||||
}
|
|
||||||
|
wait_healthy() {
|
||||||
print_summary() {
|
local i
|
||||||
local ip
|
for i in $(seq 1 40); do
|
||||||
ip=$(hostname -I 2>/dev/null | awk '{print $1}')
|
if curl -sf "http://127.0.0.1:${WEB_PORT}/api/health" >/dev/null; then
|
||||||
ip="${ip:-127.0.0.1}"
|
log_info "健康检查通过"
|
||||||
# shellcheck source=/dev/null
|
return 0
|
||||||
source "${INSTALL_DIR}/.env"
|
fi
|
||||||
|
sleep 3
|
||||||
echo ""
|
done
|
||||||
echo "=========================================="
|
log_warn "健康检查超时,请查看: pm2 logs"
|
||||||
echo " 中学成绩档案系统 部署完成"
|
}
|
||||||
echo " 版权所有 (c) 马建军"
|
|
||||||
echo "=========================================="
|
print_summary() {
|
||||||
echo ""
|
local ip
|
||||||
echo " 访问地址: http://${ip}:${WEB_PORT}"
|
ip=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||||
echo " 本地访问: http://127.0.0.1:${WEB_PORT}"
|
ip="${ip:-127.0.0.1}"
|
||||||
echo " 安装目录: ${INSTALL_DIR}"
|
echo ""
|
||||||
echo ""
|
echo "=========================================="
|
||||||
echo " 常用命令:"
|
echo " 中学成绩档案系统 PM2 部署完成"
|
||||||
echo " 查看状态: cd ${INSTALL_DIR} && docker compose ps"
|
echo " 版权所有 (c) 马建军"
|
||||||
echo " 查看日志: cd ${INSTALL_DIR} && docker compose logs -f"
|
echo "=========================================="
|
||||||
echo " 停止服务: cd ${INSTALL_DIR} && docker compose down"
|
echo " 访问: http://${ip}:${WEB_PORT}"
|
||||||
echo " 更新版本: bash ${INSTALL_DIR}/deploy/update.sh"
|
echo " 目录: ${INSTALL_DIR}"
|
||||||
echo " 数据备份: bash ${INSTALL_DIR}/deploy/backup.sh"
|
echo ""
|
||||||
echo ""
|
echo " pm2 status"
|
||||||
echo " 反向代理(Nginx/Caddy 等)请自行配置,本项目不包含。"
|
echo " pm2 logs"
|
||||||
echo " 详见文档: ${INSTALL_DIR}/docs/DEPLOY.md"
|
echo " bash ${INSTALL_DIR}/deploy/update.sh"
|
||||||
echo ""
|
echo " bash ${INSTALL_DIR}/deploy/backup.sh"
|
||||||
echo " 技术支持: 微信 dekun03 手机 18364911125"
|
echo ""
|
||||||
echo "=========================================="
|
echo " 反向代理请自行配置,本项目不包含"
|
||||||
}
|
echo " 微信 dekun03 手机 18364911125"
|
||||||
|
echo "=========================================="
|
||||||
main() {
|
}
|
||||||
echo ""
|
|
||||||
log_info "中学成绩档案系统 — 一键部署开始"
|
main() {
|
||||||
require_root
|
log_info "PM2 一键部署开始"
|
||||||
check_os
|
require_root
|
||||||
check_resources
|
check_os
|
||||||
check_port
|
check_port
|
||||||
install_packages
|
install_base_packages
|
||||||
install_docker
|
install_node_pm2
|
||||||
clone_or_update_repo
|
clone_or_update_repo
|
||||||
generate_env
|
generate_env
|
||||||
start_services
|
setup_postgresql
|
||||||
wait_healthy
|
setup_backend
|
||||||
print_summary
|
setup_frontend
|
||||||
}
|
setup_gateway
|
||||||
|
start_pm2
|
||||||
main "$@"
|
wait_healthy
|
||||||
|
print_summary
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
node_modules/
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
Generated
+1052
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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}`)
|
||||||
|
})
|
||||||
+11
-28
@@ -1,28 +1,11 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
#
|
set -euo pipefail
|
||||||
# 停止并移除容器(默认保留数据卷与 uploads)
|
|
||||||
# 版权所有 (c) 马建军
|
INSTALL_DIR="${INSTALL_DIR:-/opt/secondary-school-grade-archive}"
|
||||||
#
|
|
||||||
set -euo pipefail
|
cd "${INSTALL_DIR}" || exit 1
|
||||||
|
pm2 delete grade-api grade-web 2>/dev/null || true
|
||||||
INSTALL_DIR="${INSTALL_DIR:-/opt/secondary-school-grade-archive}"
|
pm2 save --force
|
||||||
REMOVE_VOLUMES="${REMOVE_VOLUMES:-0}"
|
|
||||||
|
echo "PM2 服务已停止。PostgreSQL 数据与 ${INSTALL_DIR}/uploads 仍保留。"
|
||||||
cd "${INSTALL_DIR}" || exit 1
|
echo "如需删除源码: rm -rf ${INSTALL_DIR}"
|
||||||
|
|
||||||
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"
|
|
||||||
|
|||||||
+39
-29
@@ -1,29 +1,39 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
#
|
set -euo pipefail
|
||||||
# 更新已部署实例:拉取代码并重建容器
|
|
||||||
# 版权所有 (c) 马建军
|
INSTALL_DIR="${INSTALL_DIR:-/opt/secondary-school-grade-archive}"
|
||||||
#
|
BRANCH="${BRANCH:-main}"
|
||||||
set -euo pipefail
|
|
||||||
|
GREEN='\033[0;32m'
|
||||||
INSTALL_DIR="${INSTALL_DIR:-/opt/secondary-school-grade-archive}"
|
NC='\033[0m'
|
||||||
BRANCH="${BRANCH:-main}"
|
log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||||
|
|
||||||
GREEN='\033[0;32m'
|
cd "${INSTALL_DIR}" || exit 1
|
||||||
NC='\033[0m'
|
find "${INSTALL_DIR}" -name "*.sh" -exec sed -i 's/\r$//' {} +
|
||||||
log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
|
||||||
|
log_info "拉取最新代码…"
|
||||||
if [[ ! -d "${INSTALL_DIR}/.git" ]]; then
|
git fetch origin
|
||||||
echo "未找到部署目录 ${INSTALL_DIR},请先运行 deploy/install.sh"
|
git checkout "${BRANCH}" 2>/dev/null || true
|
||||||
exit 1
|
git pull origin "${BRANCH}"
|
||||||
fi
|
|
||||||
|
log_info "更新后端依赖…"
|
||||||
cd "${INSTALL_DIR}"
|
cd backend
|
||||||
log_info "拉取最新代码…"
|
source venv/bin/activate
|
||||||
git fetch origin
|
pip install -r requirements.txt -q
|
||||||
git checkout "${BRANCH}" 2>/dev/null || true
|
deactivate
|
||||||
git pull origin "${BRANCH}"
|
|
||||||
|
log_info "重建前端…"
|
||||||
log_info "重建并重启服务…"
|
cd ../frontend
|
||||||
docker compose --env-file .env up -d --build
|
npm ci --silent
|
||||||
|
npm run build
|
||||||
log_info "更新完成。访问端口见 .env 中 WEB_PORT"
|
|
||||||
|
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 "更新完成"
|
||||||
|
|||||||
@@ -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
|
|
||||||
+70
-219
@@ -1,235 +1,126 @@
|
|||||||
# Ubuntu 部署文档
|
# Ubuntu PM2 部署文档
|
||||||
|
|
||||||
> **中学成绩档案系统**(Secondary School Grade Archive)
|
> **中学成绩档案系统** · 版权所有 © 马建军 · 微信 **dekun03** · 手机 **18364911125**
|
||||||
> 版权所有 © 马建军 · 微信 **dekun03** · 手机 **18364911125**
|
> 仓库:[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)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. 部署概述
|
## 1. 部署方式
|
||||||
|
|
||||||
| 项目 | 说明 |
|
| 项目 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 目标系统 | Ubuntu 20.04 / 22.04 / 24.04(推荐 22.04+) |
|
| 方式 | **PM2**(非 Docker) |
|
||||||
| 运行用户 | **root**(一键脚本要求) |
|
| 系统 | Ubuntu 20.04 / 22.04 / 24.04 |
|
||||||
| 安装目录 | **`/opt/secondary-school-grade-archive`** |
|
| 用户 | **root** |
|
||||||
| 部署方式 | Docker Compose |
|
| 目录 | `/opt/secondary-school-grade-archive` |
|
||||||
| 对外端口 | **`23566`**(HTTP,可在 `.env` 修改 `WEB_PORT`) |
|
| 端口 | **23566**(Web + API 统一入口) |
|
||||||
| 反向代理 | **不包含在本项目中**,需用户自行配置 Nginx/Caddy 等 |
|
| 反向代理 | **不包含**,请自行配置 |
|
||||||
|
|
||||||
### 1.1 架构说明
|
### 架构
|
||||||
|
|
||||||
```
|
```
|
||||||
浏览器 ──► :23566 (Nginx/Web) ──► API (内部) ──► PostgreSQL (内部)
|
浏览器 → :23566 (PM2: grade-web, Express 静态 + /api 反代)
|
||||||
│
|
└──→ 127.0.0.1:8000 (PM2: grade-api, Uvicorn)
|
||||||
└──► Ollama (宿主机,可选)
|
└──→ PostgreSQL (本机)
|
||||||
└──► uploads/ (宿主机挂载)
|
└──→ uploads/
|
||||||
|
└──→ Ollama (本机可选, :11434)
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Web**:Nginx 提供前端静态文件,并将 `/api` 反向代理到后端容器
|
PM2 进程:
|
||||||
- **API**:FastAPI,不直接暴露端口到宿主机
|
|
||||||
- **PostgreSQL**:仅 Docker 内网访问,不映射宿主机端口
|
| 名称 | 说明 |
|
||||||
- **Ollama**:可选,运行在宿主机,容器通过 `host.docker.internal` 访问
|
|------|------|
|
||||||
|
| `grade-api` | FastAPI / Uvicorn |
|
||||||
|
| `grade-web` | 前端静态资源 + `/api` 反向代理 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. 环境要求
|
## 2. 环境要求
|
||||||
|
|
||||||
### 2.1 硬件(建议)
|
- CPU 2 核+,内存 4 GB+(OCR 建议 8 GB)
|
||||||
|
- 磁盘 15 GB+
|
||||||
| 资源 | 最低 | 推荐 |
|
- 可访问 Git 仓库与 npm / PyPI
|
||||||
|------|------|------|
|
|
||||||
| 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 | **否**(已关闭对外映射) |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. 一键部署(推荐)
|
## 3. 一键部署
|
||||||
|
|
||||||
以 **root** 登录 Ubuntu 服务器后执行:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 方式 A:克隆后执行(仓库已有代码时)
|
|
||||||
git clone https://git.bz121.com/dekun/secondary-school-grade-archive.git /opt/secondary-school-grade-archive
|
git clone https://git.bz121.com/dekun/secondary-school-grade-archive.git /opt/secondary-school-grade-archive
|
||||||
cd /opt/secondary-school-grade-archive
|
cd /opt/secondary-school-grade-archive
|
||||||
chmod +x deploy/*.sh
|
chmod +x deploy/*.sh
|
||||||
bash deploy/install.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 用户
|
部署成功后访问:**`http://<服务器IP>:23566`**
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
首次使用请在页面 **注册** 账号,然后登录添加学生。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. 环境变量(`.env`)
|
## 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
|
```bash
|
||||||
cd /opt/secondary-school-grade-archive
|
cd /opt/secondary-school-grade-archive
|
||||||
docker compose --env-file .env up -d
|
pm2 reload deploy/pm2/ecosystem.config.cjs --update-env
|
||||||
```
|
|
||||||
|
|
||||||
修改 `WEB_PORT` 后需重新创建容器:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose --env-file .env up -d --force-recreate web
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. 常用运维命令
|
## 5. 常用命令
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /opt/secondary-school-grade-archive
|
cd /opt/secondary-school-grade-archive
|
||||||
|
|
||||||
# 查看运行状态
|
pm2 status # 进程状态
|
||||||
docker compose ps
|
pm2 logs # 全部日志
|
||||||
|
pm2 logs grade-api # 后端日志
|
||||||
|
pm2 logs grade-web # 网关日志
|
||||||
|
|
||||||
# 查看日志
|
bash deploy/update.sh # 拉代码 + 重建 + 重启
|
||||||
docker compose logs -f
|
bash deploy/backup.sh # 备份数据库与 uploads
|
||||||
docker compose logs -f api
|
bash deploy/uninstall.sh # 停止 PM2 服务
|
||||||
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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Ollama 可选配置(错题 AI 解法)
|
## 6. Ollama(可选)
|
||||||
|
|
||||||
错题上传后的「整理题目 / 生成解法」依赖宿主机 Ollama。若不安装,OCR 仍可用,AI 解法需手动填写。
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 安装 Ollama(参考 https://ollama.com)
|
|
||||||
curl -fsSL https://ollama.com/install.sh | sh
|
curl -fsSL https://ollama.com/install.sh | sh
|
||||||
|
|
||||||
# 拉取模型
|
|
||||||
ollama pull qwen2.5:7b
|
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 等。
|
将域名/HTTPS 流量转发到 **`http://127.0.0.1:23566`** 即可。
|
||||||
|
使用 HTTPS 后请更新 `.env` 中 `CORS_ORIGINS`。
|
||||||
### 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`。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. 防火墙示例(UFW)
|
## 8. 防火墙
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ufw allow 22/tcp
|
ufw allow 22/tcp
|
||||||
@@ -243,62 +134,22 @@ ufw enable
|
|||||||
|
|
||||||
| 现象 | 处理 |
|
| 现象 | 处理 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 无法访问 23566 | `docker compose ps` 确认 web 运行;`ufw status` 检查防火墙 |
|
| 无法访问 | `pm2 status` · `ss -tlnp \| grep 23566` |
|
||||||
| 注册/登录 502 | `docker compose logs api` 查看后端;确认 db 健康 |
|
| 502 / API 错误 | `pm2 logs grade-api` |
|
||||||
| OCR 很慢/失败 | 首次运行需下载 PaddleOCR 模型;查看 api 日志 |
|
| 数据库连接失败 | `systemctl status postgresql` · 检查 `.env` 中 `DATABASE_URL` |
|
||||||
| AI 解法失败 | 确认宿主机 Ollama 运行;`curl host.docker.internal:11434` 从 api 容器内测试 |
|
| 前端空白 | 确认 `frontend/dist` 存在 · `pm2 logs grade-web` |
|
||||||
| 端口被占用 | 修改 `.env` 中 `WEB_PORT` 或释放占用进程 |
|
|
||||||
|
|
||||||
进入 API 容器调试:
|
---
|
||||||
|
|
||||||
|
## 10. 自定义参数
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose exec api bash
|
WEB_PORT=23566 INSTALL_DIR=/opt/secondary-school-grade-archive bash deploy/install.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. 数据备份与恢复
|
## 11. 版权
|
||||||
|
|
||||||
### 备份
|
见 [LICENSE](../LICENSE) · [COPYRIGHT.md](../COPYRIGHT.md)
|
||||||
|
技术支持:微信 **dekun03** · 手机 **18364911125**
|
||||||
```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
|
|
||||||
```
|
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@
|
|||||||
- 错题库:拍照上传 → OCR 识别 → AI 生成解法(可编辑)
|
- 错题库:拍照上传 → OCR 识别 → AI 生成解法(可编辑)
|
||||||
- 成绩 CSV 导出
|
- 成绩 CSV 导出
|
||||||
|
|
||||||
部署方式见 [DEPLOY.md](./DEPLOY.md)。
|
部署方式见 [DEPLOY.md](./DEPLOY.md)(PM2,端口 23566)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user