#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 情绪博物馆 - 统一部署脚本 合并 deploy-all.sh / deploy-to-prod.sh / deploy-domain.sh 使用方法: python deploy.py # 部署所有服务 python deploy.py backend # 仅部署后端 python deploy.py frontend # 仅部署前端 python deploy.py admin # 仅部署管理后台 python deploy.py life-script # 仅部署 Life-Script python deploy.py ssl # 仅申请 SSL 证书 python deploy.py nginx # 仅部署 Nginx 配置 python deploy.py verify # 仅验证部署结果 python deploy.py all # 部署所有服务(同无参数) Author: Peanut Created: 2026-05-17 Purpose: 统一部署所有服务到生产服务器,替代原有的多个 shell 脚本 """ import io import os import sys import time import subprocess from pathlib import Path # 强制 stdout/stderr 使用 UTF-8 编码,避免 Windows GBK 编码错误 if hasattr(sys.stdout, 'buffer'): sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace', line_buffering=True) sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace', line_buffering=True) # ============================================================================ # 配置 # ============================================================================ SERVER_IP = "101.200.208.45" USERNAME = "root" DOMAIN = "lifescript.happylifeos.com" # 项目根目录(脚本所在目录) PROJECT_DIR = Path(__file__).parent.absolute() SSH_OPTS = f"-o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=no" # ============================================================================ # 日志 # ============================================================================ class Colors: RED = '\033[0;31m' GREEN = '\033[0;32m' YELLOW = '\033[1;33m' BLUE = '\033[0;34m' NC = '\033[0m' def log_info(msg): print(f"{Colors.GREEN}[INFO]{Colors.NC} {msg}") def log_warn(msg): print(f"{Colors.YELLOW}[WARN]{Colors.NC} {msg}") def log_error(msg): print(f"{Colors.RED}[ERROR]{Colors.NC} {msg}") def log_section(msg): print(f"\n{Colors.BLUE}{'=' * 60}{Colors.NC}") print(f"{Colors.BLUE}{msg}{Colors.NC}") print(f"{Colors.BLUE}{'=' * 60}{Colors.NC}\n") # ============================================================================ # 工具函数 # ============================================================================ def run_command(cmd, cwd=None, timeout=120, capture=True): """执行本地命令""" try: if capture: result = subprocess.run( cmd, shell=True, cwd=cwd, capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=timeout ) return result.returncode == 0, result.stdout.strip(), result.stderr.strip() else: result = subprocess.run( cmd, shell=True, cwd=cwd, timeout=timeout ) return result.returncode == 0, "", "" except subprocess.TimeoutExpired: return False, "", f"命令执行超时 ({timeout}s): {cmd}" except Exception as e: return False, "", str(e) def run_ssh_args(args, timeout=30, capture=True): """执行 ssh/scp 命令(使用列表参数,避免 Windows cmd.exe 找不到 ssh)""" try: if capture: result = subprocess.run( args, shell=False, cwd=PROJECT_DIR, capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=timeout ) return result.returncode == 0, result.stdout.strip(), result.stderr.strip() else: result = subprocess.run( args, shell=False, cwd=PROJECT_DIR, timeout=timeout ) return result.returncode == 0, "", "" except subprocess.TimeoutExpired: return False, "", f"命令执行超时 ({timeout}s): {' '.join(args)}" except Exception as e: return False, "", str(e) def ssh_command(cmd, timeout=30): """在远程服务器执行命令""" return run_ssh_args([ "ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=10", "-o", "StrictHostKeyChecking=no", f"{USERNAME}@{SERVER_IP}", cmd ], timeout=timeout) def scp_file(local, remote, timeout=120): """上传文件到远程服务器""" return run_ssh_args([ "scp", "-o", "BatchMode=yes", "-o", "ConnectTimeout=10", "-o", "StrictHostKeyChecking=no", local, f"{USERNAME}@{SERVER_IP}:{remote}" ], timeout=timeout) def check_ssh(): """检查 SSH 连接""" log_info("检查 SSH 连接...") success, stdout, stderr = ssh_command("echo ok", timeout=15) if not success: log_error(f"SSH 连接失败,请先配置免密登录: ssh {USERNAME}@{SERVER_IP}") if stderr: log_error(f"错误详情: {stderr}") sys.exit(1) log_info("SSH 连接正常") # ============================================================================ # Nginx 配置 # ============================================================================ def deploy_nginx(): """部署 Nginx 配置""" log_section("部署 Nginx 配置") nginx_conf = PROJECT_DIR / "conf" / "emotion-museum.conf" if not nginx_conf.exists(): log_error(f"Nginx 配置文件不存在: {nginx_conf}") return False log_info("上传 Nginx 配置文件...") remote_conf = f"/etc/nginx/sites-available/{DOMAIN}.conf" # 先创建远程目录 ssh_command("mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled", timeout=15) ok, _, err = scp_file(str(nginx_conf), remote_conf, timeout=60) if not ok: log_error(f"上传 Nginx 配置失败: {err}") return False log_info("启用站点配置...") ok, _, err = ssh_command( f"ln -snf {remote_conf} /etc/nginx/sites-enabled/{DOMAIN}.conf " f"&& rm -f /etc/nginx/sites-enabled/default" ) if not ok: log_error(f"启用站点失败: {err}") return False log_info("验证 Nginx 配置...") ok, _, err = ssh_command("nginx -t", timeout=15) if ok: ssh_command("systemctl reload nginx", timeout=30) log_info("Nginx 配置重载完成") return True else: log_error(f"Nginx 配置验证失败: {err}") return False # ============================================================================ # 后端 - 调用 backend-single/deploy.sh remote # ============================================================================ def deploy_backend(): """部署后端服务""" log_section("部署后端服务") deploy_script = PROJECT_DIR / "backend-single" / "deploy.py" if not deploy_script.exists(): log_error(f"后端部署脚本不存在: {deploy_script}") return False log_info("执行后端部署...") ok, _, err = run_command( "python3 deploy.py remote", cwd=str(PROJECT_DIR / "backend-single"), timeout=600, capture=False ) if ok: log_info("后端部署完成") return True else: log_error(f"后端部署失败: {err}") return False # ============================================================================ # 前端 - 调用 web/deploy.sh # ============================================================================ def deploy_frontend(): """部署用户前端""" log_section("部署用户前端") deploy_script = PROJECT_DIR / "web" / "deploy.py" if not deploy_script.exists(): log_error(f"前端部署脚本不存在: {deploy_script}") return False log_info("执行前端部署...") ok, _, err = run_command( "python3 deploy.py", cwd=str(PROJECT_DIR / "web"), timeout=600, capture=False ) if ok: log_info("前端部署完成") return True else: log_error(f"前端部署失败: {err}") return False # ============================================================================ # 管理后台 - 调用 web-admin/deploy.py # ============================================================================ def deploy_admin(): """部署管理后台""" log_section("部署管理后台") deploy_script = PROJECT_DIR / "web-admin" / "deploy.py" if not deploy_script.exists(): log_error(f"管理后台部署脚本不存在: {deploy_script}") return False log_info("执行管理后台部署...") ok, _, err = run_command( "python3 deploy.py", cwd=str(PROJECT_DIR / "web-admin"), timeout=600, capture=False ) if ok: log_info("管理后台部署完成") return True else: log_error(f"管理后台部署失败: {err}") return False # ============================================================================ # Life-Script - 调用 life-script/deploy.sh # ============================================================================ def deploy_life_script(): """部署 Life-Script""" log_section("部署 Life-Script") deploy_script = PROJECT_DIR / "life-script" / "deploy.py" if not deploy_script.exists(): log_error(f"Life-Script 部署脚本不存在: {deploy_script}") return False log_info("执行 Life-Script 部署...") ok, _, err = run_command( "python3 deploy.py", cwd=str(PROJECT_DIR / "life-script"), timeout=600, capture=False ) if ok: log_info("Life-Script 部署完成") return True else: log_error(f"Life-Script 部署失败: {err}") return False # ============================================================================ # 验证部署 # ============================================================================ def verify_deploy(): """验证部署结果""" log_section("验证部署") log_info("检查 HTTPS 访问...") endpoints = [ (f"https://{DOMAIN}/", "前端页面"), (f"https://{DOMAIN}/emotion-museum-admin/", "管理后台"), (f"https://{DOMAIN}/api/", "API 代理"), (f"http://{DOMAIN}/", "HTTP 跳转"), ] for url, label in endpoints: ok, stdout, _ = run_command( f'curl -k -s -o /dev/null -w "HTTP %{{http_code}}" {url}', timeout=30 ) if ok and stdout: log_info(f" {label}: {stdout}") else: log_warn(f" {label}: 访问异常") log_info("验证完成") return True # ============================================================================ # 主程序 # ============================================================================ def print_usage(): """打印使用说明""" print("情绪博物馆 - 统一部署脚本") print() print("使用方法:") print(" python deploy.py # 部署所有服务") print(" python deploy.py backend # 仅部署后端") print(" python deploy.py frontend # 仅部署前端") print(" python deploy.py admin # 仅部署管理后台") print(" python deploy.py life-script # 仅部署 Life-Script") print(" python deploy.py ssl # 仅申请 SSL 证书") print(" python deploy.py nginx # 仅部署 Nginx 配置") print(" python deploy.py verify # 仅验证部署结果") print(" python deploy.py all # 部署所有服务(同无参数)") def main(): deploy_type = sys.argv[1] if len(sys.argv) > 1 else "all" if deploy_type in ("--help", "-h", "help"): print_usage() return start_time = time.time() log_section("情绪博物馆 - 统一部署") log_info(f"部署类型: {deploy_type}") log_info(f"部署时间: {time.strftime('%Y-%m-%d %H:%M:%S')}") actions = { "backend": lambda: check_ssh() or True and deploy_backend(), "frontend": lambda: check_ssh() or True and deploy_frontend(), "admin": lambda: check_ssh() or True and deploy_admin(), "life-script": lambda: check_ssh() or True and deploy_life_script(), "nginx": lambda: check_ssh() or True and deploy_nginx(), "verify": lambda: verify_deploy(), "all": None, # handled separately } if deploy_type == "all": check_ssh() deploy_nginx() or True deploy_backend() deploy_frontend() deploy_admin() deploy_life_script() verify_deploy() elif deploy_type in actions: actions[deploy_type]() else: log_error(f"无效的部署类型: {deploy_type}") print("使用方法: python deploy.py [backend|frontend|admin|life-script|nginx|verify|all]") sys.exit(1) duration = int(time.time() - start_time) log_section(f"部署完成 (耗时 {duration}s)") log_info(f"用户前端: https://{DOMAIN}/") log_info(f"管理后台: https://{DOMAIN}/emotion-museum-admin/") log_info(f"Life-Script: https://{DOMAIN}/life-script/") log_info(f"API 地址: https://{DOMAIN}/api") log_info(f"WebSocket: wss://{DOMAIN}/ws") log_info(f"API 文档: https://{DOMAIN}/doc.html") log_info(f"Swagger UI: https://{DOMAIN}/swagger-ui/index.html") log_info(f"API Specs: https://{DOMAIN}/v3/api-docs") if __name__ == "__main__": main()