重构:统一 Python 部署脚本并修复编码问题
- 新增 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/bin/bash
|
||||
# 向后兼容 wrapper - 调用统一的 deploy.sh
|
||||
# 原 deploy-all.sh 的参数 [backend|frontend|admin|all] 保持不变
|
||||
exec bash "$(dirname "$0")/deploy.sh" "$@"
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/bin/bash
|
||||
# 向后兼容 wrapper - 调用统一的 deploy.sh
|
||||
# 原 deploy-domain.sh 为无参数全量部署,等价于 deploy.sh all
|
||||
exec bash "$(dirname "$0")/deploy.sh" all
|
||||
@@ -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" "$@"
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
+10
-4
@@ -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地址是否正确")
|
||||
|
||||
Reference in New Issue
Block a user