From 13badac2dc26db6239938868d8f2c6f06f06ab43 Mon Sep 17 00:00:00 2001 From: chenqi Date: Sat, 28 Feb 2026 15:26:09 +0800 Subject: [PATCH] feat: add Spug auto-deployment scripts for fullstack project --- spug/QUICKSTART.md | 291 +++++++++++++++++++++++++ spug/README.md | 256 ++++++++++++++++++++++ spug/deploy.py | 389 ++++++++++++++++++++++++++++++++++ spug/deploy.sh | 284 +++++++++++++++++++++++++ spug/hospital-backend.service | 40 ++++ 5 files changed, 1260 insertions(+) create mode 100644 spug/QUICKSTART.md create mode 100644 spug/README.md create mode 100644 spug/deploy.py create mode 100644 spug/deploy.sh create mode 100644 spug/hospital-backend.service diff --git a/spug/QUICKSTART.md b/spug/QUICKSTART.md new file mode 100644 index 0000000..5e58659 --- /dev/null +++ b/spug/QUICKSTART.md @@ -0,0 +1,291 @@ +# Spug 部署快速参考 + +## 🚀 快速开始(5 分钟配置) + +### 1. 上传脚本到 Spug 服务器 +```bash +scp spug/deploy.sh user@spug-server:/opt/spug/scripts/ +scp spug/hospital-backend.service user@spug-server:/tmp/ +ssh user@spug-server "chmod +x /opt/spug/scripts/deploy.sh" +``` + +### 2. 在目标服务器配置 systemd 服务 +```bash +# 登录目标服务器 +ssh user@target-server + +# 复制服务文件 +sudo cp /tmp/hospital-backend.service /etc/systemd/system/ + +# 启用服务 +sudo systemctl daemon-reload +sudo systemctl enable hospital-backend +``` + +### 3. 在 Spug Web 界面配置 + +#### 步骤 1: 添加主机 +- 菜单:**系统管理** -> **主机管理** -> **创建主机** +- 填写: + - 名称:`production` + - IP:你的服务器 IP + - 端口:22 + - 认证:选择密钥或密码 + +#### 步骤 2: 创建应用 +- 菜单:**应用管理** -> **创建应用** +- 填写: + - 名称:`hospital-performance` + - 类型:`其他` + - 仓库地址:`https://gitea.gentronhealth.com/chenqi/hospital_performance.git` + - 分支:`main` + +#### 步骤 3: 配置环境变量 +在应用详情页的 **环境变量** 标签页添加: +``` +SPUG_DEPLOY_DIR=/var/www/hospital-performance +PYTHON_VERSION=python3.10 +BACKEND_SERVICE=hospital-backend +BACKEND_PORT=8000 +FRONTEND_SERVICE=nginx +LOG_FILE=/var/log/spug/deploy.log +``` + +#### 步骤 4: 配置发布 +- 进入应用的 **发布配置** 标签页 +- 构建类型:`跳过构建` +- 发布脚本:`/opt/spug/scripts/deploy.sh` +- 保存 + +#### 步骤 5: 执行发布 +- 菜单:**发布管理** -> **创建发布单** +- 选择应用和版本 +- 选择主机 `production` +- 点击 **立即发布** + +--- + +## 📋 常用命令速查 + +### 查看部署日志 +```bash +# 实时查看 +tail -f /var/log/spug/deploy.log + +# 查看最近 100 行 +tail -n 100 /var/log/spug/deploy.log + +# 搜索错误 +grep ERROR /var/log/spug/deploy.log +``` + +### 服务管理 +```bash +# 查看状态 +systemctl status hospital-backend +systemctl status nginx + +# 重启服务 +systemctl restart hospital-backend +systemctl restart nginx + +# 查看日志 +journalctl -u hospital-backend -f +``` + +### Git 操作 +```bash +# 查看当前版本 +cd /var/www/hospital-performance +git log --oneline -5 + +# 回退版本 +git reset --hard +``` + +### 数据库操作 +```bash +# 手动迁移 +cd /var/www/hospital-performance/backend +source ../venv/bin/activate +alembic upgrade head + +# 查看迁移历史 +alembic history + +# 数据库备份 +pg_dump -h 192.168.110.252 -p 15432 -U postgres hospital_performance > backup.sql + +# 数据库恢复 +psql -h 192.168.110.252 -p 15432 -U postgres hospital_performance < backup.sql +``` + +### 前端调试 +```bash +# 检查 Node 版本 +node -v +npm -v + +# 清理重建 +cd /var/www/hospital-performance/frontend +rm -rf node_modules dist +npm install --production +npm run build +``` + +--- + +## 🔧 故障排查流程图 + +``` +部署失败 +├─ 前置检查失败 +│ ├─ 缺少命令 → 安装 git/python/node/npm +│ └─ 磁盘不足 → 清理空间或扩容 +│ +├─ 代码更新失败 +│ ├─ Git 连接失败 → 检查网络和凭证 +│ └─ 冲突 → 手动解决或重置 +│ +├─ 后端部署失败 +│ ├─ 虚拟环境 → 删除重建 venv +│ ├─ 依赖安装 → 检查 requirements.txt +│ ├─ 数据库迁移 → 手动执行 alembic +│ └─ 服务启动 → 检查 systemd 配置 +│ +└─ 前端部署失败 + ├─ npm install 失败 → 清理缓存 npm cache clean + ├─ 构建失败 → 检查 Node.js 版本 + └─ Nginx 加载 → systemctl reload nginx +``` + +--- + +## ⚠️ 常见问题速查 + +### Q1: 权限错误 "Permission denied" +```bash +# 修复目录权限 +sudo chown -R www-data:www-data /var/www/hospital-performance +sudo chmod -R 755 /var/www/hospital-performance +``` + +### Q2: 端口被占用 +```bash +# 查看端口占用 +sudo lsof -i :8000 +sudo netstat -tlnp | grep 8000 + +# 杀死进程 +sudo kill -9 +``` + +### Q3: 数据库连接失败 +```bash +# 测试连接 +psql -h 192.168.110.252 -p 15432 -U postgres -d hospital_performance + +# 检查防火墙 +sudo ufw status +sudo telnet 192.168.110.252 15432 +``` + +### Q4: Python 模块找不到 +```bash +# 确认虚拟环境激活 +source /var/www/hospital-performance/venv/bin/activate + +# 重新安装依赖 +pip install -r requirements.txt --force-reinstall +``` + +### Q5: 前端页面空白 +```bash +# 检查浏览器控制台错误 +F12 -> Console + +# 检查 Nginx 配置 +sudo nginx -t +sudo cat /etc/nginx/sites-available/default + +# 检查文件权限 +ls -la /var/www/hospital-performance/frontend/dist/ +``` + +--- + +## 📊 监控指标 + +### 关键指标 +- **API 响应时间**: < 200ms +- **页面加载时间**: < 3s +- **服务可用性**: > 99.9% +- **数据库连接数**: < 100 + +### 监控命令 +```bash +# CPU 使用率 +top -bn1 | grep "Cpu(s)" + +# 内存使用 +free -h + +# 磁盘 IO +iostat -x 1 + +# 网络流量 +iftop -P -n + +# 进程监控 +ps aux | grep uvicorn +ps aux | grep nginx +``` + +--- + +## 🎯 发布清单 + +### 发布前检查 +- [ ] 代码已合并到 main 分支 +- [ ] 通过所有测试 +- [ ] 数据库迁移脚本就绪 +- [ ] 备份策略已配置 +- [ ] 回滚方案已准备 + +### 发布中检查 +- [ ] 代码更新成功 +- [ ] 依赖安装完成 +- [ ] 数据库迁移成功 +- [ ] 服务启动正常 +- [ ] 健康检查通过 + +### 发布后验证 +- [ ] 访问前端页面 +- [ ] 测试登录功能 +- [ ] 验证核心功能 +- [ ] 检查错误日志 +- [ ] 监控系统资源 + +--- + +## 📞 应急联系 + +### 升级流程 +1. **一级故障**(功能异常)→ 开发团队 +2. **二级故障**(性能下降)→ 运维团队 + 开发负责人 +3. **三级故障**(服务中断)→ 紧急响应小组 + +### 回滚命令 +```bash +# 快速回滚到上一个版本 +cd /var/www/hospital-performance +git reset --hard HEAD~1 + +# 重启服务 +systemctl restart hospital-backend +systemctl restart nginx +``` + +--- + +**提示**: 将此文件打印或保存为书签,便于快速查阅! diff --git a/spug/README.md b/spug/README.md new file mode 100644 index 0000000..fd2a520 --- /dev/null +++ b/spug/README.md @@ -0,0 +1,256 @@ +# Spug 自动部署脚本说明 + +## 📁 文件说明 + +### 1. `deploy.sh` - Bash 版本 +适用于 Linux/Unix 系统的 Shell 脚本,可在 Spug 的"执行任务"中直接调用。 + +**特点:** +- 轻量级,无需额外依赖 +- 直接在目标服务器上执行 +- 适合简单的部署场景 + +### 2. `deploy.py` - Python 版本 +使用 Python 编写的部署脚本,功能更强大,错误处理更完善。 + +**特点:** +- 更好的错误处理和日志记录 +- 支持健康检查(需要 requests 库) +- 更适合复杂的部署逻辑 + +--- + +## 🚀 在 Spug 中的配置步骤 + +### 方案一:使用 Bash 脚本(推荐) + +#### 1. 上传脚本到服务器 +```bash +# 将 deploy.sh 上传到 Spug 服务器 +scp deploy.sh spug-server:/opt/spug/scripts/ +chmod +x /opt/spug/scripts/deploy.sh +``` + +#### 2. 在 Spug 创建应用 +- 进入 **应用管理** -> **创建应用** +- 填写基本信息: + - 应用名称:`hospital-performance` + - 应用类型:`其他` + - 部署方式:`自定义` + +#### 3. 配置发布设置 +在应用的 **发布配置** 中: +- **构建类型**:选择 `本地构建` 或 `跳过构建` +- **发布脚本**:填写 `/opt/spug/scripts/deploy.sh` + +#### 4. 设置环境变量 +在 Spug 应用的 **环境变量** 中添加: + +```bash +# 项目配置 +SPUG_APP_NAME=hospital-performance +SPUG_DEPLOY_DIR=/var/www/hospital-performance + +# Git 配置 +SPUG_GIT_REPO=https://gitea.gentronhealth.com/chenqi/hospital_performance.git +SPUG_GIT_BRANCH=main + +# Python 配置 +PYTHON_VERSION=python3.10 + +# Node.js 配置 +NODE_VERSION=18 + +# 服务配置 +BACKEND_SERVICE=hospital-backend +BACKEND_PORT=8000 +FRONTEND_SERVICE=nginx + +# 日志配置 +LOG_FILE=/var/log/spug/deploy.log +``` + +#### 5. 配置 SSH 连接 +在 Spug 的 **主机管理** 中添加目标服务器: +- 主机名:例如 `production-server` +- IP 地址:你的服务器 IP +- SSH 端口:22(默认) +- 认证方式:密钥或密码 + +#### 6. 创建发布单 +- 进入 **发布管理** -> **创建发布单** +- 选择应用和要发布的版本 +- 选择目标主机 +- 执行发布 + +--- + +### 方案二:使用 Python 脚本 + +#### 1. 安装依赖 +在 Spug 服务器上安装 Python 依赖: +```bash +pip3 install requests +``` + +#### 2. 上传脚本 +```bash +scp deploy.py spug-server:/opt/spug/scripts/ +chmod +x /opt/spug/scripts/deploy.py +``` + +#### 3. Spug 配置 +与 Bash 脚本类似,只需将发布脚本改为: +```bash +/opt/spug/scripts/deploy.py +``` + +或在 Spug 执行任务中配置: +```bash +python3 /opt/spug/scripts/deploy.py +``` + +--- + +## 🔧 systemd 服务配置 + +### 后端服务配置示例 + +创建 `/etc/systemd/system/hospital-backend.service`: + +```ini +[Unit] +Description=Hospital Performance Backend Service +After=network.target postgresql.service + +[Service] +Type=simple +User=www-data +Group=www-data +WorkingDirectory=/var/www/hospital-performance/backend +Environment="PATH=/var/www/hospital-performance/venv/bin" +ExecStart=/var/www/hospital-performance/venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000 +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +``` + +启用服务: +```bash +sudo systemctl daemon-reload +sudo systemctl enable hospital-backend +sudo systemctl start hospital-backend +``` + +--- + +## 📝 环境变量说明 + +| 变量名 | 说明 | 默认值 | +|--------|------|--------| +| `SPUG_APP_NAME` | 应用名称 | `hospital-performance` | +| `SPUG_DEPLOY_DIR` | 部署目录 | `/var/www/hospital-performance` | +| `SPUG_GIT_REPO` | Git 仓库地址 | Gitea 地址 | +| `SPUG_GIT_BRANCH` | Git 分支 | `main` | +| `PYTHON_VERSION` | Python 版本 | `python3.10` | +| `NODE_VERSION` | Node.js 版本 | `18` | +| `BACKEND_SERVICE` | 后端服务名 | `hospital-backend` | +| `BACKEND_PORT` | 后端端口 | `8000` | +| `FRONTEND_SERVICE` | 前端服务名 | `nginx` | +| `LOG_FILE` | 日志文件 | `/var/log/spug/deploy.log` | + +--- + +## 🔍 故障排查 + +### 查看部署日志 +```bash +tail -f /var/log/spug/deploy.log +``` + +### 手动执行脚本测试 +```bash +cd /opt/spug/scripts +./deploy.sh +``` + +### 检查服务状态 +```bash +systemctl status hospital-backend +systemctl status nginx +``` + +### 常见问题 + +#### 1. 权限问题 +确保脚本有执行权限: +```bash +chmod +x /opt/spug/scripts/deploy.sh +``` + +#### 2. Python 虚拟环境问题 +删除并重建虚拟环境: +```bash +rm -rf /var/www/hospital-performance/venv +python3.10 -m venv /var/www/hospital-performance/venv +``` + +#### 3. 数据库迁移失败 +手动执行迁移: +```bash +cd /var/www/hospital-performance/backend +source venv/bin/activate +alembic upgrade head +``` + +#### 4. 前端构建失败 +检查 Node.js 版本: +```bash +node -v +npm -v +``` + +清理后重新构建: +```bash +cd frontend +rm -rf node_modules package-lock.json +npm install +npm run build +``` + +--- + +## 🎯 最佳实践 + +### 1. 灰度发布 +先在测试环境验证,再发布到生产环境。 + +### 2. 备份策略 +脚本会自动保留 30 天备份,重要发布前建议手动备份数据库: +```bash +pg_dump -h localhost -U postgres hospital_performance > backup_$(date +%Y%m%d).sql +``` + +### 3. 回滚方案 +如果发布失败,从备份恢复: +```bash +# 停止服务 +systemctl stop hospital-backend + +# 恢复代码 +cp -r /var/www/hospital-performance/backups/backup_20260228_120000_backend /var/www/hospital-performance/backend + +# 重启服务 +systemctl start hospital-backend +``` + +### 4. 监控告警 +结合 Spug 的监控功能,配置服务健康检查和告警通知。 + +--- + +## 📞 技术支持 + +如有问题,请联系开发团队或查看项目文档。 diff --git a/spug/deploy.py b/spug/deploy.py new file mode 100644 index 0000000..4ca1f4c --- /dev/null +++ b/spug/deploy.py @@ -0,0 +1,389 @@ +#!/usr/bin/env python3 +""" +Spug 自动发布脚本 - Python 版本 +用途:医院绩效考核系统自动化部署(Python 实现) +使用场景:在 Spug 的"执行任务"中通过 Python 调用 +""" + +import os +import sys +import subprocess +import shutil +from datetime import datetime +from pathlib import Path +from typing import Optional, List + + +class SpugDeploy: + """Spug 部署类""" + + def __init__(self): + # 配置参数(从环境变量获取,可在 Spug 中设置) + self.project_name = os.getenv('SPUG_APP_NAME', 'hospital-performance') + self.project_dir = Path(os.getenv('SPUG_DEPLOY_DIR', '/var/www/hospital-performance')) + self.backup_dir = self.project_dir / 'backups' + self.frontend_dir = self.project_dir / 'frontend' + self.backend_dir = self.project_dir / 'backend' + + # Git 配置 + self.git_repo = os.getenv('SPUG_GIT_REPO', + 'https://gitea.gentronhealth.com/chenqi/hospital_performance.git') + self.git_branch = os.getenv('SPUG_GIT_BRANCH', 'main') + + # Python 配置 + self.python_version = os.getenv('PYTHON_VERSION', 'python3.10') + self.venv_dir = self.project_dir / 'venv' + + # Node.js 配置 + self.node_version = os.getenv('NODE_VERSION', '18') + + # 服务配置 + self.backend_service = os.getenv('BACKEND_SERVICE', 'hospital-backend') + self.backend_port = int(os.getenv('BACKEND_PORT', '8000')) + self.frontend_service = os.getenv('FRONTEND_SERVICE', 'nginx') + + # 日志配置 + log_file = os.getenv('LOG_FILE', '/var/log/spug/deploy.log') + self.log_file = Path(log_file) + self.deploy_time = datetime.now().strftime('%Y%m%d_%H%M%S') + + # 确保日志目录存在 + self.log_file.parent.mkdir(parents=True, exist_ok=True) + + def log(self, level: str, message: str): + """记录日志""" + timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + log_msg = f"[{timestamp}] [{level}] {message}" + print(log_msg) + + try: + with open(self.log_file, 'a', encoding='utf-8') as f: + f.write(log_msg + '\n') + except Exception as e: + print(f"写入日志失败:{e}") + + def info(self, message: str): + self.log("INFO", message) + + def error(self, message: str): + self.log("ERROR", message) + + def success(self, message: str): + self.log("SUCCESS", message) + + def run_command(self, cmd: List[str], cwd: Optional[Path] = None, + check: bool = True, shell: bool = False) -> subprocess.CompletedProcess: + """运行命令""" + try: + result = subprocess.run( + cmd, + cwd=cwd, + capture_output=True, + text=True, + shell=shell, + check=check + ) + + if result.stdout: + self.info(result.stdout.strip()) + if result.stderr: + self.info(result.stderr.strip()) + + return result + except subprocess.CalledProcessError as e: + self.error(f"命令执行失败:{' '.join(cmd)}") + self.error(f"错误信息:{e.stderr}") + raise + + def check_command(self, cmd: str) -> bool: + """检查命令是否存在""" + return shutil.which(cmd) is not None + + def pre_check(self): + """前置检查""" + self.info("========== 开始前置检查 ==========") + + # 检查必要命令 + required_commands = ['git', 'python3', 'node', 'npm'] + for cmd in required_commands: + if not self.check_command(cmd): + self.error(f"命令 {cmd} 未安装,请先安装") + sys.exit(1) + + # 检查磁盘空间 + try: + stat = shutil.disk_usage(self.project_dir) + available_gb = stat.free / (1024 ** 3) + if available_gb < 1: + self.error(f"磁盘空间不足 1GB,当前可用:{available_gb:.2f}GB") + sys.exit(1) + except Exception as e: + self.error(f"检查磁盘空间失败:{e}") + + self.success("✓ 前置检查通过") + + def update_code(self): + """更新代码""" + self.info("========== 更新代码 ==========") + + os.chdir(self.project_dir) + + git_dir = self.project_dir / '.git' + if not git_dir.exists(): + self.info("首次部署,克隆仓库...") + self.run_command(['git', 'clone', self.git_repo, '.']) + self.run_command(['git', 'checkout', self.git_branch]) + else: + self.info("更新现有代码...") + self.run_command(['git', 'fetch', 'origin', self.git_branch]) + self.run_command(['git', 'reset', '--hard', f'origin/{self.git_branch}']) + self.run_command(['git', 'clean', '-fd']) + + # 获取当前版本信息 + result = self.run_command(['git', 'rev-parse', '--short', 'HEAD'], + capture_output=True, text=True) + commit_hash = result.stdout.strip() + + result = self.run_command(['git', 'log', '-1', '--pretty=format:%s'], + capture_output=True, text=True) + commit_msg = result.stdout.strip() + + self.success(f"✓ 代码更新完成,当前版本:{commit_hash} - {commit_msg}") + + def backup_old_version(self): + """备份旧版本""" + self.info("========== 备份当前版本 ==========") + + self.backup_dir.mkdir(parents=True, exist_ok=True) + backup_name = f"backup_{self.deploy_time}" + backup_path = self.backup_dir / backup_name + + # 备份后端 + if self.backend_dir.exists(): + backup_backend = backup_path.with_name(f"{backup_name}_backend") + shutil.copytree(self.backend_dir, backup_backend, dirs_exist_ok=True) + self.info(f"✓ 后端代码已备份到:{backup_backend}") + + # 备份前端构建 + frontend_dist = self.frontend_dir / 'dist' + if frontend_dist.exists(): + backup_frontend = backup_path.with_name(f"{backup_name}_frontend_dist") + shutil.copytree(frontend_dist, backup_frontend, dirs_exist_ok=True) + self.info(f"✓ 前端构建已备份到:{backup_frontend}") + + # 清理 30 天前的备份 + try: + for item in self.backup_dir.iterdir(): + if item.is_dir() and item.name.startswith('backup_'): + mtime = datetime.fromtimestamp(item.stat().st_mtime) + age = (datetime.now() - mtime).days + if age > 30: + shutil.rmtree(item) + self.info(f"清理过期备份:{item.name}") + except Exception as e: + self.error(f"清理备份失败:{e}") + + self.success("✓ 备份完成(保留最近 30 天)") + + def deploy_backend(self): + """部署后端""" + self.info("========== 部署后端服务 ==========") + + os.chdir(self.backend_dir) + + # 创建虚拟环境 + if not self.venv_dir.exists(): + self.info("创建 Python 虚拟环境...") + self.run_command([self.python_version, '-m', 'venv', str(self.venv_dir)]) + + # 激活虚拟环境(通过设置 PATH) + venv_bin = self.venv_dir / 'bin' + env = os.environ.copy() + env['PATH'] = str(venv_bin) + os.pathsep + env['PATH'] + env['VIRTUAL_ENV'] = str(self.venv_dir) + + # 升级 pip + self.run_command(['pip', 'install', '--upgrade', 'pip'], env=env) + + # 安装依赖 + self.info("安装 Python 依赖...") + self.run_command(['pip', 'install', '-r', 'requirements.txt'], env=env) + + # 数据库迁移 + alembic_ini = self.backend_dir / 'alembic.ini' + if alembic_ini.exists(): + self.info("执行数据库迁移...") + self.run_command(['alembic', 'upgrade', 'head'], env=env) + + # 初始化数据 + init_db_py = self.backend_dir / 'init_db.py' + if init_db_py.exists(): + self.info("初始化数据库...") + try: + self.run_command([str(self.venv_dir / 'bin/python'), 'init_db.py'], + env=env, check=False) + except Exception as e: + self.info(f"初始化数据跳过或失败:{e}") + + # 重启后端服务 + try: + result = subprocess.run( + ['systemctl', 'list-units', '--type=service', '--all'], + capture_output=True, + text=True + ) + + if self.backend_service in result.stdout: + self.info(f"重启后端服务...") + self.run_command(['systemctl', 'restart', self.backend_service]) + + import time + time.sleep(2) + + # 检查服务状态 + result = subprocess.run( + ['systemctl', 'is-active', self.backend_service], + capture_output=True, + text=True + ) + + if result.stdout.strip() == 'active': + self.success("✓ 后端服务重启成功") + else: + self.error("✗ 后端服务启动失败") + sys.exit(1) + else: + self.info("未找到 systemd 服务,跳过重启") + except Exception as e: + self.error(f"服务管理操作失败:{e}") + + self.success("✓ 后端部署完成") + + def deploy_frontend(self): + """部署前端""" + self.info("========== 部署前端服务 ==========") + + os.chdir(self.frontend_dir) + + # 安装依赖 + self.info("安装 Node.js 依赖...") + self.run_command(['npm', 'install', '--production']) + + # 构建前端 + self.info("构建前端项目...") + self.run_command(['npm', 'run', 'build']) + + # 检查构建结果 + dist_dir = self.frontend_dir / 'dist' + if dist_dir.exists() and any(dist_dir.iterdir()): + self.success("✓ 前端构建成功") + else: + self.error("✗ 前端构建失败,dist 目录为空") + sys.exit(1) + + # 重新加载 Nginx + try: + result = subprocess.run( + ['systemctl', 'list-units', '--type=service', '--all'], + capture_output=True, + text=True + ) + + if self.frontend_service in result.stdout: + self.info("重新加载 Nginx...") + self.run_command(['systemctl', 'reload', self.frontend_service]) + except Exception as e: + self.error(f"Nginx 操作失败:{e}") + + self.success("✓ 前端部署完成") + + def health_check(self): + """健康检查""" + self.info("========== 执行健康检查 ==========") + + import time + time.sleep(5) + + # 检查后端 API + import requests + + backend_url = f"http://localhost:{self.backend_port}/api/v1/health" + try: + response = requests.get(backend_url, timeout=10) + if response.status_code == 200: + self.success("✓ 后端 API 健康检查通过") + else: + self.error(f"✗ 后端 API 返回异常状态码:{response.status_code}") + sys.exit(1) + except requests.exceptions.RequestException as e: + self.error(f"✗ 后端 API 连接失败:{e}") + sys.exit(1) + + # 检查前端文件 + index_html = self.frontend_dir / 'dist' / 'index.html' + if index_html.exists(): + self.success("✓ 前端文件存在") + else: + self.error("✗ 前端文件缺失") + sys.exit(1) + + self.success("✓ 所有健康检查通过") + + def cleanup(self): + """清理临时文件""" + self.info("========== 清理临时文件 ==========") + + # 清理 npm 缓存 + try: + self.run_command(['npm', 'cache', 'clean', '--force'], check=False) + except: + pass + + # 清理 Python 缓存 + import glob + for pycache in glob.glob(str(self.project_dir / '**/__pycache__'), recursive=True): + try: + shutil.rmtree(pycache) + except: + pass + + for pyc in glob.glob(str(self.project_dir / '**/*.pyc'), recursive=True): + try: + os.remove(pyc) + except: + pass + + self.success("✓ 清理完成") + + def run(self): + """主流程""" + self.info("========================================") + self.info("Spug 自动发布开始") + self.info(f"项目名称:{self.project_name}") + self.info(f"部署目录:{self.project_dir}") + self.info(f"Git 分支:{self.git_branch}") + self.info("========================================") + + try: + self.pre_check() + self.update_code() + self.backup_old_version() + self.deploy_backend() + self.deploy_frontend() + self.health_check() + self.cleanup() + + self.info("========================================") + self.success("🎉 部署成功完成!") + self.info("========================================") + + except Exception as e: + self.error(f"部署失败:{e}") + import traceback + self.error(traceback.format_exc()) + sys.exit(1) + + +if __name__ == '__main__': + deployer = SpugDeploy() + deployer.run() diff --git a/spug/deploy.sh b/spug/deploy.sh new file mode 100644 index 0000000..f8cf48f --- /dev/null +++ b/spug/deploy.sh @@ -0,0 +1,284 @@ +#!/bin/bash +# Spug 自动发布脚本 - 全栈项目 +# 用途:医院绩效考核系统自动化部署脚本 +# 执行方式:在 Spug 的"执行任务"中调用此脚本 + +set -e + +# ==================== 配置参数 ==================== +# 这些参数可以在 Spug 中通过环境变量传入 +PROJECT_NAME="${SPUG_APP_NAME:-hospital-performance}" +PROJECT_DIR="${SPUG_DEPLOY_DIR:-/var/www/hospital-performance}" +BACKUP_DIR="${PROJECT_DIR}/backups" +FRONTEND_DIR="${PROJECT_DIR}/frontend" +BACKEND_DIR="${PROJECT_DIR}/backend" + +# Git 配置 +GIT_REPO="${SPUG_GIT_REPO:-https://gitea.gentronhealth.com/chenqi/hospital_performance.git}" +GIT_BRANCH="${SPUG_GIT_BRANCH:-main}" + +# Python 虚拟环境 +VENV_DIR="${PROJECT_DIR}/venv" +PYTHON_VERSION="${PYTHON_VERSION:-python3.10}" + +# Node.js 配置 +NODE_VERSION="${NODE_VERSION:-18}" + +# 服务配置 +BACKEND_SERVICE="${BACKEND_SERVICE:-hospital-backend}" +BACKEND_PORT="${BACKEND_PORT:-8000}" +FRONTEND_SERVICE="${FRONTEND_SERVICE:-nginx}" + +# 日志配置 +LOG_FILE="${LOG_FILE:-/var/log/spug/deploy.log}" +DEPLOY_TIME=$(date +"%Y%m%d_%H%M%S") + +# ==================== 工具函数 ==================== +log() { + local level=$1 + shift + local message="$*" + local timestamp=$(date '+%Y-%m-%d %H:%M:%S') + echo "[${timestamp}] [${level}] ${message}" | tee -a "${LOG_FILE}" +} + +info() { + log "INFO" "$@" +} + +error() { + log "ERROR" "$@" +} + +success() { + log "SUCCESS" "$@" +} + +check_command() { + if ! command -v $1 &> /dev/null; then + error "命令 $1 未安装,请先安装" + exit 1 + fi +} + +# ==================== 前置检查 ==================== +pre_check() { + info "========== 开始前置检查 ==========" + + # 检查必要命令 + check_command git + check_command python3 + check_command node + check_command npm + + # 检查磁盘空间 + local available_space=$(df -P "${PROJECT_DIR}" | awk 'NR==2 {print $4}') + if [ "${available_space}" -lt 1048576 ]; then + error "磁盘空间不足 1GB,当前可用:${available_space}KB" + exit 1 + fi + + info "✓ 前置检查通过" +} + +# ==================== 代码更新 ==================== +update_code() { + info "========== 更新代码 ==========" + + cd "${PROJECT_DIR}" + + if [ ! -d ".git" ]; then + info "首次部署,克隆仓库..." + git clone "${GIT_REPO}" . + git checkout "${GIT_BRANCH}" + else + info "更新现有代码..." + git fetch origin "${GIT_BRANCH}" + git reset --hard "origin/${GIT_BRANCH}" + git clean -fd + fi + + local commit_hash=$(git rev-parse --short HEAD) + local commit_msg=$(git log -1 --pretty=format:"%s") + info "✓ 代码更新完成,当前版本:${commit_hash} - ${commit_msg}" +} + +# ==================== 备份旧版本 ==================== +backup_old_version() { + info "========== 备份当前版本 ==========" + + mkdir -p "${BACKUP_DIR}" + local backup_name="backup_${DEPLOY_TIME}" + local backup_path="${BACKUP_DIR}/${backup_name}" + + # 备份关键目录 + if [ -d "${BACKEND_DIR}" ]; then + cp -r "${BACKEND_DIR}" "${backup_path}_backend" + info "✓ 后端代码已备份到:${backup_path}_backend" + fi + + if [ -d "${FRONTEND_DIR}/dist" ]; then + cp -r "${FRONTEND_DIR}/dist" "${backup_path}_frontend_dist" + info "✓ 前端构建已备份到:${backup_path}_frontend_dist" + fi + + # 清理 30 天前的备份 + find "${BACKUP_DIR}" -type d -name "backup_*" -mtime +30 -exec rm -rf {} \; 2>/dev/null || true + + info "✓ 备份完成(保留最近 30 天)" +} + +# ==================== 后端部署 ==================== +deploy_backend() { + info "========== 部署后端服务 ==========" + + cd "${BACKEND_DIR}" + + # 创建虚拟环境 + if [ ! -d "${VENV_DIR}" ]; then + info "创建 Python 虚拟环境..." + ${PYTHON_VERSION} -m venv "${VENV_DIR}" + fi + + # 激活虚拟环境 + source "${VENV_DIR}/bin/activate" + + # 升级 pip + pip install --upgrade pip -q + + # 安装依赖 + info "安装 Python 依赖..." + pip install -r requirements.txt -q + + # 数据库迁移 + if [ -f "alembic.ini" ]; then + info "执行数据库迁移..." + alembic upgrade head + fi + + # 初始化数据(如果存在) + if [ -f "init_db.py" ]; then + info "初始化数据库..." + python init_db.py || true + fi + + # 重启后端服务 + if systemctl list-units --type=service --all | grep -q "${BACKEND_SERVICE}"; then + info "重启后端服务..." + systemctl restart "${BACKEND_SERVICE}" + sleep 2 + + # 检查服务状态 + if systemctl is-active --quiet "${BACKEND_SERVICE}"; then + success "✓ 后端服务重启成功" + else + error "✗ 后端服务启动失败" + exit 1 + fi + else + info "未找到 systemd 服务,跳过重启" + fi + + deactivate + info "✓ 后端部署完成" +} + +# ==================== 前端部署 ==================== +deploy_frontend() { + info "========== 部署前端服务 ==========" + + cd "${FRONTEND_DIR}" + + # 安装依赖 + info "安装 Node.js 依赖..." + npm install --production + + # 构建前端 + info "构建前端项目..." + npm run build + + # 检查构建结果 + if [ -d "dist" ] && [ "$(ls -A dist)" ]; then + success "✓ 前端构建成功" + else + error "✗ 前端构建失败,dist 目录为空" + exit 1 + fi + + # 如果使用 Nginx,重新加载配置 + if systemctl list-units --type=service --all | grep -q "${FRONTEND_SERVICE}"; then + info "重新加载 Nginx..." + systemctl reload "${FRONTEND_SERVICE}" + fi + + info "✓ 前端部署完成" +} + +# ==================== 健康检查 ==================== +health_check() { + info "========== 执行健康检查 ==========" + + # 等待服务启动 + sleep 5 + + # 检查后端 API + local backend_url="http://localhost:${BACKEND_PORT}/api/v1/health" + if curl -f -s "${backend_url}" > /dev/null; then + success "✓ 后端 API 健康检查通过" + else + error "✗ 后端 API 健康检查失败" + # 回滚逻辑(可选) + # rollback + exit 1 + fi + + # 检查前端 + if [ -d "${FRONTEND_DIR}/dist/index.html" ]; then + success "✓ 前端文件存在" + else + error "✗ 前端文件缺失" + exit 1 + fi + + info "✓ 所有健康检查通过" +} + +# ==================== 清理工作 ==================== +cleanup() { + info "========== 清理临时文件 ==========" + + # 清理 npm 缓存 + npm cache clean --force 2>/dev/null || true + + # 清理 Python 缓存 + find "${PROJECT_DIR}" -type d -name "__pycache__" -exec rm -rf {} \; 2>/dev/null || true + find "${PROJECT_DIR}" -type f -name "*.pyc" -delete 2>/dev/null || true + + info "✓ 清理完成" +} + +# ==================== 主流程 ==================== +main() { + info "========================================" + info "Spug 自动发布开始" + info "项目名称:${PROJECT_NAME}" + info "部署目录:${PROJECT_DIR}" + info "Git 分支:${GIT_BRANCH}" + info "========================================" + + # 执行部署流程 + pre_check + update_code + backup_old_version + deploy_backend + deploy_frontend + health_check + cleanup + + info "========================================" + success "🎉 部署成功完成!" + info "========================================" +} + +# 执行主流程 +main "$@" diff --git a/spug/hospital-backend.service b/spug/hospital-backend.service new file mode 100644 index 0000000..e456654 --- /dev/null +++ b/spug/hospital-backend.service @@ -0,0 +1,40 @@ +[Unit] +Description=Hospital Performance Backend Service +Documentation=https://gitea.gentronhealth.com/chenqi/hospital_performance +After=network.target postgresql.service + +[Service] +Type=simple +User=www-data +Group=www-data + +# 工作目录 +WorkingDirectory=/var/www/hospital-performance/backend + +# 环境变量 +Environment="PATH=/var/www/hospital-performance/venv/bin" +Environment="DATABASE_URL=postgresql+asyncpg://postgresql:Jchl1528@192.168.110.252:15432/hospital_performance" +Environment="SECRET_KEY=your-secret-key-change-in-production" +Environment="DEBUG=False" + +# 启动命令 +ExecStart=/var/www/hospital-performance/venv/bin/uvicorn app.main:app \ + --host 0.0.0.0 \ + --port 8000 \ + --workers 4 \ + --access-log + +# 重启策略 +Restart=always +RestartSec=10 + +# 资源限制(可选) +LimitNOFILE=65535 + +# 日志配置 +StandardOutput=journal +StandardError=journal +SyslogIdentifier=hospital-backend + +[Install] +WantedBy=multi-user.target