import os import shutil import json # 👈 必需:用于序列化原始数据 from flask import Blueprint, jsonify, request from datetime import datetime from models import db, Device, DeviceHistory, MaintenanceLog try: from services.core import execute_monitor_task except ImportError: execute_monitor_task = None api_bp = Blueprint('api', __name__, url_prefix='/api') # ========================================================================= # 常规接口 (隐藏、总览、地点、维修) # ========================================================================= @api_bp.route('/toggle_hidden', methods=['POST']) def toggle_hidden(): try: data = request.json device = Device.query.filter_by(name=data.get('name')).first() if device: device.is_hidden = data.get('is_hidden', False) db.session.commit() return jsonify({'message': '状态更新成功'}), 200 return jsonify({'error': '设备不存在'}), 404 except Exception as e: db.session.rollback() return jsonify({'error': str(e)}), 500 @api_bp.route('/devices_overview', methods=['GET']) def devices_overview(): try: # 直接读取 Device 表快照 devices = Device.query.all() return jsonify({'data': [d.to_dict() for d in devices]}) except Exception as e: return jsonify({'error': str(e)}), 500 @api_bp.route('/update_site', methods=['POST']) def update_site(): try: data = request.json record = Device.query.filter_by(name=data.get('name')).first() if record: record.install_site = data.get('site') db.session.commit() return jsonify({'code': 200, 'message': '更新成功'}) return jsonify({'code': 404, 'message': '设备不存在'}) except Exception as e: return jsonify({'code': 500, 'message': str(e)}) @api_bp.route('/toggle_maintenance', methods=['POST']) def toggle_maintenance(): try: data = request.json record = Device.query.filter_by(name=data.get('name')).first() if record: record.is_maintaining = data.get('is_maintaining', False) db.session.commit() return jsonify({'code': 200, 'message': '状态更新成功'}) return jsonify({'code': 404, 'message': '设备不存在'}) except Exception as e: return jsonify({'code': 500, 'message': str(e)}) @api_bp.route('/logs/add', methods=['POST']) def add_log(): try: data = request.json new_log = MaintenanceLog(device_name=data.get('device_name'), content=data.get('content')) db.session.add(new_log) db.session.commit() return jsonify({'code': 200, 'message': '日志已保存'}) except Exception as e: return jsonify({'code': 500, 'message': str(e)}) # ========================================================================= # 🔥 核心功能区 # ========================================================================= @api_bp.route('/device_history', methods=['GET']) def get_device_history(): """获取单个设备的历史,包含原始 JSON 数据""" try: name = request.args.get('name') if not name: return jsonify({'error': 'Missing name parameter'}), 400 device = Device.query.filter_by(name=name).first() if not device: return jsonify({'error': 'Device not found'}), 404 history = DeviceHistory.query.filter_by(device_id=device.id) \ .order_by(DeviceHistory.recorded_at.desc()) \ .limit(100).all() history_data = [] for h in history: rec_time = h.recorded_at.strftime('%Y-%m-%d %H:%M:%S') if h.recorded_at else 'N/A' # 🔥 将数据库里存的 JSON 字符串转回对象,发给前端 raw_obj = None if h.json_data: try: raw_obj = json.loads(h.json_data) except: raw_obj = h.json_data history_data.append({ 'data_time': h.data_time, 'status': h.status, 'value': h.result_data, 'raw_data': raw_obj, # 🔥 前端可查看详细原始数据 'file_path': h.file_path, 'recorded_at': rec_time }) return jsonify({ 'device': device.name, 'history': history_data }) except Exception as e: return jsonify({'error': str(e)}), 500 @api_bp.route('/run_monitor', methods=['POST']) def run_monitor(): """ 🔥 真实爬虫逻辑: 1. 归档文件 2. 存入 Device 表(更新快照,若设备消失则保留旧快照) 3. 存入 DeviceHistory 表(追加历史,保存 Raw JSON) """ try: print(">>> 启动真实监测任务...") if not execute_monitor_task: return jsonify({'code': 500, 'message': 'execute_monitor_task missing'}) # 1. 准备目录 base_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) binary_dir = os.path.join(base_dir, 'instance', 'binary') if not os.path.exists(binary_dir): os.makedirs(binary_dir) # 2. 调用爬虫 task_result = execute_monitor_task() if not task_result: return jsonify({'code': 500, 'message': '爬虫未返回数据'}) scraped_data_list = task_result.get('device_list', []) target_time_str = task_result.get('target_time', datetime.now().strftime("%Y-%m-%d %H:%M:%S")) temp_file_path = task_result.get('temp_file_path', None) print(f"✅ 获取到 {len(scraped_data_list)} 条数据,时间: {target_time_str}") # 3. 文件归档 db_rel_path = "" safe_filename = "data_unknown.db" if temp_file_path and os.path.exists(temp_file_path): safe_filename = target_time_str.replace(' ', '_').replace(':', '-') + ".db" final_file_path = os.path.join(binary_dir, safe_filename) shutil.move(temp_file_path, final_file_path) db_rel_path = f"binary/{safe_filename}" print(f"✅ 文件已归档: {final_file_path}") # 4. 数据库写入 (双表写入) for item in scraped_data_list: d_name = item.get('name') if not d_name: continue d_status = item.get('status', 'unknown') d_value = item.get('value', '') d_site = item.get('site', '') # 🔥 序列化:把整个字典转成 JSON 字符串 # ensure_ascii=False 确保中文可以正常显示,而不是 \uXXXX raw_json_str = json.dumps(item, ensure_ascii=False) # ----------------------------------------------------------- # 表 A: Device (快照) # 逻辑:如果设备存在,就更新它的“最新状态”;如果不存在,就新建。 # 关键点:如果爬虫这次没爬到“设备X”,这里就不会执行,“设备X”的数据就会保持在上次的状态。 # 这就完美解决了“网页上消失但我要展示”的需求。 # ----------------------------------------------------------- device = Device.query.filter_by(name=d_name).first() if not device: device = Device(name=d_name, source='Auto') db.session.add(device) db.session.flush() # 拿 ID device.status = d_status device.current_value = d_value device.latest_time = target_time_str # 数据的产生时间 device.check_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") # 系统检查时间 device.json_data = raw_json_str # 🔥 更新快照里的原始数据 if d_site: device.install_site = d_site # ----------------------------------------------------------- # 表 B: DeviceHistory (日志) # 逻辑:不管有没有,永远追加一条新记录。 # ----------------------------------------------------------- new_history = DeviceHistory( device_id=device.id, status=d_status, result_data=d_value, data_time=target_time_str, json_data=raw_json_str, # 🔥 存入历史原始数据 file_path=db_rel_path ) db.session.add(new_history) db.session.commit() return jsonify({ 'code': 200, 'message': f'检测完成,已归档 {safe_filename},更新 {len(scraped_data_list)} 台设备' }) except Exception as e: db.session.rollback() print(f"Monitor Error: {e}") return jsonify({'code': 500, 'message': str(e)})