diff --git a/2_1banben/routes/api.py b/2_1banben/routes/api.py index df489ac..d4fbcbf 100644 --- a/2_1banben/routes/api.py +++ b/2_1banben/routes/api.py @@ -52,12 +52,11 @@ def check_data_quality(content_data, source_type, data_time_str=None): if not content_data: return 'ok' - # 1. [新功能] IoT 卡不需要检查数据质量 + # 1. IoT 卡不需要检查数据质量 if str(source_type) == 'iot_card': return 'ok' - # 2. [新功能] 夜间免打扰逻辑 (08:00 - 17:00 之外不报错) - # 物理规律:晚上没有太阳,光谱仪数值低是正常的,不应报错 + # 2. 夜间免打扰逻辑 (08:00 - 17:00 之外不报错) if data_time_str and data_time_str != 'N/A': try: clean_time = str(data_time_str).replace('_', '-') @@ -70,27 +69,24 @@ def check_data_quality(content_data, source_type, data_time_str=None): except: pass - # 如果解析成功,且不在 8点-17点之间,视为夜晚,直接返回 ok if dt and (dt.hour < 8 or dt.hour >= 17): return 'ok' except: pass - # 3. [旧版核心] 数据异常判断逻辑 + # 3. 数据异常判断逻辑 status = 'ok' source_str = str(source_type) # --- Type A: 106 设备逻辑 (CSV格式) --- if '106' in source_str: try: - # 兼容处理:如果 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] @@ -99,16 +95,12 @@ def check_data_quality(content_data, source_type, data_time_str=None): 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: @@ -119,19 +111,16 @@ def check_data_quality(content_data, source_type, data_time_str=None): 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: @@ -142,7 +131,6 @@ def check_data_quality(content_data, source_type, data_time_str=None): # --- Type B: 82 设备逻辑 (JSON格式) --- else: try: - # 82 设备 content_data 应该已经是字典 if not isinstance(content_data, dict): return 'ok' @@ -153,11 +141,9 @@ def check_data_quality(content_data, source_type, data_time_str=None): 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: @@ -174,57 +160,53 @@ def check_data_quality(content_data, source_type, data_time_str=None): def save_iot_cards_to_db(card_list): """ [核心修复] IoT数据入库逻辑 - 增量更新模式 - 1. 只操作 source='iot_card' 的记录。 - 2. 核心:使用 'update' 逻辑而不是 'replace' 逻辑。 - 即使自动任务运行,也不会弄丢白名单 (is_whitelist) 或其他已存在的数据。 + 这里负责将 iot_api.py 获取到的新字段保存到数据库的 JSON 字段中 """ if not card_list: return 0, None update_count = 0 try: for card in card_list: - iccid = card.get('iccid') or card.get('card_id') # 兼容字段名 + iccid = card.get('iccid') or card.get('card_id') if not iccid: continue # 1. 查找是否存在该 SIM 卡记录 sim_record = Device.query.filter_by(name=iccid, source='iot_card').first() - # 初始化旧数据容器 old_json = {} if not sim_record: - # 插入新卡片 sim_record = Device(name=iccid, source='iot_card', install_site="IoT库") db.session.add(sim_record) - db.session.flush() # 立即获取ID + db.session.flush() else: - # 旧卡:尝试读取现有 JSON,确保不丢失之前的数据 try: if sim_record.json_data: old_json = json.loads(sim_record.json_data) except: old_json = {} - # 2. 准备需要更新的 API 数据 (只更新变动的字段) + # 2. 准备需要更新的 API 数据 api_updates = { "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', ''), + + # === [新增] 这里保存刚才在 iot_api.py 里生成的中文状态描述 === + "statusDesc": card.get('statusDesc', '未知') + # ======================================================== } - # 3. [关键步骤] 合并数据 - # 如果 old_json 里有 is_whitelist,update 不会覆盖它,因为 api_updates 里没有这个key + # 3. 合并数据 (保留 is_whitelist) old_json.update(api_updates) - # 4. 兜底保障:如果这是新卡,或者旧卡丢失了 whitelist 字段,默认设为 False if 'is_whitelist' not in old_json: old_json['is_whitelist'] = False - # 5. 更新数据库字段 + # 4. 更新数据库字段 sim_record.status = str(card.get('cardStatus', '')) - # 序列化并写回 sim_record.json_data = json.dumps(old_json, ensure_ascii=False) sim_record.check_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") @@ -258,7 +240,7 @@ def login(): @api_bp.route('/devices_overview', methods=['GET']) def devices_overview(): try: - # A. 获取 IoT卡表 (source='iot_card') + # A. 获取 IoT卡表 iot_records = Device.query.filter_by(source='iot_card').all() iot_map = {} for rec in iot_records: @@ -268,30 +250,24 @@ def devices_overview(): except: pass - # B. 获取 真实设备 (source != 'iot_card') + # B. 获取 真实设备 devices = Device.query.filter(Device.source != 'iot_card').all() data_list = [] for d in devices: item = d.to_dict() - # =========== 【新增修复】强制格式化时间 =========== - # 无论模型返回什么,这里都强制从数据库原始字段获取,并确保包含时分秒 + # 强制格式化时间 raw_time = d.latest_time if raw_time: - # 1. 如果是 datetime 对象 (防万一) if hasattr(raw_time, 'strftime'): item['latest_time'] = raw_time.strftime("%Y-%m-%d %H:%M:%S") - # 2. 如果是字符串 else: s = str(raw_time).strip() - # 如果只有日期且用下划线 (如 2026_01_14),且没有冒号,则补全 if '_' in s and ':' not in s: item['latest_time'] = s.replace('_', '-') + " 00:00:00" else: - # 已经是正常字符串(如 2026-01-14 13:49:26),直接使用 item['latest_time'] = s - # =============================================== parsed_content = {} if d.json_data: @@ -300,29 +276,34 @@ def devices_overview(): except: pass - # --- 绑定逻辑 (将IoT卡信息注入到设备) --- + # --- 绑定逻辑 --- bound_iccid = parsed_content.get('bound_iccid') item['usedTraffic'] = None item['stopDate'] = None + item['statusDesc'] = None # 初始化字段 item['isBound'] = False item['bound_iccid'] = bound_iccid - item['is_whitelist'] = False # 默认无白名单 + item['is_whitelist'] = False - # 如果有绑定,且卡片存在,则注入卡片信息 + # 如果有绑定,注入卡片信息 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['statusDesc'] = card_info.get('statusDesc') + # ====================================== + 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卡表 的数据也传给前端 (用于计算总流量 & 绑定弹窗) + # C. IoT卡表数据 (用于卡池管理界面) for rec in iot_records: item = rec.to_dict() try: @@ -333,6 +314,11 @@ def devices_overview(): item['usedTraffic'] = j.get('usedTraffic', '0') item['stopDate'] = j.get('stopDate', '') item['is_whitelist'] = j.get('is_whitelist', False) + + # === [新增] 将卡池列表中的状态描述传给前端 === + item['statusDesc'] = j.get('statusDesc', '未知') + # ======================================= + item['isOrphanIoT'] = True item['source'] = 'iot_card' data_list.append(item) @@ -349,7 +335,7 @@ def devices_overview(): @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 + date_str = request.args.get('date') if not name or not date_str: return jsonify({'code': 400, 'message': 'Missing name or date'}), 400 @@ -359,10 +345,8 @@ def device_data_by_date(): 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}%") @@ -370,7 +354,6 @@ def device_data_by_date(): 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 @@ -384,7 +367,6 @@ def device_data_by_date(): return jsonify({'code': 404, 'message': 'No data for this date'}), 404 -# 兼容旧调用的 stub @api_bp.route('/device_data_by_date_stub', methods=['GET']) def device_data_by_date_stub(): return device_data_by_date() @@ -411,25 +393,21 @@ def run_monitor(): 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') # 默认时间 + 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 + date_part = match.group(1).replace('_', '-') + time_part = match.group(2).replace('_', ':') 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=source, install_site="") @@ -439,14 +417,11 @@ def run_monitor(): if device.source == 'iot_card': device.source = source - # 更新字段 device.status = item.get('status') device.current_value = item.get('value') device.latest_time = target_time device.check_time = current_time - # [关键逻辑] 合并模式 (update),防止覆盖掉 bound_iccid - # 先读取数据库里已有的 json_data old_json = {} try: if device.json_data: @@ -454,16 +429,13 @@ def run_monitor(): except: old_json = {} - # 只有当 raw_json 是字典时才进行合并 new_json = d_raw if isinstance(d_raw, dict) else item.get('raw_json', {}) if isinstance(new_json, dict): - # 使用 update 方法,这样 old_json 里存在的 bound_iccid 不会被删掉 old_json.update(new_json) device.json_data = json.dumps(old_json, ensure_ascii=False) device.offset = calculate_offset(device.latest_time) - # 写入历史记录 new_history = DeviceHistory( device_id=device.id, status=item.get('status'), @@ -481,7 +453,7 @@ def run_monitor(): # --- B. 执行 IoT 同步 (写入数据库) --- if sync_iot_data_service: iot_list = sync_iot_data_service() - # 复用已修复的 save_iot_cards_to_db,确保不会丢失数据 + # 复用 save_iot_cards_to_db 保存包含 statusDesc 的新数据 c, e = save_iot_cards_to_db(iot_list) if e: msg_list.append(f"IoT错: {e}") @@ -497,7 +469,7 @@ def run_monitor(): # ========================================================= -# 4. 白名单、绑定与设备管理 (新功能) +# 4. 白名单、绑定与设备管理 # ========================================================= @api_bp.route('/toggle_whitelist', methods=['POST']) diff --git a/2_1banben/services/iot_api.py b/2_1banben/services/iot_api.py index 8e86e6f..845abc2 100644 --- a/2_1banben/services/iot_api.py +++ b/2_1banben/services/iot_api.py @@ -173,29 +173,37 @@ def sync_iot_data_service(): 1. 登录 2. 遍历所有分页获取 ICCID 3. 批量查询详情 - 4. 返回完整数据列表 (List[Dict]) + 4. 解析 cardStatus 状态码 + 5. 返回完整数据列表 (List[Dict]) """ print("[IoT Service] 开始同步任务...") - # 1. 登录 + # ✅ 1. 定义状态码映射表 (根据提供的需求文档) + STATUS_MAP = { + "1": "测试期", + "2": "沉默期", + "3": "在使用", + "4": "停机", + "5": "停机保号", + "6": "销户" + } + token = get_access_token() if not token: return [] - # 2. 循环翻页获取所有 ICCID all_iccids = [] page_no = 1 page_size = 100 + # ✅ 2. 循环翻页获取所有 ICCID while True: res = get_iot_card_page(token, page_no, page_size) - # 校验响应 if not res or (res.get('code') != 0 and res.get('code') != 200): print(f"[IoT Service] 列表获取结束或中断: {res.get('msg') if res else 'No Response'}") break - # 解析数据结构 (兼容 data 为 list 或 data.rows) data_field = res.get('data', {}) rows = [] if isinstance(data_field, list): @@ -206,43 +214,47 @@ def sync_iot_data_service(): if not rows: break - # 提取 ICCID current_batch = [str(x.get('iccid')) for x in rows if x.get('iccid')] all_iccids.extend(current_batch) - # print(f"DEBUG: page {page_no} done, items: {len(current_batch)}") - - # 判断是否最后一页 if len(rows) < page_size: break page_no += 1 - time.sleep(0.2) # 避免请求过快 + time.sleep(0.2) total_count = len(all_iccids) if total_count == 0: print("[IoT Service] 未找到任何卡片") return [] - # 3. 分批查询详情 + # ✅ 3. 分批查询详情并处理状态 final_data_list = [] batch_size = 50 - # print(f"DEBUG: 开始查询 {total_count} 张卡的详情...") - for i in range(0, total_count, batch_size): batch_iccids = all_iccids[i: i + batch_size] - detail_res = get_iot_card_details_batch(token, batch_iccids) if detail_res and (detail_res.get('code') == 0 or detail_res.get('code') == 200): details = detail_res.get('data', []) if isinstance(details, list): - final_data_list.extend(details) + + # === 核心修改:增加状态解析逻辑 === + for card in details: + # 获取原始状态码 (如 "3") + raw_status = str(card.get('cardStatus', '')) + + # 匹配中文描述 (如 "在使用") + status_desc = STATUS_MAP.get(raw_status, "未知状态") + + # 将描述写入新字段,前端可直接取用 card.statusDesc + card['statusDesc'] = status_desc + + final_data_list.append(card) + # ================================= time.sleep(0.2) print(f"[IoT Service] 同步完成,共获取 {len(final_data_list)} 条详情数据") - - # 4. 返回列表供 api.py 写入数据库 return final_data_list \ No newline at end of file diff --git a/zhandianxinxi/光谱数据监控/src/views/Dashboard.vue b/zhandianxinxi/光谱数据监控/src/views/Dashboard.vue index 83d060b..6d0dd95 100644 --- a/zhandianxinxi/光谱数据监控/src/views/Dashboard.vue +++ b/zhandianxinxi/光谱数据监控/src/views/Dashboard.vue @@ -136,6 +136,22 @@ + + + +