Files
dekun 530a8b70a1 学生资料设置、头像与自动备份恢复。
首页卡片支持修改/删除;详情页设置 Tab 可维护学校、年级与头像;系统设置新增数据备份下载与恢复;备份默认存 /root/grade-archive-backups,详见 docs/BACKUP.md。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-28 17:56:09 +08:00

330 lines
11 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
#
# 中学成绩档案系统 — 一键部署
# 架构:主程序 + OCR(GPU/screen 同机) | Ollama(其他电脑局域网)
# 版权所有 (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}"
OCR_PORT="${OCR_PORT:-23567}"
BRANCH="${BRANCH:-main}"
PIP_MIRROR="${PIP_MIRROR:-https://pypi.tuna.tsinghua.edu.cn/simple}"
# Ollama 在其他电脑时安装前指定,例如: OLLAMA_BASE_URL=http://192.168.8.100:11434
OLLAMA_BASE_URL="${OLLAMA_BASE_URL:-}"
OLLAMA_MODEL="${OLLAMA_MODEL:-qwen2.5:7b}"
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; }
# shellcheck source=proxy.sh
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/proxy.sh"
# shellcheck source=ocr-common.sh
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/ocr-common.sh"
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() {
for p in "${WEB_PORT}" "${OCR_PORT}"; do
if command -v ss &>/dev/null && ss -tln | grep -q ":${p} "; then
if [[ "${p}" == "${OCR_PORT}" ]] && ocr_screen_running 2>/dev/null; then
log_warn "端口 ${OCR_PORT} 已被占用(可能已有 OCR Worker)"
elif [[ "${p}" == "${WEB_PORT}" ]]; then
log_error "端口 ${WEB_PORT} 已被占用"
exit 1
fi
fi
done
log_info "Web 端口 ${WEB_PORT} / OCR 端口 ${OCR_PORT} 检查完成"
}
prompt_ollama_url() {
if [[ -n "${OLLAMA_BASE_URL}" ]]; then
log_info "Ollama 地址: ${OLLAMA_BASE_URL}"
return
fi
if [[ -t 0 ]]; then
echo ""
echo "Ollama 部署在【其他电脑】上,请填写局域网地址。"
read -rp "Ollama 地址 [http://192.168.8.100:11434]: " input
OLLAMA_BASE_URL="${input:-http://192.168.8.100:11434}"
else
OLLAMA_BASE_URL="http://127.0.0.1:11434"
log_warn "未设置 OLLAMA_BASE_URL,默认 ${OLLAMA_BASE_URL}(可在 .env 或系统设置中修改)"
fi
log_info "Ollama 地址: ${OLLAMA_BASE_URL}"
}
install_base_packages() {
log_info "安装系统依赖…"
apt-get update -qq
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
git curl ca-certificates openssl screen \
python3 python3-venv python3-pip python3-dev \
build-essential libpq-dev \
postgresql postgresql-contrib \
libgl1 libglx-mesa0 libgbm1 \
libgomp1 libglib2.0-0 libsm6 libxrender1 libxext6 libxcb1 libfontconfig1
}
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$//' {} +
chmod +x "${INSTALL_DIR}/deploy/start.sh" \
"${INSTALL_DIR}/deploy/ocr-screen.sh" \
"${INSTALL_DIR}/deploy/ocr-worker/"*.sh 2>/dev/null || true
}
verify_frontend_dist() {
if [[ ! -f "${INSTALL_DIR}/frontend/dist/index.html" ]]; then
log_error "未找到 frontend/dist/index.html"
log_error "仓库应已包含预构建前端;若缺失请在开发机 npm run build 后推送"
exit 1
fi
log_info "前端静态资源已就绪"
}
env_set_or_append() {
local key="$1" val="$2" file="${INSTALL_DIR}/.env"
if grep -q "^${key}=" "${file}" 2>/dev/null; then
sed -i "s|^${key}=.*|${key}=${val}|" "${file}"
else
echo "${key}=${val}" >> "${file}"
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}"
prompt_ollama_url
if [[ ! -f "${env_file}" ]]; then
local secret pg_pass pg_user
secret=$(openssl rand -hex 32)
pg_pass=$(openssl rand -hex 16)
pg_user="gradeapp"
cat > "${env_file}" <<EOF
# generated by deploy/install.sh — $(date -Iseconds)
WEB_PORT=${WEB_PORT}
FRONTEND_DIST=${INSTALL_DIR}/frontend/dist
SECRET_KEY=${secret}
POSTGRES_USER=${pg_user}
POSTGRES_PASSWORD=${pg_pass}
POSTGRES_DB=student_archive
DATABASE_URL=postgresql://${pg_user}:${pg_pass}@127.0.0.1:5432/student_archive
UPLOAD_DIR=${INSTALL_DIR}/uploads
BACKUP_DIR=/root/grade-archive-backups
BACKUP_RETENTION_DAYS=30
AUTO_BACKUP_INTERVAL_HOURS=24
CORS_ORIGINS=http://${server_ip}:${WEB_PORT},http://127.0.0.1:${WEB_PORT},http://localhost:${WEB_PORT}
# OCR 同机 GPU Workerscreen 常驻)
OCR_SERVICE_URL=http://127.0.0.1:${OCR_PORT}
OCR_PORT=${OCR_PORT}
# Ollama 在其他电脑(局域网)
OLLAMA_BASE_URL=${OLLAMA_BASE_URL}
OLLAMA_MODEL=${OLLAMA_MODEL}
FLUCTUATION_THRESHOLD=0.08
ADMIN_DEFAULT_USERNAME=admin
ADMIN_DEFAULT_PASSWORD=admin123
EOF
chmod 600 "${env_file}"
log_info ".env 已生成(默认 admin / admin123"
else
log_info "更新 .env 中的 OCR / Ollama 配置…"
env_set_or_append "OCR_SERVICE_URL" "http://127.0.0.1:${OCR_PORT}"
env_set_or_append "OCR_PORT" "${OCR_PORT}"
env_set_or_append "OLLAMA_BASE_URL" "${OLLAMA_BASE_URL}"
env_set_or_append "OLLAMA_MODEL" "${OLLAMA_MODEL}"
fi
}
setup_postgresql() {
# shellcheck disable=SC1090
source "${INSTALL_DIR}/.env"
log_info "配置 PostgreSQL…"
systemctl enable postgresql
systemctl start postgresql
if ! sudo -u postgres psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='${POSTGRES_USER}'" | grep -q 1; then
sudo -u postgres psql -c "CREATE USER ${POSTGRES_USER} WITH PASSWORD '${POSTGRES_PASSWORD}';"
else
sudo -u postgres psql -c "ALTER USER ${POSTGRES_USER} WITH PASSWORD '${POSTGRES_PASSWORD}';"
fi
if ! sudo -u postgres psql -tAc "SELECT 1 FROM pg_database WHERE datname='${POSTGRES_DB}'" | grep -q 1; then
sudo -u postgres psql -c "CREATE DATABASE ${POSTGRES_DB} OWNER ${POSTGRES_USER};"
fi
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE ${POSTGRES_DB} TO ${POSTGRES_USER};"
}
setup_backend() {
log_info "安装主程序 Python 依赖…"
cd "${INSTALL_DIR}/backend"
if [[ ! -d venv ]]; then
python3 -m venv venv
fi
# shellcheck disable=SC1091
source venv/bin/activate
pip install --upgrade pip --progress-bar on -i "${PIP_MIRROR}"
pip install -r requirements.txt --progress-bar on -i "${PIP_MIRROR}"
deactivate
}
setup_ocr_gpu() {
if command -v nvidia-smi >/dev/null; then
log_info "检测到 NVIDIA GPUOCR 将常驻显存 (screen)"
nvidia-smi --query-gpu=name,memory.total --format=csv,noheader 2>/dev/null || true
else
log_warn "未检测到 NVIDIA GPUOCR 将使用 CPU(较慢)"
fi
if [[ -x "${INSTALL_DIR}/deploy/install-ocr-deps.sh" ]]; then
bash "${INSTALL_DIR}/deploy/install-ocr-deps.sh" || log_warn "OCR 系统库安装跳过"
fi
install_ocr_worker
start_ocr_screen
wait_ocr_healthy 30 || log_warn "OCR 后台加载中,继续安装主程序…"
}
stop_legacy_pm2() {
if command -v pm2 &>/dev/null; then
pm2 delete grade-api grade-web 2>/dev/null || true
pm2 save 2>/dev/null || true
log_info "已停止旧版 PM2 进程"
fi
}
setup_systemd() {
log_info "配置 systemd 服务 grade-archive(主程序)…"
sed "s|/opt/secondary-school-grade-archive|${INSTALL_DIR}|g" \
"${INSTALL_DIR}/deploy/grade-archive.service" > /etc/systemd/system/grade-archive.service
systemctl daemon-reload
systemctl enable grade-archive
}
start_service() {
log_info "启动主程序…"
cd "${INSTALL_DIR}"
mkdir -p uploads backups /root/grade-archive-backups
chmod +x deploy/backup.sh deploy/restore.sh 2>/dev/null || true
systemctl restart grade-archive
}
setup_backup_cron() {
log_info "配置每日自动备份(/root/grade-archive-backups)…"
cat > /etc/cron.d/grade-archive-backup <<EOF
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
0 3 * * * root INSTALL_DIR=${INSTALL_DIR} BACKUP_DIR=/root/grade-archive-backups bash ${INSTALL_DIR}/deploy/backup.sh >> /var/log/grade-archive-backup.log 2>&1
EOF
chmod 644 /etc/cron.d/grade-archive-backup
}
wait_healthy() {
local i
log_info "等待主程序就绪(最多 2 分钟)…"
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
if (( i % 5 == 0 )); then
echo -ne "\r${YELLOW}[INFO]${NC} 等待主程序… ${i}/40"
fi
sleep 3
done
echo ""
log_warn "主程序健康检查超时: journalctl -u grade-archive -f"
}
print_summary() {
# shellcheck disable=SC1090
source "${INSTALL_DIR}/.env"
local ip
ip=$(hostname -I 2>/dev/null | awk '{print $1}')
ip="${ip:-127.0.0.1}"
echo ""
echo "=========================================="
echo " 中学成绩档案 — 一键部署完成"
echo " 版权所有 (c) 马建军"
echo "=========================================="
echo " 访问: http://${ip}:${WEB_PORT}"
echo " 管理员: admin / admin123(请立即修改)"
echo ""
echo " 【同机 OCR — GPU 常驻 screen】"
echo " OCR 地址: http://127.0.0.1:${OCR_PORT}"
echo " 状态: bash ${INSTALL_DIR}/deploy/ocr-screen.sh status"
echo " 进入终端: screen -r ocr-worker (Ctrl+A D 退出)"
echo " 重启 OCR: bash ${INSTALL_DIR}/deploy/ocr-screen.sh restart"
echo ""
echo " 【Ollama — 其他电脑】"
echo " 地址: ${OLLAMA_BASE_URL}"
echo " 模型: ${OLLAMA_MODEL}"
echo " 可在「系统设置 → AI 模型」修改"
echo ""
echo " 主程序: systemctl status grade-archive"
echo " 更新: sudo bash ${INSTALL_DIR}/deploy/update.sh"
echo " 备份说明: docs/BACKUP.md"
echo " 卸载: sudo bash ${INSTALL_DIR}/deploy/uninstall.sh"
echo " 微信 dekun03 手机 18364911125"
echo "=========================================="
}
main() {
log_info "一键部署开始(主程序 + OCR/GPU/screen | Ollama 外置)"
require_root
setup_deploy_proxy
check_os
clone_or_update_repo
verify_frontend_dist
check_port
install_base_packages
generate_env
setup_postgresql
setup_backend
setup_ocr_gpu
stop_legacy_pm2
setup_systemd
setup_backup_cron
start_service
wait_healthy
print_summary
}
main "$@"