616 lines
22 KiB
Python
616 lines
22 KiB
Python
#!/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 re
|
||
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)
|
||
|
||
# 当前 Python 可执行文件(Windows 用 python.exe,Linux/Mac 用 python3)
|
||
PYTHON = sys.executable
|
||
|
||
|
||
# ============================================================================
|
||
# 环境变量修复(Windows 专用)
|
||
# ============================================================================
|
||
def _build_full_path():
|
||
"""构造包含所有关键目录的完整 PATH 字符串。
|
||
|
||
Windows cmd.exe 启动时会用自己的"标准 PATH"(只有 2 段用户 PATH),
|
||
完全忽略父进程传入的 PATH 变量。解决办法是在每个 cmd.exe 命令前
|
||
用 `set "PATH=..."` 显式设置。这个函数返回要设置的完整 PATH。
|
||
"""
|
||
if sys.platform != 'win32':
|
||
return None
|
||
parts = [
|
||
r'C:\Windows\system32',
|
||
r'C:\Windows',
|
||
r'C:\Windows\System32\Wbem',
|
||
r'C:\Windows\System32\OpenSSH',
|
||
]
|
||
# 从已知环境变量自动推断关键目录(不硬编码)
|
||
if os.environ.get('MAVEN_HOME'):
|
||
parts.append(os.environ['MAVEN_HOME'] + r'\bin')
|
||
if os.environ.get('JAVA_HOME'):
|
||
parts.append(os.environ['JAVA_HOME'] + r'\bin')
|
||
# Node.js 常见安装位置自动检测
|
||
for d in [r'C:\Program Files\nodejs', r'F:\Program Files\nodejs',
|
||
r'C:\Program Files (x86)\nodejs', r'F:\Programs\nvm']:
|
||
if Path(d).exists():
|
||
parts.append(d)
|
||
# 保留用户已有的 PATH(不丢失用户自定义工具)
|
||
user_path = os.environ.get('PATH', '')
|
||
for p in user_path.split(';'):
|
||
if p and p not in parts:
|
||
parts.append(p)
|
||
return ';'.join(parts)
|
||
|
||
|
||
def _wrap_cmd_for_windows(cmd):
|
||
"""为 Windows cmd.exe 命令包装 set PATH 头部,解决 PATH 截断问题。
|
||
|
||
原因:cmd.exe 启动后 PATH 只有 2 段(pnpm/npm),找不到 mvn/node/npm。
|
||
修复:在每个命令前用 `set "PATH=完整路径" &&` 显式设置 PATH。
|
||
"""
|
||
if sys.platform != 'win32':
|
||
return cmd
|
||
full_path = _build_full_path()
|
||
return f'set "PATH={full_path}" && {cmd}'
|
||
|
||
|
||
def _ensure_env():
|
||
"""修复 Windows 子进程编码问题 + 准备 PATH 工具。
|
||
|
||
Windows cmd.exe 启动时强制用 GBK 编码输出(即使父进程是 UTF-8),
|
||
设置 PYTHONIOENCODING/PYTHONUTF8 强制 Python 子进程使用 UTF-8。
|
||
实际 PATH 修复在 _wrap_cmd_for_windows() 中按需包装每个命令。
|
||
"""
|
||
if sys.platform != 'win32':
|
||
return
|
||
# 强制 Python 子进程使用 UTF-8 编码
|
||
os.environ.setdefault('PYTHONIOENCODING', 'utf-8')
|
||
os.environ.setdefault('PYTHONUTF8', '1')
|
||
os.environ.setdefault('PYTHONLEGACYWINDOWSSTDIO', '0')
|
||
|
||
|
||
# ============================================================================
|
||
# 配置
|
||
# ============================================================================
|
||
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):
|
||
"""执行本地命令(自动处理 Windows cmd.exe PATH 截断)"""
|
||
# 设置 PYTHONUNBUFFERED=1 确保 Python 子进程不缓冲 stdout
|
||
env = os.environ.copy()
|
||
env['PYTHONUNBUFFERED'] = '1'
|
||
# Windows 上包装 set PATH 前缀,解决 cmd.exe PATH 截断问题
|
||
effective_cmd = _wrap_cmd_for_windows(cmd)
|
||
try:
|
||
if capture:
|
||
result = subprocess.run(
|
||
effective_cmd, shell=True, cwd=cwd, env=env,
|
||
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(
|
||
effective_cmd, shell=True, cwd=cwd, env=env, 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 站点启用(防御性诊断 + 清理)
|
||
# ============================================================================
|
||
|
||
# 变量安全校验正则
|
||
_SAFE_DOMAIN = re.compile(r'^[a-zA-Z0-9.-]+$')
|
||
_SAFE_REMOTE_CONF = re.compile(r'^[a-zA-Z0-9./_-]+$')
|
||
|
||
|
||
def _validate_nginx_inputs(remote_conf: str, domain: str) -> bool:
|
||
"""校验 domain 和 remote_conf 防止 shell 注入和路径遍历。
|
||
|
||
Returns: True 合法;False 非法(已 log_error)
|
||
"""
|
||
if not domain or not _SAFE_DOMAIN.match(domain):
|
||
log_error(f"非法 domain 名称: {domain!r}")
|
||
return False
|
||
if domain.startswith('-') or domain.endswith('-') \
|
||
or domain.startswith('.') or domain.endswith('.') \
|
||
or '..' in domain:
|
||
log_error(f"非法 domain 结构: {domain!r} (首尾连字符/点 或 连续点非法)")
|
||
return False
|
||
if not remote_conf or not _SAFE_REMOTE_CONF.match(remote_conf):
|
||
log_error(f"非法 remote_conf 路径: {remote_conf!r}")
|
||
return False
|
||
if '//' in remote_conf or '..' in remote_conf:
|
||
log_error(f"remote_conf 包含可疑路径片段: {remote_conf!r}")
|
||
return False
|
||
return True
|
||
|
||
|
||
def enable_nginx_site(remote_conf: str, domain: str) -> bool:
|
||
"""幂等地启用 nginx 站点(sites-enabled symlink)。
|
||
|
||
流程:
|
||
1. 诊断目标路径当前状态(symlink/dir/file/missing)
|
||
2. 清理异常状态(目录/文件/断链);失败短路
|
||
3. 二次检查 + 创建 symlink
|
||
4. 移除 default 站点(失败仅 warning)
|
||
5. 验证 symlink + default 状态
|
||
|
||
Returns: True 成功;False 失败(stderr 已打印)
|
||
"""
|
||
if not _validate_nginx_inputs(remote_conf, domain):
|
||
return False
|
||
|
||
target = f"/etc/nginx/sites-enabled/{domain}.conf"
|
||
log_info(f"启用 nginx 站点: {target} -> {remote_conf}")
|
||
|
||
# ===== 步骤 1: 诊断目标路径状态 =====
|
||
log_info("诊断步骤: 检查目标路径...")
|
||
diagnose_cmd = (
|
||
f'test -e {target} && '
|
||
f'(test -L {target} && echo "symlink" || '
|
||
f'(test -d {target} && echo "dir" || echo "file")) || '
|
||
f'echo "missing"'
|
||
)
|
||
ok, status, err = ssh_command(diagnose_cmd, timeout=30)
|
||
if not ok:
|
||
log_error(f"诊断步骤失败: {err}")
|
||
return False
|
||
status = status.strip()
|
||
log_info(f"诊断结果: {status}")
|
||
|
||
# ===== 步骤 2: 清理异常状态 =====
|
||
log_info("清理步骤: 处理异常状态...")
|
||
cleanup_cmd = (
|
||
f'case "{status}" in '
|
||
f'symlink) unlink {target} ;; '
|
||
f'dir) rm -rf {target} ;; '
|
||
f'file) rm -f {target} ;; '
|
||
f'missing) ;; '
|
||
f'esac'
|
||
)
|
||
ok, _, err = ssh_command(cleanup_cmd, timeout=30)
|
||
if not ok:
|
||
log_error(f"清理步骤失败: {err}")
|
||
log_error("上下文: 目标 = " + target)
|
||
log_error("提示: 权限可能不足(检查 /etc/nginx/sites-enabled 写权限)")
|
||
return False
|
||
log_info("清理步骤完成")
|
||
|
||
# ===== 步骤 3: 建链(两段式:先二次检查,再 ln -snf) =====
|
||
log_info("建链步骤: 创建 symlink...")
|
||
# 第一段:二次检查 target 不存在(防并发进程干扰)
|
||
guard_cmd = f'if [ -e {target} ]; then echo "二次检查失败:target 仍存在" >&2; exit 1; fi'
|
||
ok, _, err = ssh_command(guard_cmd, timeout=30)
|
||
if not ok:
|
||
log_error(f"建链步骤失败 - 二次检查: {err}")
|
||
log_error("提示: 并发进程干扰(其他进程在步骤 2 后、步骤 3 前创建了同名项)")
|
||
log_error("上下文: 目标 = " + target)
|
||
return False
|
||
# 第二段:执行 ln -snf(独立 SSH 调用,错误信息不会被二次检查误报)
|
||
link_cmd = f'ln -snf {remote_conf} {target}'
|
||
ok, _, err = ssh_command(link_cmd, timeout=30)
|
||
if not ok:
|
||
log_error(f"建链步骤失败 - ln 执行: {err}")
|
||
log_error("提示: 确认 scp_file 是否成功上传了 sites-available 下的 conf 文件")
|
||
log_error("上下文: 目标 = " + target)
|
||
return False
|
||
log_info("建链步骤完成")
|
||
|
||
# ===== 步骤 4: 清理 default 站点(失败仅 warning) =====
|
||
log_info("清默认步骤: 移除 default 站点...")
|
||
ok, _, err = ssh_command("rm -f /etc/nginx/sites-enabled/default", timeout=30)
|
||
if not ok:
|
||
log_warn(f"清默认步骤失败(不阻塞): {err}")
|
||
log_warn("提示: default 文件可能仍存在,nginx 可能有端口冲突;步骤 5 验证会确认")
|
||
else:
|
||
log_info("清默认步骤完成")
|
||
|
||
# ===== 步骤 5: 验证结果 =====
|
||
log_info("验证步骤: 检查 symlink 和 default 状态...")
|
||
verify_cmd = (
|
||
f'readlink {target} && '
|
||
f'(test ! -e /etc/nginx/sites-enabled/default && echo "default:OK" || echo "default:STILL_EXISTS") && '
|
||
f'ls -la /etc/nginx/sites-enabled/'
|
||
)
|
||
ok, stdout, err = ssh_command(verify_cmd, timeout=30)
|
||
if not ok:
|
||
log_error(f"验证步骤失败: {err}")
|
||
log_error(f"实际输出: {stdout}")
|
||
return False
|
||
log_info("验证结果:")
|
||
for line in stdout.split('\n'):
|
||
if line.strip():
|
||
log_info(f" {line}")
|
||
log_info("启用站点配置完成")
|
||
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("启用站点配置...")
|
||
if not enable_nginx_site(remote_conf, DOMAIN):
|
||
log_error("启用站点失败(详见 enable_nginx_site 内部日志)")
|
||
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(
|
||
f"{PYTHON} 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(
|
||
f"{PYTHON} 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(
|
||
f"{PYTHON} 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(
|
||
f"{PYTHON} 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():
|
||
# 修复 Windows cmd.exe 子进程环境块截断问题
|
||
_ensure_env()
|
||
|
||
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}/api/doc.html")
|
||
log_info(f"Swagger UI: https://{DOMAIN}/api/swagger-ui/index.html")
|
||
log_info(f"API Specs: https://{DOMAIN}/api/v3/api-docs")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|