614 lines
20 KiB
Python
614 lines
20 KiB
Python
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/<int:id>', 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/<int:fault_id>', 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/<int:id>', 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() |