233 lines
8.7 KiB
Python
233 lines
8.7 KiB
Python
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)}) |