diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..712ed28 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,54 @@ +# Git +.git +.gitignore + +# Spug +spug/ + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +venv/ +env/ +*.egg-info/ +.eggs/ + +# Node +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +backend/logs/ +*.log + +# Database +*.db +*.sqlite + +# Docker +Dockerfile.backend +Dockerfile.frontend +docker-compose.yml + +# Documentation +*.md +docs/ + +# Test +tests/ +pytest_cache/ diff --git a/Dockerfile.backend b/Dockerfile.backend new file mode 100644 index 0000000..a9a63d2 --- /dev/null +++ b/Dockerfile.backend @@ -0,0 +1,51 @@ +# 医院绩效考核系统 - 后端 Dockerfile +FROM python:3.10-slim + +# 设置工作目录 +WORKDIR /app + +# 设置环境变量 +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 + +# 安装系统依赖 +RUN apt-get update && apt-get install -y \ + gcc \ + postgresql-client \ + && rm -rf /var/lib/apt/lists/* + +# 复制依赖文件 +COPY backend/requirements.txt . + +# 安装 Python 依赖 +RUN pip install --no-cache-dir -r requirements.txt + +# 复制后端代码 +COPY backend/ ./backend/ +WORKDIR /app/backend + +# 构建参数 +ARG DATABASE_HOST +ARG DATABASE_PORT +ARG SECRET_KEY +ARG DEBUG + +ENV DATABASE_HOST=${DATABASE_HOST} \ + DATABASE_PORT=${DATABASE_PORT} \ + SECRET_KEY=${SECRET_KEY} \ + DEBUG=${DEBUG} + +# 创建日志目录 +RUN mkdir -p logs + +# 暴露端口 +EXPOSE 8000 + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/v1/health')" || exit 1 + +# 启动命令 +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"] diff --git a/Dockerfile.frontend b/Dockerfile.frontend new file mode 100644 index 0000000..3f00161 --- /dev/null +++ b/Dockerfile.frontend @@ -0,0 +1,35 @@ +# 医院绩效考核系统 - 前端 Dockerfile +FROM node:18-alpine AS builder + +WORKDIR /app + +# 复制 package 文件 +COPY frontend/package*.json ./ + +# 安装依赖 +RUN npm install --production + +# 复制源代码 +COPY frontend/ ./ + +# 构建前端 +RUN npm run build + +# 生产阶段:使用 Nginx +FROM nginx:alpine + +# 复制构建产物 +COPY --from=builder /app/dist /usr/share/nginx/html + +# 复制 Nginx 配置 +COPY frontend/nginx.conf /etc/nginx/conf.d/default.conf + +# 暴露端口 +EXPOSE 80 + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=3s \ + CMD wget --quiet --tries=1 --spider http://localhost/ || exit 1 + +# 启动 Nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9fb68d9 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,56 @@ +version: '3.8' + +services: + # 后端服务 + backend: + build: + context: . + dockerfile: Dockerfile.backend + args: + DATABASE_HOST: ${DATABASE_HOST:-192.168.110.252} + DATABASE_PORT: ${DATABASE_PORT:-15432} + SECRET_KEY: ${SECRET_KEY:-change-this-secret-key} + DEBUG: ${DEBUG:-False} + image: hospital-performance-backend:${DOCKER_TAG:-latest} + container_name: hospital-performance-backend + restart: unless-stopped + ports: + - "${BACKEND_PORT:-8000}:8000" + environment: + DATABASE_URL: postgresql+asyncpg://${DATABASE_USER:-postgresql}:${DATABASE_PASSWORD:-Jchl1528}@${DATABASE_HOST:-192.168.110.252}:${DATABASE_PORT:-15432}/${DATABASE_NAME:-hospital_performance} + SECRET_KEY: ${SECRET_KEY:-change-this-secret-key} + DEBUG: ${DEBUG:-False} + volumes: + - ./backend/logs:/app/backend/logs + networks: + - hospital-network + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/v1/health')"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + + # 前端服务 + frontend: + build: + context: . + dockerfile: Dockerfile.frontend + image: hospital-performance-frontend:${DOCKER_TAG:-latest} + container_name: hospital-performance-frontend + restart: unless-stopped + ports: + - "${FRONTEND_PORT:-80}:80" + depends_on: + - backend + networks: + - hospital-network + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/"] + interval: 30s + timeout: 3s + retries: 3 + +networks: + hospital-network: + driver: bridge diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..b536cce --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,46 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Gzip 压缩 + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + + # 缓存静态资源 + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # API 代理到后端容器 + location /api/ { + proxy_pass http://backend:8000; + 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; + + # WebSocket 支持 + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + # 超时设置 + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + # 前端路由 fallback + location / { + try_files $uri $uri/ /index.html; + } + + # 错误页面 + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } +} diff --git a/spug/deploy-docker-compose.sh b/spug/deploy-docker-compose.sh new file mode 100644 index 0000000..b3fca52 --- /dev/null +++ b/spug/deploy-docker-compose.sh @@ -0,0 +1,129 @@ +#!/bin/bash +# Spug Docker Compose 自动部署脚本 +# 用途:使用 docker-compose 快速部署医院绩效考核系统 + +set -e + +# ==================== 配置参数 ==================== +PROJECT_DIR="${SPUG_DEPLOY_DIR:-/var/www/hospital-performance}" +GIT_REPO="${SPUG_GIT_URL:-https://gitea.gentronhealth.com/chenqi/hospital_performance.git}" +GIT_BRANCH="${SPUG_GIT_BRANCH:-main}" + +# Docker 配置 +DOCKER_TAG="${DOCKER_TAG:-latest}" +COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-hospital-performance}" + +# 环境变量 +export DATABASE_HOST="${DATABASE_HOST:-192.168.110.252}" +export DATABASE_PORT="${DATABASE_PORT:-15432}" +export DATABASE_USER="${DATABASE_USER:-postgresql}" +export DATABASE_PASSWORD="${DATABASE_PASSWORD:-Jchl1528}" +export DATABASE_NAME="${DATABASE_NAME:-hospital_performance}" +export SECRET_KEY="${SECRET_KEY:-change-this-secret-key-in-production}" +export DEBUG="${DEBUG:-False}" +export BACKEND_PORT="${BACKEND_PORT:-8000}" +export FRONTEND_PORT="${FRONTEND_PORT:-80}" +export DOCKER_TAG="${DOCKER_TAG}" + +# 日志配置 +LOG_FILE="${LOG_FILE:-/var/log/spug/deploy-docker-compose.log}" +mkdir -p /var/log/spug 2>/dev/null || true + +# ==================== 工具函数 ==================== +log() { + local level=$1 + shift + local timestamp=$(date '+%Y-%m-%d %H:%M:%S') + echo "[${timestamp}] [${level}] $*" | tee -a "${LOG_FILE}" +} + +info() { log "INFO" "$@"; } +error() { log "ERROR" "$@"; } +success() { log "SUCCESS" "$@"; } + +# ==================== 主流程 ==================== +main() { + info "========================================" + info "Spug Docker Compose 自动部署开始" + info "项目目录:${PROJECT_DIR}" + info "Git 分支:${GIT_BRANCH}" + info "========================================" + + # 进入项目目录 + cd "${PROJECT_DIR}" + + # 更新代码 + info "========== 更新代码 ==========" + 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 + + commit_hash=$(git rev-parse --short HEAD) + commit_msg=$(git log -1 --pretty=format:"%s") + success "✓ 代码更新完成,当前版本:${commit_hash} - ${commit_msg}" + + # 停止旧容器 + info "========== 停止旧服务 ==========" + if docker-compose ps &>/dev/null; then + docker-compose down || true + success "✓ 旧服务已停止" + else + info "未发现运行中的服务" + fi + + # 构建镜像 + info "========== 构建 Docker 镜像 ==========" + docker-compose build --no-cache + + # 启动服务 + info "========== 启动服务 ==========" + docker-compose up -d + + # 等待服务启动 + info "等待服务启动..." + sleep 15 + + # 健康检查 + info "========== 执行健康检查 ==========" + max_attempts=10 + attempt=1 + + while [ $attempt -le $max_attempts ]; do + if curl -f -s "http://localhost:${BACKEND_PORT}/api/v1/health" > /dev/null 2>&1; then + success "✓ 后端 API 健康检查通过" + break + else + if [ $attempt -eq $max_attempts ]; then + error "✗ 后端 API 健康检查失败" + docker-compose logs backend + exit 1 + fi + info "后端服务未就绪,等待... (${attempt}/${max_attempts})" + sleep 5 + attempt=$((attempt + 1)) + fi + done + + # 查看状态 + info "========== 服务状态 ==========" + docker-compose ps + + # 清理旧镜像 + info "========== 清理旧镜像 ==========" + docker image prune -f + + info "========================================" + success "🎉 Docker Compose 部署成功完成!" + info "后端地址:http://localhost:${BACKEND_PORT}" + info "前端地址:http://localhost:${FRONTEND_PORT}" + info "========================================" +} + +main "$@" diff --git a/spug/deploy-docker.sh b/spug/deploy-docker.sh new file mode 100644 index 0000000..10ec300 --- /dev/null +++ b/spug/deploy-docker.sh @@ -0,0 +1,317 @@ +#!/bin/bash +# Spug 自动部署脚本 - Docker 版本 +# 用途:医院绩效考核系统 Docker 自动化部署 +# 执行方式:在 Spug 的"执行任务"中调用此脚本 + +set -e + +# ==================== 配置参数 ==================== +# 项目配置 +PROJECT_NAME="${SPUG_APP_NAME:-hospital-performance}" +PROJECT_DIR="${SPUG_DEPLOY_DIR:-/var/www/hospital-performance}" +DOCKER_REGISTRY="${DOCKER_REGISTRY:-}" # 可选:私有仓库地址 +DOCKER_IMAGE_NAME="${DOCKER_IMAGE_NAME:-hospital-performance}" +DOCKER_TAG="${DOCKER_TAG:-latest}" + +# Git 配置 +if [ -n "${SPUG_GIT_URL}" ]; then + GIT_REPO="${SPUG_GIT_URL}" +elif [ -n "${SPUG_GIT_REPO}" ]; then + GIT_REPO="${SPUG_GIT_REPO}" +else + GIT_REPO="https://gitea.gentronhealth.com/chenqi/hospital_performance.git" +fi +GIT_BRANCH="${SPUG_GIT_BRANCH:-main}" + +# Docker 配置 +DOCKER_NETWORK="${DOCKER_NETWORK:-hospital-network}" +DOCKER_BACKEND_PORT="${DOCKER_BACKEND_PORT:-8000}" +DOCKER_FRONTEND_PORT="${DOCKER_FRONTEND_PORT:-80}" +CONTAINER_NAME="${CONTAINER_NAME:-hospital-performance}" + +# 数据库配置(从环境变量获取) +DATABASE_HOST="${DATABASE_HOST:-192.168.110.252}" +DATABASE_PORT="${DATABASE_PORT:-15432}" +DATABASE_USER="${DATABASE_USER:-postgresql}" +DATABASE_PASSWORD="${DATABASE_PASSWORD:-Jchl1528}" +DATABASE_NAME="${DATABASE_NAME:-hospital_performance}" + +# JWT 配置 +SECRET_KEY="${SECRET_KEY:-change-this-secret-key-in-production}" +DEBUG="${DEBUG:-False}" + +# 日志配置 +if [ -z "${LOG_FILE}" ]; then + LOG_FILE="/var/log/spug/deploy-docker.log" + mkdir -p /var/log/spug 2>/dev/null || true +fi +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 docker + + # 检查 Docker 是否运行 + if ! docker info > /dev/null 2>&1; then + error "Docker 未运行,请启动 Docker 服务" + exit 1 + fi + + # 检查部署目录 + if [ ! -d "${PROJECT_DIR}" ]; then + info "创建部署目录:${PROJECT_DIR}" + mkdir -p "${PROJECT_DIR}" + fi + + info "✓ 前置检查通过" +} + +# ==================== 更新代码 ==================== +update_code() { + info "========== 更新代码 ==========" + + cd "${PROJECT_DIR}" + + if [ ! -d ".git" ]; then + info "首次部署,克隆仓库..." + if [ "$(ls -A ${PROJECT_DIR} 2>/dev/null)" ]; then + info "目录非空,先备份现有文件..." + mkdir -p "${PROJECT_DIR}/backup_$(date +%Y%m%d_%H%M%S)_non_git" + find "${PROJECT_DIR}" -maxdepth 1 -type f -o -type d ! -name '.' -exec mv {} "${PROJECT_DIR}/backup_$(date +%Y%m%d_%H%M%S)_non_git/" \; 2>/dev/null || true + fi + 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") + success "✓ 代码更新完成,当前版本:${commit_hash} - ${commit_msg}" +} + +# ==================== 构建 Docker 镜像 ==================== +build_docker_image() { + info "========== 构建 Docker 镜像 ==========" + + cd "${PROJECT_DIR}" + + # 检查 Dockerfile 是否存在 + if [ ! -f "Dockerfile" ]; then + error "Dockerfile 不存在,无法构建镜像" + exit 1 + fi + + # 构建后端镜像 + info "构建后端 Docker 镜像..." + docker build -t ${DOCKER_IMAGE_NAME}-backend:${DOCKER_TAG} \ + -f Dockerfile.backend . \ + --build-arg DATABASE_HOST=${DATABASE_HOST} \ + --build-arg DATABASE_PORT=${DATABASE_PORT} \ + --build-arg SECRET_KEY=${SECRET_KEY} \ + --build-arg DEBUG=${DEBUG} + + # 构建前端镜像(如果有) + if [ -d "frontend" ] && [ -f "Dockerfile.frontend" ]; then + info "构建前端 Docker 镜像..." + docker build -t ${DOCKER_IMAGE_NAME}-frontend:${DOCKER_TAG} \ + -f Dockerfile.frontend . + fi + + # 推送到私有仓库(如果配置了) + if [ -n "${DOCKER_REGISTRY}" ]; then + info "推送镜像到私有仓库:${DOCKER_REGISTRY}" + docker tag ${DOCKER_IMAGE_NAME}-backend:${DOCKER_TAG} \ + ${DOCKER_REGISTRY}/${DOCKER_IMAGE_NAME}-backend:${DOCKER_TAG} + docker push ${DOCKER_REGISTRY}/${DOCKER_IMAGE_NAME}-backend:${DOCKER_TAG} + + if [ -d "frontend" ] && [ -f "Dockerfile.frontend" ]; then + docker tag ${DOCKER_IMAGE_NAME}-frontend:${DOCKER_TAG} \ + ${DOCKER_REGISTRY}/${DOCKER_IMAGE_NAME}-frontend:${DOCKER_TAG} + docker push ${DOCKER_REGISTRY}/${DOCKER_IMAGE_NAME}-frontend:${DOCKER_TAG} + fi + fi + + success "✓ Docker 镜像构建完成" +} + +# ==================== 停止旧容器 ==================== +stop_old_containers() { + info "========== 停止旧容器 ==========" + + # 停止并删除旧容器 + if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}-backend$"; then + info "停止后端容器:${CONTAINER_NAME}-backend" + docker stop "${CONTAINER_NAME}-backend" || true + docker rm "${CONTAINER_NAME}-backend" || true + fi + + if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}-frontend$"; then + info "停止前端容器:${CONTAINER_NAME}-frontend" + docker stop "${CONTAINER_NAME}-frontend" || true + docker rm "${CONTAINER_NAME}-frontend" || true + fi + + success "✓ 旧容器已清理" +} + +# ==================== 启动新容器 ==================== +start_new_containers() { + info "========== 启动新容器 ==========" + + # 创建网络(如果不存在) + if ! docker network ls --format '{{.Name}}' | grep -q "^${DOCKER_NETWORK}$"; then + info "创建 Docker 网络:${DOCKER_NETWORK}" + docker network create ${DOCKER_NETWORK} + fi + + # 启动后端容器 + info "启动后端容器..." + docker run -d \ + --name ${CONTAINER_NAME}-backend \ + --network ${DOCKER_NETWORK} \ + -p ${DOCKER_BACKEND_PORT}:8000 \ + -e DATABASE_URL="postgresql+asyncpg://${DATABASE_USER}:${DATABASE_PASSWORD}@${DATABASE_HOST}:${DATABASE_PORT}/${DATABASE_NAME}" \ + -e SECRET_KEY=${SECRET_KEY} \ + -e DEBUG=${DEBUG} \ + -v ${PROJECT_DIR}/backend/logs:/app/logs \ + --restart unless-stopped \ + ${DOCKER_IMAGE_NAME}-backend:${DOCKER_TAG} + + # 等待后端启动 + info "等待后端服务启动..." + sleep 10 + + # 启动前端容器(如果有) + if [ -d "frontend" ] && [ -f "Dockerfile.frontend" ]; then + info "启动前端容器..." + docker run -d \ + --name ${CONTAINER_NAME}-frontend \ + --network ${DOCKER_NETWORK} \ + -p ${DOCKER_FRONTEND_PORT}:80 \ + --link ${CONTAINER_NAME}-backend:backend \ + --restart unless-stopped \ + ${DOCKER_IMAGE_NAME}-frontend:${DOCKER_TAG} + fi + + success "✓ 容器启动成功" +} + +# ==================== 健康检查 ==================== +health_check() { + info "========== 执行健康检查 ==========" + + # 等待服务启动 + sleep 5 + + # 检查后端 API + local backend_url="http://localhost:${DOCKER_BACKEND_PORT}/api/v1/health" + local max_attempts=10 + local attempt=1 + + while [ $attempt -le $max_attempts ]; do + if curl -f -s "${backend_url}" > /dev/null 2>&1; then + success "✓ 后端 API 健康检查通过" + break + else + if [ $attempt -eq $max_attempts ]; then + error "✗ 后端 API 健康检查失败(尝试 ${attempt}/${max_attempts})" + exit 1 + fi + info "后端服务未就绪,等待 5 秒后重试... (${attempt}/${max_attempts})" + sleep 5 + attempt=$((attempt + 1)) + fi + done + + # 检查容器状态 + if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}-backend$"; then + success "✓ 后端容器运行正常" + else + error "✗ 后端容器未运行" + exit 1 + fi + + if [ -d "frontend" ] && [ -f "Dockerfile.frontend" ]; then + if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}-frontend$"; then + success "✓ 前端容器运行正常" + fi + fi + + info "✓ 所有健康检查通过" +} + +# ==================== 清理旧镜像 ==================== +cleanup_old_images() { + info "========== 清理旧 Docker 镜像 ==========" + + # 保留最近 3 个版本的镜像 + docker images ${DOCKER_IMAGE_NAME}-backend --format "{{.Tag}}" | \ + tail -n +4 | xargs -r docker rmi 2>/dev/null || true + + if [ -f "Dockerfile.frontend" ]; then + docker images ${DOCKER_IMAGE_NAME}-frontend --format "{{.Tag}}" | \ + tail -n +4 | xargs -r docker rmi 2>/dev/null || true + fi + + # 清理悬空镜像 + docker image prune -f + + info "✓ 清理完成" +} + +# ==================== 主流程 ==================== +main() { + info "========================================" + info "Spug Docker 自动部署开始" + info "项目名称:${PROJECT_NAME}" + info "部署目录:${PROJECT_DIR}" + info "Git 分支:${GIT_BRANCH}" + info "Docker 镜像:${DOCKER_IMAGE_NAME}:${DOCKER_TAG}" + info "========================================" + + pre_check + update_code + build_docker_image + stop_old_containers + start_new_containers + health_check + cleanup_old_images + + info "========================================" + success "🎉 Docker 部署成功完成!" + info "后端地址:http://localhost:${DOCKER_BACKEND_PORT}" + if [ -d "frontend" ] && [ -f "Dockerfile.frontend" ]; then + info "前端地址:http://localhost:${DOCKER_FRONTEND_PORT}" + fi + info "========================================" +} + +main "$@"