BodyBalanceEvaluation/backend/main.py

412 lines
15 KiB
Python
Raw Normal View History

2025-07-28 11:59:56 +08:00
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
身体平衡评估系统 - 主启动脚本
这个脚本负责启动整个应用程序包括后端服务和前端界面
支持开发模式和生产模式
"""
import os
import sys
import time
import signal
import subprocess
import threading
import webbrowser
import logging
from pathlib import Path
# 添加项目根目录到Python路径
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
sys.path.insert(0, str(project_root / 'backend'))
from utils import Config, Logger
class ApplicationLauncher:
"""应用程序启动器"""
def __init__(self):
self.config = Config()
# 设置日志
Logger.setup_logging('INFO', 'logs/app.log')
self.logger = logging.getLogger('main')
self.backend_process = None
self.frontend_process = None
self.running = False
# 设置信号处理
signal.signal(signal.SIGINT, self._signal_handler)
signal.signal(signal.SIGTERM, self._signal_handler)
def _signal_handler(self, signum, frame):
"""信号处理器"""
self.logger.info(f"接收到信号 {signum},正在关闭应用程序...")
self.stop()
def check_dependencies(self):
"""检查依赖项"""
self.logger.info("检查系统依赖项...")
# 检查Python版本
if sys.version_info < (3, 8):
self.logger.error("需要Python 3.8或更高版本")
return False
# 检查必要的Python包
required_packages = [
'flask', 'flask_cors', 'flask_socketio',
'numpy', 'pandas', 'opencv-python',
'sqlite3'
]
missing_packages = []
for package in required_packages:
try:
if package == 'sqlite3':
import sqlite3
elif package == 'opencv-python':
import cv2
elif package == 'flask_cors':
import flask_cors
elif package == 'flask_socketio':
import flask_socketio
else:
__import__(package)
except ImportError:
missing_packages.append(package)
if missing_packages:
self.logger.error(f"缺少必要的Python包: {', '.join(missing_packages)}")
self.logger.info("请运行: pip install -r backend/requirements.txt")
return False
# 检查Node.js和npm用于前端开发
if self.config.get('APP', 'mode', 'development') == 'development':
try:
result = subprocess.run(['node', '--version'],
capture_output=True, text=True)
if result.returncode != 0:
self.logger.warning("未找到Node.js将跳过前端开发服务器")
else:
self.logger.info(f"Node.js版本: {result.stdout.strip()}")
except FileNotFoundError:
self.logger.warning("未找到Node.js将跳过前端开发服务器")
self.logger.info("依赖项检查完成")
return True
def setup_directories(self):
"""设置必要的目录"""
self.logger.info("设置应用程序目录...")
directories = [
'data',
'data/patients',
'data/sessions',
'data/exports',
'data/backups',
'logs',
'temp'
]
for directory in directories:
dir_path = project_root / directory
dir_path.mkdir(parents=True, exist_ok=True)
self.logger.debug(f"创建目录: {dir_path}")
self.logger.info("目录设置完成")
def start_backend(self):
"""启动后端服务"""
self.logger.info("启动后端服务...")
backend_script = project_root / 'backend' / 'app.py'
if not backend_script.exists():
self.logger.error(f"后端脚本不存在: {backend_script}")
return False
try:
# 设置环境变量
env = os.environ.copy()
env['PYTHONPATH'] = str(project_root)
env['FLASK_APP'] = str(backend_script)
if self.config.get('APP', 'mode', 'development') == 'development':
env['FLASK_ENV'] = 'development'
env['FLASK_DEBUG'] = '1'
else:
env['FLASK_ENV'] = 'production'
env['FLASK_DEBUG'] = '0'
# 启动后端进程
cmd = [
sys.executable,
str(backend_script),
'--host', self.config.get('SERVER', 'host', '127.0.0.1'),
'--port', self.config.get('SERVER', 'port', '5000')
]
self.backend_process = subprocess.Popen(
cmd,
env=env,
cwd=str(project_root),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
self.logger.info(f"后端服务已启动 (PID: {self.backend_process.pid})")
# 等待后端服务启动
self._wait_for_backend()
return True
except Exception as e:
self.logger.error(f"启动后端服务失败: {e}")
return False
def _wait_for_backend(self, timeout=30):
"""等待后端服务启动"""
import requests
backend_url = f"http://{self.config.get('SERVER', 'host', '127.0.0.1')}:{self.config.get('SERVER', 'port', '5000')}"
health_url = f"{backend_url}/api/health"
self.logger.info("等待后端服务启动...")
start_time = time.time()
while time.time() - start_time < timeout:
try:
response = requests.get(health_url, timeout=2)
if response.status_code == 200:
self.logger.info("后端服务已就绪")
return True
except requests.exceptions.RequestException:
pass
time.sleep(1)
self.logger.warning("后端服务启动超时")
return False
def start_frontend_dev(self):
"""启动前端开发服务器"""
if self.config.get('APP', 'mode', 'development') != 'development':
return True
self.logger.info("启动前端开发服务器...")
frontend_dir = project_root / 'frontend' / 'src' / 'renderer'
if not frontend_dir.exists():
self.logger.warning("前端目录不存在,跳过前端开发服务器")
return True
package_json = frontend_dir / 'package.json'
if not package_json.exists():
self.logger.warning("package.json不存在跳过前端开发服务器")
return True
try:
# 检查是否已安装依赖
node_modules = frontend_dir / 'node_modules'
if not node_modules.exists():
self.logger.info("安装前端依赖...")
install_process = subprocess.run(
['npm', 'install'],
cwd=str(frontend_dir),
capture_output=True,
text=True,
shell=True
)
if install_process.returncode != 0:
self.logger.error(f"安装前端依赖失败: {install_process.stderr}")
return False
# 启动开发服务器
self.frontend_process = subprocess.Popen(
['npm', 'run', 'dev'],
cwd=str(frontend_dir),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
shell=True
)
self.logger.info(f"前端开发服务器已启动 (PID: {self.frontend_process.pid})")
return True
except Exception as e:
self.logger.error(f"启动前端开发服务器失败: {e}")
return False
def open_browser(self):
"""打开浏览器"""
if not self.config.getboolean('APP', 'auto_open_browser', True):
return
if self.config.get('APP', 'mode', 'development') == 'development':
# 开发模式下打开前端开发服务器
url = "http://localhost:3000" # Vite配置端口
else:
# 生产模式下打开后端服务
url = f"http://{self.config.get('SERVER', 'host', '127.0.0.1')}:{self.config.get('SERVER', 'port', '5000')}"
def delayed_open():
time.sleep(3) # 等待服务启动
try:
webbrowser.open(url)
self.logger.info(f"已打开浏览器: {url}")
except Exception as e:
self.logger.warning(f"打开浏览器失败: {e}")
threading.Thread(target=delayed_open, daemon=True).start()
def monitor_processes(self):
"""监控子进程"""
while self.running:
try:
# 检查后端进程
if self.backend_process and self.backend_process.poll() is not None:
self.logger.error("后端进程意外退出")
if self.backend_process.returncode != 0:
stderr = self.backend_process.stderr.read()
if stderr:
self.logger.error(f"后端错误: {stderr}")
self.running = False
break
# 检查前端进程
if (self.frontend_process and
self.frontend_process.poll() is not None and
self.config.get('APP', 'mode', 'development') == 'development'):
self.logger.warning("前端开发服务器意外退出")
time.sleep(5)
except Exception as e:
self.logger.error(f"进程监控错误: {e}")
break
def start(self):
"""启动应用程序"""
self.logger.info("=" * 50)
self.logger.info("身体平衡评估系统启动中...")
self.logger.info(f"模式: {self.config.get('APP', 'mode', 'development')}")
self.logger.info("=" * 50)
# 检查依赖项
if not self.check_dependencies():
return False
# 设置目录
self.setup_directories()
# 启动后端服务
if not self.start_backend():
return False
# 启动前端开发服务器(仅开发模式)
if not self.start_frontend_dev():
self.logger.warning("前端开发服务器启动失败,但继续运行")
self.running = True
# 打开浏览器
self.open_browser()
# 启动进程监控
monitor_thread = threading.Thread(target=self.monitor_processes, daemon=True)
monitor_thread.start()
self.logger.info("应用程序启动完成")
self.logger.info(f"后端服务: http://{self.config.get('SERVER', 'host', '127.0.0.1')}:{self.config.get('SERVER', 'port', '5000')}")
if self.config.get('APP', 'mode', 'development') == 'development':
self.logger.info("前端开发服务器: http://localhost:3000")
self.logger.info("按 Ctrl+C 退出应用程序")
# 主循环
try:
while self.running:
time.sleep(1)
except KeyboardInterrupt:
self.logger.info("接收到中断信号")
self.stop()
return True
def stop(self):
"""停止应用程序"""
if not self.running:
return
self.logger.info("正在停止应用程序...")
self.running = False
# 停止前端进程
if self.frontend_process:
try:
self.frontend_process.terminate()
self.frontend_process.wait(timeout=5)
self.logger.info("前端开发服务器已停止")
except subprocess.TimeoutExpired:
self.frontend_process.kill()
self.logger.warning("强制终止前端开发服务器")
except Exception as e:
self.logger.error(f"停止前端服务器失败: {e}")
# 停止后端进程
if self.backend_process:
try:
self.backend_process.terminate()
self.backend_process.wait(timeout=10)
self.logger.info("后端服务已停止")
except subprocess.TimeoutExpired:
self.backend_process.kill()
self.logger.warning("强制终止后端服务")
except Exception as e:
self.logger.error(f"停止后端服务失败: {e}")
self.logger.info("应用程序已停止")
def main():
"""主函数"""
import argparse
parser = argparse.ArgumentParser(description='身体平衡评估系统')
parser.add_argument('--mode', choices=['development', 'production'],
default='development', help='运行模式')
parser.add_argument('--host', default='127.0.0.1', help='服务器主机')
parser.add_argument('--port', type=int, default=5000, help='服务器端口')
parser.add_argument('--no-browser', action='store_true', help='不自动打开浏览器')
parser.add_argument('--log-level', choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'],
default='INFO', help='日志级别')
args = parser.parse_args()
# 更新配置
config = Config()
config.set('APP', 'mode', args.mode)
config.set('APP', 'auto_open_browser', str(not args.no_browser))
config.set('SERVER', 'host', args.host)
config.set('SERVER', 'port', str(args.port))
# 设置日志级别
import logging
logging.getLogger().setLevel(getattr(logging, args.log_level))
# 启动应用程序
launcher = ApplicationLauncher()
try:
success = launcher.start()
sys.exit(0 if success else 1)
except Exception as e:
print(f"启动失败: {e}")
sys.exit(1)
if __name__ == '__main__':
main()