e329d3398a
Add FastAPI/React app with Docker deployment, Ubuntu one-click install, and docs for junior/senior high score tracking and mistake bank. Co-authored-by: Cursor <cursoragent@cursor.com>
232 lines
7.3 KiB
Bash
232 lines
7.3 KiB
Bash
#!/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}" <<EOF
|
||
# 由 deploy/install.sh 自动生成 — $(date -Iseconds)
|
||
WEB_PORT=${WEB_PORT}
|
||
SECRET_KEY=${secret}
|
||
POSTGRES_USER=postgres
|
||
POSTGRES_PASSWORD=${pg_pass}
|
||
POSTGRES_DB=student_archive
|
||
CORS_ORIGINS=http://${server_ip}:${WEB_PORT},http://127.0.0.1:${WEB_PORT},http://localhost:${WEB_PORT}
|
||
OLLAMA_BASE_URL=http://host.docker.internal:11434
|
||
OLLAMA_MODEL=qwen2.5:7b
|
||
FLUCTUATION_THRESHOLD=0.08
|
||
EOF
|
||
chmod 600 "${env_file}"
|
||
log_info ".env 已写入 ${env_file}"
|
||
}
|
||
|
||
start_services() {
|
||
log_info "构建并启动 Docker 服务(首次可能需 10–30 分钟)…"
|
||
cd "${INSTALL_DIR}"
|
||
mkdir -p uploads backups
|
||
docker compose --env-file .env pull 2>/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 "$@"
|