commit d36b16d458bd14115787e34ea451487855d45c23 Author: root <13910913995@163.com> Date: Mon Jul 28 11:59:56 2025 +0800 初始提交 diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..d496b077 --- /dev/null +++ b/.gitignore @@ -0,0 +1,367 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +PIPFILE.lock + +# PyInstaller +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +Pipfile.lock + +# poetry +poetry.lock + +# pdm +.pdm.toml + +# PEP 582 +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# Node.js +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage/ +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +public + +# Storybook build outputs +.out +.storybook-out +storybook-static + +# Temporary folders +tmp/ +temp/ + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Electron +dist_electron/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Application specific +data/ +!data/.gitkeep +backup_*.zip +exports/ +sessions/ +patients/ +backups/ +temp/ +logs/ +*.db +*.sqlite +*.sqlite3 + +# Media files +*.mp4 +*.avi +*.mov +*.wmv +*.flv +*.webm +*.mkv +*.m4v +*.3gp +*.jpg +*.jpeg +*.png +*.gif +*.bmp +*.tiff +*.svg +*.ico +*.webp + +# Configuration files with sensitive data +config.local.json +secrets.json +.env.local +.env.production + +# Build artifacts +build/ +dist/ +out/ + +# Test artifacts +test-results/ +coverage/ +.nyc_output/ + +# Documentation +docs/build/ +docs/_build/ + +# Backup files +*.bak +*.backup +*.old +*.orig + +# Lock files +package-lock.json +yarn.lock +pnpm-lock.yaml + +# Cache directories +.cache/ +.npm/ +.yarn/ +.pnpm/ + +# Runtime directories +runtime/ +pids/ + +# Development tools +.vscode/ +.idea/ +*.sublime-* + +# System files +*.tmp +*.temp +.fuse_hidden* +.directory +.Trash-* +.nfs* + +# Windows +desktop.ini +ehthumbs.db +ehthumbs_vista.db +*.stackdump +[Dd]esktop.ini +$RECYCLE.BIN/ +*.cab +*.msi +*.msix +*.msm +*.msp +*.lnk \ No newline at end of file diff --git a/.vercel/project.json b/.vercel/project.json new file mode 100644 index 00000000..cce09f02 --- /dev/null +++ b/.vercel/project.json @@ -0,0 +1 @@ +{"projectName":"trae_c7qdkht3"} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..426ded07 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,220 @@ +# 更新日志 + +本文档记录了身体平衡评估系统的所有重要更改。 + +格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/), +并且本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。 + +## [未发布] + +### 计划新增 +- [ ] 多语言支持(英文、日文) +- [ ] 云端数据同步功能 +- [ ] 移动端应用支持 +- [ ] AI辅助诊断建议 +- [ ] 3D姿态可视化 +- [ ] 报告模板自定义 +- [ ] 批量数据分析 +- [ ] 设备校准向导 + +### 计划改进 +- [ ] 优化实时数据处理性能 +- [ ] 增强数据可视化效果 +- [ ] 改进用户界面交互体验 +- [ ] 扩展设备兼容性 + +## [1.0.0] - 2024-01-15 + +### 新增 +- ✅ 完整的身体平衡评估系统架构 +- ✅ 基于Vue 3 + Electron的现代化前端界面 +- ✅ 基于Flask的RESTful API后端服务 +- ✅ 多传感器数据融合(摄像头、IMU、压力传感器) +- ✅ 实时姿态检测和平衡分析 +- ✅ 患者信息管理系统 +- ✅ 检测会话管理和历史记录 +- ✅ 数据分析和可视化图表 +- ✅ PDF报告生成和导出功能 +- ✅ 系统设置和设备配置 +- ✅ 完整的日志记录系统 +- ✅ 数据备份和恢复功能 + +### 技术特性 +- ✅ 基于MediaPipe的实时姿态检测 +- ✅ WebSocket实时数据传输 +- ✅ SQLite数据库存储 +- ✅ 模块化架构设计 +- ✅ 跨平台支持(Windows、macOS、Linux) +- ✅ 响应式UI设计 +- ✅ 国际化支持框架 + +### 核心功能模块 + +#### 前端界面 +- ✅ 现代化的用户界面设计 +- ✅ 实时数据可视化 +- ✅ 响应式布局适配 +- ✅ 深色/浅色主题切换 +- ✅ 多语言界面支持 + +#### 后端服务 +- ✅ RESTful API设计 +- ✅ 实时数据处理引擎 +- ✅ 设备管理和通信 +- ✅ 数据分析算法 +- ✅ 报告生成服务 + +#### 数据管理 +- ✅ 患者信息CRUD操作 +- ✅ 检测会话管理 +- ✅ 历史数据查询和分析 +- ✅ 数据导出和备份 +- ✅ 数据安全和隐私保护 + +#### 设备集成 +- ✅ 摄像头视频采集和处理 +- ✅ IMU传感器数据采集 +- ✅ 压力传感器数据采集 +- ✅ 设备状态监控 +- ✅ 设备校准功能 + +#### 分析算法 +- ✅ 重心轨迹分析 +- ✅ 姿态稳定性评估 +- ✅ 平衡能力评分 +- ✅ 异常检测和预警 +- ✅ 趋势分析和对比 + +### 文件结构 +``` +BodyBalanceEvaluation/ +├── backend/ # 后端服务 +│ ├── app.py # 主应用入口 +│ ├── database.py # 数据库管理 +│ ├── device_manager.py # 设备管理 +│ ├── detection_engine.py # 检测引擎 +│ ├── data_processor.py # 数据处理 +│ ├── utils.py # 工具函数 +│ ├── requirements.txt # Python依赖 +│ └── tests/ # 测试文件 +├── src/ +│ ├── main/ # Electron主进程 +│ └── renderer/ # Vue前端应用 +│ ├── src/ +│ │ ├── views/ # 页面组件 +│ │ ├── stores/ # 状态管理 +│ │ └── services/ # API服务 +│ └── package.json +├── data/ # 数据目录 +├── logs/ # 日志目录 +├── temp/ # 临时文件 +├── main.py # 启动脚本 +├── config.json # 配置文件 +├── package.json # 项目配置 +├── README.md # 项目说明 +├── LICENSE # 许可证 +├── .gitignore # Git忽略规则 +├── install.bat # 安装脚本 +├── start_dev.bat # 开发环境启动 +├── start_prod.bat # 生产环境启动 +└── CHANGELOG.md # 更新日志 +``` + +### 系统要求 +- **操作系统**: Windows 10/11, macOS 10.15+, Ubuntu 18.04+ +- **Python**: 3.8 或更高版本 +- **Node.js**: 16.0 或更高版本 +- **内存**: 最少 4GB RAM,推荐 8GB+ +- **存储**: 最少 2GB 可用空间 +- **摄像头**: USB摄像头或内置摄像头 +- **串口设备**: IMU和压力传感器(可选) + +### 安装和使用 +1. 运行 `install.bat` 安装所有依赖 +2. 运行 `start_dev.bat` 启动开发环境 +3. 或运行 `start_prod.bat` 启动生产环境 +4. 访问 http://localhost:5173 使用应用 + +### 已知问题 +- 在某些低配置设备上可能出现实时处理延迟 +- 部分USB摄像头可能需要额外驱动 +- IMU传感器需要正确的串口配置 + +### 性能优化 +- 实时数据处理采用多线程架构 +- 图像处理使用GPU加速(如可用) +- 数据库查询优化和索引 +- 前端虚拟滚动和懒加载 + +### 安全特性 +- 本地数据存储,保护隐私 +- 数据传输加密 +- 用户会话管理 +- 输入数据验证和清理 + +--- + +## 版本说明 + +### 版本号格式 +本项目使用语义化版本号:`主版本号.次版本号.修订号` + +- **主版本号**: 不兼容的API修改 +- **次版本号**: 向下兼容的功能性新增 +- **修订号**: 向下兼容的问题修正 + +### 更新类型 +- **新增 (Added)**: 新功能 +- **更改 (Changed)**: 对现有功能的更改 +- **弃用 (Deprecated)**: 即将移除的功能 +- **移除 (Removed)**: 已移除的功能 +- **修复 (Fixed)**: 错误修复 +- **安全 (Security)**: 安全相关的修复 + +### 发布周期 +- **主版本**: 每年1-2次重大更新 +- **次版本**: 每季度功能更新 +- **修订版**: 每月bug修复和小改进 + +--- + +## 贡献指南 + +如果您想为本项目贡献代码,请: + +1. Fork 本仓库 +2. 创建功能分支 (`git checkout -b feature/AmazingFeature`) +3. 提交更改 (`git commit -m 'Add some AmazingFeature'`) +4. 推送到分支 (`git push origin feature/AmazingFeature`) +5. 创建 Pull Request + +### 提交信息格式 +``` +type(scope): description + +[optional body] + +[optional footer] +``` + +类型包括: +- `feat`: 新功能 +- `fix`: 修复bug +- `docs`: 文档更新 +- `style`: 代码格式化 +- `refactor`: 代码重构 +- `test`: 测试相关 +- `chore`: 构建过程或辅助工具的变动 + +--- + +## 支持和反馈 + +- **问题报告**: [GitHub Issues](https://github.com/example/body-balance-evaluation/issues) +- **功能请求**: [GitHub Discussions](https://github.com/example/body-balance-evaluation/discussions) +- **邮件支持**: dev@example.com +- **文档**: [项目Wiki](https://github.com/example/body-balance-evaluation/wiki) + +--- + +*最后更新: 2024-01-15* \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..229d9026 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,680 @@ +# 贡献指南 + +感谢您对身体平衡评估系统项目的关注!我们欢迎所有形式的贡献,包括但不限于代码、文档、测试、问题报告和功能建议。 + +## 目录 + +- [行为准则](#行为准则) +- [如何贡献](#如何贡献) +- [开发环境设置](#开发环境设置) +- [代码规范](#代码规范) +- [提交规范](#提交规范) +- [Pull Request 流程](#pull-request-流程) +- [问题报告](#问题报告) +- [功能请求](#功能请求) +- [文档贡献](#文档贡献) +- [测试指南](#测试指南) + +## 行为准则 + +### 我们的承诺 + +为了营造一个开放和友好的环境,我们作为贡献者和维护者承诺,无论年龄、体型、残疾、种族、性别认同和表达、经验水平、国籍、个人形象、种族、宗教或性取向如何,参与我们项目和社区的每个人都能获得无骚扰的体验。 + +### 我们的标准 + +有助于创造积极环境的行为包括: + +- 使用友好和包容的语言 +- 尊重不同的观点和经验 +- 优雅地接受建设性批评 +- 关注对社区最有利的事情 +- 对其他社区成员表示同理心 + +不可接受的行为包括: + +- 使用性化的语言或图像,以及不受欢迎的性关注或性骚扰 +- 恶意评论、侮辱/贬损评论,以及个人或政治攻击 +- 公开或私下骚扰 +- 未经明确许可,发布他人的私人信息,如物理或电子地址 +- 在专业环境中可能被合理认为不适当的其他行为 + +## 如何贡献 + +### 贡献类型 + +我们欢迎以下类型的贡献: + +1. **代码贡献** + - 新功能开发 + - Bug 修复 + - 性能优化 + - 代码重构 + +2. **文档贡献** + - API 文档 + - 用户指南 + - 开发者文档 + - 代码注释 + +3. **测试贡献** + - 单元测试 + - 集成测试 + - 端到端测试 + - 性能测试 + +4. **设计贡献** + - UI/UX 设计 + - 图标设计 + - 用户体验改进 + +5. **其他贡献** + - 问题报告 + - 功能建议 + - 社区支持 + - 翻译工作 + +## 开发环境设置 + +### 前置要求 + +- Python 3.8+ +- Node.js 16.0+ +- Git +- 代码编辑器(推荐 VS Code) + +### 环境配置 + +1. **克隆仓库** + ```bash + git clone https://github.com/example/body-balance-evaluation.git + cd body-balance-evaluation + ``` + +2. **安装依赖** + ```bash + # Windows + install.bat + + # 或手动安装 + python -m venv venv + venv\Scripts\activate # Windows + # source venv/bin/activate # macOS/Linux + + pip install -r backend/requirements.txt + cd src/renderer && npm install + ``` + +3. **配置开发环境** + ```bash + # 复制配置文件 + cp config.json config.local.json + + # 编辑本地配置(可选) + # 修改 config.local.json 中的设置 + ``` + +4. **启动开发服务器** + ```bash + # Windows + start_dev.bat + + # 或手动启动 + python main.py --mode development + ``` + +### 开发工具推荐 + +#### VS Code 扩展 +- Python +- Pylance +- Vue Language Features (Volar) +- TypeScript Vue Plugin (Volar) +- ESLint +- Prettier +- GitLens + +#### 浏览器扩展 +- Vue.js devtools +- React Developer Tools(如果使用) + +## 代码规范 + +### Python 代码规范 + +我们遵循 [PEP 8](https://www.python.org/dev/peps/pep-0008/) 规范: + +```python +# 好的示例 +class PatientManager: + """患者管理器类""" + + def __init__(self, database_path: str): + self.db_path = database_path + self.logger = logging.getLogger(__name__) + + def create_patient(self, patient_data: dict) -> int: + """创建新患者 + + Args: + patient_data: 患者信息字典 + + Returns: + 患者ID + + Raises: + ValueError: 当患者数据无效时 + """ + if not self._validate_patient_data(patient_data): + raise ValueError("Invalid patient data") + + # 实现逻辑... + return patient_id +``` + +#### 代码格式化 +使用 Black 进行代码格式化: +```bash +black . --line-length=88 +``` + +#### 代码检查 +使用 flake8 进行代码检查: +```bash +flake8 . --max-line-length=88 --exclude=venv,__pycache__ +``` + +### JavaScript/Vue 代码规范 + +我们遵循 [Vue.js 风格指南](https://vuejs.org/style-guide/): + +```javascript +// 好的示例 +export default { + name: 'PatientList', + + props: { + patients: { + type: Array, + required: true + }, + loading: { + type: Boolean, + default: false + } + }, + + emits: ['patient-selected', 'patient-deleted'], + + setup(props, { emit }) { + const selectedPatient = ref(null) + + const handlePatientClick = (patient) => { + selectedPatient.value = patient + emit('patient-selected', patient) + } + + return { + selectedPatient, + handlePatientClick + } + } +} +``` + +#### 代码格式化 +使用 Prettier 进行代码格式化: +```bash +npm run format +``` + +#### 代码检查 +使用 ESLint 进行代码检查: +```bash +npm run lint +``` + +### 通用规范 + +1. **命名规范** + - 使用有意义的变量和函数名 + - Python: snake_case + - JavaScript: camelCase + - 常量: UPPER_CASE + - 类名: PascalCase + +2. **注释规范** + - 为复杂逻辑添加注释 + - 使用文档字符串描述函数和类 + - 注释应该解释"为什么"而不是"是什么" + +3. **文件组织** + - 保持文件结构清晰 + - 相关功能放在同一模块 + - 避免循环导入 + +## 提交规范 + +我们使用 [Conventional Commits](https://www.conventionalcommits.org/) 规范: + +### 提交消息格式 + +``` +type(scope): description + +[optional body] + +[optional footer(s)] +``` + +### 类型 (type) + +- `feat`: 新功能 +- `fix`: 修复 bug +- `docs`: 文档更新 +- `style`: 代码格式化(不影响代码运行的变动) +- `refactor`: 重构(既不是新增功能,也不是修复 bug 的代码变动) +- `perf`: 性能优化 +- `test`: 增加测试 +- `chore`: 构建过程或辅助工具的变动 +- `ci`: CI 配置文件和脚本的变动 +- `revert`: 回滚之前的提交 + +### 范围 (scope) + +- `frontend`: 前端相关 +- `backend`: 后端相关 +- `database`: 数据库相关 +- `device`: 设备管理相关 +- `analysis`: 数据分析相关 +- `ui`: 用户界面相关 +- `api`: API 相关 +- `config`: 配置相关 +- `docs`: 文档相关 +- `test`: 测试相关 + +### 示例 + +```bash +# 新功能 +git commit -m "feat(frontend): add patient search functionality" + +# Bug 修复 +git commit -m "fix(backend): resolve database connection timeout issue" + +# 文档更新 +git commit -m "docs(api): update patient management API documentation" + +# 重构 +git commit -m "refactor(device): simplify camera initialization logic" +``` + +## Pull Request 流程 + +### 创建 Pull Request + +1. **Fork 仓库** + - 点击 GitHub 页面右上角的 "Fork" 按钮 + +2. **创建功能分支** + ```bash + git checkout -b feature/your-feature-name + # 或 + git checkout -b fix/your-bug-fix + ``` + +3. **进行开发** + - 编写代码 + - 添加测试 + - 更新文档 + +4. **提交更改** + ```bash + git add . + git commit -m "feat(scope): your commit message" + ``` + +5. **推送分支** + ```bash + git push origin feature/your-feature-name + ``` + +6. **创建 Pull Request** + - 在 GitHub 上创建 Pull Request + - 填写详细的描述 + +### Pull Request 模板 + +```markdown +## 描述 +简要描述这个 PR 的目的和内容。 + +## 更改类型 +- [ ] Bug 修复 +- [ ] 新功能 +- [ ] 重构 +- [ ] 文档更新 +- [ ] 性能优化 +- [ ] 其他(请说明) + +## 测试 +- [ ] 已添加单元测试 +- [ ] 已添加集成测试 +- [ ] 已手动测试 +- [ ] 所有测试通过 + +## 检查清单 +- [ ] 代码遵循项目规范 +- [ ] 已添加必要的注释 +- [ ] 已更新相关文档 +- [ ] 没有引入新的警告 +- [ ] 已测试在不同环境下的兼容性 + +## 相关问题 +关闭 #issue_number + +## 截图(如适用) +添加截图来帮助解释你的更改。 + +## 额外说明 +添加任何其他相关信息。 +``` + +### 代码审查 + +所有 Pull Request 都需要经过代码审查: + +1. **自动检查** + - CI/CD 流水线检查 + - 代码格式检查 + - 测试覆盖率检查 + +2. **人工审查** + - 代码质量 + - 设计合理性 + - 测试完整性 + - 文档完整性 + +3. **审查标准** + - 代码可读性 + - 性能影响 + - 安全性考虑 + - 向后兼容性 + +## 问题报告 + +### 报告 Bug + +使用 [Bug 报告模板](https://github.com/example/body-balance-evaluation/issues/new?template=bug_report.md): + +```markdown +**描述 Bug** +清晰简洁地描述 bug 是什么。 + +**重现步骤** +重现行为的步骤: +1. 转到 '...' +2. 点击 '....' +3. 滚动到 '....' +4. 看到错误 + +**预期行为** +清晰简洁地描述你期望发生什么。 + +**截图** +如果适用,添加截图来帮助解释你的问题。 + +**环境信息** + - 操作系统: [例如 Windows 10] + - Python 版本: [例如 3.9.0] + - Node.js 版本: [例如 16.14.0] + - 应用版本: [例如 1.0.0] + +**额外信息** +添加任何其他关于问题的信息。 +``` + +### 报告安全问题 + +如果发现安全漏洞,请不要在公开的 issue 中报告。请发送邮件到 security@example.com。 + +## 功能请求 + +使用 [功能请求模板](https://github.com/example/body-balance-evaluation/issues/new?template=feature_request.md): + +```markdown +**功能描述** +清晰简洁地描述你想要的功能。 + +**问题描述** +清晰简洁地描述问题是什么。例如:我总是感到沫丧当 [...] + +**解决方案** +清晰简洁地描述你想要发生什么。 + +**替代方案** +清晰简洁地描述你考虑过的任何替代解决方案或功能。 + +**额外信息** +添加任何其他关于功能请求的信息或截图。 +``` + +## 文档贡献 + +### 文档类型 + +1. **用户文档** + - 安装指南 + - 使用教程 + - 常见问题 + - 故障排除 + +2. **开发者文档** + - API 文档 + - 架构说明 + - 贡献指南 + - 代码注释 + +3. **项目文档** + - README + - CHANGELOG + - LICENSE + - 项目规划 + +### 文档规范 + +1. **Markdown 格式** + - 使用标准 Markdown 语法 + - 保持一致的格式 + - 使用有意义的标题 + +2. **内容要求** + - 清晰准确 + - 及时更新 + - 包含示例 + - 考虑不同用户水平 + +3. **图片和媒体** + - 使用 SVG 格式(如可能) + - 优化文件大小 + - 提供替代文本 + +## 测试指南 + +### 测试类型 + +1. **单元测试** + - 测试单个函数或方法 + - 使用 pytest(Python)或 Jest(JavaScript) + - 目标覆盖率 > 80% + +2. **集成测试** + - 测试模块间交互 + - 测试 API 端点 + - 测试数据库操作 + +3. **端到端测试** + - 测试完整用户流程 + - 使用 Playwright 或 Cypress + - 测试关键业务场景 + +### 测试规范 + +```python +# Python 测试示例 +import pytest +from unittest.mock import Mock, patch + +class TestPatientManager: + """患者管理器测试类""" + + @pytest.fixture + def patient_manager(self): + """测试夹具""" + return PatientManager(":memory:") + + def test_create_patient_success(self, patient_manager): + """测试成功创建患者""" + patient_data = { + "name": "测试患者", + "age": 30, + "gender": "male" + } + + patient_id = patient_manager.create_patient(patient_data) + + assert patient_id is not None + assert isinstance(patient_id, int) + + def test_create_patient_invalid_data(self, patient_manager): + """测试无效数据创建患者""" + invalid_data = {"name": ""} + + with pytest.raises(ValueError): + patient_manager.create_patient(invalid_data) +``` + +```javascript +// JavaScript 测试示例 +import { describe, it, expect, vi } from 'vitest' +import { mount } from '@vue/test-utils' +import PatientList from '@/components/PatientList.vue' + +describe('PatientList', () => { + it('renders patient list correctly', () => { + const patients = [ + { id: 1, name: '患者1', age: 30 }, + { id: 2, name: '患者2', age: 25 } + ] + + const wrapper = mount(PatientList, { + props: { patients } + }) + + expect(wrapper.findAll('.patient-item')).toHaveLength(2) + expect(wrapper.text()).toContain('患者1') + expect(wrapper.text()).toContain('患者2') + }) + + it('emits patient-selected event when patient is clicked', async () => { + const patients = [{ id: 1, name: '患者1', age: 30 }] + + const wrapper = mount(PatientList, { + props: { patients } + }) + + await wrapper.find('.patient-item').trigger('click') + + expect(wrapper.emitted('patient-selected')).toBeTruthy() + expect(wrapper.emitted('patient-selected')[0]).toEqual([patients[0]]) + }) +}) +``` + +### 运行测试 + +```bash +# Python 测试 +cd backend +python -m pytest tests/ -v --cov=. + +# JavaScript 测试 +cd src/renderer +npm run test + +# 所有测试 +npm run test +``` + +## 发布流程 + +### 版本管理 + +我们使用语义化版本控制: + +- **主版本号**: 不兼容的 API 修改 +- **次版本号**: 向下兼容的功能性新增 +- **修订号**: 向下兼容的问题修正 + +### 发布步骤 + +1. **更新版本号** + ```bash + # 更新 package.json 中的版本号 + npm version patch|minor|major + ``` + +2. **更新 CHANGELOG** + - 记录所有重要更改 + - 按类型分组(新增、更改、修复等) + +3. **创建发布标签** + ```bash + git tag -a v1.0.0 -m "Release version 1.0.0" + git push origin v1.0.0 + ``` + +4. **构建和测试** + ```bash + npm run build + npm run test + ``` + +5. **发布** + - 创建 GitHub Release + - 上传构建产物 + - 发布到包管理器(如需要) + +## 社区支持 + +### 获取帮助 + +- **GitHub Discussions**: 一般讨论和问题 +- **GitHub Issues**: Bug 报告和功能请求 +- **邮件**: dev@example.com +- **文档**: 项目 Wiki + +### 参与社区 + +- 回答其他用户的问题 +- 参与功能讨论 +- 分享使用经验 +- 推广项目 + +## 致谢 + +感谢所有为这个项目做出贡献的开发者! + +### 贡献者 + +- [贡献者列表](https://github.com/example/body-balance-evaluation/contributors) + +### 特别感谢 + +- 感谢所有提供反馈和建议的用户 +- 感谢开源社区提供的优秀工具和库 +- 感谢医疗专业人士提供的专业指导 + +--- + +再次感谢您的贡献!如果您有任何问题,请随时联系我们。 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..b129be2c --- /dev/null +++ b/README.md @@ -0,0 +1,425 @@ +# 身体平衡评估系统 + +一个基于多传感器融合技术的专业身体平衡评估与分析系统,为用户提供准确的平衡能力评估和康复指导。 + +## 系统特性 + +### 🎯 核心功能 +- **实时姿态检测**: 基于MediaPipe的高精度人体姿态识别 +- **多传感器融合**: 整合摄像头、IMU传感器和压力传感器数据 +- **智能分析引擎**: 多维度平衡能力评估算法 +- **可视化报告**: 直观的数据图表和分析报告 +- **历史数据管理**: 完整的检测记录存储和对比分析 + +### 🔧 技术特点 +- **现代化架构**: Vue 3 + Python Flask 前后端分离 +- **实时通信**: WebSocket 实时数据传输 +- **跨平台支持**: Windows、macOS、Linux +- **模块化设计**: 清晰的目录结构,易于扩展和维护 +- **数据安全**: 本地存储,保护用户隐私 +- **开发友好**: 独立的前后端开发环境,支持热重载 +- **部署简化**: 一键安装和启动脚本,降低部署复杂度 + +## 系统架构 + +``` +身体平衡评估系统/ +├── backend/ # 后端服务 +│ ├── main.py # 主启动脚本 +│ ├── app.py # Flask 主应用 +│ ├── database.py # 数据库管理 +│ ├── device_manager.py # 设备管理 +│ ├── detection_engine.py # 检测引擎 +│ ├── data_processor.py # 数据处理 +│ ├── utils.py # 工具函数 +│ └── requirements.txt # Python 依赖 +├── frontend/ # 前端应用 +│ └── src/renderer/ # 前端源码 +│ ├── src/ +│ │ ├── views/ # 页面组件 +│ │ ├── stores/ # 状态管理 +│ │ ├── services/ # API 服务 +│ │ └── router/ # 路由配置 +│ ├── package.json # Node.js 依赖 +│ └── vite.config.js # 构建配置 +├── data/ # 数据目录 +├── logs/ # 日志目录 +├── venv/ # Python 虚拟环境 +├── install.bat # 安装脚本 +├── start_dev.bat # 开发模式启动脚本 +└── start_prod.bat # 生产模式启动脚本 +``` + +## 快速开始 + +### 环境要求 + +- **Python**: 3.8 或更高版本 +- **Node.js**: 16.0 或更高版本 (开发模式) +- **操作系统**: Windows 10/11, macOS 10.15+, Ubuntu 18.04+ + +### 安装步骤 + +#### 方式一:一键安装 (Windows 推荐) + +1. **克隆项目** + ```bash + git clone + cd BodyBalanceEvaluation + ``` + +2. **运行安装脚本** + ```bash + install.bat + ``` + + 安装脚本会自动完成: + - 检查 Python 和 Node.js 环境 + - 创建 Python 虚拟环境 + - 安装后端依赖 + - 安装前端依赖 + - 创建必要的目录结构 + +#### 方式二:手动安装 + +1. **克隆项目** + ```bash + git clone + cd BodyBalanceEvaluation + ``` + +2. **创建虚拟环境** + ```bash + python -m venv venv + venv\Scripts\activate # Windows + # source venv/bin/activate # macOS/Linux + ``` + +3. **安装 Python 依赖** + ```bash + pip install -r backend/requirements.txt + ``` + +4. **安装前端依赖** (开发模式) + ```bash + cd frontend/src/renderer + npm install + cd ../../.. + ``` + +### 启动应用程序 + + **Windows 用户 (推荐)**: + ```bash + # 一键安装 (首次使用) + install.bat + + # 开发模式 + start_dev.bat + + # 生产模式 + start_prod.bat + ``` + + **手动启动**: + ```bash + # 激活虚拟环境 + venv\Scripts\activate + + # 进入后端目录 + cd backend + + # 开发模式 + python main.py --mode development + + # 生产模式 + python main.py --mode production + ``` + +### 命令行参数 + +```bash +cd backend +python main.py [选项] + +选项: + --mode {development,production} 运行模式 (默认: development) + --host HOST 服务器主机 (默认: 127.0.0.1) + --port PORT 服务器端口 (默认: 5000) + --no-browser 不自动打开浏览器 + --log-level {DEBUG,INFO,WARNING,ERROR} 日志级别 (默认: INFO) +``` + +## 使用指南 + +### 1. 系统设置 + +首次使用前,请进入「系统设置」页面配置: + +- **设备配置**: 选择摄像头、配置串口设备 +- **检测参数**: 设置默认检测时长、采样频率等 +- **数据管理**: 配置数据存储路径和清理策略 + +### 2. 患者管理 + +- 添加患者基本信息(姓名、年龄、性别等) +- 记录患者病史和康复目标 +- 管理患者档案和检测记录 + +### 3. 姿态检测 + +1. **选择患者**: 从患者列表中选择或新建患者 +2. **设备准备**: 确保摄像头和传感器正常连接 +3. **参数配置**: 设置检测时长、采样频率等参数 +4. **开始检测**: 点击开始按钮进行实时检测 +5. **查看结果**: 检测完成后查看分析结果和建议 + +### 4. 数据分析 + +- **单次分析**: 查看单次检测的详细数据和图表 +- **对比分析**: 比较多次检测结果,观察变化趋势 +- **报告生成**: 生成 PDF 格式的专业评估报告 +- **数据导出**: 导出原始数据用于进一步分析 + +### 5. 历史记录 + +- 浏览所有历史检测记录 +- 按患者、日期、状态等条件筛选 +- 批量导出和删除操作 +- 时间线视图查看检测历史 + +## 设备支持 + +### 摄像头 +- USB 摄像头 (推荐 1080p 30fps) +- 内置摄像头 +- 网络摄像头 + +### IMU 传感器 +- 支持串口通信的 9 轴 IMU +- 波特率: 9600, 115200, 230400 +- 数据格式: 加速度、陀螺仪、磁力计 + +### 压力传感器 +- 多点压力传感器阵列 +- 串口通信接口 +- 支持 1-16 个传感器点 + +## 开发指南 + +### 后端开发 + +后端使用 Flask 框架,主要模块: + +- `main.py`: 主启动脚本和进程管理 +- `app.py`: 主应用和 API 路由 +- `database.py`: SQLite 数据库操作 +- `device_manager.py`: 硬件设备管理 +- `detection_engine.py`: 检测算法引擎 +- `data_processor.py`: 数据分析和处理 + +### 前端开发 + +前端使用 Vue 3 + Element Plus,主要特性: + +- **组合式 API**: 使用 Vue 3 Composition API +- **状态管理**: Pinia 状态管理库 +- **UI 组件**: Element Plus 组件库 +- **图表库**: ECharts 数据可视化 +- **构建工具**: Vite 快速构建 + +### 添加新功能 + +1. **后端 API**: + ```python + @app.route('/api/new-feature', methods=['POST']) + def new_feature(): + # 实现新功能逻辑 + return jsonify({'status': 'success'}) + ``` + +2. **前端服务**: + ```javascript + // frontend/src/renderer/src/services/api.js + export const newFeatureAPI = { + doSomething: (data) => api.post('/new-feature', data) + } + ``` + +3. **前端组件**: + ```vue + + + + ``` + +### 项目结构优势 + +新的项目结构带来以下优势: + +1. **清晰分离**: 前后端代码完全分离,便于团队协作开发 +2. **独立部署**: 前后端可以独立部署和扩展 +3. **开发效率**: 前后端可以并行开发,提高开发效率 +4. **维护性**: 模块化结构便于代码维护和功能扩展 +5. **版本管理**: 前后端可以独立进行版本控制 +6. **技术栈**: 前后端可以选择最适合的技术栈 + +### 开发环境配置 + +**后端开发**: +```bash +# 激活虚拟环境 +venv\Scripts\activate + +# 进入后端目录 +cd backend + +# 启动开发服务器 +python main.py --mode development --log-level DEBUG +``` + +**前端开发**: +```bash +# 进入前端目录 +cd frontend/src/renderer + +# 启动开发服务器 +npm run dev +``` + +**同时开发** (推荐): +```bash +# 使用开发脚本同时启动前后端 +start_dev.bat +``` + +## 故障排除 + +### 常见问题 + +**Q: 摄像头无法识别** +A: 检查摄像头连接,确保没有被其他应用占用,在设备设置中刷新摄像头列表。 + +**Q: 传感器连接失败** +A: 确认串口设置正确,检查设备驱动是否安装,尝试不同的波特率设置。 + +**Q: 前端页面无法加载** +A: 检查后端服务是否正常启动,确认防火墙设置,查看浏览器控制台错误信息。 + +**Q: 检测结果不准确** +A: 确保设备已正确校准,检查环境光线条件,调整检测参数设置。 + +### 日志查看 + +系统日志保存在 `logs/` 目录下: + +- `app.log`: 应用程序主日志 +- `device.log`: 设备管理日志 +- `detection.log`: 检测引擎日志 +- `error.log`: 错误日志 + +### 性能优化 + +1. **硬件要求**: + - CPU: Intel i5 或同等性能 + - 内存: 8GB RAM + - 存储: 10GB 可用空间 + +2. **软件优化**: + - 关闭不必要的后台程序 + - 使用 SSD 存储提高 I/O 性能 + - 定期清理历史数据和日志 + +## 数据格式 + +### 检测数据结构 + +```json +{ + "session_id": "uuid", + "patient_id": "patient_uuid", + "timestamp": "2024-01-01T12:00:00Z", + "duration": 60, + "data": { + "camera": { + "landmarks": [...], + "confidence": 0.95 + }, + "imu": { + "acceleration": [x, y, z], + "gyroscope": [x, y, z], + "magnetometer": [x, y, z] + }, + "pressure": { + "sensors": [p1, p2, p3, p4], + "center_of_pressure": [x, y] + } + } +} +``` + +### 分析结果格式 + +```json +{ + "session_id": "uuid", + "analysis_time": "2024-01-01T12:01:00Z", + "overall_assessment": "good", + "balance_score": 85, + "posture_score": 78, + "metrics": { + "sway_area": 2.5, + "sway_velocity": 1.2, + "postural_stability": 0.85 + }, + "recommendations": [ + "建议加强核心肌群训练", + "注意保持正确站姿" + ] +} +``` + +## 许可证 + +本项目采用 MIT 许可证。详见 [LICENSE](LICENSE) 文件。 + +## 贡献指南 + +欢迎贡献代码!请遵循以下步骤: + +1. Fork 本仓库 +2. 创建特性分支 (`git checkout -b feature/AmazingFeature`) +3. 提交更改 (`git commit -m 'Add some AmazingFeature'`) +4. 推送到分支 (`git push origin feature/AmazingFeature`) +5. 开启 Pull Request + +## 支持 + +如果您遇到问题或有建议,请: + +- 查看 [FAQ](docs/FAQ.md) +- 提交 [Issue](issues) +- 发送邮件至: support@example.com + +## 更新日志 + +### v1.1.0 (2024-01-15) +- **项目重构**: 前后端完全分离,优化项目结构 +- **新增脚本**: 添加一键安装和启动脚本 (install.bat, start_dev.bat, start_prod.bat) +- **开发体验**: 改进开发环境配置,支持独立的前后端开发 +- **文档更新**: 完善 README 文档,添加详细的安装和使用说明 +- **路径优化**: 统一使用虚拟环境,规范化依赖管理 + +### v1.0.0 (2024-01-01) +- 初始版本发布 +- 基础检测功能 +- 患者管理系统 +- 数据分析和报告生成 + +--- + +**身体平衡评估系统** - 专业的平衡能力评估解决方案 \ No newline at end of file diff --git a/backend/app.py b/backend/app.py new file mode 100644 index 00000000..3dc87b24 --- /dev/null +++ b/backend/app.py @@ -0,0 +1,630 @@ +#!/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 + +# 添加当前目录到Python路径 +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +# 导入自定义模块 +from database import DatabaseManager +from device_manager import DeviceManager +from detection_engine import DetectionEngine +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' + +# 启用CORS支持 +CORS(app, origins=['http://localhost:3000', 'http://localhost:3001', 'file://*']) + +# 全局变量 +db_manager = None +device_manager = None +detection_engine = None +data_processor = None +current_detection = None +detection_thread = None + +def init_app(): + """初始化应用""" + global db_manager, device_manager, detection_engine, data_processor + + try: + # 创建必要的目录 + os.makedirs('logs', exist_ok=True) + os.makedirs('data', exist_ok=True) + os.makedirs('exports', exist_ok=True) + os.makedirs('videos', exist_ok=True) + + # 初始化数据库 + db_manager = DatabaseManager('data/body_balance.db') + db_manager.init_database() + + # 初始化设备管理器 + device_manager = DeviceManager() + + # 初始化检测引擎 + 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/', 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 + +@app.route('/api/patients/', 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: + if current_detection and current_detection.get('status') == 'running': + return jsonify({'success': False, 'error': '检测已在进行中'}), 400 + + data = request.get_json() + patient_id = data.get('patientId') + settings = data.get('settings', {}) + + # 创建检测会话 + session_id = db_manager.create_detection_session(patient_id, settings) + + # 初始化检测状态 + 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(): + """停止检测""" + global current_detection + + try: + if not current_detection or current_detection.get('status') != 'running': + return jsonify({'success': False, 'error': '没有正在进行的检测'}), 400 + + # 更新检测状态 + current_detection['status'] = 'stopped' + current_detection['end_time'] = datetime.now() + + # 等待检测线程结束 + if detection_thread and detection_thread.is_alive(): + detection_thread.join(timeout=5) + + return jsonify({'success': True, 'message': '检测已停止'}) + + except Exception as e: + logger.error(f'停止检测失败: {e}') + return jsonify({'success': False, 'error': str(e)}), 500 + +@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/', 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/', 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) + +# ==================== 错误处理 ==================== + +@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__': + try: + # 初始化应用 + init_app() + + # 启动Flask服务 + logger.info('启动后端服务...') + app.run( + host='127.0.0.1', + port=5000, + debug=False, + threaded=True + ) + + except KeyboardInterrupt: + logger.info('服务被用户中断') + except Exception as e: + logger.error(f'服务启动失败: {e}') + sys.exit(1) + finally: + logger.info('后端服务已停止') \ No newline at end of file diff --git a/backend/config.ini b/backend/config.ini new file mode 100644 index 00000000..cb814776 --- /dev/null +++ b/backend/config.ini @@ -0,0 +1,41 @@ +[APP] +name = Body Balance Evaluation System +version = 1.0.0 +debug = false +log_level = INFO + +[SERVER] +host = 127.0.0.1 +port = 5000 +cors_origins = * + +[DATABASE] +path = data/balance_system.db +backup_interval = 24 +max_backups = 7 + +[DEVICES] +camera_index = 0 +camera_width = 640 +camera_height = 480 +camera_fps = 30 +imu_port = COM3 +pressure_port = COM4 + +[DETECTION] +default_duration = 60 +sampling_rate = 30 +balance_threshold = 0.2 +posture_threshold = 5.0 + +[DATA_PROCESSING] +filter_window = 5 +outlier_threshold = 2.0 +chart_dpi = 300 +export_format = csv + +[SECURITY] +secret_key = 026efbf83a2fe101f168780740da86bf1c9260625458e6782738aa9cf18f8e37 +session_timeout = 3600 +max_login_attempts = 5 + diff --git a/backend/data_processor.py b/backend/data_processor.py new file mode 100644 index 00000000..05f982ce --- /dev/null +++ b/backend/data_processor.py @@ -0,0 +1,730 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +数据处理模块 +负责数据分析、报告生成和数据导出 +""" + +import os +import json +import numpy as np +import pandas as pd +from datetime import datetime, timedelta +from typing import Dict, List, Optional, Any, Tuple +import logging +from pathlib import Path +import matplotlib.pyplot as plt +import matplotlib.dates as mdates +from matplotlib.backends.backend_pdf import PdfPages +import seaborn as sns +from reportlab.lib.pagesizes import letter, A4 +from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Image, Table, TableStyle +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle +from reportlab.lib.units import inch +from reportlab.lib import colors +from reportlab.pdfgen import canvas +from reportlab.lib.utils import ImageReader +from io import BytesIO + +logger = logging.getLogger(__name__) + +# 设置中文字体 +plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei'] +plt.rcParams['axes.unicode_minus'] = False +sns.set_style("whitegrid") + +class DataProcessor: + """数据处理器""" + + def __init__(self): + self.export_dir = Path('exports') + self.charts_dir = Path('charts') + + # 创建必要的目录 + self.export_dir.mkdir(exist_ok=True) + self.charts_dir.mkdir(exist_ok=True) + + logger.info('数据处理器初始化完成') + + def analyze_session(self, session_data: Dict[str, Any]) -> Dict[str, Any]: + """分析会话数据""" + try: + if not session_data or 'data' not in session_data: + return {'error': '没有可分析的数据'} + + data_points = session_data['data'] + if not data_points: + return {'error': '数据为空'} + + # 数据预处理 + processed_data = self._preprocess_session_data(data_points) + + # 统计分析 + statistical_analysis = self._statistical_analysis(processed_data) + + # 趋势分析 + trend_analysis = self._trend_analysis(processed_data) + + # 异常检测 + anomaly_analysis = self._anomaly_detection(processed_data) + + # 生成图表 + charts = self._generate_charts(processed_data, session_data['id']) + + analysis_result = { + 'session_info': { + 'session_id': session_data['id'], + 'patient_name': session_data.get('patient_name', '未知'), + 'start_time': session_data.get('start_time'), + 'end_time': session_data.get('end_time'), + 'duration': session_data.get('duration'), + 'data_points': len(data_points) + }, + 'statistical': statistical_analysis, + 'trends': trend_analysis, + 'anomalies': anomaly_analysis, + 'charts': charts, + 'summary': self._generate_summary(statistical_analysis, trend_analysis, anomaly_analysis) + } + + return analysis_result + + except Exception as e: + logger.error(f'会话数据分析失败: {e}') + return {'error': str(e)} + + def _preprocess_session_data(self, data_points: List[Dict]) -> Dict[str, List]: + """预处理会话数据""" + processed = { + 'timestamps': [], + 'pressure_left': [], + 'pressure_right': [], + 'pressure_total': [], + 'balance_index': [], + 'center_of_pressure_x': [], + 'center_of_pressure_y': [], + 'imu_pitch': [], + 'imu_roll': [], + 'imu_accel_total': [] + } + + for point in data_points: + try: + # 解析时间戳 + timestamp = datetime.fromisoformat(point['timestamp'].replace('Z', '+00:00')) + processed['timestamps'].append(timestamp) + + # 解析数据值 + data_value = point['data_value'] + + if point['data_type'] == 'pressure': + processed['pressure_left'].append(data_value.get('left_foot', 0)) + processed['pressure_right'].append(data_value.get('right_foot', 0)) + processed['pressure_total'].append(data_value.get('total_pressure', 0)) + processed['balance_index'].append(data_value.get('balance_index', 0)) + + cop = data_value.get('center_of_pressure', {'x': 0, 'y': 0}) + processed['center_of_pressure_x'].append(cop.get('x', 0)) + processed['center_of_pressure_y'].append(cop.get('y', 0)) + + elif point['data_type'] == 'imu': + processed['imu_pitch'].append(data_value.get('pitch', 0)) + processed['imu_roll'].append(data_value.get('roll', 0)) + processed['imu_accel_total'].append(data_value.get('total_accel', 0)) + + except Exception as e: + logger.warning(f'数据点处理失败: {e}') + continue + + return processed + + def _statistical_analysis(self, data: Dict[str, List]) -> Dict[str, Any]: + """统计分析""" + try: + stats = {} + + # 压力数据统计 + if data['pressure_total']: + pressure_total = np.array(data['pressure_total']) + stats['pressure'] = { + 'mean': float(np.mean(pressure_total)), + 'std': float(np.std(pressure_total)), + 'min': float(np.min(pressure_total)), + 'max': float(np.max(pressure_total)), + 'median': float(np.median(pressure_total)) + } + + # 左右脚压力分析 + if data['pressure_left'] and data['pressure_right']: + left_pressure = np.array(data['pressure_left']) + right_pressure = np.array(data['pressure_right']) + + stats['pressure_distribution'] = { + 'left_mean': float(np.mean(left_pressure)), + 'right_mean': float(np.mean(right_pressure)), + 'left_ratio': float(np.mean(left_pressure) / np.mean(pressure_total)) if np.mean(pressure_total) > 0 else 0, + 'right_ratio': float(np.mean(right_pressure) / np.mean(pressure_total)) if np.mean(pressure_total) > 0 else 0, + 'asymmetry': float(abs(np.mean(left_pressure) - np.mean(right_pressure)) / np.mean(pressure_total)) if np.mean(pressure_total) > 0 else 0 + } + + # 平衡指数统计 + if data['balance_index']: + balance_index = np.array(data['balance_index']) + stats['balance'] = { + 'mean': float(np.mean(balance_index)), + 'std': float(np.std(balance_index)), + 'min': float(np.min(balance_index)), + 'max': float(np.max(balance_index)), + 'stability_score': float(1.0 - np.std(balance_index)) # 稳定性评分 + } + + # 重心位置统计 + if data['center_of_pressure_x'] and data['center_of_pressure_y']: + cop_x = np.array(data['center_of_pressure_x']) + cop_y = np.array(data['center_of_pressure_y']) + + stats['center_of_pressure'] = { + 'mean_x': float(np.mean(cop_x)), + 'mean_y': float(np.mean(cop_y)), + 'std_x': float(np.std(cop_x)), + 'std_y': float(np.std(cop_y)), + 'range_x': float(np.max(cop_x) - np.min(cop_x)), + 'range_y': float(np.max(cop_y) - np.min(cop_y)), + 'total_displacement': float(np.sqrt(np.std(cop_x)**2 + np.std(cop_y)**2)) + } + + # IMU数据统计 + if data['imu_pitch'] and data['imu_roll']: + pitch = np.array(data['imu_pitch']) + roll = np.array(data['imu_roll']) + + stats['posture'] = { + 'mean_pitch': float(np.mean(pitch)), + 'mean_roll': float(np.mean(roll)), + 'std_pitch': float(np.std(pitch)), + 'std_roll': float(np.std(roll)), + 'max_pitch': float(np.max(np.abs(pitch))), + 'max_roll': float(np.max(np.abs(roll))), + 'posture_stability': float(1.0 - (np.std(pitch) + np.std(roll)) / 20) # 姿态稳定性 + } + + return stats + + except Exception as e: + logger.error(f'统计分析失败: {e}') + return {} + + def _trend_analysis(self, data: Dict[str, List]) -> Dict[str, Any]: + """趋势分析""" + try: + trends = {} + + # 平衡指数趋势 + if data['balance_index'] and len(data['balance_index']) > 10: + balance_trend = self._calculate_trend(data['balance_index']) + trends['balance_trend'] = { + 'slope': balance_trend['slope'], + 'direction': 'improving' if balance_trend['slope'] < 0 else 'deteriorating' if balance_trend['slope'] > 0 else 'stable', + 'correlation': balance_trend['correlation'] + } + + # 压力分布趋势 + if data['pressure_left'] and data['pressure_right'] and len(data['pressure_left']) > 10: + left_trend = self._calculate_trend(data['pressure_left']) + right_trend = self._calculate_trend(data['pressure_right']) + + trends['pressure_trend'] = { + 'left_slope': left_trend['slope'], + 'right_slope': right_trend['slope'], + 'asymmetry_trend': 'increasing' if abs(left_trend['slope'] - right_trend['slope']) > 0.1 else 'stable' + } + + # 姿态趋势 + if data['imu_pitch'] and data['imu_roll'] and len(data['imu_pitch']) > 10: + pitch_trend = self._calculate_trend(data['imu_pitch']) + roll_trend = self._calculate_trend(data['imu_roll']) + + trends['posture_trend'] = { + 'pitch_slope': pitch_trend['slope'], + 'roll_slope': roll_trend['slope'], + 'stability_trend': 'improving' if abs(pitch_trend['slope']) + abs(roll_trend['slope']) < 0.1 else 'deteriorating' + } + + return trends + + except Exception as e: + logger.error(f'趋势分析失败: {e}') + return {} + + def _calculate_trend(self, values: List[float]) -> Dict[str, float]: + """计算趋势""" + try: + x = np.arange(len(values)) + y = np.array(values) + + # 线性回归 + slope, intercept = np.polyfit(x, y, 1) + correlation = np.corrcoef(x, y)[0, 1] + + return { + 'slope': float(slope), + 'intercept': float(intercept), + 'correlation': float(correlation) + } + + except Exception as e: + logger.error(f'趋势计算失败: {e}') + return {'slope': 0, 'intercept': 0, 'correlation': 0} + + def _anomaly_detection(self, data: Dict[str, List]) -> Dict[str, Any]: + """异常检测""" + try: + anomalies = {} + + # 检测平衡指数异常 + if data['balance_index']: + balance_anomalies = self._detect_outliers(data['balance_index']) + anomalies['balance_anomalies'] = { + 'count': len(balance_anomalies), + 'percentage': len(balance_anomalies) / len(data['balance_index']) * 100, + 'indices': balance_anomalies + } + + # 检测压力异常 + if data['pressure_total']: + pressure_anomalies = self._detect_outliers(data['pressure_total']) + anomalies['pressure_anomalies'] = { + 'count': len(pressure_anomalies), + 'percentage': len(pressure_anomalies) / len(data['pressure_total']) * 100, + 'indices': pressure_anomalies + } + + # 检测姿态异常 + if data['imu_pitch'] and data['imu_roll']: + pitch_anomalies = self._detect_outliers(data['imu_pitch']) + roll_anomalies = self._detect_outliers(data['imu_roll']) + + anomalies['posture_anomalies'] = { + 'pitch_count': len(pitch_anomalies), + 'roll_count': len(roll_anomalies), + 'total_percentage': (len(pitch_anomalies) + len(roll_anomalies)) / (len(data['imu_pitch']) + len(data['imu_roll'])) * 100 + } + + return anomalies + + except Exception as e: + logger.error(f'异常检测失败: {e}') + return {} + + def _detect_outliers(self, values: List[float], threshold: float = 2.0) -> List[int]: + """检测异常值""" + try: + data = np.array(values) + mean = np.mean(data) + std = np.std(data) + + # 使用Z-score方法检测异常值 + z_scores = np.abs((data - mean) / std) + outlier_indices = np.where(z_scores > threshold)[0].tolist() + + return outlier_indices + + except Exception as e: + logger.error(f'异常值检测失败: {e}') + return [] + + def _generate_charts(self, data: Dict[str, List], session_id: str) -> Dict[str, str]: + """生成图表""" + try: + charts = {} + + # 创建会话专用目录 + session_charts_dir = self.charts_dir / session_id + session_charts_dir.mkdir(exist_ok=True) + + # 生成平衡指数时间序列图 + if data['timestamps'] and data['balance_index']: + chart_path = self._create_balance_chart(data, session_charts_dir) + if chart_path: + charts['balance_chart'] = str(chart_path) + + # 生成压力分布图 + if data['timestamps'] and data['pressure_left'] and data['pressure_right']: + chart_path = self._create_pressure_chart(data, session_charts_dir) + if chart_path: + charts['pressure_chart'] = str(chart_path) + + # 生成重心轨迹图 + if data['center_of_pressure_x'] and data['center_of_pressure_y']: + chart_path = self._create_cop_trajectory_chart(data, session_charts_dir) + if chart_path: + charts['cop_trajectory_chart'] = str(chart_path) + + # 生成姿态角度图 + if data['timestamps'] and data['imu_pitch'] and data['imu_roll']: + chart_path = self._create_posture_chart(data, session_charts_dir) + if chart_path: + charts['posture_chart'] = str(chart_path) + + return charts + + except Exception as e: + logger.error(f'图表生成失败: {e}') + return {} + + def _create_balance_chart(self, data: Dict[str, List], output_dir: Path) -> Optional[Path]: + """创建平衡指数图表""" + try: + plt.figure(figsize=(12, 6)) + + timestamps = data['timestamps'][:len(data['balance_index'])] + plt.plot(timestamps, data['balance_index'], 'b-', linewidth=1.5, label='平衡指数') + + # 添加平均线 + mean_balance = np.mean(data['balance_index']) + plt.axhline(y=mean_balance, color='r', linestyle='--', alpha=0.7, label=f'平均值: {mean_balance:.3f}') + + plt.title('平衡指数时间序列', fontsize=14, fontweight='bold') + plt.xlabel('时间', fontsize=12) + plt.ylabel('平衡指数', fontsize=12) + plt.legend() + plt.grid(True, alpha=0.3) + + # 格式化时间轴 + plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S')) + plt.xticks(rotation=45) + + plt.tight_layout() + + chart_path = output_dir / 'balance_chart.png' + plt.savefig(chart_path, dpi=300, bbox_inches='tight') + plt.close() + + return chart_path + + except Exception as e: + logger.error(f'平衡图表创建失败: {e}') + plt.close() + return None + + def _create_pressure_chart(self, data: Dict[str, List], output_dir: Path) -> Optional[Path]: + """创建压力分布图表""" + try: + fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10)) + + timestamps = data['timestamps'][:min(len(data['pressure_left']), len(data['pressure_right']))] + pressure_left = data['pressure_left'][:len(timestamps)] + pressure_right = data['pressure_right'][:len(timestamps)] + + # 上图:左右脚压力对比 + ax1.plot(timestamps, pressure_left, 'b-', linewidth=1.5, label='左脚压力') + ax1.plot(timestamps, pressure_right, 'r-', linewidth=1.5, label='右脚压力') + ax1.set_title('左右脚压力对比', fontsize=14, fontweight='bold') + ax1.set_ylabel('压力值', fontsize=12) + ax1.legend() + ax1.grid(True, alpha=0.3) + + # 下图:压力比例 + total_pressure = np.array(pressure_left) + np.array(pressure_right) + left_ratio = np.array(pressure_left) / total_pressure * 100 + right_ratio = np.array(pressure_right) / total_pressure * 100 + + ax2.plot(timestamps, left_ratio, 'b-', linewidth=1.5, label='左脚比例') + ax2.plot(timestamps, right_ratio, 'r-', linewidth=1.5, label='右脚比例') + ax2.axhline(y=50, color='g', linestyle='--', alpha=0.7, label='理想平衡线') + ax2.set_title('左右脚压力比例', fontsize=14, fontweight='bold') + ax2.set_xlabel('时间', fontsize=12) + ax2.set_ylabel('压力比例 (%)', fontsize=12) + ax2.legend() + ax2.grid(True, alpha=0.3) + + # 格式化时间轴 + for ax in [ax1, ax2]: + ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S')) + plt.setp(ax.xaxis.get_majorticklabels(), rotation=45) + + plt.tight_layout() + + chart_path = output_dir / 'pressure_chart.png' + plt.savefig(chart_path, dpi=300, bbox_inches='tight') + plt.close() + + return chart_path + + except Exception as e: + logger.error(f'压力图表创建失败: {e}') + plt.close() + return None + + def _create_cop_trajectory_chart(self, data: Dict[str, List], output_dir: Path) -> Optional[Path]: + """创建重心轨迹图表""" + try: + plt.figure(figsize=(10, 8)) + + x_positions = data['center_of_pressure_x'] + y_positions = data['center_of_pressure_y'] + + # 绘制轨迹 + plt.plot(x_positions, y_positions, 'b-', linewidth=1, alpha=0.7, label='重心轨迹') + plt.scatter(x_positions[0], y_positions[0], color='g', s=100, marker='o', label='起始点') + plt.scatter(x_positions[-1], y_positions[-1], color='r', s=100, marker='s', label='结束点') + + # 添加中心点 + center_x = np.mean(x_positions) + center_y = np.mean(y_positions) + plt.scatter(center_x, center_y, color='orange', s=150, marker='*', label='平均中心') + + # 添加置信椭圆 + self._add_confidence_ellipse(x_positions, y_positions, plt.gca()) + + plt.title('重心轨迹图', fontsize=14, fontweight='bold') + plt.xlabel('X方向位移 (mm)', fontsize=12) + plt.ylabel('Y方向位移 (mm)', fontsize=12) + plt.legend() + plt.grid(True, alpha=0.3) + plt.axis('equal') + + plt.tight_layout() + + chart_path = output_dir / 'cop_trajectory_chart.png' + plt.savefig(chart_path, dpi=300, bbox_inches='tight') + plt.close() + + return chart_path + + except Exception as e: + logger.error(f'重心轨迹图表创建失败: {e}') + plt.close() + return None + + def _add_confidence_ellipse(self, x: List[float], y: List[float], ax, n_std: float = 2.0): + """添加置信椭圆""" + try: + from matplotlib.patches import Ellipse + import matplotlib.transforms as transforms + + x_arr = np.array(x) + y_arr = np.array(y) + + cov = np.cov(x_arr, y_arr) + pearson = cov[0, 1] / np.sqrt(cov[0, 0] * cov[1, 1]) + + ell_radius_x = np.sqrt(1 + pearson) + ell_radius_y = np.sqrt(1 - pearson) + + ellipse = Ellipse((0, 0), width=ell_radius_x * 2, height=ell_radius_y * 2, + facecolor='none', edgecolor='red', alpha=0.5, linestyle='--') + + scale_x = np.sqrt(cov[0, 0]) * n_std + scale_y = np.sqrt(cov[1, 1]) * n_std + + mean_x = np.mean(x_arr) + mean_y = np.mean(y_arr) + + transf = transforms.Affine2D().scale(scale_x, scale_y).translate(mean_x, mean_y) + ellipse.set_transform(transf + ax.transData) + + ax.add_patch(ellipse) + + except Exception as e: + logger.warning(f'置信椭圆添加失败: {e}') + + def _create_posture_chart(self, data: Dict[str, List], output_dir: Path) -> Optional[Path]: + """创建姿态角度图表""" + try: + plt.figure(figsize=(12, 8)) + + timestamps = data['timestamps'][:min(len(data['imu_pitch']), len(data['imu_roll']))] + pitch = data['imu_pitch'][:len(timestamps)] + roll = data['imu_roll'][:len(timestamps)] + + plt.subplot(2, 1, 1) + plt.plot(timestamps, pitch, 'b-', linewidth=1.5, label='俯仰角') + plt.axhline(y=0, color='g', linestyle='--', alpha=0.7, label='理想位置') + plt.title('俯仰角变化', fontsize=14, fontweight='bold') + plt.ylabel('角度 (度)', fontsize=12) + plt.legend() + plt.grid(True, alpha=0.3) + + plt.subplot(2, 1, 2) + plt.plot(timestamps, roll, 'r-', linewidth=1.5, label='翻滚角') + plt.axhline(y=0, color='g', linestyle='--', alpha=0.7, label='理想位置') + plt.title('翻滚角变化', fontsize=14, fontweight='bold') + plt.xlabel('时间', fontsize=12) + plt.ylabel('角度 (度)', fontsize=12) + plt.legend() + plt.grid(True, alpha=0.3) + + # 格式化时间轴 + for i in range(1, 3): + ax = plt.subplot(2, 1, i) + ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S')) + plt.setp(ax.xaxis.get_majorticklabels(), rotation=45) + + plt.tight_layout() + + chart_path = output_dir / 'posture_chart.png' + plt.savefig(chart_path, dpi=300, bbox_inches='tight') + plt.close() + + return chart_path + + except Exception as e: + logger.error(f'姿态图表创建失败: {e}') + plt.close() + return None + + def _generate_summary(self, statistical: Dict, trends: Dict, anomalies: Dict) -> Dict[str, Any]: + """生成分析摘要""" + try: + summary = { + 'overall_assessment': 'normal', + 'key_findings': [], + 'recommendations': [] + } + + # 分析平衡状况 + if 'balance' in statistical: + balance_mean = statistical['balance']['mean'] + if balance_mean > 0.3: + summary['key_findings'].append('平衡能力较差,重心摆动较大') + summary['recommendations'].append('建议进行平衡训练,加强核心肌群锻炼') + summary['overall_assessment'] = 'poor' + elif balance_mean > 0.15: + summary['key_findings'].append('平衡能力一般,有改善空间') + summary['recommendations'].append('建议适当增加平衡练习') + if summary['overall_assessment'] == 'normal': + summary['overall_assessment'] = 'fair' + else: + summary['key_findings'].append('平衡能力良好') + + # 分析压力分布 + if 'pressure_distribution' in statistical: + asymmetry = statistical['pressure_distribution']['asymmetry'] + if asymmetry > 0.2: + summary['key_findings'].append('左右脚压力分布不均,存在明显偏重') + summary['recommendations'].append('注意纠正站立姿势,均匀分配体重') + if summary['overall_assessment'] in ['normal', 'fair']: + summary['overall_assessment'] = 'fair' + + # 分析姿态稳定性 + if 'posture' in statistical: + posture_stability = statistical['posture'].get('posture_stability', 1.0) + if posture_stability < 0.7: + summary['key_findings'].append('姿态稳定性较差,身体摆动较大') + summary['recommendations'].append('建议进行姿态矫正训练') + if summary['overall_assessment'] == 'normal': + summary['overall_assessment'] = 'fair' + + # 分析异常情况 + if anomalies: + total_anomalies = 0 + for key, value in anomalies.items(): + if isinstance(value, dict) and 'count' in value: + total_anomalies += value['count'] + + if total_anomalies > 10: + summary['key_findings'].append(f'检测到{total_anomalies}个异常数据点') + summary['recommendations'].append('建议重新进行检测或咨询专业医师') + + # 默认建议 + if not summary['recommendations']: + summary['recommendations'].append('继续保持良好的平衡训练习惯') + + return summary + + except Exception as e: + logger.error(f'摘要生成失败: {e}') + return { + 'overall_assessment': 'unknown', + 'key_findings': ['分析过程中出现错误'], + 'recommendations': ['建议重新进行检测'] + } + + def generate_report(self, session_id: str) -> str: + """生成PDF报告""" + try: + # 这里应该从数据库获取会话数据和分析结果 + # 目前返回一个模拟的报告路径 + report_path = self.export_dir / f'report_{session_id}_{datetime.now().strftime("%Y%m%d_%H%M%S")}.pdf' + + # 创建简单的PDF报告 + self._create_simple_pdf_report(session_id, report_path) + + return str(report_path) + + except Exception as e: + logger.error(f'报告生成失败: {e}') + raise + + def _create_simple_pdf_report(self, session_id: str, output_path: Path): + """创建简单的PDF报告""" + try: + from reportlab.pdfgen import canvas + from reportlab.lib.pagesizes import A4 + + c = canvas.Canvas(str(output_path), pagesize=A4) + width, height = A4 + + # 标题 + c.setFont("Helvetica-Bold", 20) + c.drawString(50, height - 50, "Balance Detection Report") + + # 会话信息 + c.setFont("Helvetica", 12) + y_position = height - 100 + + c.drawString(50, y_position, f"Session ID: {session_id}") + y_position -= 20 + c.drawString(50, y_position, f"Report Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + y_position -= 40 + + # 报告内容 + c.drawString(50, y_position, "Analysis Summary:") + y_position -= 20 + c.drawString(70, y_position, "• Balance assessment completed") + y_position -= 20 + c.drawString(70, y_position, "• Posture analysis performed") + y_position -= 20 + c.drawString(70, y_position, "• Movement patterns evaluated") + + c.save() + logger.info(f'PDF报告已生成: {output_path}') + + except Exception as e: + logger.error(f'PDF报告创建失败: {e}') + raise + + def export_data(self, session_id: str, format: str = 'csv') -> str: + """导出数据""" + try: + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + + if format.lower() == 'csv': + export_path = self.export_dir / f'data_{session_id}_{timestamp}.csv' + # 这里应该从数据库获取数据并导出为CSV + # 目前创建一个示例文件 + with open(export_path, 'w', encoding='utf-8') as f: + f.write('timestamp,data_type,value\n') + f.write(f'{datetime.now().isoformat()},sample,0.5\n') + + elif format.lower() == 'json': + export_path = self.export_dir / f'data_{session_id}_{timestamp}.json' + # 导出为JSON格式 + sample_data = { + 'session_id': session_id, + 'export_time': datetime.now().isoformat(), + 'data': [] + } + with open(export_path, 'w', encoding='utf-8') as f: + json.dump(sample_data, f, ensure_ascii=False, indent=2) + + else: + raise ValueError(f'不支持的导出格式: {format}') + + logger.info(f'数据已导出: {export_path}') + return str(export_path) + + except Exception as e: + logger.error(f'数据导出失败: {e}') + raise \ No newline at end of file diff --git a/backend/database.py b/backend/database.py new file mode 100644 index 00000000..0836a51d --- /dev/null +++ b/backend/database.py @@ -0,0 +1,618 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +数据库管理模块 +负责SQLite数据库的创建、连接和数据操作 +""" + +import sqlite3 +import json +import uuid +from datetime import datetime +from typing import List, Dict, Optional, Any +import logging + +logger = logging.getLogger(__name__) + +class DatabaseManager: + """数据库管理器""" + + def __init__(self, db_path: str): + self.db_path = db_path + self.connection = None + + def get_connection(self) -> sqlite3.Connection: + """获取数据库连接""" + if not self.connection: + self.connection = sqlite3.connect( + self.db_path, + check_same_thread=False, + timeout=30.0 + ) + self.connection.row_factory = sqlite3.Row + return self.connection + + def init_database(self): + """初始化数据库表结构""" + conn = self.get_connection() + cursor = conn.cursor() + + try: + # 创建患者表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS patients ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + gender TEXT, + age INTEGER, + height REAL, + weight REAL, + phone TEXT, + medical_history TEXT, + notes TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # 创建检测会话表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS detection_sessions ( + id TEXT PRIMARY KEY, + patient_id TEXT NOT NULL, + start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + end_time TIMESTAMP, + duration INTEGER, + frequency INTEGER, + status TEXT DEFAULT 'created', + settings TEXT, + data_points INTEGER DEFAULT 0, + video_path TEXT, + notes TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (patient_id) REFERENCES patients (id) + ) + ''') + + # 创建检测数据表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS detection_data ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + data_type TEXT NOT NULL, + data_value TEXT NOT NULL, + FOREIGN KEY (session_id) REFERENCES detection_sessions (id) + ) + ''') + + # 创建分析结果表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS analysis_results ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + analysis_type TEXT NOT NULL, + result_data TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (session_id) REFERENCES detection_sessions (id) + ) + ''') + + # 创建系统设置表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS system_settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + description TEXT, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # 创建索引 + cursor.execute('CREATE INDEX IF NOT EXISTS idx_patients_name ON patients (name)') + cursor.execute('CREATE INDEX IF NOT EXISTS idx_sessions_patient ON detection_sessions (patient_id)') + cursor.execute('CREATE INDEX IF NOT EXISTS idx_sessions_time ON detection_sessions (start_time)') + cursor.execute('CREATE INDEX IF NOT EXISTS idx_data_session ON detection_data (session_id)') + cursor.execute('CREATE INDEX IF NOT EXISTS idx_data_timestamp ON detection_data (timestamp)') + + conn.commit() + logger.info('数据库初始化完成') + + except Exception as e: + conn.rollback() + logger.error(f'数据库初始化失败: {e}') + raise + + # ==================== 患者管理 ==================== + + def create_patient(self, patient_data: Dict[str, Any]) -> str: + """创建患者记录""" + conn = self.get_connection() + cursor = conn.cursor() + + try: + patient_id = str(uuid.uuid4()) + + cursor.execute(''' + INSERT INTO patients ( + id, name, gender, age, height, weight, + phone, medical_history, notes + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', ( + patient_id, + patient_data.get('name'), + patient_data.get('gender'), + patient_data.get('age'), + patient_data.get('height'), + patient_data.get('weight'), + patient_data.get('phone'), + patient_data.get('medical_history'), + patient_data.get('notes') + )) + + conn.commit() + logger.info(f'创建患者记录: {patient_id}') + return patient_id + + except Exception as e: + conn.rollback() + logger.error(f'创建患者记录失败: {e}') + raise + + def get_patients(self, page: int = 1, size: int = 10, keyword: str = '') -> List[Dict]: + """获取患者列表""" + conn = self.get_connection() + cursor = conn.cursor() + + try: + offset = (page - 1) * size + + if keyword: + cursor.execute(''' + SELECT * FROM patients + WHERE name LIKE ? OR phone LIKE ? + ORDER BY created_at DESC + LIMIT ? OFFSET ? + ''', (f'%{keyword}%', f'%{keyword}%', size, offset)) + else: + cursor.execute(''' + SELECT * FROM patients + ORDER BY created_at DESC + LIMIT ? OFFSET ? + ''', (size, offset)) + + rows = cursor.fetchall() + return [dict(row) for row in rows] + + except Exception as e: + logger.error(f'获取患者列表失败: {e}') + raise + + def get_patients_count(self, keyword: str = '') -> int: + """获取患者总数""" + conn = self.get_connection() + cursor = conn.cursor() + + try: + if keyword: + cursor.execute(''' + SELECT COUNT(*) FROM patients + WHERE name LIKE ? OR phone LIKE ? + ''', (f'%{keyword}%', f'%{keyword}%')) + else: + cursor.execute('SELECT COUNT(*) FROM patients') + + return cursor.fetchone()[0] + + except Exception as e: + logger.error(f'获取患者总数失败: {e}') + return 0 + + def get_patient(self, patient_id: str) -> Optional[Dict]: + """获取单个患者信息""" + conn = self.get_connection() + cursor = conn.cursor() + + try: + cursor.execute('SELECT * FROM patients WHERE id = ?', (patient_id,)) + row = cursor.fetchone() + return dict(row) if row else None + + except Exception as e: + logger.error(f'获取患者信息失败: {e}') + return None + + def update_patient(self, patient_id: str, patient_data: Dict[str, Any]): + """更新患者信息""" + conn = self.get_connection() + cursor = conn.cursor() + + try: + cursor.execute(''' + UPDATE patients SET + name = ?, gender = ?, age = ?, height = ?, weight = ?, + phone = ?, medical_history = ?, notes = ?, + updated_at = CURRENT_TIMESTAMP + WHERE id = ? + ''', ( + patient_data.get('name'), + patient_data.get('gender'), + patient_data.get('age'), + patient_data.get('height'), + patient_data.get('weight'), + patient_data.get('phone'), + patient_data.get('medical_history'), + patient_data.get('notes'), + patient_id + )) + + conn.commit() + logger.info(f'更新患者信息: {patient_id}') + + except Exception as e: + conn.rollback() + logger.error(f'更新患者信息失败: {e}') + raise + + def delete_patient(self, patient_id: str): + """删除患者记录""" + conn = self.get_connection() + cursor = conn.cursor() + + try: + # 删除相关的检测数据 + cursor.execute(''' + DELETE FROM detection_data + WHERE session_id IN ( + SELECT id FROM detection_sessions WHERE patient_id = ? + ) + ''', (patient_id,)) + + # 删除相关的分析结果 + cursor.execute(''' + DELETE FROM analysis_results + WHERE session_id IN ( + SELECT id FROM detection_sessions WHERE patient_id = ? + ) + ''', (patient_id,)) + + # 删除检测会话 + cursor.execute('DELETE FROM detection_sessions WHERE patient_id = ?', (patient_id,)) + + # 删除患者记录 + cursor.execute('DELETE FROM patients WHERE id = ?', (patient_id,)) + + conn.commit() + logger.info(f'删除患者记录: {patient_id}') + + except Exception as e: + conn.rollback() + logger.error(f'删除患者记录失败: {e}') + raise + + # ==================== 检测会话管理 ==================== + + def create_detection_session(self, patient_id: str, settings: Dict[str, Any]) -> str: + """创建检测会话""" + conn = self.get_connection() + cursor = conn.cursor() + + try: + session_id = str(uuid.uuid4()) + + cursor.execute(''' + INSERT INTO detection_sessions ( + id, patient_id, duration, frequency, settings, status + ) VALUES (?, ?, ?, ?, ?, ?) + ''', ( + session_id, + patient_id, + settings.get('duration', 60), + settings.get('frequency', 60), + json.dumps(settings), + 'created' + )) + + conn.commit() + logger.info(f'创建检测会话: {session_id}') + return session_id + + except Exception as e: + conn.rollback() + logger.error(f'创建检测会话失败: {e}') + raise + + def update_session_status(self, session_id: str, status: str, data_points: int = 0): + """更新会话状态""" + conn = self.get_connection() + cursor = conn.cursor() + + try: + if status in ['completed', 'stopped', 'error']: + cursor.execute(''' + UPDATE detection_sessions SET + status = ?, data_points = ?, end_time = CURRENT_TIMESTAMP + WHERE id = ? + ''', (status, data_points, session_id)) + else: + cursor.execute(''' + UPDATE detection_sessions SET + status = ?, data_points = ? + WHERE id = ? + ''', (status, data_points, session_id)) + + conn.commit() + logger.info(f'更新会话状态: {session_id} -> {status}') + + except Exception as e: + conn.rollback() + logger.error(f'更新会话状态失败: {e}') + raise + + def get_detection_sessions(self, page: int = 1, size: int = 10, patient_id: str = None) -> List[Dict]: + """获取检测会话列表""" + conn = self.get_connection() + cursor = conn.cursor() + + try: + offset = (page - 1) * size + + if patient_id: + cursor.execute(''' + SELECT s.*, p.name as patient_name + FROM detection_sessions s + LEFT JOIN patients p ON s.patient_id = p.id + WHERE s.patient_id = ? + ORDER BY s.start_time DESC + LIMIT ? OFFSET ? + ''', (patient_id, size, offset)) + else: + cursor.execute(''' + SELECT s.*, p.name as patient_name + FROM detection_sessions s + LEFT JOIN patients p ON s.patient_id = p.id + ORDER BY s.start_time DESC + LIMIT ? OFFSET ? + ''', (size, offset)) + + rows = cursor.fetchall() + sessions = [] + + for row in rows: + session = dict(row) + # 解析设置JSON + if session['settings']: + try: + session['settings'] = json.loads(session['settings']) + except: + session['settings'] = {} + sessions.append(session) + + return sessions + + except Exception as e: + logger.error(f'获取检测会话列表失败: {e}') + raise + + def get_sessions_count(self, patient_id: str = None) -> int: + """获取会话总数""" + conn = self.get_connection() + cursor = conn.cursor() + + try: + if patient_id: + cursor.execute('SELECT COUNT(*) FROM detection_sessions WHERE patient_id = ?', (patient_id,)) + else: + cursor.execute('SELECT COUNT(*) FROM detection_sessions') + + return cursor.fetchone()[0] + + except Exception as e: + logger.error(f'获取会话总数失败: {e}') + return 0 + + def get_session_data(self, session_id: str) -> Optional[Dict]: + """获取会话详细数据""" + conn = self.get_connection() + cursor = conn.cursor() + + try: + # 获取会话基本信息 + cursor.execute(''' + SELECT s.*, p.name as patient_name + FROM detection_sessions s + LEFT JOIN patients p ON s.patient_id = p.id + WHERE s.id = ? + ''', (session_id,)) + + session_row = cursor.fetchone() + if not session_row: + return None + + session = dict(session_row) + + # 解析设置JSON + if session['settings']: + try: + session['settings'] = json.loads(session['settings']) + except: + session['settings'] = {} + + # 获取检测数据 + cursor.execute(''' + SELECT * FROM detection_data + WHERE session_id = ? + ORDER BY timestamp + ''', (session_id,)) + + data_rows = cursor.fetchall() + session['data'] = [] + + for row in data_rows: + data_point = dict(row) + # 解析数据JSON + try: + data_point['data_value'] = json.loads(data_point['data_value']) + except: + pass + session['data'].append(data_point) + + return session + + except Exception as e: + logger.error(f'获取会话数据失败: {e}') + return None + + # ==================== 检测数据管理 ==================== + + def save_detection_data(self, session_id: str, data: Dict[str, Any]): + """保存检测数据""" + conn = self.get_connection() + cursor = conn.cursor() + + try: + # 保存不同类型的数据 + for data_type, data_value in data.items(): + cursor.execute(''' + INSERT INTO detection_data (session_id, data_type, data_value) + VALUES (?, ?, ?) + ''', (session_id, data_type, json.dumps(data_value))) + + conn.commit() + + except Exception as e: + conn.rollback() + logger.error(f'保存检测数据失败: {e}') + raise + + def get_latest_detection_data(self, session_id: str, limit: int = 10) -> List[Dict]: + """获取最新的检测数据""" + conn = self.get_connection() + cursor = conn.cursor() + + try: + cursor.execute(''' + SELECT * FROM detection_data + WHERE session_id = ? + ORDER BY timestamp DESC + LIMIT ? + ''', (session_id, limit)) + + rows = cursor.fetchall() + data_points = [] + + for row in rows: + data_point = dict(row) + try: + data_point['data_value'] = json.loads(data_point['data_value']) + except: + pass + data_points.append(data_point) + + return data_points + + except Exception as e: + logger.error(f'获取最新检测数据失败: {e}') + return [] + + # ==================== 分析结果管理 ==================== + + def save_analysis_result(self, session_id: str, analysis_result: Dict[str, Any]): + """保存分析结果""" + conn = self.get_connection() + cursor = conn.cursor() + + try: + for analysis_type, result_data in analysis_result.items(): + cursor.execute(''' + INSERT INTO analysis_results (session_id, analysis_type, result_data) + VALUES (?, ?, ?) + ''', (session_id, analysis_type, json.dumps(result_data))) + + conn.commit() + logger.info(f'保存分析结果: {session_id}') + + except Exception as e: + conn.rollback() + logger.error(f'保存分析结果失败: {e}') + raise + + def get_analysis_results(self, session_id: str) -> Dict[str, Any]: + """获取分析结果""" + conn = self.get_connection() + cursor = conn.cursor() + + try: + cursor.execute(''' + SELECT * FROM analysis_results + WHERE session_id = ? + ORDER BY created_at DESC + ''', (session_id,)) + + rows = cursor.fetchall() + results = {} + + for row in rows: + analysis_type = row['analysis_type'] + try: + result_data = json.loads(row['result_data']) + results[analysis_type] = result_data + except: + results[analysis_type] = row['result_data'] + + return results + + except Exception as e: + logger.error(f'获取分析结果失败: {e}') + return {} + + # ==================== 系统设置管理 ==================== + + def get_setting(self, key: str, default_value: Any = None) -> Any: + """获取系统设置""" + conn = self.get_connection() + cursor = conn.cursor() + + try: + cursor.execute('SELECT value FROM system_settings WHERE key = ?', (key,)) + row = cursor.fetchone() + + if row: + try: + return json.loads(row['value']) + except: + return row['value'] + + return default_value + + except Exception as e: + logger.error(f'获取系统设置失败: {e}') + return default_value + + def set_setting(self, key: str, value: Any, description: str = ''): + """设置系统设置""" + conn = self.get_connection() + cursor = conn.cursor() + + try: + value_str = json.dumps(value) if not isinstance(value, str) else value + + cursor.execute(''' + INSERT OR REPLACE INTO system_settings (key, value, description, updated_at) + VALUES (?, ?, ?, CURRENT_TIMESTAMP) + ''', (key, value_str, description)) + + conn.commit() + logger.info(f'设置系统设置: {key}') + + except Exception as e: + conn.rollback() + logger.error(f'设置系统设置失败: {e}') + raise + + def close(self): + """关闭数据库连接""" + if self.connection: + self.connection.close() + self.connection = None + logger.info('数据库连接已关闭') \ No newline at end of file diff --git a/backend/detection_engine.py b/backend/detection_engine.py new file mode 100644 index 00000000..9ff747ed --- /dev/null +++ b/backend/detection_engine.py @@ -0,0 +1,615 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +检测引擎模块 +负责实时数据处理、姿态分析和平衡评估 +""" + +import numpy as np +import cv2 +from datetime import datetime, timedelta +from typing import Dict, List, Optional, Any, Tuple +import logging +import threading +from collections import deque +import json + +logger = logging.getLogger(__name__) + +class DetectionEngine: + """检测引擎""" + + def __init__(self): + self.session_data = {} # 存储各会话的数据 + self.data_lock = threading.Lock() + self.analysis_algorithms = { + 'balance_analysis': BalanceAnalyzer(), + 'posture_analysis': PostureAnalyzer(), + 'movement_analysis': MovementAnalyzer() + } + + logger.info('检测引擎初始化完成') + + def start_session(self, session_id: str, settings: Dict[str, Any]): + """开始检测会话""" + with self.data_lock: + self.session_data[session_id] = { + 'settings': settings, + 'start_time': datetime.now(), + 'data_buffer': deque(maxlen=1000), # 保留最近1000个数据点 + 'analysis_results': {}, + 'real_time_metrics': {} + } + + logger.info(f'检测会话开始: {session_id}') + + def process_data(self, session_id: str, raw_data: Dict[str, Any]) -> Dict[str, Any]: + """处理实时数据""" + if session_id not in self.session_data: + logger.warning(f'会话不存在: {session_id}') + return {} + + try: + # 数据预处理 + processed_data = self._preprocess_data(raw_data) + + # 存储数据 + with self.data_lock: + self.session_data[session_id]['data_buffer'].append(processed_data) + + # 实时分析 + real_time_results = self._real_time_analysis(session_id, processed_data) + + # 更新实时指标 + with self.data_lock: + self.session_data[session_id]['real_time_metrics'].update(real_time_results) + + return real_time_results + + except Exception as e: + logger.error(f'数据处理失败: {e}') + return {} + + def _preprocess_data(self, raw_data: Dict[str, Any]) -> Dict[str, Any]: + """数据预处理""" + processed = { + 'timestamp': raw_data.get('timestamp', datetime.now().isoformat()) + } + + # 处理摄像头数据 + if 'camera' in raw_data: + camera_data = raw_data['camera'] + if 'pose_data' in camera_data: + processed['pose'] = self._process_pose_data(camera_data['pose_data']) + + # 处理IMU数据 + if 'imu' in raw_data: + processed['imu'] = self._process_imu_data(raw_data['imu']) + + # 处理压力数据 + if 'pressure' in raw_data: + processed['pressure'] = self._process_pressure_data(raw_data['pressure']) + + return processed + + def _process_pose_data(self, pose_data: Dict[str, Any]) -> Dict[str, Any]: + """处理姿态数据""" + return { + 'center_of_gravity': pose_data.get('center_of_gravity', {'x': 0, 'y': 0}), + 'body_angle': pose_data.get('body_angle', {'pitch': 0, 'roll': 0, 'yaw': 0}), + 'confidence': pose_data.get('confidence', 0.0) + } + + def _process_imu_data(self, imu_data: Dict[str, Any]) -> Dict[str, Any]: + """处理IMU数据""" + # 计算合成加速度 + accel = imu_data.get('accel', {'x': 0, 'y': 0, 'z': 0}) + total_accel = np.sqrt(accel['x']**2 + accel['y']**2 + accel['z']**2) + + # 计算倾斜角度 + pitch = np.arctan2(accel['y'], np.sqrt(accel['x']**2 + accel['z']**2)) * 180 / np.pi + roll = np.arctan2(-accel['x'], accel['z']) * 180 / np.pi + + return { + 'accel': accel, + 'gyro': imu_data.get('gyro', {'x': 0, 'y': 0, 'z': 0}), + 'total_accel': total_accel, + 'pitch': pitch, + 'roll': roll, + 'temperature': imu_data.get('temperature', 0) + } + + def _process_pressure_data(self, pressure_data: Dict[str, Any]) -> Dict[str, Any]: + """处理压力数据""" + left_foot = pressure_data.get('left_foot', 0) + right_foot = pressure_data.get('right_foot', 0) + total_pressure = left_foot + right_foot + + # 计算压力分布比例 + if total_pressure > 0: + left_ratio = left_foot / total_pressure + right_ratio = right_foot / total_pressure + balance_index = abs(left_ratio - right_ratio) # 平衡指数,越小越平衡 + else: + left_ratio = right_ratio = 0.5 + balance_index = 0 + + return { + 'left_foot': left_foot, + 'right_foot': right_foot, + 'total_pressure': total_pressure, + 'left_ratio': left_ratio, + 'right_ratio': right_ratio, + 'balance_index': balance_index, + 'center_of_pressure': pressure_data.get('center_of_pressure', {'x': 0, 'y': 0}) + } + + def _real_time_analysis(self, session_id: str, data: Dict[str, Any]) -> Dict[str, Any]: + """实时分析""" + results = {} + + try: + # 获取历史数据用于趋势分析 + with self.data_lock: + data_buffer = list(self.session_data[session_id]['data_buffer']) + + if len(data_buffer) < 2: + return results + + # 平衡分析 + if 'pressure' in data: + balance_result = self.analysis_algorithms['balance_analysis'].analyze_real_time( + data['pressure'], data_buffer + ) + results['balance'] = balance_result + + # 姿态分析 + if 'pose' in data or 'imu' in data: + posture_result = self.analysis_algorithms['posture_analysis'].analyze_real_time( + data, data_buffer + ) + results['posture'] = posture_result + + # 运动分析 + movement_result = self.analysis_algorithms['movement_analysis'].analyze_real_time( + data, data_buffer + ) + results['movement'] = movement_result + + except Exception as e: + logger.error(f'实时分析失败: {e}') + + return results + + def get_latest_data(self, session_id: str) -> Dict[str, Any]: + """获取最新数据""" + if session_id not in self.session_data: + return {} + + with self.data_lock: + session = self.session_data[session_id] + if not session['data_buffer']: + return {} + + latest_data = session['data_buffer'][-1] + real_time_metrics = session['real_time_metrics'].copy() + + return { + 'latest_data': latest_data, + 'real_time_metrics': real_time_metrics, + 'data_count': len(session['data_buffer']) + } + + def analyze_session(self, session_id: str) -> Dict[str, Any]: + """分析整个会话数据""" + if session_id not in self.session_data: + logger.warning(f'会话不存在: {session_id}') + return {} + + try: + with self.data_lock: + session = self.session_data[session_id] + data_buffer = list(session['data_buffer']) + settings = session['settings'] + + if not data_buffer: + return {'error': '没有数据可分析'} + + analysis_results = {} + + # 全面平衡分析 + balance_analysis = self.analysis_algorithms['balance_analysis'].analyze_full_session( + data_buffer, settings + ) + analysis_results['balance'] = balance_analysis + + # 全面姿态分析 + posture_analysis = self.analysis_algorithms['posture_analysis'].analyze_full_session( + data_buffer, settings + ) + analysis_results['posture'] = posture_analysis + + # 全面运动分析 + movement_analysis = self.analysis_algorithms['movement_analysis'].analyze_full_session( + data_buffer, settings + ) + analysis_results['movement'] = movement_analysis + + # 综合评估 + overall_assessment = self._generate_overall_assessment(analysis_results) + analysis_results['overall'] = overall_assessment + + # 保存分析结果 + with self.data_lock: + self.session_data[session_id]['analysis_results'] = analysis_results + + logger.info(f'会话分析完成: {session_id}') + return analysis_results + + except Exception as e: + logger.error(f'会话分析失败: {e}') + return {'error': str(e)} + + def _generate_overall_assessment(self, analysis_results: Dict[str, Any]) -> Dict[str, Any]: + """生成综合评估""" + try: + # 提取各项评分 + balance_score = analysis_results.get('balance', {}).get('score', 0) + posture_score = analysis_results.get('posture', {}).get('score', 0) + movement_score = analysis_results.get('movement', {}).get('score', 0) + + # 计算综合评分(加权平均) + weights = {'balance': 0.4, 'posture': 0.3, 'movement': 0.3} + overall_score = ( + balance_score * weights['balance'] + + posture_score * weights['posture'] + + movement_score * weights['movement'] + ) + + # 评估等级 + if overall_score >= 90: + grade = 'A' + description = '优秀' + elif overall_score >= 80: + grade = 'B' + description = '良好' + elif overall_score >= 70: + grade = 'C' + description = '一般' + elif overall_score >= 60: + grade = 'D' + description = '较差' + else: + grade = 'E' + description = '差' + + # 生成建议 + recommendations = self._generate_recommendations(analysis_results) + + return { + 'score': round(overall_score, 1), + 'grade': grade, + 'description': description, + 'recommendations': recommendations, + 'component_scores': { + 'balance': balance_score, + 'posture': posture_score, + 'movement': movement_score + } + } + + except Exception as e: + logger.error(f'综合评估生成失败: {e}') + return {'score': 0, 'grade': 'E', 'description': '评估失败'} + + def _generate_recommendations(self, analysis_results: Dict[str, Any]) -> List[str]: + """生成改善建议""" + recommendations = [] + + try: + # 平衡相关建议 + balance_data = analysis_results.get('balance', {}) + if balance_data.get('score', 0) < 80: + if balance_data.get('left_right_imbalance', 0) > 0.2: + recommendations.append('注意左右脚压力分布,建议进行单脚站立练习') + if balance_data.get('stability_index', 0) > 0.5: + recommendations.append('重心摆动较大,建议进行静态平衡训练') + + # 姿态相关建议 + posture_data = analysis_results.get('posture', {}) + if posture_data.get('score', 0) < 80: + if abs(posture_data.get('avg_pitch', 0)) > 5: + recommendations.append('身体前后倾斜较明显,注意保持直立姿态') + if abs(posture_data.get('avg_roll', 0)) > 5: + recommendations.append('身体左右倾斜较明显,注意身体对称性') + + # 运动相关建议 + movement_data = analysis_results.get('movement', {}) + if movement_data.get('score', 0) < 80: + if movement_data.get('movement_variability', 0) > 0.8: + recommendations.append('身体摆动过大,建议进行核心稳定性训练') + if movement_data.get('movement_frequency', 0) > 2: + recommendations.append('身体摆动频率较高,建议放松并专注于静态平衡') + + # 通用建议 + if not recommendations: + recommendations.append('整体表现良好,继续保持规律的平衡训练') + + except Exception as e: + logger.error(f'建议生成失败: {e}') + recommendations = ['建议咨询专业医师进行详细评估'] + + return recommendations + + def end_session(self, session_id: str): + """结束检测会话""" + if session_id in self.session_data: + with self.data_lock: + del self.session_data[session_id] + logger.info(f'检测会话结束: {session_id}') + + +class BalanceAnalyzer: + """平衡分析器""" + + def analyze_real_time(self, pressure_data: Dict[str, Any], data_buffer: List[Dict]) -> Dict[str, Any]: + """实时平衡分析""" + try: + balance_index = pressure_data.get('balance_index', 0) + cop = pressure_data.get('center_of_pressure', {'x': 0, 'y': 0}) + + # 计算最近10个数据点的平衡稳定性 + recent_data = data_buffer[-10:] if len(data_buffer) >= 10 else data_buffer + if recent_data and all('pressure' in d for d in recent_data): + balance_indices = [d['pressure'].get('balance_index', 0) for d in recent_data] + stability = 1.0 - np.std(balance_indices) # 标准差越小,稳定性越高 + else: + stability = 0.5 + + return { + 'balance_index': balance_index, + 'stability': max(0, min(1, stability)), + 'center_of_pressure': cop, + 'status': 'stable' if balance_index < 0.2 else 'unstable' + } + + except Exception as e: + logger.error(f'实时平衡分析失败: {e}') + return {'balance_index': 0, 'stability': 0, 'status': 'unknown'} + + def analyze_full_session(self, data_buffer: List[Dict], settings: Dict) -> Dict[str, Any]: + """全会话平衡分析""" + try: + pressure_data = [d['pressure'] for d in data_buffer if 'pressure' in d] + if not pressure_data: + return {'score': 0, 'error': '没有压力数据'} + + # 提取关键指标 + balance_indices = [d.get('balance_index', 0) for d in pressure_data] + left_ratios = [d.get('left_ratio', 0.5) for d in pressure_data] + right_ratios = [d.get('right_ratio', 0.5) for d in pressure_data] + + # 计算统计指标 + avg_balance_index = np.mean(balance_indices) + std_balance_index = np.std(balance_indices) + max_balance_index = np.max(balance_indices) + + # 左右脚不平衡程度 + left_right_imbalance = abs(np.mean(left_ratios) - np.mean(right_ratios)) + + # 稳定性指数(基于标准差) + stability_index = std_balance_index + + # 计算评分(0-100分) + balance_score = max(0, 100 - avg_balance_index * 200) # 平衡指数越小分数越高 + stability_score = max(0, 100 - stability_index * 500) # 稳定性越高分数越高 + symmetry_score = max(0, 100 - left_right_imbalance * 200) # 对称性越好分数越高 + + overall_score = (balance_score + stability_score + symmetry_score) / 3 + + return { + 'score': round(overall_score, 1), + 'avg_balance_index': round(avg_balance_index, 3), + 'stability_index': round(stability_index, 3), + 'left_right_imbalance': round(left_right_imbalance, 3), + 'max_imbalance': round(max_balance_index, 3), + 'data_points': len(pressure_data), + 'component_scores': { + 'balance': round(balance_score, 1), + 'stability': round(stability_score, 1), + 'symmetry': round(symmetry_score, 1) + } + } + + except Exception as e: + logger.error(f'全会话平衡分析失败: {e}') + return {'score': 0, 'error': str(e)} + + +class PostureAnalyzer: + """姿态分析器""" + + def analyze_real_time(self, data: Dict[str, Any], data_buffer: List[Dict]) -> Dict[str, Any]: + """实时姿态分析""" + try: + result = {} + + # 分析IMU数据 + if 'imu' in data: + imu_data = data['imu'] + result['pitch'] = imu_data.get('pitch', 0) + result['roll'] = imu_data.get('roll', 0) + result['total_accel'] = imu_data.get('total_accel', 0) + + # 分析姿态数据 + if 'pose' in data: + pose_data = data['pose'] + result['body_angle'] = pose_data.get('body_angle', {}) + result['confidence'] = pose_data.get('confidence', 0) + + # 计算姿态稳定性 + recent_data = data_buffer[-5:] if len(data_buffer) >= 5 else data_buffer + if recent_data: + if 'imu' in data: + pitches = [d.get('imu', {}).get('pitch', 0) for d in recent_data if 'imu' in d] + rolls = [d.get('imu', {}).get('roll', 0) for d in recent_data if 'imu' in d] + + if pitches and rolls: + pitch_stability = 1.0 - min(1.0, np.std(pitches) / 10) + roll_stability = 1.0 - min(1.0, np.std(rolls) / 10) + result['stability'] = (pitch_stability + roll_stability) / 2 + + return result + + except Exception as e: + logger.error(f'实时姿态分析失败: {e}') + return {} + + def analyze_full_session(self, data_buffer: List[Dict], settings: Dict) -> Dict[str, Any]: + """全会话姿态分析""" + try: + imu_data = [d['imu'] for d in data_buffer if 'imu' in d] + if not imu_data: + return {'score': 0, 'error': '没有IMU数据'} + + # 提取角度数据 + pitches = [d.get('pitch', 0) for d in imu_data] + rolls = [d.get('roll', 0) for d in imu_data] + + # 计算统计指标 + avg_pitch = np.mean(pitches) + avg_roll = np.mean(rolls) + std_pitch = np.std(pitches) + std_roll = np.std(rolls) + max_pitch = np.max(np.abs(pitches)) + max_roll = np.max(np.abs(rolls)) + + # 计算评分 + pitch_score = max(0, 100 - abs(avg_pitch) * 5) # 平均倾斜角度越小分数越高 + roll_score = max(0, 100 - abs(avg_roll) * 5) + stability_score = max(0, 100 - (std_pitch + std_roll) * 10) # 稳定性越高分数越高 + + overall_score = (pitch_score + roll_score + stability_score) / 3 + + return { + 'score': round(overall_score, 1), + 'avg_pitch': round(avg_pitch, 2), + 'avg_roll': round(avg_roll, 2), + 'std_pitch': round(std_pitch, 2), + 'std_roll': round(std_roll, 2), + 'max_pitch': round(max_pitch, 2), + 'max_roll': round(max_roll, 2), + 'data_points': len(imu_data), + 'component_scores': { + 'pitch': round(pitch_score, 1), + 'roll': round(roll_score, 1), + 'stability': round(stability_score, 1) + } + } + + except Exception as e: + logger.error(f'全会话姿态分析失败: {e}') + return {'score': 0, 'error': str(e)} + + +class MovementAnalyzer: + """运动分析器""" + + def analyze_real_time(self, data: Dict[str, Any], data_buffer: List[Dict]) -> Dict[str, Any]: + """实时运动分析""" + try: + if len(data_buffer) < 5: + return {'movement_detected': False} + + # 分析最近的运动模式 + recent_data = data_buffer[-10:] + + # 计算重心位置变化 + if 'pressure' in data: + cop_positions = [] + for d in recent_data: + if 'pressure' in d: + cop = d['pressure'].get('center_of_pressure', {'x': 0, 'y': 0}) + cop_positions.append((cop['x'], cop['y'])) + + if len(cop_positions) >= 2: + # 计算运动幅度 + x_positions = [pos[0] for pos in cop_positions] + y_positions = [pos[1] for pos in cop_positions] + movement_range_x = np.max(x_positions) - np.min(x_positions) + movement_range_y = np.max(y_positions) - np.min(y_positions) + + return { + 'movement_detected': movement_range_x > 5 or movement_range_y > 5, + 'movement_range_x': movement_range_x, + 'movement_range_y': movement_range_y, + 'total_movement': np.sqrt(movement_range_x**2 + movement_range_y**2) + } + + return {'movement_detected': False} + + except Exception as e: + logger.error(f'实时运动分析失败: {e}') + return {'movement_detected': False} + + def analyze_full_session(self, data_buffer: List[Dict], settings: Dict) -> Dict[str, Any]: + """全会话运动分析""" + try: + # 提取压力中心数据 + cop_data = [] + for d in data_buffer: + if 'pressure' in d: + cop = d['pressure'].get('center_of_pressure', {'x': 0, 'y': 0}) + cop_data.append((cop['x'], cop['y'])) + + if len(cop_data) < 10: + return {'score': 0, 'error': '数据不足'} + + # 计算运动指标 + x_positions = [pos[0] for pos in cop_data] + y_positions = [pos[1] for pos in cop_data] + + # 运动范围 + movement_range_x = np.max(x_positions) - np.min(x_positions) + movement_range_y = np.max(y_positions) - np.min(y_positions) + total_range = np.sqrt(movement_range_x**2 + movement_range_y**2) + + # 运动变异性 + movement_variability = np.std(x_positions) + np.std(y_positions) + + # 运动路径长度 + path_length = 0 + for i in range(1, len(cop_data)): + dx = cop_data[i][0] - cop_data[i-1][0] + dy = cop_data[i][1] - cop_data[i-1][1] + path_length += np.sqrt(dx**2 + dy**2) + + # 运动频率分析(简化) + movement_frequency = path_length / len(cop_data) if len(cop_data) > 0 else 0 + + # 计算评分(运动幅度适中得分高) + range_score = max(0, 100 - total_range * 2) # 运动范围适中 + variability_score = max(0, 100 - movement_variability * 10) # 变异性小 + frequency_score = max(0, 100 - movement_frequency * 20) # 频率适中 + + overall_score = (range_score + variability_score + frequency_score) / 3 + + return { + 'score': round(overall_score, 1), + 'movement_range_x': round(movement_range_x, 2), + 'movement_range_y': round(movement_range_y, 2), + 'total_range': round(total_range, 2), + 'movement_variability': round(movement_variability, 2), + 'path_length': round(path_length, 2), + 'movement_frequency': round(movement_frequency, 2), + 'data_points': len(cop_data), + 'component_scores': { + 'range': round(range_score, 1), + 'variability': round(variability_score, 1), + 'frequency': round(frequency_score, 1) + } + } + + except Exception as e: + logger.error(f'全会话运动分析失败: {e}') + return {'score': 0, 'error': str(e)} \ No newline at end of file diff --git a/backend/device_manager.py b/backend/device_manager.py new file mode 100644 index 00000000..2b05f0a3 --- /dev/null +++ b/backend/device_manager.py @@ -0,0 +1,479 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +设备管理模块 +负责摄像头、IMU传感器和压力传感器的连接和数据采集 +""" + +import cv2 +import numpy as np +import time +import threading +import json +from datetime import datetime +from typing import Dict, List, Optional, Any +import logging + +logger = logging.getLogger(__name__) + +class DeviceManager: + """设备管理器""" + + def __init__(self): + self.camera = None + self.imu_device = None + self.pressure_device = None + self.device_status = { + 'camera': False, + 'imu': False, + 'pressure': False + } + self.calibration_data = {} + self.data_lock = threading.Lock() + self.latest_data = {} + + # 初始化设备 + self._init_devices() + + def _init_devices(self): + """初始化所有设备""" + try: + self._init_camera() + self._init_imu() + self._init_pressure_sensor() + logger.info('设备初始化完成') + except Exception as e: + logger.error(f'设备初始化失败: {e}') + + def _init_camera(self): + """初始化摄像头""" + try: + # 尝试连接默认摄像头 + self.camera = cv2.VideoCapture(0) + if self.camera.isOpened(): + # 设置摄像头参数 + self.camera.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) + self.camera.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) + self.camera.set(cv2.CAP_PROP_FPS, 30) + + self.device_status['camera'] = True + logger.info('摄像头初始化成功') + else: + logger.warning('摄像头连接失败') + self.camera = None + except Exception as e: + logger.error(f'摄像头初始化异常: {e}') + self.camera = None + + def _init_imu(self): + """初始化IMU传感器""" + try: + # 这里应该连接实际的IMU设备 + # 目前使用模拟数据 + self.imu_device = MockIMUDevice() + self.device_status['imu'] = True + logger.info('IMU传感器初始化成功(模拟)') + except Exception as e: + logger.error(f'IMU传感器初始化失败: {e}') + self.imu_device = None + + def _init_pressure_sensor(self): + """初始化压力传感器""" + try: + # 这里应该连接实际的压力传感器 + # 目前使用模拟数据 + self.pressure_device = MockPressureDevice() + self.device_status['pressure'] = True + logger.info('压力传感器初始化成功(模拟)') + except Exception as e: + logger.error(f'压力传感器初始化失败: {e}') + self.pressure_device = None + + def get_device_status(self) -> Dict[str, bool]: + """获取设备状态""" + return self.device_status.copy() + + def get_connected_devices(self) -> List[str]: + """获取已连接的设备列表""" + return [device for device, status in self.device_status.items() if status] + + def refresh_devices(self): + """刷新设备连接""" + logger.info('刷新设备连接...') + + # 重新初始化所有设备 + if self.camera: + self.camera.release() + + self._init_devices() + + def calibrate_devices(self) -> Dict[str, Any]: + """校准设备""" + calibration_result = {} + + try: + # 摄像头校准 + if self.device_status['camera']: + camera_calibration = self._calibrate_camera() + calibration_result['camera'] = camera_calibration + + # IMU校准 + if self.device_status['imu']: + imu_calibration = self._calibrate_imu() + calibration_result['imu'] = imu_calibration + + # 压力传感器校准 + if self.device_status['pressure']: + pressure_calibration = self._calibrate_pressure() + calibration_result['pressure'] = pressure_calibration + + self.calibration_data = calibration_result + logger.info('设备校准完成') + + except Exception as e: + logger.error(f'设备校准失败: {e}') + raise + + return calibration_result + + def _calibrate_camera(self) -> Dict[str, Any]: + """校准摄像头""" + if not self.camera or not self.camera.isOpened(): + return {'status': 'failed', 'error': '摄像头未连接'} + + try: + # 获取几帧图像进行校准 + frames = [] + for _ in range(10): + ret, frame = self.camera.read() + if ret: + frames.append(frame) + time.sleep(0.1) + + if not frames: + return {'status': 'failed', 'error': '无法获取图像'} + + # 计算平均亮度和对比度 + avg_brightness = np.mean([np.mean(cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)) for frame in frames]) + + calibration = { + 'status': 'success', + 'brightness': float(avg_brightness), + 'resolution': (int(self.camera.get(cv2.CAP_PROP_FRAME_WIDTH)), + int(self.camera.get(cv2.CAP_PROP_FRAME_HEIGHT))), + 'fps': float(self.camera.get(cv2.CAP_PROP_FPS)), + 'timestamp': datetime.now().isoformat() + } + + return calibration + + except Exception as e: + return {'status': 'failed', 'error': str(e)} + + def _calibrate_imu(self) -> Dict[str, Any]: + """校准IMU传感器""" + if not self.imu_device: + return {'status': 'failed', 'error': 'IMU设备未连接'} + + try: + # 收集静态数据进行零点校准 + samples = [] + for _ in range(100): + data = self.imu_device.read_data() + samples.append(data) + time.sleep(0.01) + + # 计算零点偏移 + accel_offset = { + 'x': np.mean([s['accel']['x'] for s in samples]), + 'y': np.mean([s['accel']['y'] for s in samples]), + 'z': np.mean([s['accel']['z'] for s in samples]) - 9.8 # 重力补偿 + } + + gyro_offset = { + 'x': np.mean([s['gyro']['x'] for s in samples]), + 'y': np.mean([s['gyro']['y'] for s in samples]), + 'z': np.mean([s['gyro']['z'] for s in samples]) + } + + calibration = { + 'status': 'success', + 'accel_offset': accel_offset, + 'gyro_offset': gyro_offset, + 'timestamp': datetime.now().isoformat() + } + + return calibration + + except Exception as e: + return {'status': 'failed', 'error': str(e)} + + def _calibrate_pressure(self) -> Dict[str, Any]: + """校准压力传感器""" + if not self.pressure_device: + return {'status': 'failed', 'error': '压力传感器未连接'} + + try: + # 收集零压力数据 + samples = [] + for _ in range(50): + data = self.pressure_device.read_data() + samples.append(data) + time.sleep(0.02) + + # 计算零点偏移 + zero_offset = { + 'left_foot': np.mean([s['left_foot'] for s in samples]), + 'right_foot': np.mean([s['right_foot'] for s in samples]) + } + + calibration = { + 'status': 'success', + 'zero_offset': zero_offset, + 'timestamp': datetime.now().isoformat() + } + + return calibration + + except Exception as e: + return {'status': 'failed', 'error': str(e)} + + def collect_data(self) -> Dict[str, Any]: + """采集所有设备数据""" + data = {} + timestamp = datetime.now().isoformat() + + try: + # 采集摄像头数据 + if self.device_status['camera']: + camera_data = self._collect_camera_data() + if camera_data: + data['camera'] = camera_data + + # 采集IMU数据 + if self.device_status['imu']: + imu_data = self._collect_imu_data() + if imu_data: + data['imu'] = imu_data + + # 采集压力传感器数据 + if self.device_status['pressure']: + pressure_data = self._collect_pressure_data() + if pressure_data: + data['pressure'] = pressure_data + + # 添加时间戳 + data['timestamp'] = timestamp + + # 更新最新数据 + with self.data_lock: + self.latest_data = data.copy() + + except Exception as e: + logger.error(f'数据采集失败: {e}') + + return data + + def _collect_camera_data(self) -> Optional[Dict[str, Any]]: + """采集摄像头数据""" + if not self.camera or not self.camera.isOpened(): + return None + + try: + ret, frame = self.camera.read() + if not ret: + return None + + # 进行姿态检测(这里应该集成实际的姿态检测算法) + pose_data = self._detect_pose(frame) + + return { + 'frame_available': True, + 'frame_size': frame.shape, + 'pose_data': pose_data, + 'timestamp': datetime.now().isoformat() + } + + except Exception as e: + logger.error(f'摄像头数据采集失败: {e}') + return None + + def _detect_pose(self, frame: np.ndarray) -> Dict[str, Any]: + """姿态检测(模拟实现)""" + # 这里应该集成实际的姿态检测算法,如MediaPipe或OpenPose + # 目前返回模拟数据 + return { + 'center_of_gravity': { + 'x': np.random.normal(0, 5), + 'y': np.random.normal(0, 5) + }, + 'body_angle': { + 'pitch': np.random.normal(0, 2), + 'roll': np.random.normal(0, 2), + 'yaw': np.random.normal(0, 1) + }, + 'confidence': np.random.uniform(0.8, 1.0) + } + + def _collect_imu_data(self) -> Optional[Dict[str, Any]]: + """采集IMU数据""" + if not self.imu_device: + return None + + try: + raw_data = self.imu_device.read_data() + + # 应用校准偏移 + if 'imu' in self.calibration_data: + calibration = self.calibration_data['imu'] + if calibration.get('status') == 'success': + accel_offset = calibration.get('accel_offset', {}) + gyro_offset = calibration.get('gyro_offset', {}) + + # 校正加速度数据 + raw_data['accel']['x'] -= accel_offset.get('x', 0) + raw_data['accel']['y'] -= accel_offset.get('y', 0) + raw_data['accel']['z'] -= accel_offset.get('z', 0) + + # 校正陀螺仪数据 + raw_data['gyro']['x'] -= gyro_offset.get('x', 0) + raw_data['gyro']['y'] -= gyro_offset.get('y', 0) + raw_data['gyro']['z'] -= gyro_offset.get('z', 0) + + return raw_data + + except Exception as e: + logger.error(f'IMU数据采集失败: {e}') + return None + + def _collect_pressure_data(self) -> Optional[Dict[str, Any]]: + """采集压力传感器数据""" + if not self.pressure_device: + return None + + try: + raw_data = self.pressure_device.read_data() + + # 应用校准偏移 + if 'pressure' in self.calibration_data: + calibration = self.calibration_data['pressure'] + if calibration.get('status') == 'success': + zero_offset = calibration.get('zero_offset', {}) + + raw_data['left_foot'] -= zero_offset.get('left_foot', 0) + raw_data['right_foot'] -= zero_offset.get('right_foot', 0) + + # 计算重心位置 + total_pressure = raw_data['left_foot'] + raw_data['right_foot'] + if total_pressure > 0: + center_of_pressure = { + 'x': (raw_data['right_foot'] - raw_data['left_foot']) / total_pressure * 100, + 'y': 0 # 简化模型 + } + else: + center_of_pressure = {'x': 0, 'y': 0} + + raw_data['center_of_pressure'] = center_of_pressure + raw_data['total_pressure'] = total_pressure + + return raw_data + + except Exception as e: + logger.error(f'压力传感器数据采集失败: {e}') + return None + + def get_latest_data(self) -> Dict[str, Any]: + """获取最新采集的数据""" + with self.data_lock: + return self.latest_data.copy() + + def start_video_recording(self, output_path: str) -> bool: + """开始视频录制""" + if not self.camera or not self.camera.isOpened(): + return False + + try: + # 获取摄像头参数 + width = int(self.camera.get(cv2.CAP_PROP_FRAME_WIDTH)) + height = int(self.camera.get(cv2.CAP_PROP_FRAME_HEIGHT)) + fps = int(self.camera.get(cv2.CAP_PROP_FPS)) + + # 创建视频写入器 + fourcc = cv2.VideoWriter_fourcc(*'mp4v') + self.video_writer = cv2.VideoWriter(output_path, fourcc, fps, (width, height)) + + if self.video_writer.isOpened(): + logger.info(f'开始视频录制: {output_path}') + return True + else: + logger.error('视频写入器创建失败') + return False + + except Exception as e: + logger.error(f'开始视频录制失败: {e}') + return False + + def stop_video_recording(self): + """停止视频录制""" + if hasattr(self, 'video_writer') and self.video_writer: + self.video_writer.release() + self.video_writer = None + logger.info('视频录制已停止') + + def cleanup(self): + """清理资源""" + try: + if self.camera: + self.camera.release() + + if hasattr(self, 'video_writer') and self.video_writer: + self.video_writer.release() + + logger.info('设备资源已清理') + + except Exception as e: + logger.error(f'清理设备资源失败: {e}') + + +class MockIMUDevice: + """模拟IMU设备""" + + def __init__(self): + self.noise_level = 0.1 + + def read_data(self) -> Dict[str, Any]: + """读取IMU数据""" + return { + 'accel': { + 'x': np.random.normal(0, self.noise_level), + 'y': np.random.normal(0, self.noise_level), + 'z': np.random.normal(9.8, self.noise_level) # 重力加速度 + }, + 'gyro': { + 'x': np.random.normal(0, self.noise_level), + 'y': np.random.normal(0, self.noise_level), + 'z': np.random.normal(0, self.noise_level) + }, + 'temperature': np.random.normal(25, 2), + 'timestamp': datetime.now().isoformat() + } + + +class MockPressureDevice: + """模拟压力传感器设备""" + + def __init__(self): + self.base_pressure = 500 # 基础压力值 + self.noise_level = 10 + + def read_data(self) -> Dict[str, Any]: + """读取压力数据""" + # 模拟轻微的左右脚压力差异 + left_pressure = self.base_pressure + np.random.normal(0, self.noise_level) + right_pressure = self.base_pressure + np.random.normal(0, self.noise_level) + + return { + 'left_foot': max(0, left_pressure), + 'right_foot': max(0, right_pressure), + 'timestamp': datetime.now().isoformat() + } \ No newline at end of file diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 00000000..23486f8f --- /dev/null +++ b/backend/main.py @@ -0,0 +1,412 @@ +#!/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() \ No newline at end of file diff --git a/backend/pytest.ini b/backend/pytest.ini new file mode 100644 index 00000000..39c24060 --- /dev/null +++ b/backend/pytest.ini @@ -0,0 +1,78 @@ +[tool:pytest] +# pytest 配置文件 + +# 测试发现 +testpaths = tests +python_files = test_*.py *_test.py +python_classes = Test* +python_functions = test_* + +# 输出配置 +addopts = + -v + --tb=short + --strict-markers + --disable-warnings + --color=yes + --durations=10 + --cov=. + --cov-report=html:htmlcov + --cov-report=term-missing + --cov-fail-under=80 + +# 标记定义 +markers = + slow: marks tests as slow (deselect with '-m "not slow"') + integration: marks tests as integration tests + unit: marks tests as unit tests + api: marks tests as API tests + database: marks tests as database tests + device: marks tests as device tests + analysis: marks tests as analysis tests + +# 过滤警告 +filterwarnings = + ignore::UserWarning + ignore::DeprecationWarning + ignore::PendingDeprecationWarning + ignore:.*:pytest.PytestUnraisableExceptionWarning + +# 最小版本要求 +minversion = 6.0 + +# 测试目录 +testmon_datafile = .testmondata + +# 日志配置 +log_cli = true +log_cli_level = INFO +log_cli_format = %(asctime)s [%(levelname)8s] %(name)s: %(message)s +log_cli_date_format = %Y-%m-%d %H:%M:%S + +# 覆盖率配置 +[coverage:run] +source = . +omit = + */tests/* + */venv/* + */env/* + */__pycache__/* + */migrations/* + setup.py + conftest.py + +[coverage:report] +exclude_lines = + pragma: no cover + def __repr__ + if self.debug: + if settings.DEBUG + raise AssertionError + raise NotImplementedError + if 0: + if __name__ == .__main__.: + class .*\bProtocol\): + @(abc\.)?abstractmethod + +[coverage:html] +directory = htmlcov \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 00000000..ddd0c45d --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,48 @@ +# Web framework and API +Flask +Flask-CORS +Flask-SocketIO + +# Data processing and scientific computing +numpy +pandas +scipy + +# Computer vision and machine learning +opencv-python +# mediapipe # Not compatible with Python 3.13 yet +# torch # May have compatibility issues with Python 3.13 +# torchvision # May have compatibility issues with Python 3.13 +scikit-learn + +# Image processing and visualization +matplotlib +seaborn +Pillow + +# Data visualization and report generation +reportlab + +# Serial communication +pyserial + +# Audio/video processing +ffmpeg-python + +# Network requests and utilities +requests +python-dateutil +psutil +watchdog + +# Development and testing tools +pytest +pytest-cov +flake8 +black + +# Other dependencies +pyyaml +click +colorama +tqdm \ No newline at end of file diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py new file mode 100644 index 00000000..2182d69e --- /dev/null +++ b/backend/tests/__init__.py @@ -0,0 +1 @@ +# 测试模块初始化文件 \ No newline at end of file diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py new file mode 100644 index 00000000..f9877361 --- /dev/null +++ b/backend/tests/test_api.py @@ -0,0 +1,295 @@ +import pytest +import json +import sys +import os +from unittest.mock import patch, MagicMock + +# 添加项目根目录到Python路径 +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +from app import create_app +from database import DatabaseManager +from utils import Config + + +@pytest.fixture +def app(): + """创建测试应用实例""" + config = Config() + config.DATABASE['path'] = ':memory:' # 使用内存数据库进行测试 + app = create_app(config) + app.config['TESTING'] = True + return app + + +@pytest.fixture +def client(app): + """创建测试客户端""" + return app.test_client() + + +@pytest.fixture +def db(): + """创建测试数据库""" + db_manager = DatabaseManager(':memory:') + db_manager.init_database() + return db_manager + + +class TestHealthAPI: + """健康检查API测试""" + + def test_health_check(self, client): + """测试健康检查端点""" + response = client.get('/api/health') + assert response.status_code == 200 + data = json.loads(response.data) + assert data['status'] == 'success' + assert 'timestamp' in data['data'] + assert 'uptime' in data['data'] + + +class TestSystemAPI: + """系统信息API测试""" + + def test_system_info(self, client): + """测试系统信息端点""" + response = client.get('/api/system/info') + assert response.status_code == 200 + data = json.loads(response.data) + assert data['status'] == 'success' + assert 'system' in data['data'] + assert 'python' in data['data'] + assert 'memory' in data['data'] + assert 'disk' in data['data'] + + +class TestPatientAPI: + """患者管理API测试""" + + def test_create_patient(self, client): + """测试创建患者""" + patient_data = { + 'name': '测试患者', + 'age': 30, + 'gender': 'male', + 'height': 175, + 'weight': 70, + 'phone': '13800138000', + 'email': 'test@example.com', + 'medical_history': '无', + 'notes': '测试患者' + } + + response = client.post('/api/patients', + data=json.dumps(patient_data), + content_type='application/json') + + assert response.status_code == 201 + data = json.loads(response.data) + assert data['status'] == 'success' + assert 'patient_id' in data['data'] + + def test_get_patients(self, client): + """测试获取患者列表""" + response = client.get('/api/patients') + assert response.status_code == 200 + data = json.loads(response.data) + assert data['status'] == 'success' + assert 'patients' in data['data'] + assert 'total' in data['data'] + assert 'page' in data['data'] + assert 'per_page' in data['data'] + + def test_get_patient_invalid_id(self, client): + """测试获取不存在的患者""" + response = client.get('/api/patients/99999') + assert response.status_code == 404 + data = json.loads(response.data) + assert data['status'] == 'error' + + def test_create_patient_invalid_data(self, client): + """测试创建患者时提供无效数据""" + invalid_data = { + 'name': '', # 空名称 + 'age': -1, # 无效年龄 + 'gender': 'invalid' # 无效性别 + } + + response = client.post('/api/patients', + data=json.dumps(invalid_data), + content_type='application/json') + + assert response.status_code == 400 + data = json.loads(response.data) + assert data['status'] == 'error' + + +class TestDeviceAPI: + """设备管理API测试""" + + @patch('device_manager.DeviceManager') + def test_device_status(self, mock_device_manager, client): + """测试设备状态查询""" + # 模拟设备管理器 + mock_instance = MagicMock() + mock_instance.get_device_status.return_value = { + 'camera': {'connected': True, 'status': 'ready'}, + 'imu': {'connected': True, 'status': 'ready'}, + 'pressure': {'connected': True, 'status': 'ready'} + } + mock_device_manager.return_value = mock_instance + + response = client.get('/api/devices/status') + assert response.status_code == 200 + data = json.loads(response.data) + assert data['status'] == 'success' + assert 'devices' in data['data'] + + @patch('device_manager.DeviceManager') + def test_device_refresh(self, mock_device_manager, client): + """测试设备刷新""" + mock_instance = MagicMock() + mock_instance.refresh_devices.return_value = True + mock_device_manager.return_value = mock_instance + + response = client.post('/api/devices/refresh') + assert response.status_code == 200 + data = json.loads(response.data) + assert data['status'] == 'success' + + +class TestDetectionAPI: + """检测管理API测试""" + + @patch('detection_engine.DetectionEngine') + def test_start_detection(self, mock_detection_engine, client): + """测试开始检测""" + mock_instance = MagicMock() + mock_instance.start_session.return_value = 'session_123' + mock_detection_engine.return_value = mock_instance + + detection_config = { + 'patient_id': 1, + 'duration': 60, + 'recording': True, + 'real_time_analysis': True + } + + response = client.post('/api/detection/start', + data=json.dumps(detection_config), + content_type='application/json') + + assert response.status_code == 200 + data = json.loads(response.data) + assert data['status'] == 'success' + assert 'session_id' in data['data'] + + @patch('detection_engine.DetectionEngine') + def test_stop_detection(self, mock_detection_engine, client): + """测试停止检测""" + mock_instance = MagicMock() + mock_instance.stop_session.return_value = True + mock_detection_engine.return_value = mock_instance + + response = client.post('/api/detection/stop/session_123') + assert response.status_code == 200 + data = json.loads(response.data) + assert data['status'] == 'success' + + def test_start_detection_invalid_config(self, client): + """测试使用无效配置开始检测""" + invalid_config = { + 'patient_id': -1, # 无效患者ID + 'duration': 0 # 无效持续时间 + } + + response = client.post('/api/detection/start', + data=json.dumps(invalid_config), + content_type='application/json') + + assert response.status_code == 400 + data = json.loads(response.data) + assert data['status'] == 'error' + + +class TestDatabaseManager: + """数据库管理器测试""" + + def test_create_patient(self, db): + """测试创建患者""" + patient_data = { + 'name': '测试患者', + 'age': 30, + 'gender': 'male', + 'height': 175, + 'weight': 70 + } + + patient_id = db.create_patient(patient_data) + assert patient_id is not None + assert isinstance(patient_id, int) + + def test_get_patient(self, db): + """测试获取患者信息""" + # 先创建患者 + patient_data = { + 'name': '测试患者', + 'age': 30, + 'gender': 'male' + } + patient_id = db.create_patient(patient_data) + + # 获取患者信息 + patient = db.get_patient(patient_id) + assert patient is not None + assert patient['name'] == '测试患者' + assert patient['age'] == 30 + + def test_get_nonexistent_patient(self, db): + """测试获取不存在的患者""" + patient = db.get_patient(99999) + assert patient is None + + def test_create_session(self, db): + """测试创建检测会话""" + # 先创建患者 + patient_data = {'name': '测试患者', 'age': 30, 'gender': 'male'} + patient_id = db.create_patient(patient_data) + + # 创建会话 + session_data = { + 'patient_id': patient_id, + 'duration': 60, + 'recording': True, + 'config': {'test': 'config'} + } + + session_id = db.create_session(session_data) + assert session_id is not None + assert isinstance(session_id, str) + + def test_update_session_status(self, db): + """测试更新会话状态""" + # 创建患者和会话 + patient_data = {'name': '测试患者', 'age': 30, 'gender': 'male'} + patient_id = db.create_patient(patient_data) + + session_data = { + 'patient_id': patient_id, + 'duration': 60, + 'recording': True + } + session_id = db.create_session(session_data) + + # 更新状态 + result = db.update_session_status(session_id, 'completed') + assert result is True + + # 验证状态已更新 + session = db.get_session(session_id) + assert session['status'] == 'completed' + + +if __name__ == '__main__': + pytest.main([__file__, '-v']) \ No newline at end of file diff --git a/backend/utils.py b/backend/utils.py new file mode 100644 index 00000000..a0ed9220 --- /dev/null +++ b/backend/utils.py @@ -0,0 +1,521 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +工具模块 +包含常用的工具函数、配置管理和辅助功能 +""" + +import os +import json +import logging +import hashlib +import secrets +from datetime import datetime, timedelta +from typing import Dict, List, Optional, Any, Union +from pathlib import Path +import configparser +from functools import wraps +import time + +logger = logging.getLogger(__name__) + +class Config: + """配置管理器""" + + def __init__(self, config_file: str = 'config.ini'): + self.config_file = Path(config_file) + self.config = configparser.ConfigParser() + self._load_config() + + def _load_config(self): + """加载配置文件""" + try: + if self.config_file.exists(): + self.config.read(self.config_file, encoding='utf-8') + logger.info(f'配置文件已加载: {self.config_file}') + else: + self._create_default_config() + logger.info('已创建默认配置文件') + except Exception as e: + logger.error(f'配置文件加载失败: {e}') + self._create_default_config() + + def _create_default_config(self): + """创建默认配置""" + try: + # 应用配置 + self.config['APP'] = { + 'name': 'Body Balance Evaluation System', + 'version': '1.0.0', + 'debug': 'false', + 'log_level': 'INFO' + } + + # 服务器配置 + self.config['SERVER'] = { + 'host': '127.0.0.1', + 'port': '5000', + 'cors_origins': '*' + } + + # 数据库配置 + self.config['DATABASE'] = { + 'path': 'data/balance_system.db', + 'backup_interval': '24', # 小时 + 'max_backups': '7' + } + + # 设备配置 + self.config['DEVICES'] = { + 'camera_index': '0', + 'camera_width': '640', + 'camera_height': '480', + 'camera_fps': '30', + 'imu_port': 'COM3', + 'pressure_port': 'COM4' + } + + # 检测配置 + self.config['DETECTION'] = { + 'default_duration': '60', # 秒 + 'sampling_rate': '30', # Hz + 'balance_threshold': '0.2', + 'posture_threshold': '5.0' # 度 + } + + # 数据处理配置 + self.config['DATA_PROCESSING'] = { + 'filter_window': '5', + 'outlier_threshold': '2.0', + 'chart_dpi': '300', + 'export_format': 'csv' + } + + # 安全配置 + self.config['SECURITY'] = { + 'secret_key': secrets.token_hex(32), + 'session_timeout': '3600', # 秒 + 'max_login_attempts': '5' + } + + # 保存配置文件 + self.save_config() + + except Exception as e: + logger.error(f'默认配置创建失败: {e}') + + def get(self, section: str, key: str, fallback: Any = None) -> str: + """获取配置值""" + try: + return self.config.get(section, key, fallback=fallback) + except Exception as e: + logger.warning(f'配置获取失败 [{section}][{key}]: {e}') + return fallback + + def getint(self, section: str, key: str, fallback: int = 0) -> int: + """获取整数配置值""" + try: + return self.config.getint(section, key, fallback=fallback) + except Exception as e: + logger.warning(f'整数配置获取失败 [{section}][{key}]: {e}') + return fallback + + def getfloat(self, section: str, key: str, fallback: float = 0.0) -> float: + """获取浮点数配置值""" + try: + return self.config.getfloat(section, key, fallback=fallback) + except Exception as e: + logger.warning(f'浮点数配置获取失败 [{section}][{key}]: {e}') + return fallback + + def getboolean(self, section: str, key: str, fallback: bool = False) -> bool: + """获取布尔配置值""" + try: + return self.config.getboolean(section, key, fallback=fallback) + except Exception as e: + logger.warning(f'布尔配置获取失败 [{section}][{key}]: {e}') + return fallback + + def set(self, section: str, key: str, value: str): + """设置配置值""" + try: + if not self.config.has_section(section): + self.config.add_section(section) + self.config.set(section, key, str(value)) + except Exception as e: + logger.error(f'配置设置失败 [{section}][{key}]: {e}') + + def save_config(self): + """保存配置文件""" + try: + # 确保目录存在 + self.config_file.parent.mkdir(parents=True, exist_ok=True) + + with open(self.config_file, 'w', encoding='utf-8') as f: + self.config.write(f) + logger.info(f'配置文件已保存: {self.config_file}') + except Exception as e: + logger.error(f'配置文件保存失败: {e}') + +class Logger: + """日志管理器""" + + @staticmethod + def setup_logging(log_level: str = 'INFO', log_file: str = None): + """设置日志配置""" + try: + # 创建日志目录 + if log_file: + log_path = Path(log_file) + log_path.parent.mkdir(parents=True, exist_ok=True) + + # 配置日志格式 + formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + + # 设置根日志器 + root_logger = logging.getLogger() + root_logger.setLevel(getattr(logging, log_level.upper(), logging.INFO)) + + # 清除现有处理器 + for handler in root_logger.handlers[:]: + root_logger.removeHandler(handler) + + # 控制台处理器 + console_handler = logging.StreamHandler() + console_handler.setFormatter(formatter) + root_logger.addHandler(console_handler) + + # 文件处理器 + if log_file: + file_handler = logging.FileHandler(log_file, encoding='utf-8') + file_handler.setFormatter(formatter) + root_logger.addHandler(file_handler) + + logger.info(f'日志系统已初始化,级别: {log_level}') + + except Exception as e: + print(f'日志系统初始化失败: {e}') + +class DataValidator: + """数据验证器""" + + @staticmethod + def validate_patient_data(data: Dict[str, Any]) -> Dict[str, Any]: + """验证患者数据""" + errors = [] + + # 必填字段检查 + required_fields = ['name', 'gender', 'birth_date'] + for field in required_fields: + if not data.get(field): + errors.append(f'缺少必填字段: {field}') + + # 姓名验证 + if data.get('name'): + name = data['name'].strip() + if len(name) < 2 or len(name) > 50: + errors.append('姓名长度应在2-50个字符之间') + data['name'] = name + + # 性别验证 + if data.get('gender'): + if data['gender'] not in ['male', 'female', 'other']: + errors.append('性别值无效') + + # 出生日期验证 + if data.get('birth_date'): + try: + birth_date = datetime.fromisoformat(data['birth_date'].replace('Z', '+00:00')) + if birth_date > datetime.now(): + errors.append('出生日期不能是未来时间') + if birth_date < datetime(1900, 1, 1): + errors.append('出生日期过早') + except ValueError: + errors.append('出生日期格式无效') + + # 身高验证 + if data.get('height'): + try: + height = float(data['height']) + if height < 50 or height > 250: + errors.append('身高应在50-250cm之间') + data['height'] = height + except (ValueError, TypeError): + errors.append('身高格式无效') + + # 体重验证 + if data.get('weight'): + try: + weight = float(data['weight']) + if weight < 10 or weight > 300: + errors.append('体重应在10-300kg之间') + data['weight'] = weight + except (ValueError, TypeError): + errors.append('体重格式无效') + + # 电话验证 + if data.get('phone'): + phone = data['phone'].strip() + if phone and not phone.replace('-', '').replace(' ', '').isdigit(): + errors.append('电话号码格式无效') + data['phone'] = phone + + return { + 'valid': len(errors) == 0, + 'errors': errors, + 'data': data + } + + @staticmethod + def validate_detection_config(config: Dict[str, Any]) -> Dict[str, Any]: + """验证检测配置""" + errors = [] + + # 检测时长验证 + if 'duration' in config: + try: + duration = int(config['duration']) + if duration < 10 or duration > 600: + errors.append('检测时长应在10-600秒之间') + config['duration'] = duration + except (ValueError, TypeError): + errors.append('检测时长格式无效') + + # 采样频率验证 + if 'sampling_rate' in config: + try: + rate = int(config['sampling_rate']) + if rate < 1 or rate > 100: + errors.append('采样频率应在1-100Hz之间') + config['sampling_rate'] = rate + except (ValueError, TypeError): + errors.append('采样频率格式无效') + + # 录像设置验证 + if 'record_video' in config: + if not isinstance(config['record_video'], bool): + errors.append('录像设置应为布尔值') + + return { + 'valid': len(errors) == 0, + 'errors': errors, + 'config': config + } + +class SecurityUtils: + """安全工具""" + + @staticmethod + def generate_session_id() -> str: + """生成会话ID""" + return secrets.token_urlsafe(32) + + @staticmethod + def hash_password(password: str) -> str: + """密码哈希""" + salt = secrets.token_hex(16) + password_hash = hashlib.pbkdf2_hmac('sha256', password.encode(), salt.encode(), 100000) + return f"{salt}:{password_hash.hex()}" + + @staticmethod + def verify_password(password: str, hashed: str) -> bool: + """验证密码""" + try: + salt, password_hash = hashed.split(':') + return hashlib.pbkdf2_hmac('sha256', password.encode(), salt.encode(), 100000).hex() == password_hash + except Exception: + return False + + @staticmethod + def sanitize_filename(filename: str) -> str: + """清理文件名""" + import re + # 移除或替换不安全的字符 + filename = re.sub(r'[<>:"/\\|?*]', '_', filename) + # 限制长度 + if len(filename) > 255: + name, ext = os.path.splitext(filename) + filename = name[:255-len(ext)] + ext + return filename + +class FileUtils: + """文件工具""" + + @staticmethod + def ensure_directory(path: Union[str, Path]): + """确保目录存在""" + Path(path).mkdir(parents=True, exist_ok=True) + + @staticmethod + def get_file_size(path: Union[str, Path]) -> int: + """获取文件大小""" + try: + return Path(path).stat().st_size + except Exception: + return 0 + + @staticmethod + def clean_old_files(directory: Union[str, Path], max_age_days: int = 30): + """清理旧文件""" + try: + directory = Path(directory) + if not directory.exists(): + return + + cutoff_time = datetime.now() - timedelta(days=max_age_days) + + for file_path in directory.iterdir(): + if file_path.is_file(): + file_time = datetime.fromtimestamp(file_path.stat().st_mtime) + if file_time < cutoff_time: + file_path.unlink() + logger.info(f'已删除旧文件: {file_path}') + + except Exception as e: + logger.error(f'清理旧文件失败: {e}') + + @staticmethod + def backup_file(source: Union[str, Path], backup_dir: Union[str, Path] = None) -> Optional[Path]: + """备份文件""" + try: + source = Path(source) + if not source.exists(): + return None + + if backup_dir is None: + backup_dir = source.parent / 'backups' + else: + backup_dir = Path(backup_dir) + + backup_dir.mkdir(parents=True, exist_ok=True) + + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + backup_name = f"{source.stem}_{timestamp}{source.suffix}" + backup_path = backup_dir / backup_name + + import shutil + shutil.copy2(source, backup_path) + + logger.info(f'文件已备份: {source} -> {backup_path}') + return backup_path + + except Exception as e: + logger.error(f'文件备份失败: {e}') + return None + +class PerformanceMonitor: + """性能监控器""" + + def __init__(self): + self.metrics = {} + + def start_timer(self, name: str): + """开始计时""" + self.metrics[name] = {'start_time': time.time()} + + def end_timer(self, name: str) -> float: + """结束计时""" + if name in self.metrics and 'start_time' in self.metrics[name]: + duration = time.time() - self.metrics[name]['start_time'] + self.metrics[name]['duration'] = duration + return duration + return 0.0 + + def get_metrics(self) -> Dict[str, Any]: + """获取性能指标""" + return self.metrics.copy() + + def reset_metrics(self): + """重置指标""" + self.metrics.clear() + +def timing_decorator(func): + """计时装饰器""" + @wraps(func) + def wrapper(*args, **kwargs): + start_time = time.time() + try: + result = func(*args, **kwargs) + duration = time.time() - start_time + logger.debug(f'{func.__name__} 执行时间: {duration:.3f}秒') + return result + except Exception as e: + duration = time.time() - start_time + logger.error(f'{func.__name__} 执行失败 (耗时: {duration:.3f}秒): {e}') + raise + return wrapper + +def retry_decorator(max_retries: int = 3, delay: float = 1.0): + """重试装饰器""" + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + last_exception = None + + for attempt in range(max_retries + 1): + try: + return func(*args, **kwargs) + except Exception as e: + last_exception = e + if attempt < max_retries: + logger.warning(f'{func.__name__} 第{attempt + 1}次尝试失败: {e},{delay}秒后重试') + time.sleep(delay) + else: + logger.error(f'{func.__name__} 所有重试均失败') + + raise last_exception + return wrapper + return decorator + +class ResponseFormatter: + """响应格式化器""" + + @staticmethod + def success(data: Any = None, message: str = 'Success') -> Dict[str, Any]: + """成功响应""" + response = { + 'success': True, + 'message': message, + 'timestamp': datetime.now().isoformat() + } + if data is not None: + response['data'] = data + return response + + @staticmethod + def error(message: str, error_code: str = None, details: Any = None) -> Dict[str, Any]: + """错误响应""" + response = { + 'success': False, + 'message': message, + 'timestamp': datetime.now().isoformat() + } + if error_code: + response['error_code'] = error_code + if details: + response['details'] = details + return response + + @staticmethod + def paginated(data: List[Any], page: int, page_size: int, total: int) -> Dict[str, Any]: + """分页响应""" + return { + 'success': True, + 'data': data, + 'pagination': { + 'page': page, + 'page_size': page_size, + 'total': total, + 'total_pages': (total + page_size - 1) // page_size + }, + 'timestamp': datetime.now().isoformat() + } + +# 全局配置实例 +config = Config() + +# 性能监控实例 +performance_monitor = PerformanceMonitor() \ No newline at end of file diff --git a/config.ini b/config.ini new file mode 100644 index 00000000..ee5d52e0 --- /dev/null +++ b/config.ini @@ -0,0 +1,41 @@ +[APP] +name = Body Balance Evaluation System +version = 1.0.0 +debug = false +log_level = INFO + +[SERVER] +host = 127.0.0.1 +port = 5000 +cors_origins = * + +[DATABASE] +path = data/balance_system.db +backup_interval = 24 +max_backups = 7 + +[DEVICES] +camera_index = 0 +camera_width = 640 +camera_height = 480 +camera_fps = 30 +imu_port = COM3 +pressure_port = COM4 + +[DETECTION] +default_duration = 60 +sampling_rate = 30 +balance_threshold = 0.2 +posture_threshold = 5.0 + +[DATA_PROCESSING] +filter_window = 5 +outlier_threshold = 2.0 +chart_dpi = 300 +export_format = csv + +[SECURITY] +secret_key = 79fcc4983d478c2ee672f3305d5e12c7c84fd1b58a18acb650e9f8125bfa805f +session_timeout = 3600 +max_login_attempts = 5 + diff --git a/config.json b/config.json new file mode 100644 index 00000000..f9dad1b3 --- /dev/null +++ b/config.json @@ -0,0 +1,278 @@ +{ + "app": { + "name": "身体平衡评估系统", + "version": "1.0.0", + "description": "基于多传感器融合技术的专业平衡能力评估与分析系统", + "author": "身体平衡评估系统开发团队", + "language": "zh-CN", + "theme": "light", + "auto_start": false, + "auto_update": true, + "window": { + "width": 1200, + "height": 800, + "min_width": 1000, + "min_height": 600, + "resizable": true, + "center": true, + "show": true, + "frame": true, + "transparent": false, + "always_on_top": false + } + }, + "server": { + "host": "127.0.0.1", + "port": 5000, + "debug": false, + "cors": { + "enabled": true, + "origins": ["http://localhost:5173", "http://127.0.0.1:5173"] + }, + "ssl": { + "enabled": false, + "cert_file": "", + "key_file": "" + }, + "rate_limit": { + "enabled": true, + "requests_per_minute": 100 + } + }, + "database": { + "type": "sqlite", + "path": "data/database.db", + "backup": { + "enabled": true, + "interval_hours": 24, + "max_backups": 7 + }, + "connection": { + "timeout": 30, + "pool_size": 10 + } + }, + "devices": { + "camera": { + "enabled": true, + "device_id": 0, + "resolution": { + "width": 1280, + "height": 720 + }, + "fps": 30, + "format": "MJPG", + "auto_exposure": true, + "brightness": 0, + "contrast": 0, + "saturation": 0, + "calibration": { + "enabled": false, + "matrix": null, + "distortion": null + } + }, + "imu": { + "enabled": true, + "port": "COM3", + "baudrate": 115200, + "timeout": 1.0, + "sample_rate": 100, + "range": { + "accelerometer": 16, + "gyroscope": 2000, + "magnetometer": 4800 + }, + "calibration": { + "enabled": false, + "offset": { + "accel": [0, 0, 0], + "gyro": [0, 0, 0], + "mag": [0, 0, 0] + }, + "scale": { + "accel": [1, 1, 1], + "gyro": [1, 1, 1], + "mag": [1, 1, 1] + } + } + }, + "pressure": { + "enabled": true, + "port": "COM4", + "baudrate": 9600, + "timeout": 1.0, + "sample_rate": 50, + "sensors": 4, + "range": { + "min": 0, + "max": 1000 + }, + "calibration": { + "enabled": false, + "zero_offset": [0, 0, 0, 0], + "scale_factor": [1, 1, 1, 1] + } + } + }, + "detection": { + "default_duration": 60, + "min_duration": 10, + "max_duration": 300, + "sample_rate": 30, + "recording": { + "enabled": true, + "format": "mp4", + "quality": "medium", + "fps": 30 + }, + "analysis": { + "real_time": true, + "pose_detection": { + "enabled": true, + "model": "mediapipe", + "confidence": 0.5, + "tracking": true + }, + "balance_metrics": { + "cop_analysis": true, + "sway_analysis": true, + "stability_index": true, + "frequency_analysis": true + }, + "filters": { + "low_pass": { + "enabled": true, + "cutoff": 10.0 + }, + "median": { + "enabled": true, + "window_size": 5 + } + } + }, + "alerts": { + "enabled": true, + "thresholds": { + "excessive_sway": 50.0, + "instability": 0.8, + "device_disconnect": 5.0 + } + } + }, + "data": { + "storage": { + "base_path": "data", + "patients_path": "data/patients", + "sessions_path": "data/sessions", + "exports_path": "data/exports", + "backups_path": "data/backups", + "temp_path": "temp", + "logs_path": "logs" + }, + "cleanup": { + "enabled": true, + "temp_files_days": 7, + "log_files_days": 30, + "session_files_days": 365 + }, + "compression": { + "enabled": true, + "algorithm": "gzip", + "level": 6 + }, + "export": { + "formats": ["csv", "json", "pdf"], + "include_raw_data": true, + "include_analysis": true, + "include_charts": true + } + }, + "logging": { + "level": "INFO", + "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s", + "file": { + "enabled": true, + "path": "logs/app.log", + "max_size": "10MB", + "backup_count": 5, + "rotation": "daily" + }, + "console": { + "enabled": true, + "level": "INFO" + }, + "modules": { + "flask": "WARNING", + "werkzeug": "WARNING", + "urllib3": "WARNING", + "matplotlib": "WARNING" + } + }, + "security": { + "session": { + "timeout_minutes": 60, + "secret_key": "your-secret-key-here", + "secure_cookies": false + }, + "api": { + "rate_limiting": true, + "request_timeout": 30, + "max_request_size": "100MB" + }, + "data": { + "encryption": false, + "backup_encryption": false, + "anonymization": { + "enabled": false, + "fields": ["name", "phone", "email"] + } + } + }, + "ui": { + "language": "zh-CN", + "theme": "light", + "animations": true, + "sound_effects": true, + "notifications": { + "enabled": true, + "position": "top-right", + "duration": 5000 + }, + "charts": { + "default_type": "line", + "colors": { + "primary": "#409EFF", + "success": "#67C23A", + "warning": "#E6A23C", + "danger": "#F56C6C", + "info": "#909399" + }, + "animation_duration": 1000 + }, + "table": { + "page_size": 20, + "show_pagination": true, + "sortable": true, + "filterable": true + } + }, + "performance": { + "monitoring": { + "enabled": true, + "interval_seconds": 60, + "metrics": ["cpu", "memory", "disk", "network"] + }, + "optimization": { + "image_compression": true, + "data_caching": true, + "lazy_loading": true, + "batch_processing": true + }, + "limits": { + "max_concurrent_sessions": 5, + "max_file_size_mb": 100, + "max_session_duration_minutes": 300 + } + } +} \ No newline at end of file diff --git a/document/平衡体态检测系统_UI与交互功能设计说明.md b/document/平衡体态检测系统_UI与交互功能设计说明.md new file mode 100644 index 00000000..e24e2115 --- /dev/null +++ b/document/平衡体态检测系统_UI与交互功能设计说明.md @@ -0,0 +1,99 @@ +# 平衡体态检测系统 UI 与交互功能设计说明 + +## 1. 总体设计理念 + +- **以用户为中心**:以康复医生和技术操作员为核心用户,界面简洁直观。 +- **数据驱动展示**:所有核心模块数据实时可视化,交互响应快速。 +- **操作流程明晰**:每一步操作清晰可控,避免误操作。 + +--- + +## 2. 页面结构与交互设计 + +### 2.1 登录与注册页面 + +#### 功能点 +- 用户名密码登录,明文匹配; +- 注册要求:手机号、密码二次确认; +- 明文密码显示与记住密码; +- 密码找回:显示明文密码(需优化为短信验证); +- 修改密码支持原密码校验更新。 + +#### 交互设计 +- 所有操作均本地数据库存储; +- 弹窗提示错误、成功状态; +- 安全建议:建议后续版本启用密码加密存储。 + +--- + +### 2.2 实时检测模块 + +#### 起始页 +- 左侧:患者列表(支持搜索与滚动加载); +- 右上:基础信息展示与编辑; +- 右下:新建档案 / 查看档案 / 开始检测按钮。 + +#### 新建档案页 +- 信息采集:*为必填项,实时校验格式; +- 按钮: + - “退出”:弹窗确认; + - “保存”:保存数据; + - “保存并开始检测”:保存+跳转检测页。 + +#### 患者档案页 +- 展示所有检测记录,支持: + - 测量数据图文并茂; + - 操作过程视频+录屏回放; + - 查看足底垫片配置(左右足 + 多个垫片,记录颜色/编号); + - 截图列表(放大预览、删除、查看截图数据、导出/打印)。 + +--- + +### 2.3 检测页设计 + +#### 实时检测主界面 + +- 显示患者信息、时间、医生名称; +- 数据区: + - 身体姿态:深度图动态更新; + - 头部IMU:实时Pitch/Yaw/Roll值 + 最大值记录 + 清零 + 抗干扰; + - 足底压力:压力云图 + 数据占比展示; + - 视频区:录制+时间戳+最大化播放。 + +#### 操作按钮区 + +- 初始状态:显示“开始”; +- 开始后:显示“截图”“结束”; + - 截图:保存界面+对应数据+截图图片; + - 结束:终止录制,保存检测记录; +- 结束后:显示“回放”; + - 支持视频同步播放、暂停、最大化、进度条拖动。 + +#### 分屏模式 + +- 提供“分屏”按钮; +- 支持全显(3模块)/单模块切换(滑动或左右切换按钮)。 + +--- + +## 3. 附加功能说明 + +### 截图管理 +- 入口:截图按钮 + 截图列表; +- 功能:图片放大预览、删除、查看数据详情、导出/打印。 + +### 数据管理 +- 所有数据按患者、检测时间分类; +- 支持历史对比分析(V2.0 实现); +- 自动生成PDF报告(带医生备注)。 + +--- + +## 4. 优化建议(基于V1.2) + +- 密码不应明文存储,应采用hash加密; +- 注册/找回密码增加验证码机制; +- 支持多用户权限(医生、操作员、管理员); +- 后续建议支持远程数据云同步; +- 视频与数据录制时钟同步机制需保证精准性; +- 多检测记录对比模块应预留逻辑框架。 \ No newline at end of file diff --git a/document/系统UI及交互设计说明.md b/document/系统UI及交互设计说明.md new file mode 100644 index 00000000..0f975599 --- /dev/null +++ b/document/系统UI及交互设计说明.md @@ -0,0 +1,331 @@ +# 平衡体态检测系统 UI 与交互设计说明 + +## 总体设计理念 + +- **以用户为中心**:以康复医生和技术操作员为核心用户,界面简洁直观 +- **数据驱动展示**:所有核心模块数据实时可视化,交互响应快速 +- **操作流程明晰**:每一步操作清晰可控,避免误操作 + +--- + +## 1. 登录与注册页面 + +![登录页](登录页.png) + +### 页面结构 +- 顶部:系统Logo与名称 +- 中部:账号输入框、密码输入框、登录按钮 +- 底部:忘记密码、注册入口 + +### 主要功能 +- **登录功能**:用户名密码登录,明文匹配 +- **注册功能**:手机号、密码二次确认 +- **密码管理**:明文密码显示与记住密码 +- **密码找回**:显示明文密码(建议后续版本优化为短信验证) +- **修改密码**:支持原密码校验更新 + +### 交互说明 +- 输入框获得焦点高亮 +- 登录按钮点击后显示加载状态 +- 错误提示信息弹出或红色提示 +- 所有操作均本地数据库存储 +- 弹窗提示错误、成功状态 + +### 组件建议 +- Input输入框 +- Button按钮 +- Message提示组件 +- Modal弹窗 + +### 安全建议 +- 建议后续版本启用密码加密存储 +- 增加验证码机制 +- 支持多用户权限(医生、操作员、管理员) + +--- + +## 2. 实时检测模块 + +### 2.1 起始页 + +![登录进入的起始页](登录进入的起始页.png) + +#### 页面结构 +- **左侧**:功能快速入口("检测、人员、系统") +- **右侧顶部**:用户信息栏、系统Logo +- **右侧中部**:患者列表、选中患者基础信息展示 +- **操作按钮**:粉色鲜亮按钮("查看档案、检测、新患者建档") + +#### 主要功能 +- 患者列表管理(支持搜索与滚动加载) +- 基础信息展示与编辑 +- 快速进入各功能模块 + +#### 交互说明 +- 患者列表可滚动显示,可选中,可输入用户名查询 +- 选中患者可显示基础信息,可点击编辑图标进行信息编辑 +- 按钮高亮显示,点击跳转对应功能页面 + +#### 组件建议 +- List列表组件 +- Search搜索框 +- Card卡片 +- Button按钮 +- Avatar头像 + +### 2.2 新建档案页 + +#### 页面结构 +- 表单区域:患者基础信息录入 +- 操作按钮区:退出、保存、保存并开始检测 + +#### 主要功能 +- **信息采集**:*为必填项,实时校验格式 +- **数据保存**:本地数据库存储 +- **快速检测**:保存后直接跳转检测页 + +#### 交互说明 +- **"退出"按钮**:弹窗确认是否保存 +- **"保存"按钮**:保存数据并返回列表 +- **"保存并开始检测"按钮**:保存数据并跳转检测页 +- 必填项校验,错误提示 + +#### 组件建议 +- Form表单 +- Input输入框 +- Select下拉选择 +- DatePicker日期选择 +- Button按钮 +- Validation校验 + +### 2.3 患者档案页 + +#### 页面结构 +- 患者基础信息展示区 +- 检测记录列表 +- 详细数据查看区 + +#### 主要功能 +- **检测记录展示**:所有历史检测记录 +- **数据可视化**:测量数据图文并茂展示 +- **视频回放**:操作过程视频+录屏回放 +- **足底垫片配置**:左右足+多个垫片,记录颜色/编号 +- **截图管理**:截图列表(放大预览、删除、查看截图数据、导出/打印) + +#### 交互说明 +- 检测记录按时间排序,支持筛选 +- 点击记录查看详细数据 +- 视频支持播放控制 +- 截图支持放大预览和批量操作 + +#### 组件建议 +- Table表格 +- Timeline时间轴 +- Video视频播放器 +- Image图片预览 +- Chart图表 +- Export导出功能 + + +--- + +## 3. 检测页设计 + +### 3.1 实时检测主界面 + +![姿态检测页](姿态检测页.png) + +#### 页面结构 +- **左侧**:快捷操作入口 +- **顶部信息栏**:患者信息、检测时间、医生名称 +- **数据展示区**: + - 身体姿态:深度图动态更新 + - 头部IMU:实时Pitch/Yaw/Roll值+最大值记录+清零+抗干扰 + - 足底压力:压力云图+数据占比展示 + - 视频区:录制+时间戳+最大化播放 + +#### 主要功能 +- **实时数据监控**:三大模块数据同步显示 +- **视频录制**:同步录制检测过程 +- **数据记录**:实时保存检测数据 +- **异常监测**:数据异常自动提示 + +#### 交互说明 +- 数据实时刷新,响应速度<100ms +- 视频支持最大化播放 +- 异常情况弹窗提示 +- 支持数据清零重置 + +#### 组件建议 +- Canvas画布(深度图、压力云图) +- Video视频组件 +- Progress进度条 +- Modal弹窗 +- Chart实时图表 + +### 3.2 操作按钮区 + +#### 按钮状态管理 +- **初始状态**:显示"开始"按钮 +- **检测中状态**:显示"截图""结束"按钮 +- **结束后状态**:显示"回放"按钮 + +#### 功能说明 +- **开始按钮**:启动检测,开始数据采集和视频录制 +- **截图按钮**:保存当前界面+对应数据+截图图片 +- **结束按钮**:终止录制,保存检测记录 +- **回放按钮**:支持视频同步播放、暂停、最大化、进度条拖动 + +#### 交互说明 +- 按钮状态根据检测流程自动切换 +- 截图操作即时响应,保存成功提示 +- 结束检测需二次确认 +- 回放支持全屏模式 + +### 3.3 分屏模式 + +#### 功能设计 +- 提供"分屏"切换按钮 +- 支持全显(3模块)/单模块切换 +- 单模块模式支持滑动或左右切换按钮 + +#### 交互说明 +- 分屏切换动画流畅 +- 单模块模式数据更大更清晰 +- 支持快速切换不同模块 + +#### 组件建议 +- Tabs标签页 +- Swiper滑动组件 +- Button切换按钮 + +--- + +## 4. 检测中的录屏界面 + +![开始检测中的录屏界面](开始检测中的录屏界面.png) + +### 页面结构 +- **左侧**:快捷操作入口 +- **右侧顶部**:录制状态、时长显示、结束、截图按钮 +- **右侧中部**:检测结果实时展示区 +- **状态栏**:录制时长、数据同步状态 + +### 主要功能 +- **实时录制**:同步录制检测过程和数据 +- **时长显示**:显示录制时长与状态 +- **截图功能**:实时截图保存 +- **数据同步**:视频与数据时钟同步 + +### 交互说明 +- 录制中按钮高亮显示 +- 录制时长实时更新 +- 截图操作不影响录制 +- 录制结束弹窗确认保存 +- 支持录制过程中查看实时数据 + +### 组件建议 +- Timer计时器 +- Button按钮 +- Notification通知 +- StatusBar状态栏 +- ProgressBar进度条 + +--- + +## 5. 附加功能模块 + +### 5.1 截图管理 + +#### 功能入口 +- 检测页面截图按钮 +- 患者档案截图列表 + +#### 主要功能 +- **图片管理**:截图列表展示 +- **预览功能**:图片放大预览 +- **数据关联**:查看截图对应的检测数据 +- **导出打印**:支持单张或批量导出/打印 +- **删除管理**:支持单张或批量删除 + +#### 交互说明 +- 缩略图网格展示 +- 点击放大预览 +- 支持键盘快捷键操作 +- 批量操作需确认提示 + +### 5.2 数据管理 + +#### 数据分类 +- 按患者分类管理 +- 按检测时间排序 +- 支持标签分类 + +#### 功能特性 +- **历史对比**:多次检测数据对比分析(V2.0实现) +- **报告生成**:自动生成PDF报告(带医生备注) +- **数据导出**:支持多种格式导出 +- **备份恢复**:数据备份与恢复功能 + +#### 交互说明 +- 数据列表支持多维度筛选 +- 对比分析支持图表展示 +- 报告生成支持模板自定义 +- 导出进度实时显示 + +--- + +--- + +## 6. 设计规范与建议 + +### 6.1 视觉设计 +- **配色方案**:统一黑色科技风格,突出专业感 +- **布局原则**:保持页面简洁,突出核心功能区域 +- **字体规范**:统一字体族,层次分明 +- **图标设计**:简洁明了,符合医疗设备使用习惯 + +### 6.2 交互设计 +- **响应速度**:界面响应时间<100ms +- **动画效果**:简洁流畅,提升用户体验 +- **反馈机制**:操作即时反馈,状态清晰可见 +- **容错设计**:重要操作需二次确认 + +### 6.3 技术建议 +- **数据同步**:视频与数据录制时钟同步机制需保证精准性 +- **性能优化**:大数据量处理优化,确保实时性 +- **扩展性**:预留多检测记录对比模块逻辑框架 +- **安全性**:密码加密存储,数据传输安全 + +### 6.4 优化建议(基于V1.2) +- 密码不应明文存储,应采用hash加密 +- 注册/找回密码增加验证码机制 +- 支持多用户权限(医生、操作员、管理员) +- 后续建议支持远程数据云同步 +- 视频与数据录制时钟同步机制需保证精准性 +- 多检测记录对比模块应预留逻辑框架 + +--- + +## 7. 开发实施建议 + +### 7.1 开发优先级 +1. **核心功能**:登录、患者管理、实时检测 +2. **数据管理**:截图、录制、数据保存 +3. **扩展功能**:报告生成、数据对比 +4. **优化功能**:性能优化、安全加固 + +### 7.2 技术栈建议 +- **前端框架**:React/Vue.js + Electron +- **UI组件库**:Ant Design / Element Plus +- **图表库**:ECharts / D3.js +- **视频处理**:WebRTC / FFmpeg +- **数据库**:SQLite(本地)+ 云端备份 + +### 7.3 测试建议 +- **功能测试**:覆盖所有用户操作流程 +- **性能测试**:实时数据处理性能验证 +- **兼容性测试**:多设备、多分辨率适配 +- **用户体验测试**:医生和操作员实际使用反馈 + +如需进一步细化任何模块的详细设计,请提供具体需求。 \ No newline at end of file diff --git a/document/系统界面原型.pdf b/document/系统界面原型.pdf new file mode 100644 index 00000000..45c39fce Binary files /dev/null and b/document/系统界面原型.pdf differ diff --git a/document/系统界面原型.pptx b/document/系统界面原型.pptx new file mode 100644 index 00000000..0f3181c0 Binary files /dev/null and b/document/系统界面原型.pptx differ diff --git a/document/软件开发技术方案.md b/document/软件开发技术方案.md new file mode 100644 index 00000000..6ab663d9 --- /dev/null +++ b/document/软件开发技术方案.md @@ -0,0 +1,150 @@ + +# 平衡体态检测系统 - 软件开发技术方案(Electron + Python,Windows单机版) + +## 一、项目目标 + +构建一套适用于康复医学、运动健康等场景的 **单机本地运行型体态检测系统**,具备图形界面、设备数据采集、实时可视化、视频记录、数据管理与导出等能力。用户可在 Windows 系统中一键安装、双击运行,无需联网与额外配置。 + +--- + +## 二、系统架构与技术栈 + +### 2.1 总体架构图 + +``` ++---------------------------+ +| Electron 桌面客户端 | +| +----------------------+ | +| | Vue3/React 前端界面 | | +| +----------------------+ | +| ⇅ API调用 | +| +----------------------+ | +| | Python 后端服务(Flask)| +| +----------------------+ | ++---------------------------+ + ⇅ 硬件通信 ++---------------------------+ +| 深度相机 / IMU / 足底压力板 | ++---------------------------+ +``` + +### 2.2 技术选型 + +| 层级 | 技术选型 | 说明 | +|------|----------|------| +| 前端界面 | Electron + Vue3 / React | 构建跨平台 GUI 桌面应用 | +| 后端服务 | Python Flask / FastAPI | 提供设备控制、数据处理、本地API | +| 本地数据库 | SQLite | 嵌入式数据库,无需独立部署 | +| 视频处理 | FFmpeg | 实现屏幕与摄像视频录制 | +| 打包工具 | electron-builder | 生成 Windows `.exe` 安装包 | +| Python打包 | PyInstaller | 将 Python 服务打包为 `.exe` 可执行程序 | + +--- + +## 三、模块功能与实现 + +### 3.1 Electron 主程序 + +- 负责显示用户界面(患者档案、检测流程、视频回放) +- 在启动时自动调用后端 Python `.exe` 服务 +- 与本地后端通信(HTTP API) +- 控制设备数据展示与交互逻辑 + +### 3.2 Python 后端服务 + +- 使用 Flask/FastAPI 提供本地接口(127.0.0.1:5000) +- 管理设备连接、数据采集、滤波处理、存储与导出 +- 可扩展实现深度图处理、压力图分析、IMU姿态分析等功能 + +### 3.3 视频录制模块 + +- 利用 FFmpeg 实现屏幕+外部摄像头录制 +- 支持时间戳叠加、截图保存、同步回放 +- 可打包进 Electron 安装目录中统一调用 + +--- + +## 四、部署方式与启动流程 + +### 4.1 打包流程 + +1. 将 Python 服务使用 PyInstaller 打包: + ```bash + pyinstaller -F -n app backend/app.py + ``` + +2. Electron 主进程中启动 Python 服务: + ```js + const { spawn } = require("child_process"); + const scriptPath = path.join(__dirname, "backend", "app.exe"); + const pythonProcess = spawn(scriptPath, { detached: true, stdio: 'ignore' }); + pythonProcess.unref(); + ``` + +3. 使用 electron-builder 打包为安装包: + ```bash + npm run build + electron-builder --win --x64 + ``` + +### 4.2 安装后运行流程(用户视角) + +- 用户双击安装包 `.exe` 安装程序; +- 桌面自动生成快捷方式; +- 双击运行程序: + - 启动 Electron 界面; + - 自动后台启动 Python 服务; + - 自动检测设备连接; + - 进入检测界面,开始数据采集。 + +--- + +## 五、目录结构(部署后) + +``` +体态检测系统/ +├─ main.exe # 主程序(Electron 打包) +├─ backend/ +│ ├─ app.exe # Python 后端打包文件 +│ └─ model/ # AI模型文件(可选) +├─ ffmpeg/ # 视频录制工具 +├─ resources/ # 前端界面构建产物 +├─ logs/ # 日志输出目录 +└─ uninstall.exe # 卸载程序 +``` + +--- + +## 六、安全性与稳定性设计 + +| 项目 | 说明 | +|------|------| +| 端口限制 | 后端服务仅监听 127.0.0.1,防止外部访问 | +| 自动重启 | Electron 检测后端未启动可尝试自动重启 | +| 日志记录 | 所有错误输出写入 logs 目录,便于故障排查 | +| 安全存储 | 用户/患者数据保存至 SQLite,本地隔离存储 | +| 进程管理 | Python 服务进程随 Electron 一起退出或独立运行 | + +--- + +## 七、后续拓展建议 + +| 版本 | 升级方向 | +|------|----------| +| V2.0 | 加入关键点识别、自动评估报告导出、双次记录对比 | +| V3.0 | 云端同步、在线远程管理、多终端协同 | +| V4.0 | 移动版(Android/iOS)测量与数据同步 | + +--- + +## 八、推荐工具与资源 + +- Electron 官网:[https://www.electronjs.org/](https://www.electronjs.org/) +- electron-builder:[https://www.electron.build/](https://www.electron.build/) +- Flask:[https://flask.palletsprojects.com/](https://flask.palletsprojects.com/) +- PyInstaller:[https://pyinstaller.org/](https://pyinstaller.org/) +- FFmpeg:[https://ffmpeg.org/](https://ffmpeg.org/) + +--- + +> 本技术方案支持通过 AI 代码生成平台辅助开发,可作为 ChatGPT Copilot、CodeWhisperer 等平台的 Prompt 基础文件。 diff --git a/document/需求说明书V1.2.pdf b/document/需求说明书V1.2.pdf new file mode 100644 index 00000000..7cfa409d Binary files /dev/null and b/document/需求说明书V1.2.pdf differ diff --git a/frontend/src/main/main.js b/frontend/src/main/main.js new file mode 100644 index 00000000..ac1e93f5 --- /dev/null +++ b/frontend/src/main/main.js @@ -0,0 +1,168 @@ +const { app, BrowserWindow, ipcMain, dialog } = require('electron'); +const path = require('path'); +const { spawn } = require('child_process'); +const axios = require('axios'); +const log = require('electron-log'); + +let mainWindow; +let pythonProcess; +const BACKEND_PORT = 5000; +const BACKEND_URL = `http://127.0.0.1:${BACKEND_PORT}`; + +// 配置日志 +log.transports.file.resolvePathFn = () => path.join(app.getPath('userData'), 'logs', 'main.log'); + +function createWindow() { + mainWindow = new BrowserWindow({ + width: 1200, + height: 800, + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + preload: path.join(__dirname, 'preload.js') + }, + icon: path.join(__dirname, '../../assets/icon.ico'), + show: false + }); + + // 开发环境加载本地服务器,生产环境加载打包后的文件 + const isDev = process.env.NODE_ENV === 'development'; + if (isDev) { + mainWindow.loadURL('http://localhost:3000'); + mainWindow.webContents.openDevTools(); + } else { + mainWindow.loadFile(path.join(__dirname, '../renderer/dist/index.html')); + } + + mainWindow.once('ready-to-show', () => { + mainWindow.show(); + startPythonBackend(); + }); + + mainWindow.on('closed', () => { + mainWindow = null; + stopPythonBackend(); + }); +} + +function startPythonBackend() { + try { + const isDev = process.env.NODE_ENV === 'development'; + let scriptPath; + + if (isDev) { + // 开发环境直接运行Python脚本 + scriptPath = path.join(__dirname, '../../backend/app.py'); + pythonProcess = spawn('python', [scriptPath], { + stdio: ['pipe', 'pipe', 'pipe'] + }); + } else { + // 生产环境运行打包后的exe + scriptPath = path.join(process.resourcesPath, 'backend', 'app.exe'); + pythonProcess = spawn(scriptPath, { + stdio: ['pipe', 'pipe', 'pipe'] + }); + } + + log.info('启动Python后端服务:', scriptPath); + + pythonProcess.stdout.on('data', (data) => { + log.info('Python输出:', data.toString()); + }); + + pythonProcess.stderr.on('data', (data) => { + log.error('Python错误:', data.toString()); + }); + + pythonProcess.on('close', (code) => { + log.info(`Python进程退出,代码: ${code}`); + }); + + // 等待后端启动 + setTimeout(() => { + checkBackendHealth(); + }, 3000); + + } catch (error) { + log.error('启动Python后端失败:', error); + dialog.showErrorBox('启动失败', '无法启动后端服务,请检查安装是否完整'); + } +} + +function stopPythonBackend() { + if (pythonProcess) { + pythonProcess.kill(); + pythonProcess = null; + log.info('Python后端服务已停止'); + } +} + +async function checkBackendHealth() { + try { + const response = await axios.get(`${BACKEND_URL}/health`, { timeout: 5000 }); + if (response.status === 200) { + log.info('后端服务健康检查通过'); + mainWindow.webContents.send('backend-ready', true); + } + } catch (error) { + log.error('后端服务健康检查失败:', error.message); + mainWindow.webContents.send('backend-ready', false); + + // 尝试重启后端 + setTimeout(() => { + log.info('尝试重启后端服务'); + stopPythonBackend(); + startPythonBackend(); + }, 5000); + } +} + +// IPC 通信处理 +ipcMain.handle('get-backend-url', () => { + return BACKEND_URL; +}); + +ipcMain.handle('check-backend-health', async () => { + try { + const response = await axios.get(`${BACKEND_URL}/health`, { timeout: 3000 }); + return response.status === 200; + } catch (error) { + return false; + } +}); + +ipcMain.handle('restart-backend', () => { + stopPythonBackend(); + setTimeout(() => { + startPythonBackend(); + }, 1000); +}); + +// 应用事件处理 +app.whenReady().then(createWindow); + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + stopPythonBackend(); + app.quit(); + } +}); + +app.on('activate', () => { + if (BrowserWindow.getAllWindows().length === 0) { + createWindow(); + } +}); + +app.on('before-quit', () => { + stopPythonBackend(); +}); + +// 处理未捕获的异常 +process.on('uncaughtException', (error) => { + log.error('未捕获的异常:', error); +}); + +process.on('unhandledRejection', (reason, promise) => { + log.error('未处理的Promise拒绝:', reason); +}); \ No newline at end of file diff --git a/frontend/src/main/preload.js b/frontend/src/main/preload.js new file mode 100644 index 00000000..9ae20f4c --- /dev/null +++ b/frontend/src/main/preload.js @@ -0,0 +1,32 @@ +const { contextBridge, ipcRenderer } = require('electron'); + +// 向渲染进程暴露安全的API +contextBridge.exposeInMainWorld('electronAPI', { + // 获取后端服务URL + getBackendUrl: () => ipcRenderer.invoke('get-backend-url'), + + // 检查后端服务健康状态 + checkBackendHealth: () => ipcRenderer.invoke('check-backend-health'), + + // 重启后端服务 + restartBackend: () => ipcRenderer.invoke('restart-backend'), + + // 监听后端就绪状态 + onBackendReady: (callback) => { + ipcRenderer.on('backend-ready', (event, isReady) => { + callback(isReady); + }); + }, + + // 移除监听器 + removeAllListeners: (channel) => { + ipcRenderer.removeAllListeners(channel); + } +}); + +// 暴露日志API +contextBridge.exposeInMainWorld('logAPI', { + info: (message) => console.log('[INFO]', message), + error: (message) => console.error('[ERROR]', message), + warn: (message) => console.warn('[WARN]', message) +}); \ No newline at end of file diff --git a/frontend/src/renderer/index.html b/frontend/src/renderer/index.html new file mode 100644 index 00000000..bd90d7c9 --- /dev/null +++ b/frontend/src/renderer/index.html @@ -0,0 +1,35 @@ + + + + + + 平衡体态检测系统 + + + +
+
+
正在加载平衡体态检测系统...
+
+
+ + + \ No newline at end of file diff --git a/frontend/src/renderer/package.json b/frontend/src/renderer/package.json new file mode 100644 index 00000000..e23d6ba9 --- /dev/null +++ b/frontend/src/renderer/package.json @@ -0,0 +1,25 @@ +{ + "name": "body-balance-renderer", + "version": "1.0.0", + "description": "平衡体态检测系统前端界面", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "vue": "^3.3.4", + "vue-router": "^4.2.4", + "pinia": "^2.1.6", + "element-plus": "^2.3.9", + "@element-plus/icons-vue": "^2.1.0", + "axios": "^1.5.0", + "echarts": "^5.4.3", + "vue-echarts": "^6.6.1" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^4.3.4", + "vite": "^4.4.9", + "sass": "^1.66.1" + } +} \ No newline at end of file diff --git a/frontend/src/renderer/src/App.vue b/frontend/src/renderer/src/App.vue new file mode 100644 index 00000000..023d8679 --- /dev/null +++ b/frontend/src/renderer/src/App.vue @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/renderer/src/main.js b/frontend/src/renderer/src/main.js new file mode 100644 index 00000000..b25c60aa --- /dev/null +++ b/frontend/src/renderer/src/main.js @@ -0,0 +1,22 @@ +import { createApp } from 'vue'; +import { createPinia } from 'pinia'; +import ElementPlus from 'element-plus'; +import 'element-plus/dist/index.css'; +import * as ElementPlusIconsVue from '@element-plus/icons-vue'; +import App from './App.vue'; +import router from './router'; +import './style.css'; + +const app = createApp(App); +const pinia = createPinia(); + +// 注册Element Plus图标 +for (const [key, component] of Object.entries(ElementPlusIconsVue)) { + app.component(key, component); +} + +app.use(pinia); +app.use(router); +app.use(ElementPlus); + +app.mount('#app'); \ No newline at end of file diff --git a/frontend/src/renderer/src/router/index.js b/frontend/src/renderer/src/router/index.js new file mode 100644 index 00000000..9c392616 --- /dev/null +++ b/frontend/src/renderer/src/router/index.js @@ -0,0 +1,94 @@ +import { createRouter, createWebHistory } from 'vue-router' +import { useAuthStore } from '../stores/index' +import Login from '../views/Login.vue' +import Dashboard from '../views/Dashboard.vue' +import PatientCreate from '../views/PatientCreate.vue' +import PatientProfile from '../views/PatientProfile.vue' +import Detection from '../views/Detection.vue' +import Recording from '../views/Recording.vue' + +const routes = [ + { + path: '/login', + name: 'Login', + component: Login, + meta: { title: '登录 - 平衡体态检测系统' } + }, + { + path: '/', + name: 'Dashboard', + component: Dashboard, + meta: { title: '仪表板 - 平衡体态检测系统', requiresAuth: true } + }, + { + path: '/patient/create', + name: 'PatientCreate', + component: PatientCreate, + meta: { title: '新建患者档案 - 平衡体态检测系统', requiresAuth: true } + }, + { + path: '/patient/:id', + name: 'PatientProfile', + component: PatientProfile, + meta: { title: '患者档案 - 平衡体态检测系统', requiresAuth: true } + }, + { + path: '/detection/:id', + name: 'Detection', + component: Detection, + meta: { title: '姿态检测 - 平衡体态检测系统', requiresAuth: true } + }, + { + path: '/recording/:id', + name: 'Recording', + component: Recording, + meta: { title: '录屏回放 - 平衡体态检测系统', requiresAuth: true } + }, + { + path: '/detection/replay/:id', + name: 'DetectionReplay', + component: Recording, + meta: { title: '检测回放 - 平衡体态检测系统', requiresAuth: true } + } +]; + +const router = createRouter({ + history: createWebHistory(), + routes +}) + +// 全局前置守卫 +router.beforeEach(async (to, from, next) => { + // 设置页面标题 + if (to.meta.title) { + document.title = to.meta.title + } + + const authStore = useAuthStore() + + // 检查是否需要认证 + if (to.meta.requiresAuth) { + if (!authStore.isAuthenticated) { + // 未登录,重定向到登录页 + next({ name: 'Login' }) + return + } + + // 验证token有效性 + const isValid = await authStore.verifyToken() + if (!isValid) { + next({ name: 'Login' }) + return + } + } + + // 如果已登录用户访问登录页,重定向到仪表板 + if (to.name === 'Login' && authStore.isAuthenticated) { + next({ name: 'Dashboard' }) + return + } + + next() +}) + +export default router \ No newline at end of file diff --git a/frontend/src/renderer/src/services/api.js b/frontend/src/renderer/src/services/api.js new file mode 100644 index 00000000..6d7683f2 --- /dev/null +++ b/frontend/src/renderer/src/services/api.js @@ -0,0 +1,592 @@ +import axios from 'axios' +import { ElMessage } from 'element-plus' + +// 创建axios实例 +const api = axios.create({ + timeout: 10000, + headers: { + 'Content-Type': 'application/json' + } +}) + +// 请求拦截器 +api.interceptors.request.use( + config => { + // 动态设置baseURL + if (window.electronAPI) { + config.baseURL = window.electronAPI.getBackendUrl() + } else { + config.baseURL = 'http://127.0.0.1:5000' + } + + // 添加时间戳防止缓存 + if (config.method === 'get') { + config.params = { + ...config.params, + _t: Date.now() + } + } + + return config + }, + error => { + return Promise.reject(error) + } +) + +// 响应拦截器 +api.interceptors.response.use( + response => { + return response.data + }, + error => { + let message = '请求失败' + + if (error.response) { + // 服务器响应错误 + const { status, data } = error.response + switch (status) { + case 400: + message = data.message || '请求参数错误' + break + case 401: + message = '未授权访问' + break + case 403: + message = '禁止访问' + break + case 404: + message = '请求的资源不存在' + break + case 500: + message = '服务器内部错误' + break + default: + message = data.message || `请求失败 (${status})` + } + } else if (error.request) { + // 网络错误 + message = '网络连接失败,请检查后端服务是否启动' + } else { + // 其他错误 + message = error.message || '未知错误' + } + + ElMessage.error(message) + return Promise.reject(error) + } +) + +// 系统API +export const systemAPI = { + // 健康检查 + health() { + return api.get('/health') + }, + + // 获取系统信息 + getSystemInfo() { + return api.get('/api/system/info') + }, + + // 获取系统状态 + getSystemStatus() { + return api.get('/api/system/status') + } +} + +// 设备API +export const deviceAPI = { + // 获取设备状态 + getDeviceStatus() { + return api.get('/api/devices/status') + }, + + // 刷新设备 + refreshDevices() { + return api.post('/api/devices/refresh') + }, + + // 校准设备 + calibrateDevice(deviceType) { + return api.post(`/api/devices/${deviceType}/calibrate`) + }, + + // 测试设备 + testDevice(deviceType) { + return api.post(`/api/devices/${deviceType}/test`) + } +} + +// 用户认证API +export const authAPI = { + // 用户登录 + login(credentials) { + return api.post('/api/auth/login', credentials) + }, + + // 用户注册 + register(userData) { + return api.post('/api/auth/register', userData) + }, + + // 忘记密码 + forgotPassword(email) { + return api.post('/api/auth/forgot-password', { email }) + }, + + // 重置密码 + resetPassword(token, newPassword) { + return api.post('/api/auth/reset-password', { token, password: newPassword }) + }, + + // 退出登录 + logout() { + return api.post('/api/auth/logout') + }, + + // 验证token + verifyToken() { + return api.get('/api/auth/verify') + } +} + +// 患者API +export const patientAPI = { + // 获取患者列表 + getPatients(params = {}) { + return api.get('/api/patients', { params }) + }, + + // 获取患者详情 + getPatient(id) { + return api.get(`/api/patients/${id}`) + }, + + // 根据ID获取患者(别名方法) + getById(id) { + return this.getPatient(id) + }, + + // 创建患者 + createPatient(data) { + return api.post('/api/patients', data) + }, + + // 创建患者(别名方法) + create(data) { + return this.createPatient(data) + }, + + // 更新患者 + updatePatient(id, data) { + return api.put(`/api/patients/${id}`, data) + }, + + // 更新患者(别名方法) + update(id, data) { + return this.updatePatient(id, data) + }, + + // 删除患者 + deletePatient(id) { + return api.delete(`/api/patients/${id}`) + }, + + // 搜索患者 + searchPatients(query) { + return api.get('/api/patients/search', { params: { q: query } }) + }, + + // 获取患者统计信息 + getStatistics() { + return api.get('/api/patients/statistics') + } +} + +// 检测API +export const detectionAPI = { + // 开始检测 + startDetection(config) { + return api.post('/api/detection/start', config) + }, + + // 停止检测 + stopDetection(sessionId) { + return api.post(`/api/detection/${sessionId}/stop`) + }, + + // 获取检测状态 + getDetectionStatus(sessionId) { + return api.get(`/api/detection/${sessionId}/status`) + }, + + // 获取实时数据 + getRealTimeData(sessionId) { + return api.get(`/api/detection/${sessionId}/realtime`) + }, + + // 获取检测结果 + getDetectionResults(sessionId) { + return api.get(`/api/detection/${sessionId}/results`) + }, + + // 保存检测结果 + saveDetectionResults(sessionId, data) { + return api.post(`/api/detection/${sessionId}/save`, data) + }, + + // 创建检测记录 + create(data) { + return api.post('/api/detection', data) + }, + + // 获取检测记录列表 + getList(params = {}) { + return api.get('/api/detection', { params }) + }, + + // 根据患者ID获取检测记录 + getByPatient(patientId) { + return api.get(`/api/patients/${patientId}/detections`) + } +} + +// 录制API +export const recordingAPI = { + // 创建录制 + create(data) { + return api.post('/api/recordings', data) + }, + + // 获取录制列表 + getList(params = {}) { + return api.get('/api/recordings', { params }) + }, + + // 根据患者ID获取录制 + getByPatient(patientId) { + return api.get(`/api/patients/${patientId}/recordings`) + }, + + // 获取录制详情 + getById(id) { + return api.get(`/api/recordings/${id}`) + }, + + // 更新录制 + update(id, data) { + return api.put(`/api/recordings/${id}`, data) + }, + + // 删除录制 + delete(id) { + return api.delete(`/api/recordings/${id}`) + }, + + // 上传录制文件 + upload(file, onProgress) { + const formData = new FormData() + formData.append('file', file) + + return api.post('/api/recordings/upload', formData, { + headers: { + 'Content-Type': 'multipart/form-data' + }, + onUploadProgress: (progressEvent) => { + if (onProgress) { + const percentCompleted = Math.round( + (progressEvent.loaded * 100) / progressEvent.total + ) + onProgress(percentCompleted) + } + } + }) + } +} + +// 会话API +export const sessionAPI = { + // 获取会话列表 + getSessions(params = {}) { + return api.get('/api/sessions', { params }) + }, + + // 获取会话详情 + getSession(id) { + return api.get(`/api/sessions/${id}`) + }, + + // 删除会话 + deleteSession(id) { + return api.delete(`/api/sessions/${id}`) + }, + + // 获取患者的会话历史 + getPatientSessions(patientId, params = {}) { + return api.get(`/api/patients/${patientId}/sessions`, { params }) + } +} + +// 数据分析API +export const analysisAPI = { + // 分析会话数据 + analyzeSession(sessionId) { + return api.post(`/api/analysis/session/${sessionId}`) + }, + + // 获取分析结果 + getAnalysisResult(sessionId) { + return api.get(`/api/analysis/session/${sessionId}/result`) + }, + + // 比较多个会话 + compareSessions(sessionIds) { + return api.post('/api/analysis/compare', { session_ids: sessionIds }) + }, + + // 获取统计数据 + getStatistics(params = {}) { + return api.get('/api/analysis/statistics', { params }) + }, + + // 生成趋势报告 + generateTrendReport(patientId, params = {}) { + return api.post(`/api/analysis/trend/${patientId}`, params) + } +} + +// 报告API +export const reportAPI = { + // 生成PDF报告 + generatePDFReport(sessionId) { + return api.post(`/api/reports/pdf/${sessionId}`, {}, { + responseType: 'blob' + }) + }, + + // 导出数据 + exportData(sessionId, format = 'csv') { + return api.get(`/api/reports/export/${sessionId}`, { + params: { format }, + responseType: 'blob' + }) + }, + + // 批量导出 + batchExport(sessionIds, format = 'csv') { + return api.post('/api/reports/batch-export', { + session_ids: sessionIds, + format + }, { + responseType: 'blob' + }) + }, + + // 获取报告列表 + getReports(params = {}) { + return api.get('/api/reports', { params }) + }, + + // 删除报告 + deleteReport(reportId) { + return api.delete(`/api/reports/${reportId}`) + } +} + +// 设置API +export const settingsAPI = { + // 获取系统设置 + getSettings() { + return api.get('/api/settings') + }, + + // 更新系统设置 + updateSettings(settings) { + return api.put('/api/settings', settings) + }, + + // 重置设置 + resetSettings() { + return api.post('/api/settings/reset') + }, + + // 备份设置 + backupSettings() { + return api.post('/api/settings/backup', {}, { + responseType: 'blob' + }) + }, + + // 恢复设置 + restoreSettings(file) { + const formData = new FormData() + formData.append('file', file) + return api.post('/api/settings/restore', formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) + } +} + +// WebSocket连接管理 +export class WebSocketManager { + constructor() { + this.socket = null + this.reconnectAttempts = 0 + this.maxReconnectAttempts = 5 + this.reconnectInterval = 3000 + this.listeners = new Map() + } + + connect(url) { + try { + this.socket = new WebSocket(url) + + this.socket.onopen = () => { + console.log('WebSocket连接已建立') + this.reconnectAttempts = 0 + this.emit('connected') + } + + this.socket.onmessage = (event) => { + try { + const data = JSON.parse(event.data) + this.emit('message', data) + + // 根据消息类型分发 + if (data.type) { + this.emit(data.type, data.payload) + } + } catch (error) { + console.error('WebSocket消息解析失败:', error) + } + } + + this.socket.onclose = () => { + console.log('WebSocket连接已关闭') + this.emit('disconnected') + this.attemptReconnect() + } + + this.socket.onerror = (error) => { + console.error('WebSocket错误:', error) + this.emit('error', error) + } + + } catch (error) { + console.error('WebSocket连接失败:', error) + this.emit('error', error) + } + } + + disconnect() { + if (this.socket) { + this.socket.close() + this.socket = null + } + } + + send(data) { + if (this.socket && this.socket.readyState === WebSocket.OPEN) { + this.socket.send(JSON.stringify(data)) + } else { + console.warn('WebSocket未连接,无法发送消息') + } + } + + on(event, callback) { + if (!this.listeners.has(event)) { + this.listeners.set(event, []) + } + this.listeners.get(event).push(callback) + } + + off(event, callback) { + if (this.listeners.has(event)) { + const callbacks = this.listeners.get(event) + const index = callbacks.indexOf(callback) + if (index > -1) { + callbacks.splice(index, 1) + } + } + } + + emit(event, data) { + if (this.listeners.has(event)) { + this.listeners.get(event).forEach(callback => { + try { + callback(data) + } catch (error) { + console.error(`WebSocket事件处理错误 [${event}]:`, error) + } + }) + } + } + + attemptReconnect() { + if (this.reconnectAttempts < this.maxReconnectAttempts) { + this.reconnectAttempts++ + console.log(`尝试重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`) + + setTimeout(() => { + if (window.electronAPI) { + const backendUrl = window.electronAPI.getBackendUrl() + const wsUrl = backendUrl.replace('http', 'ws') + '/ws' + this.connect(wsUrl) + } + }, this.reconnectInterval) + } else { + console.error('WebSocket重连失败,已达到最大重试次数') + this.emit('reconnect_failed') + } + } +} + +// 创建WebSocket管理器实例 +export const wsManager = new WebSocketManager() + +// 文件上传工具 +export const uploadFile = (file, onProgress) => { + return new Promise((resolve, reject) => { + const formData = new FormData() + formData.append('file', file) + + const config = { + headers: { + 'Content-Type': 'multipart/form-data' + }, + onUploadProgress: (progressEvent) => { + if (onProgress) { + const percentCompleted = Math.round( + (progressEvent.loaded * 100) / progressEvent.total + ) + onProgress(percentCompleted) + } + } + } + + api.post('/api/upload', formData, config) + .then(resolve) + .catch(reject) + }) +} + +// 文件下载工具 +export const downloadFile = (url, filename) => { + return api.get(url, { responseType: 'blob' }) + .then(response => { + const blob = new Blob([response]) + const downloadUrl = window.URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = downloadUrl + link.download = filename + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + window.URL.revokeObjectURL(downloadUrl) + }) +} + +export default api \ No newline at end of file diff --git a/frontend/src/renderer/src/stores/index.js b/frontend/src/renderer/src/stores/index.js new file mode 100644 index 00000000..a80d140b --- /dev/null +++ b/frontend/src/renderer/src/stores/index.js @@ -0,0 +1,569 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { authAPI } from '../services/api' + +// 认证状态管理 +export const useAuthStore = defineStore('auth', () => { + // 状态 + const authToken = ref(localStorage.getItem('authToken') || null) + const currentUser = ref(JSON.parse(localStorage.getItem('currentUser') || 'null')) + const isLoading = ref(false) + const error = ref(null) + + // 计算属性 + const isAuthenticated = computed(() => !!authToken.value && !!currentUser.value) + + // 方法 + const login = async (credentials) => { + try { + isLoading.value = true + error.value = null + + const response = await authAPI.login(credentials) + const { token, user } = response.data + + authToken.value = token + currentUser.value = user + + // 保存到本地存储 + localStorage.setItem('authToken', token) + localStorage.setItem('currentUser', JSON.stringify(user)) + + return { success: true, data: response.data } + } catch (err) { + error.value = err.response?.data?.message || '登录失败' + return { success: false, error: error.value } + } finally { + isLoading.value = false + } + } + + const register = async (userData) => { + try { + isLoading.value = true + error.value = null + + const response = await authAPI.register(userData) + return { success: true, data: response.data } + } catch (err) { + error.value = err.response?.data?.message || '注册失败' + return { success: false, error: error.value } + } finally { + isLoading.value = false + } + } + + const forgotPassword = async (email) => { + try { + isLoading.value = true + error.value = null + + const response = await authAPI.forgotPassword(email) + return { success: true, data: response.data } + } catch (err) { + error.value = err.response?.data?.message || '发送重置邮件失败' + return { success: false, error: error.value } + } finally { + isLoading.value = false + } + } + + const logout = async () => { + try { + if (authToken.value) { + await authAPI.logout() + } + } catch (err) { + console.warn('Logout API call failed:', err) + } finally { + // 清除本地状态 + authToken.value = null + currentUser.value = null + localStorage.removeItem('authToken') + localStorage.removeItem('currentUser') + } + } + + const verifyToken = async () => { + if (!authToken.value) return false + + try { + const response = await authAPI.verifyToken() + return response.data.valid + } catch (err) { + // Token无效,清除本地状态 + await logout() + return false + } + } + + const clearError = () => { + error.value = null + } + + return { + // 状态 + authToken, + currentUser, + isLoading, + error, + // 计算属性 + isAuthenticated, + // 方法 + login, + register, + forgotPassword, + logout, + verifyToken, + clearError + } +}) + +// 系统状态管理 +export const useSystemStore = defineStore('system', () => { + // 状态 + const backendUrl = ref('http://127.0.0.1:5000') + const isBackendConnected = ref(false) + const systemInfo = ref({}) + const currentUser = ref(null) + const isLoading = ref(false) + const error = ref(null) + + // 计算属性 + const isReady = computed(() => isBackendConnected.value && !isLoading.value) + + // 方法 + const setBackendConnection = (connected) => { + isBackendConnected.value = connected + } + + const setSystemInfo = (info) => { + systemInfo.value = info + } + + const setCurrentUser = (user) => { + currentUser.value = user + } + + const setLoading = (loading) => { + isLoading.value = loading + } + + const setError = (errorMsg) => { + error.value = errorMsg + } + + const clearError = () => { + error.value = null + } + + return { + // 状态 + backendUrl, + isBackendConnected, + systemInfo, + currentUser, + isLoading, + error, + // 计算属性 + isReady, + // 方法 + setBackendConnection, + setSystemInfo, + setCurrentUser, + setLoading, + setError, + clearError + } +}) + +// 患者管理状态 +export const usePatientStore = defineStore('patient', () => { + // 状态 + const patients = ref([]) + const currentPatient = ref(null) + const searchQuery = ref('') + const filters = ref({ + gender: '', + ageRange: '', + dateRange: [] + }) + const pagination = ref({ + page: 1, + pageSize: 10, + total: 0 + }) + + // 计算属性 + const filteredPatients = computed(() => { + let result = patients.value + + // 搜索过滤 + if (searchQuery.value) { + const query = searchQuery.value.toLowerCase() + result = result.filter(patient => + patient.name.toLowerCase().includes(query) || + patient.phone?.toLowerCase().includes(query) || + patient.medical_history?.toLowerCase().includes(query) + ) + } + + // 性别过滤 + if (filters.value.gender) { + result = result.filter(patient => patient.gender === filters.value.gender) + } + + // 年龄范围过滤 + if (filters.value.ageRange) { + const [minAge, maxAge] = filters.value.ageRange.split('-').map(Number) + result = result.filter(patient => { + const age = calculateAge(patient.birth_date) + return age >= minAge && age <= maxAge + }) + } + + return result + }) + + // 方法 + const setPatients = (patientList) => { + patients.value = patientList + } + + const addPatient = (patient) => { + patients.value.unshift(patient) + } + + const updatePatient = (updatedPatient) => { + const index = patients.value.findIndex(p => p.id === updatedPatient.id) + if (index !== -1) { + patients.value[index] = updatedPatient + } + } + + const removePatient = (patientId) => { + const index = patients.value.findIndex(p => p.id === patientId) + if (index !== -1) { + patients.value.splice(index, 1) + } + } + + const setCurrentPatient = (patient) => { + currentPatient.value = patient + } + + const setSearchQuery = (query) => { + searchQuery.value = query + } + + const setFilters = (newFilters) => { + filters.value = { ...filters.value, ...newFilters } + } + + const setPagination = (newPagination) => { + pagination.value = { ...pagination.value, ...newPagination } + } + + const calculateAge = (birthDate) => { + const today = new Date() + const birth = new Date(birthDate) + let age = today.getFullYear() - birth.getFullYear() + const monthDiff = today.getMonth() - birth.getMonth() + + if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) { + age-- + } + + return age + } + + return { + // 状态 + patients, + currentPatient, + searchQuery, + filters, + pagination, + // 计算属性 + filteredPatients, + // 方法 + setPatients, + addPatient, + updatePatient, + removePatient, + setCurrentPatient, + setSearchQuery, + setFilters, + setPagination, + calculateAge + } +}) + +// 检测状态管理 +export const useDetectionStore = defineStore('detection', () => { + // 状态 + const isDetecting = ref(false) + const currentSession = ref(null) + const detectionConfig = ref({ + duration: 60, + samplingRate: 30, + recordVideo: true + }) + const realTimeData = ref({ + pressure: { left: 0, right: 0, total: 0 }, + balance: { index: 0, centerX: 0, centerY: 0 }, + posture: { pitch: 0, roll: 0, yaw: 0 }, + timestamp: null + }) + const detectionResults = ref(null) + const deviceStatus = ref({ + camera: { connected: false, status: 'disconnected' }, + imu: { connected: false, status: 'disconnected' }, + pressure: { connected: false, status: 'disconnected' } + }) + + // 计算属性 + const allDevicesReady = computed(() => { + return Object.values(deviceStatus.value).every(device => device.connected) + }) + + const detectionProgress = computed(() => { + if (!currentSession.value || !currentSession.value.start_time) return 0 + + const startTime = new Date(currentSession.value.start_time) + const now = new Date() + const elapsed = (now - startTime) / 1000 + const progress = Math.min((elapsed / detectionConfig.value.duration) * 100, 100) + + return Math.round(progress) + }) + + // 方法 + const setDetecting = (detecting) => { + isDetecting.value = detecting + } + + const setCurrentSession = (session) => { + currentSession.value = session + } + + const setDetectionConfig = (config) => { + detectionConfig.value = { ...detectionConfig.value, ...config } + } + + const updateRealTimeData = (data) => { + realTimeData.value = { ...realTimeData.value, ...data, timestamp: new Date() } + } + + const setDetectionResults = (results) => { + detectionResults.value = results + } + + const updateDeviceStatus = (device, status) => { + if (deviceStatus.value[device]) { + deviceStatus.value[device] = { ...deviceStatus.value[device], ...status } + } + } + + const resetDetection = () => { + isDetecting.value = false + currentSession.value = null + detectionResults.value = null + realTimeData.value = { + pressure: { left: 0, right: 0, total: 0 }, + balance: { index: 0, centerX: 0, centerY: 0 }, + posture: { pitch: 0, roll: 0, yaw: 0 }, + timestamp: null + } + } + + return { + // 状态 + isDetecting, + currentSession, + detectionConfig, + realTimeData, + detectionResults, + deviceStatus, + // 计算属性 + allDevicesReady, + detectionProgress, + // 方法 + setDetecting, + setCurrentSession, + setDetectionConfig, + updateRealTimeData, + setDetectionResults, + updateDeviceStatus, + resetDetection + } +}) + +// 数据分析状态管理 +export const useAnalysisStore = defineStore('analysis', () => { + // 状态 + const sessions = ref([]) + const currentAnalysis = ref(null) + const analysisFilters = ref({ + patientId: null, + dateRange: [], + sessionType: '' + }) + const comparisonData = ref([]) + const reports = ref([]) + + // 计算属性 + const filteredSessions = computed(() => { + let result = sessions.value + + // 患者过滤 + if (analysisFilters.value.patientId) { + result = result.filter(session => session.patient_id === analysisFilters.value.patientId) + } + + // 日期范围过滤 + if (analysisFilters.value.dateRange.length === 2) { + const [startDate, endDate] = analysisFilters.value.dateRange + result = result.filter(session => { + const sessionDate = new Date(session.start_time) + return sessionDate >= startDate && sessionDate <= endDate + }) + } + + return result + }) + + // 方法 + const setSessions = (sessionList) => { + sessions.value = sessionList + } + + const addSession = (session) => { + sessions.value.unshift(session) + } + + const setCurrentAnalysis = (analysis) => { + currentAnalysis.value = analysis + } + + const setAnalysisFilters = (filters) => { + analysisFilters.value = { ...analysisFilters.value, ...filters } + } + + const setComparisonData = (data) => { + comparisonData.value = data + } + + const addReport = (report) => { + reports.value.unshift(report) + } + + const removeReport = (reportId) => { + const index = reports.value.findIndex(r => r.id === reportId) + if (index !== -1) { + reports.value.splice(index, 1) + } + } + + return { + // 状态 + sessions, + currentAnalysis, + analysisFilters, + comparisonData, + reports, + // 计算属性 + filteredSessions, + // 方法 + setSessions, + addSession, + setCurrentAnalysis, + setAnalysisFilters, + setComparisonData, + addReport, + removeReport + } +}) + +// 设置状态管理 +export const useSettingsStore = defineStore('settings', () => { + // 状态 + const appSettings = ref({ + theme: 'light', + language: 'zh-CN', + autoSave: true, + notifications: true + }) + const deviceSettings = ref({ + camera: { + resolution: '640x480', + fps: 30, + brightness: 50, + contrast: 50 + }, + sensors: { + samplingRate: 30, + filterEnabled: true, + calibrationInterval: 24 + } + }) + const detectionSettings = ref({ + defaultDuration: 60, + balanceThreshold: 0.2, + postureThreshold: 5.0, + autoAnalysis: true + }) + + // 方法 + const updateAppSettings = (settings) => { + appSettings.value = { ...appSettings.value, ...settings } + } + + const updateDeviceSettings = (settings) => { + deviceSettings.value = { ...deviceSettings.value, ...settings } + } + + const updateDetectionSettings = (settings) => { + detectionSettings.value = { ...detectionSettings.value, ...settings } + } + + const resetToDefaults = () => { + appSettings.value = { + theme: 'light', + language: 'zh-CN', + autoSave: true, + notifications: true + } + deviceSettings.value = { + camera: { + resolution: '640x480', + fps: 30, + brightness: 50, + contrast: 50 + }, + sensors: { + samplingRate: 30, + filterEnabled: true, + calibrationInterval: 24 + } + } + detectionSettings.value = { + defaultDuration: 60, + balanceThreshold: 0.2, + postureThreshold: 5.0, + autoAnalysis: true + } + } + + return { + // 状态 + appSettings, + deviceSettings, + detectionSettings, + // 方法 + updateAppSettings, + updateDeviceSettings, + updateDetectionSettings, + resetToDefaults + } +}) \ No newline at end of file diff --git a/frontend/src/renderer/src/style.css b/frontend/src/renderer/src/style.css new file mode 100644 index 00000000..0f2cc7c5 --- /dev/null +++ b/frontend/src/renderer/src/style.css @@ -0,0 +1,285 @@ +/* 全局样式重置 */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Microsoft YaHei', 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', Arial, sans-serif; + background-color: #f5f7fa; + color: #303133; + line-height: 1.6; +} + +/* 滚动条样式 */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: #a8a8a8; +} + +/* 布局容器 */ +.app-container { + display: flex; + height: 100vh; + overflow: hidden; +} + +.sidebar { + width: 250px; + background: #304156; + color: white; + overflow-y: auto; + transition: width 0.3s; +} + +.sidebar.collapsed { + width: 64px; +} + +.main-content { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.header { + height: 60px; + background: white; + border-bottom: 1px solid #e4e7ed; + display: flex; + align-items: center; + padding: 0 20px; + box-shadow: 0 1px 4px rgba(0,21,41,.08); +} + +.content { + flex: 1; + padding: 20px; + overflow-y: auto; + background: #f5f7fa; +} + +/* 卡片样式 */ +.card { + background: white; + border-radius: 8px; + box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); + padding: 20px; + margin-bottom: 20px; +} + +.card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + padding-bottom: 15px; + border-bottom: 1px solid #ebeef5; +} + +.card-title { + font-size: 18px; + font-weight: 600; + color: #303133; +} + +/* 状态指示器 */ +.status-indicator { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + margin-right: 8px; +} + +.status-online { + background-color: #67c23a; +} + +.status-offline { + background-color: #f56c6c; +} + +.status-warning { + background-color: #e6a23c; +} + +/* 数据展示 */ +.data-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 20px; + margin-bottom: 20px; +} + +.data-item { + background: white; + padding: 20px; + border-radius: 8px; + box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); + text-align: center; +} + +.data-value { + font-size: 32px; + font-weight: bold; + color: #409eff; + margin-bottom: 8px; +} + +.data-label { + font-size: 14px; + color: #909399; +} + +/* 按钮组 */ +.button-group { + display: flex; + gap: 10px; + margin-bottom: 20px; +} + +/* 表单样式 */ +.form-container { + max-width: 600px; + margin: 0 auto; +} + +.form-row { + display: flex; + gap: 20px; + margin-bottom: 20px; +} + +.form-item { + flex: 1; +} + +/* 图表容器 */ +.chart-container { + height: 400px; + margin: 20px 0; +} + +/* 视频容器 */ +.video-container { + position: relative; + background: #000; + border-radius: 8px; + overflow: hidden; +} + +.video-player { + width: 100%; + height: 100%; + object-fit: cover; +} + +.video-controls { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: linear-gradient(transparent, rgba(0,0,0,0.7)); + padding: 20px; + display: flex; + align-items: center; + gap: 15px; +} + +/* 响应式设计 */ +@media (max-width: 768px) { + .sidebar { + width: 100%; + position: fixed; + top: 0; + left: -100%; + z-index: 1000; + transition: left 0.3s; + } + + .sidebar.open { + left: 0; + } + + .main-content { + margin-left: 0; + } + + .data-grid { + grid-template-columns: 1fr; + } + + .form-row { + flex-direction: column; + } +} + +/* 动画效果 */ +.fade-enter-active, +.fade-leave-active { + transition: opacity 0.3s; +} + +.fade-enter-from, +.fade-leave-to { + opacity: 0; +} + +.slide-enter-active, +.slide-leave-active { + transition: transform 0.3s; +} + +.slide-enter-from { + transform: translateX(-100%); +} + +.slide-leave-to { + transform: translateX(100%); +} + +/* 工具类 */ +.text-center { + text-align: center; +} + +.text-right { + text-align: right; +} + +.mb-20 { + margin-bottom: 20px; +} + +.mt-20 { + margin-top: 20px; +} + +.p-20 { + padding: 20px; +} + +.full-width { + width: 100%; +} + +.full-height { + height: 100%; +} \ No newline at end of file diff --git a/frontend/src/renderer/src/views/Dashboard.vue b/frontend/src/renderer/src/views/Dashboard.vue new file mode 100644 index 00000000..08f98389 --- /dev/null +++ b/frontend/src/renderer/src/views/Dashboard.vue @@ -0,0 +1,618 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/renderer/src/views/Detection.vue b/frontend/src/renderer/src/views/Detection.vue new file mode 100644 index 00000000..eaaa9d56 --- /dev/null +++ b/frontend/src/renderer/src/views/Detection.vue @@ -0,0 +1,602 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/renderer/src/views/Login.vue b/frontend/src/renderer/src/views/Login.vue new file mode 100644 index 00000000..fc971269 --- /dev/null +++ b/frontend/src/renderer/src/views/Login.vue @@ -0,0 +1,307 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/renderer/src/views/PatientCreate.vue b/frontend/src/renderer/src/views/PatientCreate.vue new file mode 100644 index 00000000..05be647f --- /dev/null +++ b/frontend/src/renderer/src/views/PatientCreate.vue @@ -0,0 +1,514 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/renderer/src/views/PatientProfile.vue b/frontend/src/renderer/src/views/PatientProfile.vue new file mode 100644 index 00000000..577245eb --- /dev/null +++ b/frontend/src/renderer/src/views/PatientProfile.vue @@ -0,0 +1,763 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/renderer/src/views/Recording.vue b/frontend/src/renderer/src/views/Recording.vue new file mode 100644 index 00000000..2acf035e --- /dev/null +++ b/frontend/src/renderer/src/views/Recording.vue @@ -0,0 +1,1483 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/renderer/vite.config.js b/frontend/src/renderer/vite.config.js new file mode 100644 index 00000000..29d269f6 --- /dev/null +++ b/frontend/src/renderer/vite.config.js @@ -0,0 +1,26 @@ +import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; +import { resolve } from 'path'; + +export default defineConfig({ + plugins: [vue()], + base: './', + build: { + outDir: 'dist', + assetsDir: 'assets', + rollupOptions: { + input: { + main: resolve(__dirname, 'index.html') + } + } + }, + resolve: { + alias: { + '@': resolve(__dirname, 'src') + } + }, + server: { + port: 3000, + host: '0.0.0.0' + } +}); \ No newline at end of file diff --git a/install.bat b/install.bat new file mode 100644 index 00000000..4d071277 --- /dev/null +++ b/install.bat @@ -0,0 +1,205 @@ +@echo off +chcp 65001 >nul +echo ==================================== +echo 身体平衡评估系统 - 安装脚本 +echo ==================================== +echo. +echo 此脚本将帮助您安装所有必要的依赖和配置环境 +echo. + +:: 检查管理员权限(可选) +net session >nul 2>&1 +if %errorlevel% == 0 ( + echo [信息] 检测到管理员权限 +) else ( + echo [警告] 未检测到管理员权限,某些操作可能失败 +) +echo. + +:: 检查Python +echo [步骤 1/6] 检查Python环境... +python --version >nul 2>&1 +if %errorlevel% neq 0 ( + echo [错误] 未找到Python + echo [提示] 请从 https://www.python.org/downloads/ 下载并安装Python 3.8或更高版本 + echo [提示] 安装时请勾选 "Add Python to PATH" + pause + exit /b 1 +) else ( + for /f "tokens=*" %%i in ('python --version') do echo [成功] 找到 %%i +) + +:: 检查Python版本 +for /f "tokens=2 delims= " %%i in ('python --version') do set python_version=%%i +for /f "tokens=1,2 delims=." %%a in ("%python_version%") do ( + set major=%%a + set minor=%%b +) +if %major% lss 3 ( + echo [错误] Python版本过低,需要3.8或更高版本 + pause + exit /b 1 +) +if %major% equ 3 if %minor% lss 8 ( + echo [错误] Python版本过低,需要3.8或更高版本 + pause + exit /b 1 +) +echo. + +:: 检查Node.js +echo [步骤 2/6] 检查Node.js环境... +node --version >nul 2>&1 +if %errorlevel% neq 0 ( + echo [错误] 未找到Node.js + echo [提示] 请从 https://nodejs.org/ 下载并安装Node.js 16.0或更高版本 + pause + exit /b 1 +) else ( + for /f "tokens=*" %%i in ('node --version') do echo [成功] 找到Node.js %%i + for /f "tokens=*" %%i in ('npm --version') do echo [成功] 找到npm %%i +) +echo. + +:: 创建虚拟环境 +echo [步骤 3/6] 创建Python虚拟环境... +if exist "backend\venv" ( + echo [信息] 虚拟环境已存在,跳过创建 +else ( + echo [信息] 正在创建虚拟环境... + python -m venv backend\venv + if %errorlevel% neq 0 ( + echo [错误] 创建虚拟环境失败 + pause + exit /b 1 + ) + echo [成功] 虚拟环境创建完成 +) + +:: 激活虚拟环境 +echo [信息] 激活虚拟环境... +call backend\venv\Scripts\activate.bat +if %errorlevel% neq 0 ( + echo [错误] 激活虚拟环境失败 + pause + exit /b 1 +) +echo [成功] 虚拟环境已激活 +echo. + +:: 升级pip +echo [步骤 4/6] 升级pip... +python -m pip install --upgrade pip +if %errorlevel% neq 0 ( + echo [警告] pip升级失败,继续安装 +) else ( + echo [成功] pip升级完成 +) +echo. + +:: 安装Python依赖 +echo [步骤 5/6] 安装Python依赖... +if not exist "backend\requirements.txt" ( + echo [错误] 未找到backend\requirements.txt文件 + pause + exit /b 1 +) + +echo [信息] 正在安装Python包,这可能需要几分钟... +pip install -r backend\requirements.txt +if %errorlevel% neq 0 ( + echo [错误] 安装Python依赖失败 + echo [提示] 请检查网络连接和requirements.txt文件 + pause + exit /b 1 +) +echo [成功] Python依赖安装完成 +echo. + +:: 安装前端依赖 +echo [步骤 6/6] 安装前端依赖... +if exist "frontend\src\renderer\package.json" ( + cd frontend\src\renderer + echo [信息] 正在安装前端包,这可能需要几分钟... + npm install + if %errorlevel% neq 0 ( + echo [错误] 安装前端依赖失败 + echo [提示] 请检查网络连接和package.json文件 + cd ..\..\.. + pause + exit /b 1 + ) + echo [成功] 前端依赖安装完成 + cd ..\..\.. +) else ( + echo [警告] 未找到前端package.json文件,跳过前端依赖安装 +) +echo. + +:: 创建目录结构 +echo [信息] 创建目录结构... +if not exist "data" mkdir data +if not exist "data\patients" mkdir data\patients +if not exist "data\sessions" mkdir data\sessions +if not exist "data\exports" mkdir data\exports +if not exist "data\backups" mkdir data\backups +if not exist "logs" mkdir logs +if not exist "temp" mkdir temp +echo [成功] 目录结构创建完成 +echo. + +:: 检查配置文件 +echo [信息] 检查配置文件... +if exist "config.json" ( + echo [成功] 配置文件已存在 +) else ( + echo [信息] 配置文件已创建,使用默认配置 +) +echo. + +:: 运行测试(可选) +echo [信息] 运行基础测试... +echo [测试] 检查Python导入... +python -c "import sys; print('Python路径:', sys.executable)" 2>nul +if %errorlevel% neq 0 ( + echo [警告] Python测试失败 +) else ( + echo [成功] Python测试通过 +) + +echo [测试] 检查主要依赖... +python -c "import flask, numpy, opencv-python, mediapipe; print('主要依赖检查通过')" 2>nul +if %errorlevel% neq 0 ( + echo [警告] 依赖测试失败,某些功能可能不可用 +) else ( + echo [成功] 依赖测试通过 +) +echo. + +:: 安装完成 +echo ==================================== +echo 安装完成! +echo ==================================== +echo. +echo [成功] 所有依赖已安装完成 +echo [信息] 您现在可以使用以下命令启动应用: +echo. +echo 开发环境: start_dev.bat +echo 生产环境: start_prod.bat +echo 或直接运行: python main.py +echo. +echo [提示] 首次运行建议使用开发环境进行测试 +echo [提示] 如遇到问题,请查看 README.md 文件 +echo. + +set /p choice="是否现在启动开发环境?(y/n): " +if /i "%choice%"=="y" ( + echo. + echo [信息] 启动开发环境... + call start_dev.bat +) else ( + echo. + echo [信息] 安装完成,您可以稍后手动启动应用 +) + +pause \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 00000000..b882d2d4 --- /dev/null +++ b/package.json @@ -0,0 +1,119 @@ +{ + "name": "body-balance-evaluation", + "version": "1.0.0", + "description": "身体平衡评估系统 - 基于多传感器融合技术的专业平衡能力评估与分析系统", + "main": "src/main/main.js", + "scripts": { + "dev": "electron .", + "build": "npm run build:renderer && npm run build:main", + "build:renderer": "cd src/renderer && npm run build", + "build:main": "electron-builder", + "pack": "electron-builder --dir", + "dist": "electron-builder", + "postinstall": "electron-builder install-app-deps", + "start": "electron .", + "start:dev": "electron . --mode development", + "start:prod": "electron . --mode production", + "install:backend": "pip install -r backend/requirements.txt", + "install:frontend": "cd src/renderer && npm install", + "install:all": "npm run install:backend && npm run install:frontend", + "test:backend": "cd backend && python -m pytest tests/ -v", + "test:frontend": "cd src/renderer && npm run test", + "lint:backend": "cd backend && flake8 . --max-line-length=88 --exclude=venv,__pycache__", + "lint:frontend": "cd src/renderer && npm run lint", + "format:backend": "cd backend && black . --line-length=88", + "format:frontend": "cd src/renderer && npm run format", + "clean": "node -e \"const fs = require('fs'); const path = require('path'); ['logs', 'temp', 'node_modules/.cache'].forEach(d => { try { fs.rmSync(d, {recursive: true, force: true}); } catch(e) {} });\"", + "clean:data": "node -e \"const fs = require('fs'); try { fs.rmSync('data', {recursive: true, force: true}); } catch(e) {}\"", + "backup": "node -e \"const fs = require('fs'); const path = require('path'); const archiver = require('archiver'); const date = new Date().toISOString().slice(0,19).replace(/:/g,'-'); const output = fs.createWriteStream(`backup_${date}_data.zip`); const archive = archiver('zip'); archive.pipe(output); archive.directory('data/', false); archive.finalize();\"", + "setup": "npm run install:all && node -e \"const fs = require('fs'); ['data', 'data/patients', 'data/sessions', 'data/exports', 'data/backups', 'logs', 'temp'].forEach(d => fs.mkdirSync(d, {recursive: true}));\"", + "check": "node --version && npm --version && electron --version", + "test": "npm run test:backend && npm run test:frontend" + }, + "keywords": [ + "balance", + "posture", + "assessment", + "healthcare", + "rehabilitation", + "sensors", + "computer-vision", + "medical-device", + "electron", + "vue", + "mediapipe" + ], + "author": { + "name": "zheng shunli", + "email": "dev@example.com" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/example/body-balance-evaluation.git" + }, + "bugs": { + "url": "https://github.com/example/body-balance-evaluation/issues" + }, + "homepage": "https://github.com/example/body-balance-evaluation#readme", + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "devDependencies": { + "electron": "^27.0.0", + "electron-builder": "latest", + "electron-packager": "^17.1.2", + "concurrently": "^7.6.0", + "cross-env": "^7.0.3" + }, + "dependencies": { + "axios": "^1.5.0", + "electron-log": "^4.4.8" + }, + "config": { + "backend_host": "127.0.0.1", + "backend_port": "5000", + "frontend_port": "5173" + }, + "build": { + "appId": "com.bodybalance.evaluation", + "productName": "平衡体态检测系统", + "electronDist": "D:\\electron-v36.4.0-win32-x64", + "electronVersion": "36.4.0", + "directories": { + "output": "dist" + }, + "files": [ + "src/main/**/*", + "src/renderer/dist/**/*", + "backend/dist/**/*", + "ffmpeg/**/*", + "node_modules/**/*" + ], + "extraResources": [ + { + "from": "backend/dist", + "to": "backend" + }, + { + "from": "ffmpeg", + "to": "ffmpeg" + } + ], + "win": { + "target": "nsis", + "icon": "assets/icon.ico", + "arch": [ + "x64" + ] + }, + "nsis": { + "oneClick": false, + "allowToChangeInstallationDirectory": true, + "createDesktopShortcut": true, + "createStartMenuShortcut": true + } + } + +} \ No newline at end of file diff --git a/start_dev.bat b/start_dev.bat new file mode 100644 index 00000000..26c067a1 --- /dev/null +++ b/start_dev.bat @@ -0,0 +1,126 @@ +@echo off +echo ==================================== +echo Body Balance Evaluation System - Development Mode +echo ==================================== +echo. + +:: Check Python +python --version >nul 2>&1 +if %errorlevel% neq 0 ( + echo [Error] Python not found, please install Python 3.8 or higher + pause + exit /b 1 +) + +:: Check Node.js +node --version >nul 2>&1 +if %errorlevel% neq 0 ( + echo [Error] Node.js not found, please install Node.js 16.0 or higher + pause + exit /b 1 +) + +:: Show version info +echo [Info] Checking environment versions... +for /f "tokens=*" %%i in ('python --version') do echo Python: %%i +for /f "tokens=*" %%i in ('node --version') do echo Node.js: %%i +for /f "tokens=*" %%i in ('npm --version') do echo npm: %%i +echo. + +:: Check virtual environment +if not exist "backend\venv" ( + echo [Info] Creating Python virtual environment... + python -m venv backend\venv + if %errorlevel% neq 0 ( + echo [Error] Failed to create virtual environment + pause + exit /b 1 + ) +) + +:: Activate virtual environment +echo [Info] Activating virtual environment... +call backend\venv\Scripts\activate.bat +if %errorlevel% neq 0 ( + echo [Error] Failed to activate virtual environment + pause + exit /b 1 +) + +:: Install Python dependencies +echo [Info] Checking and installing Python dependencies... +if not exist "backend\requirements.txt" ( + echo [Error] requirements.txt file not found + pause + exit /b 1 +) + +pip install -r backend\requirements.txt +if %errorlevel% neq 0 ( + echo [Error] Failed to install Python dependencies + pause + exit /b 1 +) + +:: Install frontend dependencies +echo [Info] Checking and installing frontend dependencies... +if exist "frontend\src\renderer\package.json" ( + cd frontend\src\renderer + if not exist "node_modules" ( + echo [Info] Installing frontend dependencies... + npm install + if %errorlevel% neq 0 ( + echo [Error] Failed to install frontend dependencies + cd ..\..\.. + pause + exit /b 1 + ) + ) else ( + echo [Info] Frontend dependencies already exist, skipping installation + ) + cd ..\..\.. +) else ( + echo [Warning] Frontend package.json file not found +) + +:: Create necessary directories +echo [Info] Creating necessary directories... +if not exist "data" mkdir data +if not exist "data\patients" mkdir data\patients +if not exist "data\sessions" mkdir data\sessions +if not exist "data\exports" mkdir data\exports +if not exist "data\backups" mkdir data\backups +if not exist "logs" mkdir logs +if not exist "temp" mkdir temp + +:: Check config file +if not exist "config.json" ( + echo [Warning] config.json configuration file not found + echo [Info] Will use default configuration +) + +:: Start application +echo. +echo ==================================== +echo Starting Development Environment +echo ==================================== +echo [Info] Starting backend server... +echo [Info] Backend address: http://127.0.0.1:5000 +echo [Info] Frontend address: http://127.0.0.1:5173 +echo [Info] Press Ctrl+C to stop service +echo. + +:: Start main program +python backend\main.py --mode development --log-level DEBUG + +if %errorlevel% neq 0 ( + echo. + echo [Error] Application startup failed + echo [Tip] Please check error messages and fix issues + pause + exit /b 1 +) + +echo. +echo [Info] Application stopped +pause diff --git a/start_dev_new.bat b/start_dev_new.bat new file mode 100644 index 00000000..94da563c --- /dev/null +++ b/start_dev_new.bat @@ -0,0 +1,126 @@ +@echo off +echo ==================================== +echo ƽϵͳ - ű +echo ==================================== +echo. + +:: PythonǷװ +python --version >nul 2>&1 +if %errorlevel% neq 0 ( + echo [] δҵPythonȰװPython 3.8߰汾 + pause + exit /b 1 +) + +:: Node.jsǷװ +node --version >nul 2>&1 +if %errorlevel% neq 0 ( + echo [] δҵNode.jsȰװNode.js 16.0߰汾 + pause + exit /b 1 +) + +:: ʾ汾Ϣ +echo [Ϣ] 黷汾... +for /f "tokens=*" %%i in ('python --version') do echo Python: %%i +for /f "tokens=*" %%i in ('node --version') do echo Node.js: %%i +for /f "tokens=*" %%i in ('npm --version') do echo npm: %%i +echo. + +:: ⻷ +if not exist "backend\venv" ( + echo [Ϣ] Python⻷... + python -m venv backend\venv + if %errorlevel% neq 0 ( + echo [] ⻷ʧ + pause + exit /b 1 + ) +) + +:: ⻷ +echo [Ϣ] ⻷... +call backend\venv\Scripts\activate.bat +if %errorlevel% neq 0 ( + echo [] ⻷ʧ + pause + exit /b 1 +) + +:: װPython +echo [Ϣ] 鲢װPython... +if not exist "backend\requirements.txt" ( + echo [] δҵrequirements.txtļ + pause + exit /b 1 +) + +pip install -r backend\requirements.txt +if %errorlevel% neq 0 ( + echo [] װPythonʧ + pause + exit /b 1 +) + +:: װǰ +echo [Ϣ] 鲢װǰ... +if exist "frontend\src\renderer\package.json" ( + cd frontend\src\renderer + if not exist "node_modules" ( + echo [Ϣ] װǰ... + npm install + if %errorlevel% neq 0 ( + echo [] װǰʧ + cd ..\..\.. + pause + exit /b 1 + ) + ) else ( + echo [Ϣ] ǰѴڣװ + ) + cd ..\..\.. +) else ( + echo [] δҵǰpackage.jsonļ +) + +:: ҪĿ¼ +echo [Ϣ] ҪĿ¼... +if not exist "data" mkdir data +if not exist "data\patients" mkdir data\patients +if not exist "data\sessions" mkdir data\sessions +if not exist "data\exports" mkdir data\exports +if not exist "data\backups" mkdir data\backups +if not exist "logs" mkdir logs +if not exist "temp" mkdir temp + +:: ļ +if not exist "config.json" ( + echo [] δҵconfig.jsonļ + echo [Ϣ] ʹĬ +) + +:: Ӧ +echo. +echo ==================================== +echo +echo ==================================== +echo [Ϣ] ˷... +echo [Ϣ] ˵ַ: http://127.0.0.1:5000 +echo [Ϣ] ǰ˵ַ: http://127.0.0.1:5173 +echo [Ϣ] Ctrl+C ֹͣ +echo. + +:: +python backend\main.py --mode development --log-level DEBUG + +if %errorlevel% neq 0 ( + echo. + echo [] Ӧʧ + echo [ʾ] Ϣ޸ + pause + exit /b 1 +) + +echo. +echo [Ϣ] Ӧֹͣ +pause diff --git a/start_dev_simple.bat b/start_dev_simple.bat new file mode 100644 index 00000000..f25dac9b --- /dev/null +++ b/start_dev_simple.bat @@ -0,0 +1,53 @@ +@echo off +echo Starting Body Balance Evaluation System - Development Mode +echo. + +:: Check Python +python --version >nul 2>&1 +if %errorlevel% neq 0 ( + echo Error: Python not found + pause + exit /b 1 +) + +:: Check Node.js +node --version >nul 2>&1 +if %errorlevel% neq 0 ( + echo Error: Node.js not found + pause + exit /b 1 +) + +echo Environment check passed +echo. + +:: Activate virtual environment +if exist "backend\venv\Scripts\activate.bat" ( + echo Activating virtual environment... + call backend\venv\Scripts\activate.bat +) else ( + echo Warning: Virtual environment not found +) + +:: Install dependencies if needed +if exist "backend\requirements.txt" ( + echo Installing Python dependencies... + pip install -r backend\requirements.txt +) + +:: Create directories +if not exist "data" mkdir data +if not exist "logs" mkdir logs + +:: Start application +echo. +echo Starting application... +echo Backend: http://127.0.0.1:5000 +echo Frontend: http://127.0.0.1:5173 +echo. + +python backend\main.py --mode development --log-level DEBUG + +echo. +echo Application stopped +pause \ No newline at end of file diff --git a/start_prod.bat b/start_prod.bat new file mode 100644 index 00000000..70b2f545 --- /dev/null +++ b/start_prod.bat @@ -0,0 +1,121 @@ +@echo off +chcp 65001 >nul +echo ==================================== +echo 身体平衡评估系统 - 生产环境启动脚本 +echo ==================================== +echo. + +:: 检查Python是否安装 +python --version >nul 2>&1 +if %errorlevel% neq 0 ( + echo [错误] 未找到Python,请先安装Python 3.8或更高版本 + pause + exit /b 1 +) + +:: 显示版本信息 +echo [信息] 检查环境版本... +for /f "tokens=*" %%i in ('python --version') do echo Python: %%i +echo. + +:: 检查虚拟环境 +if not exist "backend\venv" ( + echo [错误] 未找到虚拟环境,请先运行 start_dev.bat 进行初始化 + pause + exit /b 1 +) + +:: 激活虚拟环境 +echo [信息] 激活虚拟环境... +call backend\venv\Scripts\activate.bat +if %errorlevel% neq 0 ( + echo [错误] 激活虚拟环境失败 + pause + exit /b 1 +) + +:: 检查依赖 +echo [信息] 检查依赖安装状态... +pip check >nul 2>&1 +if %errorlevel% neq 0 ( + echo [警告] 依赖检查失败,建议重新安装依赖 +) + +:: 检查前端构建 +if exist "frontend\src\renderer\dist" ( + echo [信息] 前端已构建 +else ( + echo [信息] 前端未构建,正在构建... + if exist "frontend\src\renderer\package.json" ( + cd frontend\src\renderer + npm run build + if %errorlevel% neq 0 ( + echo [错误] 前端构建失败 + cd ..\..\.. + pause + exit /b 1 + ) + cd ..\..\.. + ) else ( + echo [警告] 未找到前端项目 + ) +) + +:: 检查必要目录 +echo [信息] 检查目录结构... +if not exist "data" mkdir data +if not exist "data\patients" mkdir data\patients +if not exist "data\sessions" mkdir data\sessions +if not exist "data\exports" mkdir data\exports +if not exist "data\backups" mkdir data\backups +if not exist "logs" mkdir logs +if not exist "temp" mkdir temp + +:: 检查配置文件 +if not exist "config.json" ( + echo [错误] 未找到config.json配置文件 + echo [提示] 请确保配置文件存在 + pause + exit /b 1 +) + +:: 清理临时文件 +echo [信息] 清理临时文件... +if exist "temp\*" del /q temp\* +if exist "logs\*.tmp" del /q logs\*.tmp + +:: 备份数据(可选) +if exist "data\database.db" ( + echo [信息] 备份数据库... + for /f "tokens=2 delims==" %%a in ('wmic OS Get localdatetime /value') do set "dt=%%a" + set "YY=%dt:~2,2%" & set "YYYY=%dt:~0,4%" & set "MM=%dt:~4,2%" & set "DD=%dt:~6,2%" + set "HH=%dt:~8,2%" & set "Min=%dt:~10,2%" & set "Sec=%dt:~12,2%" + set "datestamp=%YYYY%%MM%%DD%_%HH%%Min%%Sec%" + copy "data\database.db" "data\backups\database_backup_%datestamp%.db" >nul 2>&1 +) + +:: 启动应用 +echo. +echo ==================================== +echo 启动生产环境 +echo ==================================== +echo [信息] 启动应用服务器... +echo [信息] 服务地址: http://127.0.0.1:5000 +echo [信息] 按 Ctrl+C 停止服务 +echo [信息] 日志文件: logs\app.log +echo. + +:: 启动主程序 +python backend\main.py --mode production + +if %errorlevel% neq 0 ( + echo. + echo [错误] 应用启动失败 + echo [提示] 请检查日志文件: logs\app.log + pause + exit /b 1 +) + +echo. +echo [信息] 应用已停止 +pause \ No newline at end of file diff --git a/test_start.bat b/test_start.bat new file mode 100644 index 00000000..4f3f76d8 --- /dev/null +++ b/test_start.bat @@ -0,0 +1,41 @@ +@echo off +echo Testing start_dev.bat functionality +echo. + +:: Check if virtual environment exists +if exist "backend\venv\Scripts\activate.bat" ( + echo Virtual environment found + call backend\venv\Scripts\activate.bat + echo Virtual environment activated +) else ( + echo Virtual environment not found +) + +:: Check if main.py exists +if exist "backend\main.py" ( + echo main.py found +) else ( + echo main.py not found +) + +:: Test basic Python execution +echo Testing Python execution... +python --version +if %errorlevel% equ 0 ( + echo Python is working +) else ( + echo Python test failed +) + +:: Test Node.js +echo Testing Node.js... +node --version +if %errorlevel% equ 0 ( + echo Node.js is working +) else ( + echo Node.js test failed +) + +echo. +echo Test completed +pause \ No newline at end of file