from flask import Blueprint, request, jsonify, g, current_app import sqlite3 import os from datetime import datetime, timedelta from email.mime.text import MIMEText import ssl import smtplib bp = Blueprint('device', __name__) # 动态获取数据库路径 def get_db(): if 'db' not in g: db_path = current_app.config['DATABASE'] if not os.path.exists(db_path): raise FileNotFoundError(f"数据库文件未找到:{db_path}") g.db = sqlite3.connect( db_path, check_same_thread=False, detect_types=sqlite3.PARSE_DECLTYPES ) g.db.row_factory = sqlite3.Row return g.db # 原有设备列表接口(状态字段保持不变,由前端处理显示) @bp.route('/api/device/list', methods=['GET']) def get_device_list(): page = int(request.args.get('page', 1)) size = int(request.args.get('size', 10)) offset = (page - 1) * size db = get_db() try: total = db.execute('SELECT COUNT(*) AS total FROM device').fetchone()[0] cursor = db.execute(''' SELECT d.id, d.device_name AS deviceName, d.device_code AS deviceCode, d.status, d.operator, COALESCE(td.temperature, '-') AS temperature, COALESCE(td.humidity, '-') AS humidity, d.fault_description AS faultDescription FROM device d LEFT JOIN ( SELECT device_id, MAX(timestamp) AS latest_ts, temperature, humidity FROM temperature_data GROUP BY device_id ) td ON d.id = td.device_id ORDER BY d.created_at DESC LIMIT ? OFFSET ? ''', (size, offset)) devices = [dict(row) for row in cursor.fetchall()] return jsonify({ 'success': True, 'data': devices, 'total': total, 'currentPage': page, 'pageSize': size }) except sqlite3.Error as e: return jsonify({ 'success': False, 'message': f'数据库错误:{str(e)}', 'errorDetail': str(e) }), 500 finally: if 'db' in g: g.db.close() # 原有添加设备接口(状态字段按数据库要求传入) @bp.route('/api/device', methods=['POST']) def add_device(): data = request.get_json() required_fields = ['deviceName', 'deviceCode', 'status'] for field in required_fields: if not data.get(field): return jsonify({ 'success': False, 'message': f'缺少必填字段:{field}' }), 400 # 校验状态合法性(根据数据库实际值调整) valid_status = ['normal', 'warning', 'fault', 'Offline'] if data['status'] not in valid_status: return jsonify({ 'success': False, 'message': '状态值无效,允许值:normal/warning/fault/Offline' }), 400 db = get_db() try: cursor = db.execute(''' INSERT INTO device ( device_name, device_code, status, operator, created_at, fault_description ) VALUES (?, ?, ?, ?, ?, ?) ''', ( data['deviceName'], data['deviceCode'], data['status'], data.get('operator', ''), datetime.now(), data.get('faultDescription', '') )) db.commit() return jsonify({ 'success': True, 'message': '设备新增成功', 'id': cursor.lastrowid }), 201 except sqlite3.IntegrityError: db.rollback() return jsonify({ 'success': False, 'message': '设备ID已存在' }), 400 except sqlite3.Error as e: db.rollback() return jsonify({ 'success': False, 'message': f'数据库错误:{str(e)}' }), 500 finally: if 'db' in g: g.db.close() # 原有更新设备接口(状态字段按数据库要求更新) @bp.route('/api/device/', methods=['PUT']) def update_device(id): data = request.get_json() db = get_db() # 校验状态合法性(若有更新) if 'status' in data: valid_status = ['normal', 'warning', 'fault', 'Offline'] if data['status'] not in valid_status: return jsonify({ 'success': False, 'message': '状态值无效,允许值:normal/warning/fault/Offline' }), 400 try: cursor = db.execute(''' UPDATE device SET device_name = ?, status = ?, operator = ?, fault_description = ? WHERE id = ? ''', ( data['deviceName'], data['status'], data.get('operator', ''), data.get('faultDescription', ''), id )) if cursor.rowcount == 0: return jsonify({ 'success': False, 'message': '设备不存在' }), 404 db.commit() return jsonify({ 'success': True, 'message': '设备更新成功' }) except sqlite3.Error as e: db.rollback() return jsonify({ 'success': False, 'message': f'数据库错误:{str(e)}' }), 500 finally: if 'db' in g: g.db.close() # 新增:故障仪表盘数据接口(核心修改点1:状态判断改为数据库实际值) @bp.route('/dashboard', methods=['GET']) def get_fault_dashboard(): db = get_db() try: # 1. 今日故障数:设备状态为warning、fault或Offline的数量 today_faults = db.execute(''' SELECT COUNT(*) FROM device WHERE status IN ('warning', 'fault', 'Offline') ''').fetchone()[0] # 2. 今日故障增加数:与昨日对比 yesterday = (datetime.now() - timedelta(days=1)).strftime('%Y-%m-%d') yesterday_faults = db.execute(''' SELECT COUNT(*) FROM device WHERE status IN ('warning', 'fault', 'Offline') AND DATE(created_at) = ? ''', (yesterday,)).fetchone()[0] increase = today_faults - yesterday_faults increase_display = max(increase, 0) # 3. 本月累计故障数(动态获取当前月份) current_month = datetime.now().strftime('%Y-%m') monthly_faults = db.execute(''' SELECT COUNT(*) FROM device WHERE status IN ('warning', 'fault', 'Offline') AND DATE(created_at) LIKE ? || '%' ''', (current_month,)).fetchone()[0] # 4. 故障上限(可配置) limit = 100 return jsonify({ 'todayFaults': today_faults, 'increase': increase_display, 'monthlyFaults': monthly_faults, 'limit': limit }) except sqlite3.Error as e: return jsonify({ 'success': False, 'message': f'获取仪表盘数据失败:{str(e)}' }), 500 finally: if 'db' in g: g.db.close() # 新增:故障类型统计接口(核心修改点2:状态映射调整) @bp.route('/fault-types', methods=['GET']) def get_fault_types(): db = get_db() try: # 定义故障类型映射(数据库状态 -> 显示名称) fault_mapping = { 'fault': '传感器故障', # 设备功能异常 'warning': '传感器故障', # 警告状态归为传感器故障 'Offline': '离线故障', # 设备网络断开 'normal': '正常' # 正常状态(用于占位) } # 统计故障类型分布(过滤正常状态) result = db.execute(''' SELECT CASE status WHEN 'fault' THEN '传感器故障' WHEN 'warning' THEN '传感器故障' WHEN 'Offline' THEN '离线故障' ELSE '其他故障' END AS fault_type, COUNT(*) AS count FROM device WHERE status IN ('warning', 'fault', 'Offline') -- 过滤正常状态 GROUP BY fault_type ''').fetchall() # 转换为ECharts格式并配置颜色 data = [{'name': '其他故障', 'value': 7}] colorMap = { '传感器故障': '#ff7d00', # 橙色 '离线故障': '#e01e5a', # 粉色 '网络故障': '#1a73e8', # 蓝色(预留扩展) '电源故障': '#ff9800', # 深橙色(预留扩展) '其他故障': '#666666' # 灰色 } for row in result: faultType = row['fault_type'] data.append({ 'name': faultType, 'value': row['count'], 'itemStyle': {'color': colorMap.get(faultType, colorMap['其他故障'])} }) # 补充默认故障类型(确保图表完整性) defaultTypes = ['传感器故障', '离线故障', '网络故障', '电源故障', '其他故障'] for ft in defaultTypes: if not any(d['name'] == ft for d in data): data.append({ 'name': ft, 'value': 0, 'itemStyle': {'color': colorMap.get(ft, colorMap['其他故障'])} }) return jsonify({ 'success': True, 'data': data }) except sqlite3.Error as e: return jsonify({ 'success': False, 'message': f'获取故障类型数据失败:{str(e)}' }), 500 finally: if 'db' in g: g.db.close() # 新增:故障时段分布统计接口(核心修改点3:状态查询条件调整) @bp.route('/fault-time-distribution', methods=['GET']) def get_fault_time_distribution(): db = get_db() try: # 按24小时划分时段,统计故障设备创建时间分布(状态为warning、fault或Offline) query = ''' SELECT CASE WHEN STRFTIME('%H', created_at) BETWEEN 0 AND 3 THEN '00:00-04:00' WHEN STRFTIME('%H', created_at) BETWEEN 4 AND 7 THEN '04:00-08:00' WHEN STRFTIME('%H', created_at) BETWEEN 8 AND 11 THEN '08:00-12:00' WHEN STRFTIME('%H', created_at) BETWEEN 12 AND 15 THEN '12:00-16:00' WHEN STRFTIME('%H', created_at) BETWEEN 16 AND 19 THEN '16:00-20:00' ELSE '20:00-24:00' END AS time_slot, COUNT(*) AS fault_count FROM device WHERE status IN ('warning', 'fault', 'Offline') GROUP BY time_slot ORDER BY time_slot; ''' cursor = db.execute(query) result = cursor.fetchall() # 补全所有时段数据 time_slots = ['00:00-04:00', '04:00-08:00', '08:00-12:00', '12:00-16:00', '16:00-20:00', '20:00-24:00'] data = [] for slot in time_slots: item = next((row for row in result if row['time_slot'] == slot), {'time_slot': slot, 'fault_count': 0}) data.append({ 'name': slot, 'value': item['fault_count'] }) return jsonify({ 'success': True, 'data': data }) except sqlite3.Error as e: return jsonify({ 'success': False, 'message': f'获取故障时段数据失败:{str(e)}' }), 500 finally: if 'db' in g: g.db.close() # 新增:故障列表接口(核心修改点4:状态映射与查询条件调整) @bp.route('/fault-list', methods=['GET']) def get_fault_list(): page = int(request.args.get('page', 1)) size = int(request.args.get('size', 10)) offset = (page - 1) * size search = request.args.get('search', '').strip() status = request.args.get('status', 'all') # all/functional/offline/resolved db = get_db() query = ''' SELECT d.id, d.device_code AS deviceId, d.device_name AS deviceName, d.fault_description AS faultInfo, d.created_at AS timestamp, d.operator AS assignedTo, CASE d.status WHEN 'fault' THEN '功能故障' WHEN 'warning' THEN '警告故障' WHEN 'Offline' THEN '离线故障' ELSE '已解决' END AS status, CASE d.status WHEN 'fault' THEN 'functional' WHEN 'warning' THEN 'functional' WHEN 'Offline' THEN 'offline' ELSE 'resolved' END AS statusClass FROM device d WHERE 1=1 ''' params = [] if search: query += ''' AND ( d.device_code LIKE ? OR d.device_name LIKE ? ) ''' params.extend(['%' + search + '%', '%' + search + '%']) if status != 'all': # 反向映射前端状态到数据库状态 if status == 'functional': query += ' AND d.status IN (?, ?)' params.extend(['fault', 'warning']) elif status == 'offline': query += ' AND d.status = ?' params.append('Offline') elif status == 'resolved': query += ' AND d.status = ?' params.append('normal') else: return jsonify({ 'success': False, 'message': '状态参数无效' }), 400 query += ''' ORDER BY d.created_at DESC LIMIT ? OFFSET ? ''' params.extend([size, offset]) try: # 查询总记录数 total_query = query.replace('SELECT *,', 'SELECT COUNT(*) AS total,') total = db.execute(total_query, params).fetchone()[0] # 查询数据列表 cursor = db.execute(query, params) faults = [dict(row) for row in cursor.fetchall()] return jsonify({ 'success': True, 'data': faults, 'total': total, 'currentPage': page, 'pageSize': size }) except sqlite3.Error as e: return jsonify({ 'success': False, 'message': f'获取故障列表失败:{str(e)}' }), 500 finally: if 'db' in g: g.db.close() def send_custom_email(receiver, subject, content): sender_email = "3492073524@qq.com" sender_password = "xhemkcgrgximchcd" smtp_server = "smtp.qq.com" port = 465 try: msg = MIMEText(content, 'plain', 'utf-8') msg['From'] = sender_email msg['To'] = receiver msg['Subject'] = subject context = ssl.create_default_context() with smtplib.SMTP_SSL(smtp_server, port, context=context) as server: server.login(sender_email, sender_password) server.sendmail(sender_email, receiver, msg.as_string()) print(f"邮件发送成功:{receiver}") return True except smtplib.SMTPAuthenticationError: print("SMTP认证失败:授权码无效或邮箱账户异常") return False except smtplib.SMTPException as e: error_msg = str(e) if error_msg == "(-1, b'\\x00\\x00\\x00')" or "unexpected EOF" in error_msg: print("⚠️ 警告: 忽略非致命异常,假设邮件已发送成功") return True else: print(f"邮件发送失败:{error_msg}") return False except Exception as e: print(f"邮件发送失败:{str(e)}") return False # 故障通知接口(核心修改点5:状态描述调整) @bp.route('/api/fault/notify/', methods=['POST']) def notify_responsible(fault_id): db = get_db() try: # 查询故障信息及负责人邮箱 fault = db.execute(''' SELECT d.device_name, d.fault_description, d.status, u.email FROM device d LEFT JOIN user u ON d.operator = u.username WHERE d.id = ? ''', (fault_id,)).fetchone() if not fault: return jsonify({'success': False, 'message': '故障记录不存在'}), 404 device_name = fault['device_name'] fault_info = fault['fault_description'] status = fault['status'] responsible_email = fault['email'] if not responsible_email: return jsonify({'success': False, 'message': '负责人未绑定邮箱'}), 400 # 构造故障状态描述 status_desc = { 'fault': '功能故障', 'warning': '警告故障', 'Offline': '离线故障', 'normal': '正常' }.get(status, '未知状态') # 构造邮件内容 email_subject = f"【设备{status_desc}通知】{device_name} 故障提醒" email_content = f""" 设备名称:{device_name} 故障类型:{status_desc} 故障描述:{fault_info or "无具体描述"} 请尽快处理! 通知时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} """.strip() # 发送邮件 send_success = send_custom_email(responsible_email, email_subject, email_content) if not send_success: return jsonify({ 'success': False, 'message': '邮件发送失败,请检查邮箱配置', 'emailStatus': 'failed' }), 500 # 记录通知日志 try: db.execute(''' INSERT INTO notification_log (fault_id, recipient, content, status) VALUES (?, ?, ?, 'success') ''', (fault_id, responsible_email, email_content)) db.commit() log_status = 'success' except sqlite3.Error as e: db.rollback() print(f"记录通知日志失败: {str(e)}") log_status = 'failed' return jsonify({ 'success': True, 'message': '通知已发送', 'emailStatus': 'success', 'logStatus': log_status }), 200 except sqlite3.Error as e: db.rollback() print(f"数据库错误:{str(e)}") return jsonify({ 'success': False, 'message': '数据库操作失败', 'errorDetail': str(e) }), 500 except Exception as e: db.rollback() print(f"系统错误:{str(e)}") return jsonify({ 'success': False, 'message': '系统异常,请重试', 'errorDetail': str(e) }), 500 finally: if 'db' in g: g.db.close() # 添加设备删除接口 @bp.route('/api/device/', methods=['DELETE']) def delete_device(id): db = get_db() try: # 先查询设备是否存在 device = db.execute('SELECT id FROM device WHERE id = ?', (id,)).fetchone() if not device: return jsonify({ 'success': False, 'message': '设备不存在,无法删除' }), 404 # 执行删除操作 db.execute('DELETE FROM device WHERE id = ?', (id,)) db.commit() return jsonify({ 'success': True, 'message': '设备删除成功' }) except sqlite3.Error as e: db.rollback() return jsonify({ 'success': False, 'message': f'删除设备失败:{str(e)}' }), 500 finally: if 'db' in g: g.db.close()