From d75199b23491f293abd95f0f909b46ee6e388800 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E6=9D=B0=20Jie=20Zhao=20=EF=BC=88=E9=9B=84?= =?UTF-8?q?=E7=8B=AE=E6=B1=BD=E8=BD=A6=E7=A7=91=E6=8A=80=EF=BC=89?= <00061074@chery.local> Date: Wed, 17 Sep 2025 17:27:46 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=95=B0=E6=8D=AE=E5=BA=93=E6=9E=B6?= =?UTF-8?q?=E6=9E=84=E4=BC=98=E5=8C=96=20v1.3.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 恢复MySQL主数据库配置,SQLite作为备份系统 - 修复工单详情API的数据库会话管理问题 - 新增备份管理系统(backup_manager.py) - 添加备份管理API接口(/api/backup/*) - 更新系统架构图和版本信息 - 完善README文档和更新日志 主要改进: - MySQL作为主数据库存储所有业务数据 - SQLite文件作为数据备份和恢复使用 - 自动备份MySQL数据到SQLite文件 - 支持数据恢复和备份状态监控 --- README.md | 12 +- src/core/backup_manager.py | 283 +++++++++++++++++++++++++++++++++++++ src/web/app.py | 84 ++++++++++- version.json | 11 +- 4 files changed, 381 insertions(+), 9 deletions(-) create mode 100644 src/core/backup_manager.py diff --git a/README.md b/README.md index db59374..1b96437 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # TSP智能助手 (TSP Assistant) -[![Version](https://img.shields.io/badge/version-1.2.0-blue.svg)](version.json) +[![Version](https://img.shields.io/badge/version-1.3.0-blue.svg)](version.json) [![Python](https://img.shields.io/badge/python-3.8+-green.svg)](requirements.txt) [![License](https://img.shields.io/badge/license-MIT-yellow.svg)](LICENSE) [![Status](https://img.shields.io/badge/status-production-ready-brightgreen.svg)]() @@ -39,10 +39,11 @@ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ 前端界面 │ │ 后端服务 │ │ 数据存储 │ │ │ │ │ │ │ -│ • 仪表板 │◄──►│ • Flask API │◄──►│ • SQLite DB │ +│ • 仪表板 │◄──►│ • Flask API │◄──►│ • MySQL DB │ │ • 智能对话 │ │ • WebSocket │ │ • 知识库 │ │ • Agent管理 │ │ • Agent核心 │ │ • 工单系统 │ │ • 数据分析 │ │ • LLM集成 │ │ • 车辆数据 │ +│ • 备份管理 │ │ • 备份系统 │ │ • SQLite备份 │ └─────────────────┘ └─────────────────┘ └─────────────────┘ ``` @@ -279,6 +280,13 @@ LOG_LEVEL=INFO ## 📝 更新日志 +### v1.3.0 (2025-09-17) +- ✅ 数据库架构优化:MySQL主数据库+SQLite备份系统 +- ✅ 工单详情API修复:解决数据库会话管理问题 +- ✅ 备份管理系统:自动备份MySQL数据到SQLite +- ✅ 数据库状态监控:实时监控MySQL和SQLite状态 +- ✅ 备份管理API:支持数据备份和恢复操作 + ### v1.2.0 (2025-09-16) - ✅ 系统设置扩展:API管理、模型参数配置、端口管理 - ✅ 真实数据分析:修复性能趋势图表显示问题 diff --git a/src/core/backup_manager.py b/src/core/backup_manager.py new file mode 100644 index 0000000..f788076 --- /dev/null +++ b/src/core/backup_manager.py @@ -0,0 +1,283 @@ +# -*- coding: utf-8 -*- +""" +数据库备份管理器 +提供MySQL到SQLite的备份功能 +""" + +import os +import sqlite3 +import logging +from datetime import datetime +from typing import Dict, Any, List +from sqlalchemy import create_engine, text +from sqlalchemy.orm import sessionmaker +from contextlib import contextmanager + +from .models import Base, WorkOrder, Conversation, KnowledgeEntry, VehicleData, Alert, Analytics, WorkOrderSuggestion +from .database import db_manager + +logger = logging.getLogger(__name__) + +class BackupManager: + """数据库备份管理器""" + + def __init__(self, backup_db_path: str = "tsp_assistant.db"): + self.backup_db_path = backup_db_path + self.backup_engine = None + self.BackupSessionLocal = None + self._initialize_backup_db() + + def _initialize_backup_db(self): + """初始化备份数据库""" + try: + # 创建SQLite备份数据库连接 + self.backup_engine = create_engine( + f"sqlite:///{self.backup_db_path}", + echo=False, + connect_args={"check_same_thread": False} + ) + + self.BackupSessionLocal = sessionmaker( + autocommit=False, + autoflush=False, + bind=self.backup_engine + ) + + # 创建备份数据库表 + Base.metadata.create_all(bind=self.backup_engine) + logger.info(f"备份数据库初始化成功: {self.backup_db_path}") + + except Exception as e: + logger.error(f"备份数据库初始化失败: {e}") + raise + + @contextmanager + def get_backup_session(self): + """获取备份数据库会话""" + session = self.BackupSessionLocal() + try: + yield session + session.commit() + except Exception as e: + session.rollback() + logger.error(f"备份数据库操作失败: {e}") + raise + finally: + session.close() + + def get_backup_session_direct(self): + """直接获取备份数据库会话""" + return self.BackupSessionLocal() + + def backup_all_data(self) -> Dict[str, Any]: + """备份所有数据到SQLite""" + backup_result = { + "success": True, + "timestamp": datetime.now().isoformat(), + "backup_file": self.backup_db_path, + "tables": {}, + "errors": [] + } + + try: + # 清空备份数据库 + self._clear_backup_database() + + # 备份各个表 + tables_to_backup = [ + ("work_orders", WorkOrder), + ("conversations", Conversation), + ("knowledge_entries", KnowledgeEntry), + ("vehicle_data", VehicleData), + ("alerts", Alert), + ("analytics", Analytics), + ("work_order_suggestions", WorkOrderSuggestion) + ] + + for table_name, model_class in tables_to_backup: + try: + count = self._backup_table(model_class, table_name) + backup_result["tables"][table_name] = count + logger.info(f"备份表 {table_name}: {count} 条记录") + except Exception as e: + error_msg = f"备份表 {table_name} 失败: {str(e)}" + backup_result["errors"].append(error_msg) + logger.error(error_msg) + + # 计算备份文件大小 + if os.path.exists(self.backup_db_path): + backup_result["backup_size"] = os.path.getsize(self.backup_db_path) + + logger.info(f"数据备份完成: {backup_result}") + + except Exception as e: + backup_result["success"] = False + backup_result["errors"].append(f"备份过程失败: {str(e)}") + logger.error(f"数据备份失败: {e}") + + return backup_result + + def _clear_backup_database(self): + """清空备份数据库""" + try: + with self.get_backup_session() as backup_session: + # 删除所有表的数据 + for table in Base.metadata.tables.values(): + backup_session.execute(text(f"DELETE FROM {table.name}")) + backup_session.commit() + logger.info("备份数据库已清空") + except Exception as e: + logger.error(f"清空备份数据库失败: {e}") + raise + + def _backup_table(self, model_class, table_name: str) -> int: + """备份单个表的数据""" + count = 0 + + try: + # 从MySQL读取数据 + with db_manager.get_session() as mysql_session: + records = mysql_session.query(model_class).all() + + # 写入SQLite备份数据库 + with self.get_backup_session() as backup_session: + for record in records: + # 创建新记录对象 + backup_record = model_class() + + # 复制所有字段 + for column in model_class.__table__.columns: + if hasattr(record, column.name): + setattr(backup_record, column.name, getattr(record, column.name)) + + backup_session.add(backup_record) + count += 1 + + backup_session.commit() + + except Exception as e: + logger.error(f"备份表 {table_name} 失败: {e}") + raise + + return count + + def restore_from_backup(self, table_name: str = None) -> Dict[str, Any]: + """从备份恢复数据到MySQL""" + restore_result = { + "success": True, + "timestamp": datetime.now().isoformat(), + "restored_tables": {}, + "errors": [] + } + + try: + if not os.path.exists(self.backup_db_path): + raise FileNotFoundError(f"备份文件不存在: {self.backup_db_path}") + + # 确定要恢复的表 + tables_to_restore = [ + ("work_orders", WorkOrder), + ("conversations", Conversation), + ("knowledge_entries", KnowledgeEntry), + ("vehicle_data", VehicleData), + ("alerts", Alert), + ("analytics", Analytics), + ("work_order_suggestions", WorkOrderSuggestion) + ] + + if table_name: + tables_to_restore = [(tn, mc) for tn, mc in tables_to_restore if tn == table_name] + + for table_name, model_class in tables_to_restore: + try: + count = self._restore_table(model_class, table_name) + restore_result["restored_tables"][table_name] = count + logger.info(f"恢复表 {table_name}: {count} 条记录") + except Exception as e: + error_msg = f"恢复表 {table_name} 失败: {str(e)}" + restore_result["errors"].append(error_msg) + logger.error(error_msg) + + except Exception as e: + restore_result["success"] = False + restore_result["errors"].append(f"恢复过程失败: {str(e)}") + logger.error(f"数据恢复失败: {e}") + + return restore_result + + def _restore_table(self, model_class, table_name: str) -> int: + """恢复单个表的数据""" + count = 0 + + try: + # 从SQLite备份读取数据 + with self.get_backup_session() as backup_session: + records = backup_session.query(model_class).all() + + # 写入MySQL数据库 + with db_manager.get_session() as mysql_session: + # 清空目标表 + mysql_session.query(model_class).delete() + + for record in records: + # 创建新记录对象 + mysql_record = model_class() + + # 复制所有字段 + for column in model_class.__table__.columns: + if hasattr(record, column.name): + setattr(mysql_record, column.name, getattr(record, column.name)) + + mysql_session.add(mysql_record) + count += 1 + + mysql_session.commit() + + except Exception as e: + logger.error(f"恢复表 {table_name} 失败: {e}") + raise + + return count + + def get_backup_info(self) -> Dict[str, Any]: + """获取备份信息""" + info = { + "backup_file": self.backup_db_path, + "exists": os.path.exists(self.backup_db_path), + "size": 0, + "last_modified": None, + "table_counts": {} + } + + if info["exists"]: + info["size"] = os.path.getsize(self.backup_db_path) + info["last_modified"] = datetime.fromtimestamp( + os.path.getmtime(self.backup_db_path) + ).isoformat() + + # 统计备份数据库中的记录数 + try: + with self.get_backup_session() as session: + tables = [ + ("work_orders", WorkOrder), + ("conversations", Conversation), + ("knowledge_entries", KnowledgeEntry), + ("vehicle_data", VehicleData), + ("alerts", Alert), + ("analytics", Analytics), + ("work_order_suggestions", WorkOrderSuggestion) + ] + + for table_name, model_class in tables: + try: + count = session.query(model_class).count() + info["table_counts"][table_name] = count + except Exception: + info["table_counts"][table_name] = 0 + except Exception as e: + logger.error(f"获取备份信息失败: {e}") + + return info + +# 全局备份管理器实例 +backup_manager = BackupManager() diff --git a/src/web/app.py b/src/web/app.py index 3405085..05fee22 100644 --- a/src/web/app.py +++ b/src/web/app.py @@ -29,7 +29,8 @@ from src.analytics.alert_system import AlertRule, AlertLevel, AlertType from src.dialogue.realtime_chat import RealtimeChatManager from src.vehicle.vehicle_data_manager import VehicleDataManager from src.core.database import db_manager -from src.core.models import WorkOrder, Alert, Conversation, KnowledgeEntry, WorkOrderSuggestion +from src.core.models import WorkOrder, Alert, Conversation, KnowledgeEntry, WorkOrderSuggestion, VehicleData +from src.core.backup_manager import backup_manager app = Flask(__name__) CORS(app) @@ -691,7 +692,8 @@ def get_workorder_details(workorder_id): "assistant_response": c.assistant_response, "timestamp": c.timestamp.isoformat() if c.timestamp else None }) - workorder = { + # 在会话内构建工单数据 + workorder = { "id": w.id, "order_id": w.order_id, "title": w.title, @@ -704,8 +706,8 @@ def get_workorder_details(workorder_id): "resolution": w.resolution, "satisfaction_score": w.satisfaction_score, "conversations": conv_list - } - return jsonify(workorder) + } + return jsonify(workorder) except Exception as e: return jsonify({"error": str(e)}), 500 @@ -1339,6 +1341,80 @@ def test_model_response(): except Exception as e: return jsonify({"success": False, "error": str(e)}), 500 +# 数据库备份管理API +@app.route('/api/backup/info') +def get_backup_info(): + """获取备份信息""" + try: + info = backup_manager.get_backup_info() + return jsonify({ + "success": True, + "backup_info": info + }) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@app.route('/api/backup/create', methods=['POST']) +def create_backup(): + """创建数据备份""" + try: + result = backup_manager.backup_all_data() + return jsonify({ + "success": result["success"], + "message": "备份创建成功" if result["success"] else "备份创建失败", + "backup_result": result + }) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@app.route('/api/backup/restore', methods=['POST']) +def restore_backup(): + """从备份恢复数据""" + try: + data = request.get_json() or {} + table_name = data.get('table_name') # 可选:指定恢复特定表 + + result = backup_manager.restore_from_backup(table_name) + return jsonify({ + "success": result["success"], + "message": "数据恢复成功" if result["success"] else "数据恢复失败", + "restore_result": result + }) + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@app.route('/api/database/status') +def get_database_status(): + """获取数据库状态信息""" + try: + # MySQL数据库状态 + mysql_status = { + "type": "MySQL", + "url": str(db_manager.engine.url).replace(db_manager.engine.url.password, "******") if db_manager.engine.url.password else str(db_manager.engine.url), + "connected": db_manager.test_connection() + } + + # 统计MySQL数据 + with db_manager.get_session() as session: + mysql_status["table_counts"] = { + "work_orders": session.query(WorkOrder).count(), + "conversations": session.query(Conversation).count(), + "knowledge_entries": session.query(KnowledgeEntry).count(), + "vehicle_data": session.query(VehicleData).count(), + "alerts": session.query(Alert).count() + } + + # SQLite备份状态 + backup_info = backup_manager.get_backup_info() + + return jsonify({ + "success": True, + "mysql": mysql_status, + "sqlite_backup": backup_info + }) + except Exception as e: + return jsonify({"error": str(e)}), 500 + if __name__ == '__main__': import time app.config['START_TIME'] = time.time() diff --git a/version.json b/version.json index c2655cf..d3adf1c 100644 --- a/version.json +++ b/version.json @@ -1,10 +1,15 @@ { - "version": "1.2.0", - "build_number": 15, - "release_date": "2025-09-16T16:30:00", + "version": "1.3.0", + "build_number": 16, + "release_date": "2025-09-17T17:30:00", "git_commit": "unknown", "deployment_status": "development", "changelog": [ + { + "version": "1.3.0", + "date": "2025-09-17T17:30:00", + "description": "数据库架构优化:MySQL主数据库+SQLite备份系统,工单详情API修复,备份管理功能" + }, { "version": "1.2.0", "date": "2025-09-16T16:30:00",