BodyBalanceEvaluation/backend/app.py

820 lines
27 KiB
Python
Raw Normal View History

2025-07-28 11:59:56 +08:00
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
平衡体态检测系统 - 后端服务
基于Flask的本地API服务
"""
import os
import sys
import json
import time
import threading
from datetime import datetime
from flask import Flask, request, jsonify, send_file
from flask_cors import CORS
import sqlite3
import logging
from pathlib import Path
2025-07-29 16:44:17 +08:00
from flask_socketio import SocketIO, emit
import cv2
import configparser
import os
2025-07-28 11:59:56 +08:00
# 添加当前目录到Python路径
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
# 导入自定义模块
from database import DatabaseManager
2025-07-31 17:23:05 +08:00
from device_manager import DeviceManager, VideoStreamManager
from detection_engine import DetectionEngine, detection_bp
2025-07-28 11:59:56 +08:00
from data_processor import DataProcessor
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('logs/backend.log', encoding='utf-8'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
# 创建Flask应用
app = Flask(__name__)
app.config['SECRET_KEY'] = 'body-balance-detection-system-2024'
2025-07-29 16:44:17 +08:00
socketio = SocketIO(app, cors_allowed_origins='*', async_mode='threading')
2025-07-28 11:59:56 +08:00
# 启用CORS支持
CORS(app, origins='*', supports_credentials=True, allow_headers=['Content-Type', 'Authorization'], methods=['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'])
2025-07-28 11:59:56 +08:00
2025-07-31 17:23:05 +08:00
# 注册Blueprint
app.register_blueprint(detection_bp)
2025-07-29 16:44:17 +08:00
# 读取RTSP配置
config = configparser.ConfigParser()
config.read(os.path.join(os.path.dirname(__file__), 'config.ini'), encoding='utf-8')
rtsp_url = config.get('CAMERA', 'rtsp_url', fallback=None)
2025-07-28 11:59:56 +08:00
# 全局变量
db_manager = None
device_manager = None
detection_engine = None
data_processor = None
current_detection = None
detection_thread = None
2025-07-31 17:23:05 +08:00
video_stream_manager = None
2025-07-30 13:47:47 +08:00
2025-07-28 11:59:56 +08:00
def init_app():
"""初始化应用"""
2025-07-31 17:23:05 +08:00
global db_manager, device_manager, detection_engine, data_processor, video_stream_manager
2025-07-28 11:59:56 +08:00
try:
# 创建必要的目录
os.makedirs('logs', exist_ok=True)
2025-07-31 17:23:05 +08:00
backend_data_dir = os.path.join(os.path.dirname(__file__), 'data')
os.makedirs(backend_data_dir, exist_ok=True)
os.makedirs(os.path.join(backend_data_dir, 'patients'), exist_ok=True)
2025-07-28 11:59:56 +08:00
os.makedirs('exports', exist_ok=True)
os.makedirs('videos', exist_ok=True)
2025-07-31 17:23:05 +08:00
# 初始化数据库 - 使用backend/data目录下的数据库
db_path = os.path.join(backend_data_dir, 'body_balance.db')
db_manager = DatabaseManager(db_path)
2025-07-28 11:59:56 +08:00
db_manager.init_database()
# 初始化设备管理器
device_manager = DeviceManager()
2025-07-31 17:23:05 +08:00
# 初始化视频流管理器
video_stream_manager = VideoStreamManager(socketio)
2025-07-28 11:59:56 +08:00
# 初始化检测引擎
detection_engine = DetectionEngine()
# 初始化数据处理器
data_processor = DataProcessor()
logger.info('应用初始化完成')
except Exception as e:
logger.error(f'应用初始化失败: {e}')
raise
# ==================== 基础API ====================
@app.route('/health', methods=['GET'])
def health_check():
"""健康检查"""
return jsonify({
'status': 'ok',
'timestamp': datetime.now().isoformat(),
'version': '1.0.0'
})
@app.route('/api/health', methods=['GET'])
def api_health_check():
"""API健康检查"""
return jsonify({
'status': 'ok',
'timestamp': datetime.now().isoformat(),
'version': '1.0.0'
})
# ==================== 认证API ====================
@app.route('/api/auth/login', methods=['POST'])
def login():
"""用户登录"""
try:
data = request.get_json()
username = data.get('username')
password = data.get('password')
remember = data.get('remember', False)
# 简单的模拟登录验证
if username and password:
# 这里可以添加真实的用户验证逻辑
# 目前使用模拟数据
user_data = {
'id': 1,
'username': username,
'name': '医生',
'role': 'doctor',
'avatar': ''
}
# 生成简单的token实际项目中应使用JWT等安全token
token = f"token_{username}_{int(time.time())}"
return jsonify({
'success': True,
'data': {
'token': token,
'user': user_data
},
'message': '登录成功'
})
else:
return jsonify({
'success': False,
'message': '用户名或密码不能为空'
}), 400
except Exception as e:
logger.error(f'登录失败: {e}')
return jsonify({'success': False, 'message': '登录失败'}), 500
@app.route('/api/auth/register', methods=['POST'])
def register():
"""用户注册"""
try:
data = request.get_json()
username = data.get('username')
password = data.get('password')
email = data.get('email')
# 简单的模拟注册
if username and password:
return jsonify({
'success': True,
'message': '注册成功,请登录'
})
else:
return jsonify({
'success': False,
'message': '用户名和密码不能为空'
}), 400
except Exception as e:
logger.error(f'注册失败: {e}')
return jsonify({'success': False, 'message': '注册失败'}), 500
@app.route('/api/auth/logout', methods=['POST'])
def logout():
"""用户退出"""
try:
return jsonify({
'success': True,
'message': '退出成功'
})
except Exception as e:
logger.error(f'退出失败: {e}')
return jsonify({'success': False, 'message': '退出失败'}), 500
@app.route('/api/auth/verify', methods=['GET'])
def verify_token():
"""验证token"""
try:
# 简单的token验证实际项目中应验证JWT等
auth_header = request.headers.get('Authorization')
if auth_header and auth_header.startswith('Bearer '):
token = auth_header.split(' ')[1]
# 这里可以添加真实的token验证逻辑
return jsonify({
'success': True,
'data': {'valid': True}
})
else:
return jsonify({
'success': True,
'data': {'valid': True} # 暂时总是返回有效
})
except Exception as e:
logger.error(f'验证token失败: {e}')
return jsonify({'success': False, 'message': '验证失败'}), 500
@app.route('/api/auth/forgot-password', methods=['POST'])
def forgot_password():
"""忘记密码"""
try:
data = request.get_json()
email = data.get('email')
if email:
return jsonify({
'success': True,
'message': '重置密码邮件已发送'
})
else:
return jsonify({
'success': False,
'message': '邮箱不能为空'
}), 400
except Exception as e:
logger.error(f'忘记密码处理失败: {e}')
return jsonify({'success': False, 'message': '处理失败'}), 500
@app.route('/api/auth/reset-password', methods=['POST'])
def reset_password():
"""重置密码"""
try:
data = request.get_json()
token = data.get('token')
password = data.get('password')
if token and password:
return jsonify({
'success': True,
'message': '密码重置成功'
})
else:
return jsonify({
'success': False,
'message': '参数不完整'
}), 400
except Exception as e:
logger.error(f'重置密码失败: {e}')
return jsonify({'success': False, 'message': '重置失败'}), 500
@app.route('/api/system/info', methods=['GET'])
def get_system_info():
"""获取系统信息"""
try:
info = {
'version': '1.0.0',
'start_time': datetime.now().isoformat(),
'database_status': 'connected' if db_manager else 'disconnected',
'device_count': len(device_manager.get_connected_devices()) if device_manager else 0
}
return jsonify({'success': True, 'data': info})
except Exception as e:
logger.error(f'获取系统信息失败: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
# ==================== 设备管理API ====================
@app.route('/api/devices/status', methods=['GET'])
def get_device_status():
"""获取设备状态"""
try:
if not device_manager:
return jsonify({'camera': False, 'imu': False, 'pressure': False})
status = device_manager.get_device_status()
return jsonify(status)
except Exception as e:
logger.error(f'获取设备状态失败: {e}')
return jsonify({'camera': False, 'imu': False, 'pressure': False})
@app.route('/api/devices/refresh', methods=['POST'])
def refresh_devices():
"""刷新设备连接"""
try:
if device_manager:
device_manager.refresh_devices()
return jsonify({'success': True, 'message': '设备已刷新'})
except Exception as e:
logger.error(f'刷新设备失败: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/devices/calibrate', methods=['POST'])
def calibrate_devices():
"""校准设备"""
try:
if device_manager:
result = device_manager.calibrate_devices()
return jsonify({'success': True, 'data': result})
return jsonify({'success': False, 'error': '设备管理器未初始化'}), 500
except Exception as e:
logger.error(f'设备校准失败: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
# ==================== 患者管理API ====================
@app.route('/api/patients', methods=['GET'])
def get_patients():
"""获取患者列表"""
try:
page = int(request.args.get('page', 1))
size = int(request.args.get('size', 10))
keyword = request.args.get('keyword', '')
patients = db_manager.get_patients(page, size, keyword)
total = db_manager.get_patients_count(keyword)
return jsonify({
'success': True,
'data': {
'patients': patients,
'total': total,
'page': page,
'size': size
}
})
except Exception as e:
logger.error(f'获取患者列表失败: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/patients', methods=['POST'])
def create_patient():
"""创建患者"""
try:
data = request.get_json()
patient_id = db_manager.create_patient(data)
return jsonify({
'success': True,
'data': {'patient_id': patient_id},
'message': '患者创建成功'
})
except Exception as e:
logger.error(f'创建患者失败: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/patients/<patient_id>', methods=['PUT'])
def update_patient(patient_id):
"""更新患者信息"""
try:
data = request.get_json()
db_manager.update_patient(patient_id, data)
return jsonify({'success': True, 'message': '患者信息更新成功'})
except Exception as e:
logger.error(f'更新患者信息失败: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
2025-07-31 17:23:05 +08:00
@app.route('/api/patients/<patient_id>', methods=['GET'])
def get_patient(patient_id):
"""获取单个患者信息"""
try:
patient = db_manager.get_patient(patient_id)
if patient:
return jsonify({'success': True, 'data': patient})
else:
return jsonify({'success': False, 'error': '患者不存在'}), 404
except Exception as e:
logger.error(f'获取患者信息失败: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
2025-07-28 11:59:56 +08:00
@app.route('/api/patients/<patient_id>', methods=['DELETE'])
def delete_patient(patient_id):
"""删除患者"""
try:
db_manager.delete_patient(patient_id)
return jsonify({'success': True, 'message': '患者删除成功'})
except Exception as e:
logger.error(f'删除患者失败: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
# ==================== 检测管理API ====================
@app.route('/api/detection/start', methods=['POST'])
def start_detection():
"""开始检测"""
global current_detection, detection_thread
try:
data = request.get_json()
patient_id = data.get('patientId')
2025-07-31 17:23:05 +08:00
settings_str = data.get('settings', '{}')
if not patient_id:
return jsonify({'success': False, 'error': '缺少患者ID'}), 400
# 检查是否已有检测在进行
if current_detection and current_detection.get('status') == 'running':
return jsonify({'success': False, 'error': '已有检测在进行中'}), 400
# 解析settings参数
try:
if isinstance(settings_str, str):
settings = json.loads(settings_str)
else:
settings = settings_str or {}
except json.JSONDecodeError:
settings = {}
# 设置默认值
duration = settings.get('duration', 300) # 默认5分钟
frequency = settings.get('frequency', 30) # 默认30Hz
2025-07-28 11:59:56 +08:00
# 创建检测会话
2025-07-31 17:23:05 +08:00
session_id = db_manager.create_detection_session(
patient_id=patient_id,
settings=settings
)
logger.info(f'创建检测会话成功: {session_id}, 患者ID: {patient_id}')
2025-07-28 11:59:56 +08:00
# 初始化检测状态
current_detection = {
'session_id': session_id,
'patient_id': patient_id,
'status': 'running',
'start_time': datetime.now(),
'settings': settings,
'data_points': 0
}
# 启动检测线程
detection_thread = threading.Thread(
target=run_detection,
args=(session_id, settings)
)
detection_thread.start()
return jsonify({
'success': True,
'data': {'session_id': session_id},
'message': '检测已开始'
})
except Exception as e:
logger.error(f'开始检测失败: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/detection/stop', methods=['POST'])
def stop_detection():
"""停止检测"""
2025-07-31 17:23:05 +08:00
global current_detection, detection_thread
2025-07-28 11:59:56 +08:00
try:
2025-07-31 17:23:05 +08:00
data = request.get_json() or {}
session_id = data.get('sessionId')
frontend_duration = data.get('duration', 0) # 前端计算的持续时间
2025-07-28 11:59:56 +08:00
if not current_detection or current_detection.get('status') != 'running':
return jsonify({'success': False, 'error': '没有正在进行的检测'}), 400
# 更新检测状态
current_detection['status'] = 'stopped'
2025-07-31 17:23:05 +08:00
end_time = datetime.now()
current_detection['end_time'] = end_time
# 计算持续时间(优先使用前端传递的时间,否则使用后端计算的时间)
duration = frontend_duration
if duration <= 0:
start_time = current_detection.get('start_time')
duration = int((end_time - start_time).total_seconds()) if start_time else 0
2025-07-28 11:59:56 +08:00
# 等待检测线程结束
if detection_thread and detection_thread.is_alive():
detection_thread.join(timeout=5)
2025-07-31 17:23:05 +08:00
# 如果提供了sessionId更新数据库中的会话状态
if session_id:
# 更新会话状态为completed并设置结束时间
db_manager.update_session_status(session_id, 'completed')
# 更新持续时间
if duration > 0:
db_manager.update_session_duration(session_id, duration)
logger.info(f'检测会话已停止: {session_id}, 持续时间: {duration}')
current_detection = None
return jsonify({
'success': True,
'message': '检测已停止',
'sessionId': session_id,
'duration': duration
})
current_detection = None
return jsonify({
'success': True,
'message': '检测已停止'
})
2025-07-28 11:59:56 +08:00
except Exception as e:
logger.error(f'停止检测失败: {e}')
2025-07-31 17:23:05 +08:00
return jsonify({
'success': False,
'message': f'停止检测失败: {str(e)}'
}), 500
@app.route('/api/sessions/<session_id>/video-path', methods=['PUT'])
def update_session_video_path(session_id):
"""更新会话视频路径"""
try:
data = request.get_json()
if not data or 'videoPath' not in data:
return jsonify({
'success': False,
'message': '缺少视频路径参数'
}), 400
video_path = data['videoPath']
# 更新数据库中的视频路径
db_manager.update_session_video_path(session_id, video_path)
return jsonify({
'success': True,
'message': '视频路径更新成功',
'sessionId': session_id,
'videoPath': video_path
})
except Exception as e:
logger.error(f'更新会话视频路径失败: {e}')
return jsonify({
'success': False,
'message': f'更新失败: {str(e)}'
}), 500
2025-07-28 11:59:56 +08:00
@app.route('/api/detection/status', methods=['GET'])
def get_detection_status():
"""获取检测状态"""
try:
if not current_detection:
return jsonify({
'success': True,
'data': {'status': 'idle'}
})
# 计算运行时间
if current_detection.get('status') == 'running':
elapsed = (datetime.now() - current_detection['start_time']).total_seconds()
current_detection['elapsed_time'] = int(elapsed)
return jsonify({
'success': True,
'data': current_detection
})
except Exception as e:
logger.error(f'获取检测状态失败: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/detection/data', methods=['GET'])
def get_realtime_data():
"""获取实时检测数据"""
try:
if not current_detection or current_detection.get('status') != 'running':
return jsonify({'success': False, 'error': '没有正在进行的检测'})
# 获取最新的检测数据
session_id = current_detection['session_id']
data = detection_engine.get_latest_data(session_id)
return jsonify({
'success': True,
'data': data
})
except Exception as e:
logger.error(f'获取实时数据失败: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
# ==================== 数据分析API ====================
@app.route('/api/analysis/session/<session_id>', methods=['GET'])
def analyze_session(session_id):
"""分析检测会话数据"""
try:
# 获取会话数据
session_data = db_manager.get_session_data(session_id)
if not session_data:
return jsonify({'success': False, 'error': '会话不存在'}), 404
# 进行数据分析
analysis_result = data_processor.analyze_session(session_data)
# 保存分析结果
db_manager.save_analysis_result(session_id, analysis_result)
return jsonify({
'success': True,
'data': analysis_result
})
except Exception as e:
logger.error(f'分析会话数据失败: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/export/report/<session_id>', methods=['GET'])
def export_report(session_id):
"""导出检测报告"""
try:
# 生成报告
report_path = data_processor.generate_report(session_id)
if not os.path.exists(report_path):
return jsonify({'success': False, 'error': '报告生成失败'}), 500
return send_file(
report_path,
as_attachment=True,
download_name=f'detection_report_{session_id}.pdf'
)
except Exception as e:
logger.error(f'导出报告失败: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
# ==================== 历史记录API ====================
@app.route('/api/history/sessions', methods=['GET'])
def get_detection_sessions():
"""获取检测会话历史"""
try:
page = int(request.args.get('page', 1))
size = int(request.args.get('size', 10))
patient_id = request.args.get('patient_id')
sessions = db_manager.get_detection_sessions(page, size, patient_id)
total = db_manager.get_sessions_count(patient_id)
return jsonify({
'success': True,
'data': {
'sessions': sessions,
'total': total,
'page': page,
'size': size
}
})
except Exception as e:
logger.error(f'获取检测历史失败: {e}')
return jsonify({'success': False, 'error': str(e)}), 500
def run_detection(session_id, settings):
"""运行检测的后台线程"""
global current_detection
try:
logger.info(f'开始检测会话: {session_id}')
# 检测循环
while (current_detection and
current_detection.get('status') == 'running'):
# 采集数据
if device_manager:
data = device_manager.collect_data()
if data:
# 保存数据到数据库
db_manager.save_detection_data(session_id, data)
current_detection['data_points'] += 1
# 根据采样频率控制循环间隔
frequency = settings.get('frequency', 60)
time.sleep(1.0 / frequency)
# 检查是否达到设定时长
duration = settings.get('duration', 0)
if duration > 0:
elapsed = (datetime.now() - current_detection['start_time']).total_seconds()
if elapsed >= duration:
current_detection['status'] = 'completed'
break
# 更新会话状态
if current_detection:
db_manager.update_session_status(
session_id,
current_detection['status'],
current_detection.get('data_points', 0)
)
logger.info(f'检测会话完成: {session_id}')
except Exception as e:
logger.error(f'检测线程异常: {e}')
if current_detection:
current_detection['status'] = 'error'
current_detection['error'] = str(e)
2025-07-31 17:23:05 +08:00
# ==================== 截图和录像API已移至detection_engine.py ====================
# 相关路由现在通过detection_bp Blueprint提供
2025-07-28 11:59:56 +08:00
# ==================== 错误处理 ====================
@app.errorhandler(404)
def not_found(error):
return jsonify({'success': False, 'error': 'API接口不存在'}), 404
@app.errorhandler(500)
def internal_error(error):
return jsonify({'success': False, 'error': '服务器内部错误'}), 500
if __name__ == '__main__':
import argparse
# 解析命令行参数
parser = argparse.ArgumentParser(description='Body Balance Evaluation System Backend')
parser.add_argument('--host', default=None, help='Host address to bind to')
parser.add_argument('--port', type=int, default=None, help='Port number to bind to')
parser.add_argument('--debug', action='store_true', help='Enable debug mode')
args = parser.parse_args()
2025-07-28 11:59:56 +08:00
try:
# 初始化应用
init_app()
# 确定主机和端口
host = args.host if args.host else config.get('SERVER', 'host', fallback='127.0.0.1')
port = args.port if args.port else config.getint('SERVER', 'port', fallback=5000)
debug = args.debug if args.debug else config.getboolean('APP', 'debug', fallback=False)
2025-07-29 16:44:17 +08:00
# 启动Flask+SocketIO服务
logger.info(f'启动后端服务... Host: {host}, Port: {port}, Debug: {debug}')
2025-07-29 16:44:17 +08:00
socketio.run(app,
host=host,
port=port,
debug=debug,
use_reloader=False, # 禁用热重载以避免进程问题
log_output=True, # 启用详细日志
2025-07-29 16:44:17 +08:00
allow_unsafe_werkzeug=True
2025-07-28 11:59:56 +08:00
)
except KeyboardInterrupt:
logger.info('服务被用户中断')
except Exception as e:
logger.error(f'服务启动失败: {e}')
sys.exit(1)
finally:
2025-07-29 16:44:17 +08:00
logger.info('后端服务已停止')
2025-07-31 17:23:05 +08:00
# ==================== WebSocket 事件处理 ====================
2025-07-29 16:44:17 +08:00
@socketio.on('start_rtsp')
2025-07-29 18:28:40 +08:00
def handle_start_rtsp(data=None):
logger.info(f'收到start_rtsp事件客户端ID: {request.sid}, 数据: {data}')
try:
2025-07-31 17:23:05 +08:00
if video_stream_manager:
result = video_stream_manager.start_rtsp_stream()
emit('rtsp_status', result)
else:
emit('rtsp_status', {'status': 'error', 'message': '视频流管理器未初始化'})
2025-07-29 18:28:40 +08:00
except Exception as e:
logger.error(f'启动RTSP失败: {e}')
emit('rtsp_status', {'status': 'error', 'message': f'启动失败: {str(e)}'})
2025-07-29 16:44:17 +08:00
@socketio.on('stop_rtsp')
2025-07-29 18:28:40 +08:00
def handle_stop_rtsp(data=None):
logger.info(f'收到stop_rtsp事件客户端ID: {request.sid}, 数据: {data}')
try:
2025-07-31 17:23:05 +08:00
if video_stream_manager:
result = video_stream_manager.stop_rtsp_stream()
emit('rtsp_status', result)
else:
emit('rtsp_status', {'status': 'error', 'message': '视频流管理器未初始化'})
2025-07-29 18:28:40 +08:00
except Exception as e:
logger.error(f'停止RTSP失败: {e}')
emit('rtsp_status', {'status': 'error', 'message': f'停止失败: {str(e)}'})
@socketio.on('connect')
def handle_connect():
logger.info(f'客户端连接: {request.sid}')
emit('connect_status', {'status': 'connected', 'sid': request.sid, 'message': '连接成功'})
@socketio.on('disconnect')
def handle_disconnect():
logger.info(f'客户端断开连接: {request.sid}')
@socketio.on('ping')
def handle_ping(data=None):
logger.info(f'收到ping事件客户端ID: {request.sid}, 数据: {data}')
emit('pong', {'timestamp': time.time(), 'message': 'pong'})