BodyBalanceEvaluation/backend/main.py
2025-07-28 11:59:56 +08:00

412 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()