diff --git a/2_1banben/routes/api.py b/2_1banben/routes/api.py index c878668..d005c18 100644 --- a/2_1banben/routes/api.py +++ b/2_1banben/routes/api.py @@ -7,14 +7,13 @@ from sqlalchemy import desc, or_ from extensions import db from models import Device, DeviceHistory, MaintenanceLog -# --- 导入服务模块 --- +# --- 尝试导入服务模块 --- try: from services.core import execute_monitor_task except ImportError: execute_monitor_task = None try: - # 导入 IoT 服务 from services.iot_api import sync_iot_data_service except ImportError: sync_iot_data_service = None @@ -22,14 +21,18 @@ except ImportError: api_bp = Blueprint('api', __name__, url_prefix='/api') -# ======================= -# 0. 辅助函数区 -# ======================= +# ========================================================= +# 0. 核心算法区:数据质量分析与辅助函数 +# ========================================================= def calculate_offset(latest_time_str): - """计算时间滞后天数""" - if not latest_time_str or latest_time_str == "N/A": return "从未同步" + """ + 计算时间滞后天数 + """ + if not latest_time_str or latest_time_str == "N/A": + return "从未同步" try: + # 兼容处理 2026_01_13 和 2026-01-13 格式 clean = str(latest_time_str).split()[0].replace('_', '-') target = datetime.strptime(clean, "%Y-%m-%d").date() diff = (datetime.now().date() - target).days @@ -39,37 +42,136 @@ def calculate_offset(latest_time_str): def check_data_quality(content_data, source_type, data_time_str=None): - """数据质量分析算法 (只针对爬虫数据)""" - if not content_data: return 'ok' - if str(source_type) == 'iot_card': return 'ok' + """ + 数据质量分析算法 (融合版:旧版核心规则 + 新版夜间/IoT过滤) + """ + if not content_data: + return 'ok' - # 夜间免打扰 + # 1. [新功能] IoT 卡不需要检查数据质量 + if str(source_type) == 'iot_card': + return 'ok' + + # 2. [新功能] 夜间免打扰逻辑 (08:00 - 17:00 之外不报错) + # 物理规律:晚上没有太阳,光谱仪数值低是正常的,不应报错 if data_time_str and data_time_str != 'N/A': try: clean_time = str(data_time_str).replace('_', '-') - dt = datetime.strptime(clean_time, "%Y-%m-%d %H:%M:%S") - if dt.hour < 8 or dt.hour >= 17: return 'ok' + dt = None + try: + dt = datetime.strptime(clean_time, "%Y-%m-%d %H:%M:%S") + except: + try: + dt = datetime.strptime(clean_time, "%Y-%m-%d %H:%M") + except: + pass + + # 如果解析成功,且不在 8点-17点之间,视为夜晚,直接返回 ok + if dt and (dt.hour < 8 or dt.hour >= 17): + return 'ok' except: pass + # 3. [旧版核心] 数据异常判断逻辑 status = 'ok' - if '106' in str(source_type): + source_str = str(source_type) + + # --- Type A: 106 设备逻辑 (CSV格式) --- + if '106' in source_str: try: - text = content_data.get('content', str(content_data)) - if 'OSIFBeta' in text: - # 保留原有的复杂波形判断逻辑 - pass - except: - pass + # 兼容处理:如果 content_data 是字典,尝试取 content 字段;如果是字符串直接用 + text_content = "" + if isinstance(content_data, dict): + text_content = content_data.get('content', str(content_data)) + else: + text_content = str(content_data) + + # 只要包含 OSIFBeta 就进行解析 + if 'OSIFBeta' in text_content: + lines = text_content.split('\n') if '\n' in text_content else [text_content] + + for line in lines: + if 'OSIFBeta' not in line: + continue + + parts = line.split(',') + # 简单校验列数 + if len(parts) < 10: + continue + + # 检查积分时间 (Index 2) + try: + int_time = int(parts[2]) + # 旧代码逻辑:只有积分时间饱和 (>= 66534) 才检查数值 + if int_time >= 66534: + # 数据点通常从第4个(Index 3)开始 + data_points = [] + for p in parts[3:]: + try: + data_points.append(float(p)) + except: + pass + + if not data_points: + continue + + # 规则A:红色报错 (存在 < 100 的点) + for val in data_points: + if val < 100: + return 'error' + + # 规则B:黄色警告 (连续 5 个点在 100-500 之间) + consecutive_warning = 0 + for val in data_points: + if 100 <= val <= 500: + consecutive_warning += 1 + if consecutive_warning >= 5: + status = 'warning' + # 注意:不立即返回,继续检查后面是否有 error + else: + consecutive_warning = 0 + except: + continue + except Exception: + return 'ok' + + # --- Type B: 82 设备逻辑 (JSON格式) --- + else: + try: + # 82 设备 content_data 应该已经是字典 + if not isinstance(content_data, dict): + return 'ok' + + specs = content_data.get('downspec', []) + if not specs: + specs = content_data.get('upspec', []) + + if specs and isinstance(specs, list): + consecutive_low = 0 + for val in specs: + # 确保 val 是数字 + if not isinstance(val, (int, float)): + continue + + # 旧代码逻辑: 连续2点 < 500 -> error + if val < 500: + consecutive_low += 1 + if consecutive_low >= 2: + return 'error' + else: + consecutive_low = 0 + return 'ok' + except Exception: + return 'ok' + return status def save_iot_cards_to_db(card_list): """ - [逻辑重构] IoT SIM卡数据入库 - 1. 目标:只维护 source='iot_card' 的记录。 - 2. 策略:实时更新 (Update),不写历史 (No History)。 - 3. 格式:支持字母+数字的 ICCID。 + [新功能] IoT数据入库逻辑 + 1. 只操作 source='iot_card' 的记录。 + 2. 必须保留 is_whitelist 状态,防止被自动同步覆盖。 """ if not card_list: return 0, None update_count = 0 @@ -79,36 +181,37 @@ def save_iot_cards_to_db(card_list): iccid = card.get('iccid') if not iccid: continue - # 1. 查找 'SIM卡记录' (source 固定为 iot_card) - # name 字段直接存储 ICCID (无论是否包含字母) + # 1. 查找是否存在 sim_record = Device.query.filter_by(name=iccid, source='iot_card').first() + current_whitelist = False if not sim_record: - # 如果是新卡,创建一条记录 - # install_site 默认为 IoT库,不干扰主设备的地点 + # 插入新数据 sim_record = Device(name=iccid, source='iot_card', install_site="IoT库") db.session.add(sim_record) db.session.flush() + else: + # 旧卡:读取并保留旧的白名单设置 + try: + old_json = json.loads(sim_record.json_data) + current_whitelist = old_json.get('is_whitelist', False) + except: + current_whitelist = False - # 2. 仅更新实时状态 (Real-time update) + # 2. 更新字段 sim_record.status = str(card.get('cardStatus', '')) - # 构造数据包 card_data = { "iccid": iccid, "usedTraffic": str(card.get('usedTraffic') or '0'), "stopDate": card.get('stopDate', 'N/A'), "cardStatus": card.get('cardStatus'), - "tag": card.get('tag', '') + "tag": card.get('tag', ''), + "is_whitelist": current_whitelist # 写回保留的状态 } - # 更新 JSON 数据和检查时间 sim_record.json_data = json.dumps(card_data, ensure_ascii=False) sim_record.check_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - - # [关键]:这里不更新 latest_time,也不写入 DeviceHistory 表 - # 确保了 "IoT数据只实时更新,不留历史" 的需求 - update_count += 1 return update_count, None @@ -116,9 +219,10 @@ def save_iot_cards_to_db(card_list): return 0, str(e) -# ======================= -# 1. 认证接口 -# ======================= +# ========================================================= +# 1. 基础接口 (认证 & 概览) +# ========================================================= + @api_bp.route('/login', methods=['POST']) def login(): data = request.get_json() @@ -135,14 +239,10 @@ def login(): return jsonify({'code': 401, 'message': '用户名或密码错误'}), 401 -# ======================= -# 2. 设备概览 (逻辑聚合) -# ======================= @api_bp.route('/devices_overview', methods=['GET']) def devices_overview(): try: - # A. 获取 'IoT卡表' 数据 (source='iot_card') - # 构建字典 { ICCID: {data} },作为缓存供主设备查询 + # A. 获取 IoT卡表 (source='iot_card') iot_records = Device.query.filter_by(source='iot_card').all() iot_map = {} for rec in iot_records: @@ -152,14 +252,12 @@ def devices_overview(): except: pass - # B. 获取 '爬虫设备表' (source != 'iot_card') - # 这些才是 Dashboard 主列表展示的设备 + # B. 获取 真实设备 (source != 'iot_card') devices = Device.query.filter(Device.source != 'iot_card').all() - data_list = [] + for d in devices: item = d.to_dict() - parsed_content = {} if d.json_data: try: @@ -167,30 +265,39 @@ def devices_overview(): except: pass - # --- 关键逻辑:关联 --- - # 通过 bound_iccid 字段,将设备与 IoT 卡数据关联 + # --- 绑定逻辑 (将IoT卡信息注入到设备) --- bound_iccid = parsed_content.get('bound_iccid') item['usedTraffic'] = None item['stopDate'] = None item['isBound'] = False + item['bound_iccid'] = bound_iccid + item['is_whitelist'] = False # 默认无白名单 - # 如果绑定了 ICCID,且该 ICCID 存在于 IoT 表中 + # 如果有绑定,且卡片存在,则注入卡片信息 if bound_iccid and bound_iccid in iot_map: card_info = iot_map[bound_iccid] item['usedTraffic'] = card_info.get('usedTraffic') item['stopDate'] = card_info.get('stopDate') + item['is_whitelist'] = card_info.get('is_whitelist', False) item['isBound'] = True - # 质量检测只针对爬虫数据 + # [关键] 调用异常检测函数 (check_data_quality) item['data_quality'] = check_data_quality(parsed_content, d.source, d.latest_time) data_list.append(item) - # C. 将 'IoT卡表' 的数据也传给前端 (用于绑定弹窗) - # 前端通过 isOrphanIoT 过滤,主列表不显示,但在绑定列表中显示 + # C. 必须把 IoT卡表 的数据也传给前端 (用于计算总流量 & 绑定弹窗) for rec in iot_records: item = rec.to_dict() + try: + j = json.loads(rec.json_data) + except: + j = {} + + item['usedTraffic'] = j.get('usedTraffic', '0') + item['stopDate'] = j.get('stopDate', '') + item['is_whitelist'] = j.get('is_whitelist', False) item['isOrphanIoT'] = True item['source'] = 'iot_card' data_list.append(item) @@ -200,137 +307,129 @@ def devices_overview(): return jsonify({'code': 500, 'message': str(e)}) -# ======================= -# 3. 日志接口 (保持原有功能) -# ======================= -@api_bp.route('/logs/list', methods=['GET']) -def get_logs(): - keyword = request.args.get('keyword', '') - start_date = request.args.get('start_date') - end_date = request.args.get('end_date') - query = MaintenanceLog.query - if keyword: - kw = f"%{keyword}%" - query = query.filter(or_( - MaintenanceLog.device_name.like(kw), - MaintenanceLog.engineer.like(kw), - MaintenanceLog.location.like(kw), - MaintenanceLog.content.like(kw) - )) - if start_date and end_date: - try: - start_dt = datetime.strptime(start_date, '%Y-%m-%d') - end_dt = datetime.strptime(end_date, '%Y-%m-%d').replace(hour=23, minute=59, second=59) - query = query.filter(MaintenanceLog.timestamp.between(start_dt, end_dt)) - except ValueError: - pass - logs = query.order_by(MaintenanceLog.timestamp.desc()).all() - return jsonify({'code': 200, 'data': [l.to_dict() for l in logs]}) +# ========================================================= +# 2. 历史数据接口 +# ========================================================= + +@api_bp.route('/device_data_by_date', methods=['GET']) +def device_data_by_date(): + name = request.args.get('name') + date_str = request.args.get('date') # 格式: 2026_01_13 或 2026-01-13 + + if not name or not date_str: + return jsonify({'code': 400, 'message': 'Missing name or date'}), 400 + + device = Device.query.filter_by(name=name).first() + if not device: + return jsonify({'code': 404, 'message': 'Device not found'}), 404 + + content = None + # 统一将下划线格式转换为横杠格式进行查询 + query_date = date_str.replace('_', '-') + + # 1. 尝试从历史记录表中查找 + history_record = DeviceHistory.query.filter( + DeviceHistory.device_id == device.id, + DeviceHistory.data_time.like(f"{query_date}%") + ).order_by(desc(DeviceHistory.id)).first() + + if history_record: + content = history_record.json_data + # 2. 如果历史表中没有,查当前 Device 表 + elif device.latest_time and device.latest_time.startswith(query_date): + content = device.json_data + + if content: + return jsonify({ + 'code': 200, + 'name': device.name, + 'source': device.source, + 'content': content + }) + return jsonify({'code': 404, 'message': 'No data for this date'}), 404 -@api_bp.route('/logs/add', methods=['POST']) -def add_log(): - data = request.get_json() - try: - new_log = MaintenanceLog( - device_name=data.get('device_name', '未知设备'), - engineer=data.get('engineer', ''), - location=data.get('location', ''), - content=data.get('content', '') - ) - db.session.add(new_log) - db.session.commit() - return jsonify({'code': 200, 'message': 'Log saved'}) - except Exception as e: - db.session.rollback() - return jsonify({'code': 500, 'message': str(e)}) +# 兼容旧调用的 stub +@api_bp.route('/device_data_by_date_stub', methods=['GET']) +def device_data_by_date_stub(): + return device_data_by_date() -@api_bp.route('/logs/update', methods=['POST']) -def update_log(): - data = request.get_json() - log = MaintenanceLog.query.get(data.get('id')) - if not log: return jsonify({'code': 404, 'message': 'Not found'}), 404 - try: - log.device_name = data.get('device_name', log.device_name) - log.engineer = data.get('engineer', log.engineer) - log.location = data.get('location', log.location) - log.content = data.get('content', log.content) - db.session.commit() - return jsonify({'code': 200, 'message': 'Log updated'}) - except Exception as e: - db.session.rollback() - return jsonify({'code': 500, 'message': str(e)}) +# ========================================================= +# 3. 核心控制接口 (检测 & 写入) +# ========================================================= - -@api_bp.route('/logs/delete', methods=['POST']) -def delete_log(): - data = request.get_json() - log = MaintenanceLog.query.get(data.get('id')) - if log: - db.session.delete(log) - db.session.commit() - return jsonify({'code': 200, 'message': 'Deleted'}) - return jsonify({'code': 404, 'message': 'Not found'}), 404 - - -# ======================= -# 4. 一键检测 (双路并行,逻辑隔离) -# ======================= @api_bp.route('/run_monitor', methods=['POST']) def run_monitor(): msg_list = [] try: - # A. 爬虫任务 (写入历史) - # 这部分负责更新 106/82 设备,并记录 DeviceHistory + # --- A. 执行爬虫并入库 --- if execute_monitor_task: task_result = execute_monitor_task() if task_result: scraped_list = task_result.get('device_list', []) current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - count_crawler = 0 + count_crawler = 0 for item in scraped_list: d_name = item.get('name') if not d_name: continue + # 提取原始信息 + d_raw = item.get('raw_json', {}) + source = item.get('source', '') + target_time = item.get('target_time') # 默认时间 + + # [旧代码逻辑保留] 针对 106 设备,从路径中强制解析正确的时间格式 + if '106' in str(source): + try: + path_str = d_raw.get('path', '') + # 匹配形如 /Data/2026_01_13/xxx_15_30_00.csv + match = re.search(r'/Data/(\d{4}_\d{2}_\d{2})/\w+_(\d{2}_\d{2}_\d{2})\.csv', path_str) + if match: + date_part = match.group(1).replace('_', '-') # 2026-01-13 + time_part = match.group(2).replace('_', ':') # 15:30:00 + target_time = f"{date_part} {time_part}" + except: + pass + + # 查找或创建设备 device = Device.query.filter_by(name=d_name).first() if not device: - device = Device(name=d_name, source=item.get('source'), install_site="") + device = Device(name=d_name, source=source, install_site="") db.session.add(device) db.session.flush() - # 纠正 source (防止爬虫设备被误标为 iot_card) if device.source == 'iot_card': - device.source = item.get('source') + device.source = source - # 更新主数据 + # 更新字段 device.status = item.get('status') device.current_value = item.get('value') - device.latest_time = item.get('target_time') + device.latest_time = target_time device.check_time = current_time - # 更新 JSON + # [新代码逻辑] 合并模式 (update),防止覆盖掉 bound_iccid old_json = {} try: old_json = json.loads(device.json_data) except: pass - new_json = item.get('raw_json', {}) + new_json = d_raw if isinstance(d_raw, dict) else item.get('raw_json', {}) if isinstance(new_json, dict): old_json.update(new_json) device.json_data = json.dumps(old_json, ensure_ascii=False) device.offset = calculate_offset(device.latest_time) - # 写入历史 (History Table) + # 写入历史记录 new_history = DeviceHistory( device_id=device.id, status=item.get('status'), result_data=item.get('value'), - data_time=item.get('target_time'), + data_time=target_time, json_data=device.json_data ) db.session.add(new_history) @@ -340,15 +439,14 @@ def run_monitor(): else: msg_list.append("爬虫无数据") - # B. IoT 任务 (只更新 IoT 实时状态) - # 这部分只更新 source='iot_card' 的记录,不产生历史 + # --- B. 执行 IoT 同步 (写入数据库) --- if sync_iot_data_service: iot_list = sync_iot_data_service() c, e = save_iot_cards_to_db(iot_list) if e: msg_list.append(f"IoT错: {e}") else: - msg_list.append(f"IoT实时更新: {c}") + msg_list.append(f"IoT更新: {c}") db.session.commit() return jsonify({'code': 200, 'message': " | ".join(msg_list)}) @@ -358,15 +456,34 @@ def run_monitor(): return jsonify({'code': 500, 'message': str(e)}) -# ======================= -# 5. 绑定与其他接口 -# ======================= +# ========================================================= +# 4. 白名单、绑定与设备管理 (新功能) +# ========================================================= + +@api_bp.route('/toggle_whitelist', methods=['POST']) +def toggle_whitelist(): + data = request.get_json() + iccid = data.get('iccid') + is_whitelist = data.get('is_whitelist') + + sim_record = Device.query.filter_by(name=iccid, source='iot_card').first() + if not sim_record: + return jsonify({'code': 404, 'message': '未找到该卡片'}) + + try: + j = json.loads(sim_record.json_data) + j['is_whitelist'] = is_whitelist + sim_record.json_data = json.dumps(j, ensure_ascii=False) + db.session.commit() + return jsonify({'code': 200, 'message': '设置成功'}) + except Exception as e: + db.session.rollback() + return jsonify({'code': 500, 'message': str(e)}) + @api_bp.route('/sync_iot_cards', methods=['POST']) def sync_iot_cards(): - """单独同步 IoT (只更新 IoT 表)""" - if not sync_iot_data_service: - return jsonify({'code': 500, 'message': '服务缺失'}), 500 + if not sync_iot_data_service: return jsonify({'code': 500, 'message': '服务缺失'}), 500 try: iot_list = sync_iot_data_service() c, e = save_iot_cards_to_db(iot_list) @@ -379,32 +496,20 @@ def sync_iot_cards(): @api_bp.route('/bind_device_card', methods=['POST']) def bind_device_card(): - """将 ICCID 绑定到 设备""" data = request.get_json() - device_name = data.get('device_name') - iccid = data.get('iccid') - - target = Device.query.filter_by(name=device_name).first() + target = Device.query.filter_by(name=data.get('device_name')).first() if not target: return jsonify({'code': 404, 'message': '找不到设备'}) - - # 检查 ICCID 是否在 IoT 库 (支持字母) - sim = Device.query.filter_by(name=iccid, source='iot_card').first() - if not sim: return jsonify({'code': 404, 'message': 'IoT库中无此卡号'}) - try: d_json = {} try: d_json = json.loads(target.json_data) except: pass - - d_json['bound_iccid'] = iccid + d_json['bound_iccid'] = data.get('iccid') target.json_data = json.dumps(d_json, ensure_ascii=False) - db.session.commit() return jsonify({'code': 200, 'message': '绑定成功'}) except Exception as e: - db.session.rollback() return jsonify({'code': 500, 'message': str(e)}) @@ -417,11 +522,10 @@ def add_device(): install_site=data.get('site', ''), source='manual', status='offline', - current_value='0', - latest_time='N/A', check_time=datetime.now().strftime("%Y-%m-%d %H:%M:%S"), json_data='{}', - is_hidden=0, is_maintaining=0 + is_hidden=0, + is_maintaining=0 ) db.session.add(new_device) db.session.commit() @@ -437,7 +541,7 @@ def update_site(): d.install_site = request.json.get('site') db.session.commit() return jsonify({'code': 200}) - return jsonify({'code': 404}), 404 + return jsonify({'code': 404}) @api_bp.route('/toggle_maintenance', methods=['POST']) @@ -447,7 +551,7 @@ def toggle_maintenance(): d.is_maintaining = request.json.get('is_maintaining') db.session.commit() return jsonify({'code': 200}) - return jsonify({'code': 404}), 404 + return jsonify({'code': 404}) @api_bp.route('/toggle_hidden', methods=['POST']) @@ -457,9 +561,67 @@ def toggle_hidden(): d.is_hidden = request.json.get('is_hidden') db.session.commit() return jsonify({'code': 200}) - return jsonify({'code': 404}), 404 + return jsonify({'code': 404}) -@api_bp.route('/device_data_by_date', methods=['GET']) -def device_data_by_date_stub(): - return device_data_by_date() \ No newline at end of file +# ========================================================= +# 5. 日志管理接口 (CRUD) +# ========================================================= + +@api_bp.route('/logs/list', methods=['GET']) +def get_logs_list(): + keyword = request.args.get('keyword', '') + query = MaintenanceLog.query + if keyword: + kw = f"%{keyword}%" + query = query.filter(or_( + MaintenanceLog.device_name.like(kw), + MaintenanceLog.content.like(kw), + MaintenanceLog.engineer.like(kw) + )) + logs = query.order_by(MaintenanceLog.timestamp.desc()).all() + return jsonify({'code': 200, 'data': [l.to_dict() for l in logs]}) + + +@api_bp.route('/logs/add', methods=['POST']) +def add_log_entry(): + data = request.get_json() + try: + new_log = MaintenanceLog( + device_name=data.get('device_name', ''), + engineer=data.get('engineer', ''), + location=data.get('location', ''), + content=data.get('content', '') + ) + db.session.add(new_log) + db.session.commit() + return jsonify({'code': 200}) + except Exception as e: + return jsonify({'code': 500, 'message': str(e)}) + + +@api_bp.route('/logs/update', methods=['POST']) +def update_log_entry(): + data = request.get_json() + log = MaintenanceLog.query.get(data.get('id')) + if not log: return jsonify({'code': 404}) + try: + log.device_name = data.get('device_name', log.device_name) + log.content = data.get('content', log.content) + log.engineer = data.get('engineer', log.engineer) + log.location = data.get('location', log.location) + db.session.commit() + return jsonify({'code': 200}) + except Exception as e: + return jsonify({'code': 500}) + + +@api_bp.route('/logs/delete', methods=['POST']) +def delete_log_entry(): + data = request.get_json() + log = MaintenanceLog.query.get(data.get('id')) + if log: + db.session.delete(log) + db.session.commit() + return jsonify({'code': 200}) + return jsonify({'code': 404}) \ No newline at end of file diff --git a/zhandianxinxi/光谱数据监控/src/views/Dashboard.vue b/zhandianxinxi/光谱数据监控/src/views/Dashboard.vue index d21e67d..27e03ee 100644 --- a/zhandianxinxi/光谱数据监控/src/views/Dashboard.vue +++ b/zhandianxinxi/光谱数据监控/src/views/Dashboard.vue @@ -27,9 +27,9 @@