From 06b2e168136a4de8242ac9e0f3d7265d12b14dbd Mon Sep 17 00:00:00 2001 From: Peanut Date: Sun, 17 May 2026 18:09:34 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=EF=BC=9A=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=20Python=20=E9=83=A8=E7=BD=B2=E8=84=9A=E6=9C=AC=E5=B9=B6?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=BC=96=E7=A0=81=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 deploy.py 统一部署脚本(调用各子目录 .py 脚本) - 保留 deploy.sh 统一部署脚本(调用各子目录 .sh 脚本) - 删除旧的 deploy-all.sh / deploy-domain.sh / deploy-to-prod.sh - 修复 Windows GBK 编码导致的 UnicodeDecodeError/UnicodeEncodeError - 修复 nginx 远程目录自动创建 - 移除 backend-single/deploy.py 和 web/deploy.py 中的 emoji 字符 Co-Authored-By: Claude Opus 4.7 --- backend-single/deploy.py | 14 +- deploy-all.sh | 5 - deploy-domain.sh | 4 - deploy-to-prod.sh | 4 - deploy.py | 408 +++++++++++++++++++++++++++++++++++++++ deploy.sh | 6 +- web/deploy.py | 14 +- 7 files changed, 431 insertions(+), 24 deletions(-) delete mode 100755 deploy-all.sh delete mode 100644 deploy-domain.sh delete mode 100644 deploy-to-prod.sh create mode 100644 deploy.py diff --git a/backend-single/deploy.py b/backend-single/deploy.py index c1fe6e4..df18541 100755 --- a/backend-single/deploy.py +++ b/backend-single/deploy.py @@ -6,11 +6,17 @@ 使用系统自带的ssh/scp命令,无需额外依赖 """ +import io import os import sys 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') + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace') + # 配置变量 APP_NAME = "emotion-museum-single" JAR_NAME = "backend-single-1.0.0.jar" @@ -101,7 +107,7 @@ def build_project(): sys.exit(1) file_size = JAR_PATH.stat().st_size / (1024 * 1024) - log_info(f"✅ 项目构建成功: {JAR_PATH}") + log_info(f"[OK] 项目构建成功: {JAR_PATH}") log_info(f"文件大小: {file_size:.2f} MB") @@ -139,7 +145,7 @@ def deploy(upload_script=None): if not scp_upload(JAR_PATH, f"{REMOTE_DIR}/{REMOTE_JAR_NAME}"): log_error("上传JAR文件失败") sys.exit(1) - log_info("✅ JAR文件上传成功") + log_info("[OK] JAR文件上传成功") # 验证远程文件 log_info("验证远程文件...") @@ -157,7 +163,7 @@ def deploy(upload_script=None): if not scp_upload(script_path, f"{REMOTE_DIR}/{upload_script}"): log_error(f"上传文件失败: {upload_script}") sys.exit(1) - log_info(f"✅ 文件上传成功: {upload_script}") + log_info(f"[OK] 文件上传成功: {upload_script}") if upload_script.endswith('.sh'): exec_ssh_cmd(f"chmod +x {REMOTE_DIR}/{upload_script}") else: @@ -176,7 +182,7 @@ def deploy(upload_script=None): log_error("远程部署脚本执行失败") sys.exit(1) - log_info("✅ 部署完成!") + log_info("[OK] 部署完成!") show_status() diff --git a/deploy-all.sh b/deploy-all.sh deleted file mode 100755 index 9d2056e..0000000 --- a/deploy-all.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -# 向后兼容 wrapper - 调用统一的 deploy.sh -# 原 deploy-all.sh 的参数 [backend|frontend|admin|all] 保持不变 -exec bash "$(dirname "$0")/deploy.sh" "$@" - diff --git a/deploy-domain.sh b/deploy-domain.sh deleted file mode 100644 index c3ed7b7..0000000 --- a/deploy-domain.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -# 向后兼容 wrapper - 调用统一的 deploy.sh -# 原 deploy-domain.sh 为无参数全量部署,等价于 deploy.sh all -exec bash "$(dirname "$0")/deploy.sh" all diff --git a/deploy-to-prod.sh b/deploy-to-prod.sh deleted file mode 100644 index dd65fb3..0000000 --- a/deploy-to-prod.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -# 向后兼容 wrapper - 调用统一的 deploy.sh -# 原 deploy-to-prod.sh 的参数 [ssl|backend|frontend|admin|life-script|nginx|all] 保持不变 -exec bash "$(dirname "$0")/deploy.sh" "$@" diff --git a/deploy.py b/deploy.py new file mode 100644 index 0000000..156e63c --- /dev/null +++ b/deploy.py @@ -0,0 +1,408 @@ +#!/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') + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace') + +# ============================================================================ +# 配置 +# ============================================================================ +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 ssh_command(cmd, timeout=30): + """在远程服务器执行命令""" + full = f'ssh {SSH_OPTS} {USERNAME}@{SERVER_IP} "{cmd}"' + return run_command(full, timeout=timeout) + + +def scp_file(local, remote, timeout=120): + """上传文件到远程服务器""" + full = f'scp {SSH_OPTS} "{local}" {USERNAME}@{SERVER_IP}:"{remote}"' + return run_command(full, 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 连接正常") + + +# ============================================================================ +# SSL 证书 +# ============================================================================ + + +def deploy_ssl(): + """申请 SSL 证书""" + log_section("申请 SSL 证书") + ssl_script = PROJECT_DIR / "tools" / "deploy-ssl-cert.py" + if not ssl_script.exists(): + log_error(f"SSL 证书脚本不存在: {ssl_script}") + return False + + log_info("上传并执行 SSL 证书脚本...") + ok, _, err = scp_file(str(ssl_script), "/tmp/deploy-ssl-cert.py", timeout=120) + if not ok: + log_error(f"上传 SSL 脚本失败: {err}") + return False + + ok, _, err = ssh_command("python3 /tmp/deploy-ssl-cert.py", timeout=300) + if not ok: + log_error(f"执行 SSL 脚本失败: {err}") + return False + + log_info("SSL 证书申请完成") + return True + + +# ============================================================================ +# 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 + ) + 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 + ) + 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 + ) + 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 + ) + 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 = { + "ssl": lambda: check_ssh() or True and deploy_ssl(), + "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_ssl() or True + 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 [ssl|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") + + +if __name__ == "__main__": + main() diff --git a/deploy.sh b/deploy.sh index 1edcbce..a451f43 100644 --- a/deploy.sh +++ b/deploy.sh @@ -137,13 +137,13 @@ deploy_frontend() { # ============================================================================ deploy_admin() { log_section "部署管理后台" - if [ ! -f "web-admin/deploy.py" ]; then - log_error "管理后台部署脚本不存在: web-admin/deploy.py" + if [ ! -f "web-admin/deploy.sh" ]; then + log_error "管理后台部署脚本不存在: web-admin/deploy.sh" return 1 fi log_info "执行管理后台部署..." cd web-admin - if python3 deploy.py; then + if bash deploy.sh; then cd .. log_info "✅ 管理后台部署完成" return 0 diff --git a/web/deploy.py b/web/deploy.py index 31e3b1d..b251c60 100644 --- a/web/deploy.py +++ b/web/deploy.py @@ -5,11 +5,17 @@ 使用方法: python deploy.py """ +import io import os import sys 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') + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace') + # 服务器配置 SERVER_IP = "101.200.208.45" USERNAME = "root" @@ -29,17 +35,17 @@ class Colors: def log_info(msg): """打印信息日志""" - print(f"{Colors.GREEN}✅{Colors.RESET} {msg}") + print(f"{Colors.GREEN}[OK]{Colors.RESET} {msg}") def log_error(msg): """打印错误日志""" - print(f"{Colors.RED}❌{Colors.RESET} {msg}") + print(f"{Colors.RED}[ERR]{Colors.RESET} {msg}") def log_step(msg): """打印步骤日志""" - print(f"📦 {msg}") + print(f"[STEP] {msg}") def run_command(cmd, cwd=None, shell=True, capture=True, timeout=None): @@ -160,7 +166,7 @@ def deploy(): # 上传文件 if upload_files(): log_info("部署完成!") - print(f"📱 访问地址: http://{SERVER_IP}/emotion-museum/") + print(f"访问地址: http://{SERVER_IP}/emotion-museum/") else: log_error("部署失败,请检查:") print("1. 服务器IP地址是否正确")