2025-11-17 12:56:43 +08:00
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
"""
|
|
|
|
|
|
KaMiXiTong 部署脚本
|
|
|
|
|
|
用于生产环境的一键部署
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
|
import sys
|
|
|
|
|
|
import subprocess
|
|
|
|
|
|
import argparse
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
|
def check_python_version():
|
|
|
|
|
|
"""检查Python版本"""
|
|
|
|
|
|
if sys.version_info < (3, 6):
|
|
|
|
|
|
print("❌ 错误: 需要Python 3.6或更高版本")
|
|
|
|
|
|
print(f"当前版本: {sys.version}")
|
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
print(f"✅ Python版本检查通过: {sys.version}")
|
|
|
|
|
|
|
|
|
|
|
|
def create_directories():
|
|
|
|
|
|
"""创建必要的目录"""
|
|
|
|
|
|
directories = [
|
|
|
|
|
|
'logs',
|
|
|
|
|
|
'static/uploads',
|
|
|
|
|
|
'static/css',
|
|
|
|
|
|
'static/js'
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
for directory in directories:
|
|
|
|
|
|
Path(directory).mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
print("✅ 目录结构创建完成")
|
|
|
|
|
|
|
|
|
|
|
|
def check_env_file():
|
|
|
|
|
|
"""检查环境配置文件"""
|
|
|
|
|
|
env_file = Path('.env')
|
|
|
|
|
|
if not env_file.exists():
|
|
|
|
|
|
print("⚠️ 未找到 .env 文件,正在创建默认配置...")
|
|
|
|
|
|
try:
|
|
|
|
|
|
with open('.env.example', 'r', encoding='utf-8') as src:
|
|
|
|
|
|
with open('.env', 'w', encoding='utf-8') as dst:
|
|
|
|
|
|
dst.write(src.read())
|
|
|
|
|
|
print("✅ 已创建默认 .env 文件")
|
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
|
|
print("❌ 未找到 .env.example 文件")
|
|
|
|
|
|
return False
|
|
|
|
|
|
else:
|
|
|
|
|
|
print("✅ 环境配置文件已存在")
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
def install_dependencies():
|
|
|
|
|
|
"""安装依赖"""
|
|
|
|
|
|
print("📦 正在安装依赖包...")
|
|
|
|
|
|
try:
|
|
|
|
|
|
subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-r', 'requirements.txt'])
|
|
|
|
|
|
|
|
|
|
|
|
# 检查并安装生产环境依赖
|
|
|
|
|
|
try:
|
|
|
|
|
|
subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'gunicorn'])
|
|
|
|
|
|
print("✅ Gunicorn安装完成")
|
|
|
|
|
|
except subprocess.CalledProcessError:
|
|
|
|
|
|
print("⚠️ Gunicorn安装失败")
|
|
|
|
|
|
|
|
|
|
|
|
print("✅ 依赖安装完成")
|
|
|
|
|
|
return True
|
|
|
|
|
|
except subprocess.CalledProcessError:
|
|
|
|
|
|
print("❌ 依赖安装失败")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def init_database():
|
|
|
|
|
|
"""初始化数据库"""
|
|
|
|
|
|
print("🗄️ 正在初始化数据库...")
|
|
|
|
|
|
|
|
|
|
|
|
# 检查使用哪种数据库
|
|
|
|
|
|
env_file = Path('.env')
|
|
|
|
|
|
if env_file.exists():
|
|
|
|
|
|
with open(env_file, 'r', encoding='utf-8') as f:
|
|
|
|
|
|
content = f.read()
|
|
|
|
|
|
if 'DATABASE_URL=sqlite' in content:
|
|
|
|
|
|
init_script = 'init_db_sqlite.py'
|
|
|
|
|
|
else:
|
|
|
|
|
|
init_script = 'setup_mysql.py'
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 默认使用SQLite
|
|
|
|
|
|
init_script = 'init_db_sqlite.py'
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
if os.path.exists(init_script):
|
|
|
|
|
|
subprocess.check_call([sys.executable, init_script])
|
|
|
|
|
|
print("✅ 数据库初始化完成")
|
|
|
|
|
|
return True
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"⚠️ 数据库初始化脚本 {init_script} 不存在")
|
|
|
|
|
|
return False
|
|
|
|
|
|
except subprocess.CalledProcessError as e:
|
|
|
|
|
|
print(f"❌ 数据库初始化失败: {e}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def setup_systemd_service():
|
|
|
|
|
|
"""配置Systemd服务"""
|
|
|
|
|
|
print("⚙️ 正在配置Systemd服务...")
|
|
|
|
|
|
|
|
|
|
|
|
# 获取当前路径
|
|
|
|
|
|
app_path = os.path.abspath('.')
|
|
|
|
|
|
venv_path = os.path.join(app_path, 'venv', 'bin', 'gunicorn')
|
|
|
|
|
|
|
|
|
|
|
|
# 如果没有虚拟环境,使用系统gunicorn
|
|
|
|
|
|
if not os.path.exists(venv_path):
|
|
|
|
|
|
venv_path = 'gunicorn'
|
|
|
|
|
|
|
2025-11-22 16:48:45 +08:00
|
|
|
|
# 从环境变量获取域名
|
|
|
|
|
|
frontend_domain = os.environ.get('FRONTEND_DOMAIN') or os.environ.get('DOMAIN_NAME') or os.environ.get('SERVER_NAME') or 'your-domain.com'
|
|
|
|
|
|
# 确保域名不包含协议部分
|
|
|
|
|
|
if frontend_domain.startswith(('http://', 'https://')):
|
|
|
|
|
|
frontend_domain = frontend_domain.split('://', 1)[1]
|
|
|
|
|
|
|
2025-11-17 12:56:43 +08:00
|
|
|
|
service_content = f"""[Unit]
|
|
|
|
|
|
Description=KaMiXiTong Service
|
|
|
|
|
|
After=network.target
|
|
|
|
|
|
|
|
|
|
|
|
[Service]
|
|
|
|
|
|
User=www-data
|
|
|
|
|
|
Group=www-data
|
|
|
|
|
|
WorkingDirectory={app_path}
|
|
|
|
|
|
Environment="FLASK_ENV=production"
|
2025-11-22 16:48:45 +08:00
|
|
|
|
Environment="DOMAIN_NAME={frontend_domain}"
|
2025-11-22 20:32:49 +08:00
|
|
|
|
ExecStart={venv_path} -w 4 -b 0.0.0.0:5088 run:app
|
2025-11-17 12:56:43 +08:00
|
|
|
|
Restart=always
|
|
|
|
|
|
RestartSec=10
|
|
|
|
|
|
|
|
|
|
|
|
[Install]
|
|
|
|
|
|
WantedBy=multi-user.target
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 写入服务文件
|
|
|
|
|
|
with open('/tmp/kamaxitong.service', 'w') as f:
|
|
|
|
|
|
f.write(service_content)
|
|
|
|
|
|
|
|
|
|
|
|
print("✅ Systemd服务配置文件已生成")
|
|
|
|
|
|
print("💡 请使用以下命令安装服务:")
|
|
|
|
|
|
print(" sudo cp /tmp/kamaxitong.service /etc/systemd/system/")
|
|
|
|
|
|
print(" sudo systemctl daemon-reload")
|
|
|
|
|
|
print(" sudo systemctl enable kamaxitong.service")
|
|
|
|
|
|
print(" sudo systemctl start kamaxitong.service")
|
|
|
|
|
|
return True
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"❌ Systemd服务配置失败: {e}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def setup_nginx_config():
|
|
|
|
|
|
"""配置Nginx"""
|
|
|
|
|
|
print("⚙️ 正在生成Nginx配置...")
|
|
|
|
|
|
|
|
|
|
|
|
# 从环境变量获取域名
|
2025-11-22 16:48:45 +08:00
|
|
|
|
frontend_domain = os.environ.get('FRONTEND_DOMAIN') or os.environ.get('DOMAIN_NAME') or os.environ.get('SERVER_NAME') or ''
|
2025-11-17 12:56:43 +08:00
|
|
|
|
if frontend_domain:
|
2025-11-22 16:48:45 +08:00
|
|
|
|
# 确保域名不包含协议部分
|
|
|
|
|
|
if frontend_domain.startswith(('http://', 'https://')):
|
2025-11-17 12:56:43 +08:00
|
|
|
|
server_name = frontend_domain.split('://', 1)[1]
|
|
|
|
|
|
else:
|
|
|
|
|
|
server_name = frontend_domain
|
|
|
|
|
|
else:
|
|
|
|
|
|
server_name = 'your-domain.com'
|
|
|
|
|
|
|
|
|
|
|
|
nginx_config = f"""server {{
|
|
|
|
|
|
listen 80;
|
|
|
|
|
|
server_name {server_name};
|
2025-11-22 16:48:45 +08:00
|
|
|
|
server_tokens off;
|
2025-11-17 12:56:43 +08:00
|
|
|
|
|
2025-11-22 16:48:45 +08:00
|
|
|
|
# ACME挑战目录(用于Let's Encrypt证书)
|
|
|
|
|
|
location /.well-known/acme-challenge/ {{
|
|
|
|
|
|
root /var/www/certbot;
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
# 重定向到HTTPS
|
|
|
|
|
|
location / {{
|
|
|
|
|
|
return 301 https://$server_name$request_uri;
|
|
|
|
|
|
}}
|
2025-11-17 12:56:43 +08:00
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
server {{
|
|
|
|
|
|
listen 443 ssl http2;
|
|
|
|
|
|
server_name {server_name};
|
2025-11-22 16:48:45 +08:00
|
|
|
|
server_tokens off;
|
2025-11-17 12:56:43 +08:00
|
|
|
|
|
|
|
|
|
|
# SSL证书配置(请替换为实际路径)
|
2025-11-22 16:48:45 +08:00
|
|
|
|
# ssl_certificate /etc/letsencrypt/live/{server_name}/fullchain.pem;
|
|
|
|
|
|
# ssl_certificate_key /etc/letsencrypt/live/{server_name}/privkey.pem;
|
|
|
|
|
|
|
|
|
|
|
|
# SSL安全配置
|
|
|
|
|
|
ssl_protocols TLSv1.2 TLSv1.3;
|
|
|
|
|
|
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384;
|
|
|
|
|
|
ssl_prefer_server_ciphers off;
|
|
|
|
|
|
ssl_session_cache shared:SSL:10m;
|
|
|
|
|
|
ssl_session_timeout 10m;
|
|
|
|
|
|
|
2025-11-17 12:56:43 +08:00
|
|
|
|
# 安全头配置
|
|
|
|
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
|
|
|
|
add_header X-XSS-Protection "1; mode=block" always;
|
|
|
|
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
|
|
|
|
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
|
|
|
|
|
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
|
2025-11-22 16:48:45 +08:00
|
|
|
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
2025-11-17 12:56:43 +08:00
|
|
|
|
|
|
|
|
|
|
# Gzip压缩
|
|
|
|
|
|
gzip on;
|
|
|
|
|
|
gzip_vary on;
|
|
|
|
|
|
gzip_min_length 1024;
|
2025-11-22 20:32:49 +08:00
|
|
|
|
gzip_proxied expired no-cache no-store private;
|
2025-11-17 12:56:43 +08:00
|
|
|
|
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss;
|
|
|
|
|
|
|
|
|
|
|
|
# 代理到Flask应用
|
|
|
|
|
|
location / {{
|
2025-11-22 16:48:45 +08:00
|
|
|
|
proxy_pass http://127.0.0.1:5088;
|
2025-11-17 12:56:43 +08:00
|
|
|
|
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;
|
|
|
|
|
|
proxy_set_header X-Forwarded-Host $server_name;
|
2025-11-22 16:48:45 +08:00
|
|
|
|
proxy_redirect off;
|
|
|
|
|
|
|
|
|
|
|
|
# 超时设置
|
|
|
|
|
|
proxy_connect_timeout 60s;
|
|
|
|
|
|
proxy_send_timeout 60s;
|
|
|
|
|
|
proxy_read_timeout 60s;
|
2025-11-17 12:56:43 +08:00
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
# 静态文件直接由Nginx处理
|
|
|
|
|
|
location /static/ {{
|
2025-11-22 16:48:45 +08:00
|
|
|
|
alias /var/www/kamaxitong/static/;
|
2025-11-17 12:56:43 +08:00
|
|
|
|
expires 1y;
|
|
|
|
|
|
add_header Cache-Control "public, immutable";
|
2025-11-22 16:48:45 +08:00
|
|
|
|
add_header Access-Control-Allow-Origin "*";
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
# 上传文件处理
|
|
|
|
|
|
location /static/uploads/ {{
|
|
|
|
|
|
alias /var/www/kamaxitong/static/uploads/;
|
|
|
|
|
|
expires 1d;
|
|
|
|
|
|
add_header Cache-Control "public";
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
# ACME挑战目录(用于Let's Encrypt证书)
|
|
|
|
|
|
location /.well-known/acme-challenge/ {{
|
|
|
|
|
|
root /var/www/certbot;
|
2025-11-17 12:56:43 +08:00
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
# 日志配置
|
|
|
|
|
|
access_log /var/log/nginx/kamaxitong.access.log;
|
|
|
|
|
|
error_log /var/log/nginx/kamaxitong.error.log;
|
2025-11-22 16:48:45 +08:00
|
|
|
|
|
|
|
|
|
|
# 错误页面
|
|
|
|
|
|
error_page 500 502 503 504 /50x.html;
|
|
|
|
|
|
location = /50x.html {{
|
|
|
|
|
|
root /usr/share/nginx/html;
|
|
|
|
|
|
}}
|
|
|
|
|
|
}}"""
|
2025-11-17 12:56:43 +08:00
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 写入配置文件
|
|
|
|
|
|
with open('/tmp/kamaxitong_nginx.conf', 'w') as f:
|
|
|
|
|
|
f.write(nginx_config)
|
|
|
|
|
|
|
|
|
|
|
|
print("✅ Nginx配置文件已生成")
|
|
|
|
|
|
print("💡 请使用以下命令安装配置:")
|
|
|
|
|
|
print(" sudo cp /tmp/kamaxitong_nginx.conf /etc/nginx/sites-available/kamaxitong")
|
|
|
|
|
|
print(" sudo ln -s /etc/nginx/sites-available/kamaxitong /etc/nginx/sites-enabled/")
|
|
|
|
|
|
print(" sudo nginx -t")
|
|
|
|
|
|
print(" sudo systemctl restart nginx")
|
|
|
|
|
|
return True
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"❌ Nginx配置生成失败: {e}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
|
|
"""主函数"""
|
|
|
|
|
|
parser = argparse.ArgumentParser(description='KaMiXiTong 部署脚本')
|
|
|
|
|
|
parser.add_argument('--skip-install', action='store_true', help='跳过依赖安装')
|
|
|
|
|
|
parser.add_argument('--skip-db', action='store_true', help='跳过数据库初始化')
|
|
|
|
|
|
parser.add_argument('--setup-service', action='store_true', help='配置Systemd服务')
|
|
|
|
|
|
parser.add_argument('--setup-nginx', action='store_true', help='生成Nginx配置')
|
|
|
|
|
|
|
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
|
|
|
|
print("=" * 50)
|
|
|
|
|
|
print(" KaMiXiTong 生产环境部署脚本")
|
|
|
|
|
|
print("=" * 50)
|
|
|
|
|
|
|
|
|
|
|
|
# 检查Python版本
|
|
|
|
|
|
check_python_version()
|
|
|
|
|
|
|
|
|
|
|
|
# 创建目录
|
|
|
|
|
|
create_directories()
|
|
|
|
|
|
|
|
|
|
|
|
# 检查环境配置
|
|
|
|
|
|
if not check_env_file():
|
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
|
|
# 安装依赖
|
|
|
|
|
|
if not args.skip_install:
|
|
|
|
|
|
if not install_dependencies():
|
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
else:
|
|
|
|
|
|
print("⏭️ 跳过依赖安装")
|
|
|
|
|
|
|
|
|
|
|
|
# 初始化数据库
|
|
|
|
|
|
if not args.skip_db:
|
|
|
|
|
|
if not init_database():
|
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
else:
|
|
|
|
|
|
print("⏭️ 跳过数据库初始化")
|
|
|
|
|
|
|
|
|
|
|
|
# 配置Systemd服务
|
|
|
|
|
|
if args.setup_service:
|
|
|
|
|
|
setup_systemd_service()
|
|
|
|
|
|
|
|
|
|
|
|
# 配置Nginx
|
|
|
|
|
|
if args.setup_nginx:
|
|
|
|
|
|
setup_nginx_config()
|
|
|
|
|
|
|
|
|
|
|
|
print("\n" + "=" * 50)
|
|
|
|
|
|
print("✅ 部署准备完成!")
|
|
|
|
|
|
print("=" * 50)
|
|
|
|
|
|
|
|
|
|
|
|
if args.setup_service:
|
|
|
|
|
|
print("💡 已生成Systemd服务配置文件,请按提示安装")
|
|
|
|
|
|
|
|
|
|
|
|
if args.setup_nginx:
|
|
|
|
|
|
print("💡 已生成Nginx配置文件,请按提示安装")
|
|
|
|
|
|
|
|
|
|
|
|
print("\n📌 下一步操作:")
|
|
|
|
|
|
print("1. 编辑 .env 文件,配置数据库和其他参数")
|
|
|
|
|
|
print("2. 配置SSL证书(推荐)")
|
|
|
|
|
|
print("3. 启动服务")
|
|
|
|
|
|
print("4. 访问你的域名")
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
|
|
main()
|