增加流量卡状态信息,对流量信息上提进行调整,取消超过500MB进行的警告整行标黄和上提功能,仅保留流量数字标黄
This commit is contained in:
@ -52,12 +52,11 @@ def check_data_quality(content_data, source_type, data_time_str=None):
|
|||||||
if not content_data:
|
if not content_data:
|
||||||
return 'ok'
|
return 'ok'
|
||||||
|
|
||||||
# 1. [新功能] IoT 卡不需要检查数据质量
|
# 1. IoT 卡不需要检查数据质量
|
||||||
if str(source_type) == 'iot_card':
|
if str(source_type) == 'iot_card':
|
||||||
return 'ok'
|
return 'ok'
|
||||||
|
|
||||||
# 2. [新功能] 夜间免打扰逻辑 (08:00 - 17:00 之外不报错)
|
# 2. 夜间免打扰逻辑 (08:00 - 17:00 之外不报错)
|
||||||
# 物理规律:晚上没有太阳,光谱仪数值低是正常的,不应报错
|
|
||||||
if data_time_str and data_time_str != 'N/A':
|
if data_time_str and data_time_str != 'N/A':
|
||||||
try:
|
try:
|
||||||
clean_time = str(data_time_str).replace('_', '-')
|
clean_time = str(data_time_str).replace('_', '-')
|
||||||
@ -70,27 +69,24 @@ def check_data_quality(content_data, source_type, data_time_str=None):
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# 如果解析成功,且不在 8点-17点之间,视为夜晚,直接返回 ok
|
|
||||||
if dt and (dt.hour < 8 or dt.hour >= 17):
|
if dt and (dt.hour < 8 or dt.hour >= 17):
|
||||||
return 'ok'
|
return 'ok'
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# 3. [旧版核心] 数据异常判断逻辑
|
# 3. 数据异常判断逻辑
|
||||||
status = 'ok'
|
status = 'ok'
|
||||||
source_str = str(source_type)
|
source_str = str(source_type)
|
||||||
|
|
||||||
# --- Type A: 106 设备逻辑 (CSV格式) ---
|
# --- Type A: 106 设备逻辑 (CSV格式) ---
|
||||||
if '106' in source_str:
|
if '106' in source_str:
|
||||||
try:
|
try:
|
||||||
# 兼容处理:如果 content_data 是字典,尝试取 content 字段;如果是字符串直接用
|
|
||||||
text_content = ""
|
text_content = ""
|
||||||
if isinstance(content_data, dict):
|
if isinstance(content_data, dict):
|
||||||
text_content = content_data.get('content', str(content_data))
|
text_content = content_data.get('content', str(content_data))
|
||||||
else:
|
else:
|
||||||
text_content = str(content_data)
|
text_content = str(content_data)
|
||||||
|
|
||||||
# 只要包含 OSIFBeta 就进行解析
|
|
||||||
if 'OSIFBeta' in text_content:
|
if 'OSIFBeta' in text_content:
|
||||||
lines = text_content.split('\n') if '\n' in text_content else [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
|
continue
|
||||||
|
|
||||||
parts = line.split(',')
|
parts = line.split(',')
|
||||||
# 简单校验列数
|
|
||||||
if len(parts) < 10:
|
if len(parts) < 10:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 检查积分时间 (Index 2)
|
|
||||||
try:
|
try:
|
||||||
int_time = int(parts[2])
|
int_time = int(parts[2])
|
||||||
# 旧代码逻辑:只有积分时间饱和 (>= 66534) 才检查数值
|
|
||||||
if int_time >= 66534:
|
if int_time >= 66534:
|
||||||
# 数据点通常从第4个(Index 3)开始
|
|
||||||
data_points = []
|
data_points = []
|
||||||
for p in parts[3:]:
|
for p in parts[3:]:
|
||||||
try:
|
try:
|
||||||
@ -119,19 +111,16 @@ def check_data_quality(content_data, source_type, data_time_str=None):
|
|||||||
if not data_points:
|
if not data_points:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 规则A:红色报错 (存在 < 100 的点)
|
|
||||||
for val in data_points:
|
for val in data_points:
|
||||||
if val < 100:
|
if val < 100:
|
||||||
return 'error'
|
return 'error'
|
||||||
|
|
||||||
# 规则B:黄色警告 (连续 5 个点在 100-500 之间)
|
|
||||||
consecutive_warning = 0
|
consecutive_warning = 0
|
||||||
for val in data_points:
|
for val in data_points:
|
||||||
if 100 <= val <= 500:
|
if 100 <= val <= 500:
|
||||||
consecutive_warning += 1
|
consecutive_warning += 1
|
||||||
if consecutive_warning >= 5:
|
if consecutive_warning >= 5:
|
||||||
status = 'warning'
|
status = 'warning'
|
||||||
# 注意:不立即返回,继续检查后面是否有 error
|
|
||||||
else:
|
else:
|
||||||
consecutive_warning = 0
|
consecutive_warning = 0
|
||||||
except:
|
except:
|
||||||
@ -142,7 +131,6 @@ def check_data_quality(content_data, source_type, data_time_str=None):
|
|||||||
# --- Type B: 82 设备逻辑 (JSON格式) ---
|
# --- Type B: 82 设备逻辑 (JSON格式) ---
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
# 82 设备 content_data 应该已经是字典
|
|
||||||
if not isinstance(content_data, dict):
|
if not isinstance(content_data, dict):
|
||||||
return 'ok'
|
return 'ok'
|
||||||
|
|
||||||
@ -153,11 +141,9 @@ def check_data_quality(content_data, source_type, data_time_str=None):
|
|||||||
if specs and isinstance(specs, list):
|
if specs and isinstance(specs, list):
|
||||||
consecutive_low = 0
|
consecutive_low = 0
|
||||||
for val in specs:
|
for val in specs:
|
||||||
# 确保 val 是数字
|
|
||||||
if not isinstance(val, (int, float)):
|
if not isinstance(val, (int, float)):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 旧代码逻辑: 连续2点 < 500 -> error
|
|
||||||
if val < 500:
|
if val < 500:
|
||||||
consecutive_low += 1
|
consecutive_low += 1
|
||||||
if consecutive_low >= 2:
|
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):
|
def save_iot_cards_to_db(card_list):
|
||||||
"""
|
"""
|
||||||
[核心修复] IoT数据入库逻辑 - 增量更新模式
|
[核心修复] IoT数据入库逻辑 - 增量更新模式
|
||||||
1. 只操作 source='iot_card' 的记录。
|
这里负责将 iot_api.py 获取到的新字段保存到数据库的 JSON 字段中
|
||||||
2. 核心:使用 'update' 逻辑而不是 'replace' 逻辑。
|
|
||||||
即使自动任务运行,也不会弄丢白名单 (is_whitelist) 或其他已存在的数据。
|
|
||||||
"""
|
"""
|
||||||
if not card_list: return 0, None
|
if not card_list: return 0, None
|
||||||
update_count = 0
|
update_count = 0
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for card in card_list:
|
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
|
if not iccid: continue
|
||||||
|
|
||||||
# 1. 查找是否存在该 SIM 卡记录
|
# 1. 查找是否存在该 SIM 卡记录
|
||||||
sim_record = Device.query.filter_by(name=iccid, source='iot_card').first()
|
sim_record = Device.query.filter_by(name=iccid, source='iot_card').first()
|
||||||
|
|
||||||
# 初始化旧数据容器
|
|
||||||
old_json = {}
|
old_json = {}
|
||||||
|
|
||||||
if not sim_record:
|
if not sim_record:
|
||||||
# 插入新卡片
|
|
||||||
sim_record = Device(name=iccid, source='iot_card', install_site="IoT库")
|
sim_record = Device(name=iccid, source='iot_card', install_site="IoT库")
|
||||||
db.session.add(sim_record)
|
db.session.add(sim_record)
|
||||||
db.session.flush() # 立即获取ID
|
db.session.flush()
|
||||||
else:
|
else:
|
||||||
# 旧卡:尝试读取现有 JSON,确保不丢失之前的数据
|
|
||||||
try:
|
try:
|
||||||
if sim_record.json_data:
|
if sim_record.json_data:
|
||||||
old_json = json.loads(sim_record.json_data)
|
old_json = json.loads(sim_record.json_data)
|
||||||
except:
|
except:
|
||||||
old_json = {}
|
old_json = {}
|
||||||
|
|
||||||
# 2. 准备需要更新的 API 数据 (只更新变动的字段)
|
# 2. 准备需要更新的 API 数据
|
||||||
api_updates = {
|
api_updates = {
|
||||||
"iccid": iccid,
|
"iccid": iccid,
|
||||||
"usedTraffic": str(card.get('usedTraffic') or '0'),
|
"usedTraffic": str(card.get('usedTraffic') or '0'),
|
||||||
"stopDate": card.get('stopDate', 'N/A'),
|
"stopDate": card.get('stopDate', 'N/A'),
|
||||||
"cardStatus": card.get('cardStatus'),
|
"cardStatus": card.get('cardStatus'),
|
||||||
"tag": card.get('tag', '')
|
"tag": card.get('tag', ''),
|
||||||
|
|
||||||
|
# === [新增] 这里保存刚才在 iot_api.py 里生成的中文状态描述 ===
|
||||||
|
"statusDesc": card.get('statusDesc', '未知')
|
||||||
|
# ========================================================
|
||||||
}
|
}
|
||||||
|
|
||||||
# 3. [关键步骤] 合并数据
|
# 3. 合并数据 (保留 is_whitelist)
|
||||||
# 如果 old_json 里有 is_whitelist,update 不会覆盖它,因为 api_updates 里没有这个key
|
|
||||||
old_json.update(api_updates)
|
old_json.update(api_updates)
|
||||||
|
|
||||||
# 4. 兜底保障:如果这是新卡,或者旧卡丢失了 whitelist 字段,默认设为 False
|
|
||||||
if 'is_whitelist' not in old_json:
|
if 'is_whitelist' not in old_json:
|
||||||
old_json['is_whitelist'] = False
|
old_json['is_whitelist'] = False
|
||||||
|
|
||||||
# 5. 更新数据库字段
|
# 4. 更新数据库字段
|
||||||
sim_record.status = str(card.get('cardStatus', ''))
|
sim_record.status = str(card.get('cardStatus', ''))
|
||||||
# 序列化并写回
|
|
||||||
sim_record.json_data = json.dumps(old_json, ensure_ascii=False)
|
sim_record.json_data = json.dumps(old_json, ensure_ascii=False)
|
||||||
sim_record.check_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
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'])
|
@api_bp.route('/devices_overview', methods=['GET'])
|
||||||
def devices_overview():
|
def devices_overview():
|
||||||
try:
|
try:
|
||||||
# A. 获取 IoT卡表 (source='iot_card')
|
# A. 获取 IoT卡表
|
||||||
iot_records = Device.query.filter_by(source='iot_card').all()
|
iot_records = Device.query.filter_by(source='iot_card').all()
|
||||||
iot_map = {}
|
iot_map = {}
|
||||||
for rec in iot_records:
|
for rec in iot_records:
|
||||||
@ -268,30 +250,24 @@ def devices_overview():
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# B. 获取 真实设备 (source != 'iot_card')
|
# B. 获取 真实设备
|
||||||
devices = Device.query.filter(Device.source != 'iot_card').all()
|
devices = Device.query.filter(Device.source != 'iot_card').all()
|
||||||
data_list = []
|
data_list = []
|
||||||
|
|
||||||
for d in devices:
|
for d in devices:
|
||||||
item = d.to_dict()
|
item = d.to_dict()
|
||||||
|
|
||||||
# =========== 【新增修复】强制格式化时间 ===========
|
# 强制格式化时间
|
||||||
# 无论模型返回什么,这里都强制从数据库原始字段获取,并确保包含时分秒
|
|
||||||
raw_time = d.latest_time
|
raw_time = d.latest_time
|
||||||
if raw_time:
|
if raw_time:
|
||||||
# 1. 如果是 datetime 对象 (防万一)
|
|
||||||
if hasattr(raw_time, 'strftime'):
|
if hasattr(raw_time, 'strftime'):
|
||||||
item['latest_time'] = raw_time.strftime("%Y-%m-%d %H:%M:%S")
|
item['latest_time'] = raw_time.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
# 2. 如果是字符串
|
|
||||||
else:
|
else:
|
||||||
s = str(raw_time).strip()
|
s = str(raw_time).strip()
|
||||||
# 如果只有日期且用下划线 (如 2026_01_14),且没有冒号,则补全
|
|
||||||
if '_' in s and ':' not in s:
|
if '_' in s and ':' not in s:
|
||||||
item['latest_time'] = s.replace('_', '-') + " 00:00:00"
|
item['latest_time'] = s.replace('_', '-') + " 00:00:00"
|
||||||
else:
|
else:
|
||||||
# 已经是正常字符串(如 2026-01-14 13:49:26),直接使用
|
|
||||||
item['latest_time'] = s
|
item['latest_time'] = s
|
||||||
# ===============================================
|
|
||||||
|
|
||||||
parsed_content = {}
|
parsed_content = {}
|
||||||
if d.json_data:
|
if d.json_data:
|
||||||
@ -300,29 +276,34 @@ def devices_overview():
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# --- 绑定逻辑 (将IoT卡信息注入到设备) ---
|
# --- 绑定逻辑 ---
|
||||||
bound_iccid = parsed_content.get('bound_iccid')
|
bound_iccid = parsed_content.get('bound_iccid')
|
||||||
|
|
||||||
item['usedTraffic'] = None
|
item['usedTraffic'] = None
|
||||||
item['stopDate'] = None
|
item['stopDate'] = None
|
||||||
|
item['statusDesc'] = None # 初始化字段
|
||||||
item['isBound'] = False
|
item['isBound'] = False
|
||||||
item['bound_iccid'] = bound_iccid
|
item['bound_iccid'] = bound_iccid
|
||||||
item['is_whitelist'] = False # 默认无白名单
|
item['is_whitelist'] = False
|
||||||
|
|
||||||
# 如果有绑定,且卡片存在,则注入卡片信息
|
# 如果有绑定,注入卡片信息
|
||||||
if bound_iccid and bound_iccid in iot_map:
|
if bound_iccid and bound_iccid in iot_map:
|
||||||
card_info = iot_map[bound_iccid]
|
card_info = iot_map[bound_iccid]
|
||||||
item['usedTraffic'] = card_info.get('usedTraffic')
|
item['usedTraffic'] = card_info.get('usedTraffic')
|
||||||
item['stopDate'] = card_info.get('stopDate')
|
item['stopDate'] = card_info.get('stopDate')
|
||||||
item['is_whitelist'] = card_info.get('is_whitelist', False)
|
item['is_whitelist'] = card_info.get('is_whitelist', False)
|
||||||
|
|
||||||
|
# === [新增] 将绑定的卡片状态描述传给前端 ===
|
||||||
|
item['statusDesc'] = card_info.get('statusDesc')
|
||||||
|
# ======================================
|
||||||
|
|
||||||
item['isBound'] = True
|
item['isBound'] = True
|
||||||
|
|
||||||
# [关键] 调用异常检测函数 (check_data_quality)
|
|
||||||
item['data_quality'] = check_data_quality(parsed_content, d.source, d.latest_time)
|
item['data_quality'] = check_data_quality(parsed_content, d.source, d.latest_time)
|
||||||
|
|
||||||
data_list.append(item)
|
data_list.append(item)
|
||||||
|
|
||||||
# C. 必须把 IoT卡表 的数据也传给前端 (用于计算总流量 & 绑定弹窗)
|
# C. IoT卡表数据 (用于卡池管理界面)
|
||||||
for rec in iot_records:
|
for rec in iot_records:
|
||||||
item = rec.to_dict()
|
item = rec.to_dict()
|
||||||
try:
|
try:
|
||||||
@ -333,6 +314,11 @@ def devices_overview():
|
|||||||
item['usedTraffic'] = j.get('usedTraffic', '0')
|
item['usedTraffic'] = j.get('usedTraffic', '0')
|
||||||
item['stopDate'] = j.get('stopDate', '')
|
item['stopDate'] = j.get('stopDate', '')
|
||||||
item['is_whitelist'] = j.get('is_whitelist', False)
|
item['is_whitelist'] = j.get('is_whitelist', False)
|
||||||
|
|
||||||
|
# === [新增] 将卡池列表中的状态描述传给前端 ===
|
||||||
|
item['statusDesc'] = j.get('statusDesc', '未知')
|
||||||
|
# =======================================
|
||||||
|
|
||||||
item['isOrphanIoT'] = True
|
item['isOrphanIoT'] = True
|
||||||
item['source'] = 'iot_card'
|
item['source'] = 'iot_card'
|
||||||
data_list.append(item)
|
data_list.append(item)
|
||||||
@ -349,7 +335,7 @@ def devices_overview():
|
|||||||
@api_bp.route('/device_data_by_date', methods=['GET'])
|
@api_bp.route('/device_data_by_date', methods=['GET'])
|
||||||
def device_data_by_date():
|
def device_data_by_date():
|
||||||
name = request.args.get('name')
|
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:
|
if not name or not date_str:
|
||||||
return jsonify({'code': 400, 'message': 'Missing name or date'}), 400
|
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
|
return jsonify({'code': 404, 'message': 'Device not found'}), 404
|
||||||
|
|
||||||
content = None
|
content = None
|
||||||
# 统一将下划线格式转换为横杠格式进行查询
|
|
||||||
query_date = date_str.replace('_', '-')
|
query_date = date_str.replace('_', '-')
|
||||||
|
|
||||||
# 1. 尝试从历史记录表中查找
|
|
||||||
history_record = DeviceHistory.query.filter(
|
history_record = DeviceHistory.query.filter(
|
||||||
DeviceHistory.device_id == device.id,
|
DeviceHistory.device_id == device.id,
|
||||||
DeviceHistory.data_time.like(f"{query_date}%")
|
DeviceHistory.data_time.like(f"{query_date}%")
|
||||||
@ -370,7 +354,6 @@ def device_data_by_date():
|
|||||||
|
|
||||||
if history_record:
|
if history_record:
|
||||||
content = history_record.json_data
|
content = history_record.json_data
|
||||||
# 2. 如果历史表中没有,查当前 Device 表
|
|
||||||
elif device.latest_time and device.latest_time.startswith(query_date):
|
elif device.latest_time and device.latest_time.startswith(query_date):
|
||||||
content = device.json_data
|
content = device.json_data
|
||||||
|
|
||||||
@ -384,7 +367,6 @@ def device_data_by_date():
|
|||||||
return jsonify({'code': 404, 'message': 'No data for this date'}), 404
|
return jsonify({'code': 404, 'message': 'No data for this date'}), 404
|
||||||
|
|
||||||
|
|
||||||
# 兼容旧调用的 stub
|
|
||||||
@api_bp.route('/device_data_by_date_stub', methods=['GET'])
|
@api_bp.route('/device_data_by_date_stub', methods=['GET'])
|
||||||
def device_data_by_date_stub():
|
def device_data_by_date_stub():
|
||||||
return device_data_by_date()
|
return device_data_by_date()
|
||||||
@ -411,25 +393,21 @@ def run_monitor():
|
|||||||
d_name = item.get('name')
|
d_name = item.get('name')
|
||||||
if not d_name: continue
|
if not d_name: continue
|
||||||
|
|
||||||
# 提取原始信息
|
|
||||||
d_raw = item.get('raw_json', {})
|
d_raw = item.get('raw_json', {})
|
||||||
source = item.get('source', '')
|
source = item.get('source', '')
|
||||||
target_time = item.get('target_time') # 默认时间
|
target_time = item.get('target_time')
|
||||||
|
|
||||||
# [旧代码逻辑保留] 针对 106 设备,从路径中强制解析正确的时间格式
|
|
||||||
if '106' in str(source):
|
if '106' in str(source):
|
||||||
try:
|
try:
|
||||||
path_str = d_raw.get('path', '')
|
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)
|
match = re.search(r'/Data/(\d{4}_\d{2}_\d{2})/\w+_(\d{2}_\d{2}_\d{2})\.csv', path_str)
|
||||||
if match:
|
if match:
|
||||||
date_part = match.group(1).replace('_', '-') # 2026-01-13
|
date_part = match.group(1).replace('_', '-')
|
||||||
time_part = match.group(2).replace('_', ':') # 15:30:00
|
time_part = match.group(2).replace('_', ':')
|
||||||
target_time = f"{date_part} {time_part}"
|
target_time = f"{date_part} {time_part}"
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# 查找或创建设备
|
|
||||||
device = Device.query.filter_by(name=d_name).first()
|
device = Device.query.filter_by(name=d_name).first()
|
||||||
if not device:
|
if not device:
|
||||||
device = Device(name=d_name, source=source, install_site="")
|
device = Device(name=d_name, source=source, install_site="")
|
||||||
@ -439,14 +417,11 @@ def run_monitor():
|
|||||||
if device.source == 'iot_card':
|
if device.source == 'iot_card':
|
||||||
device.source = source
|
device.source = source
|
||||||
|
|
||||||
# 更新字段
|
|
||||||
device.status = item.get('status')
|
device.status = item.get('status')
|
||||||
device.current_value = item.get('value')
|
device.current_value = item.get('value')
|
||||||
device.latest_time = target_time
|
device.latest_time = target_time
|
||||||
device.check_time = current_time
|
device.check_time = current_time
|
||||||
|
|
||||||
# [关键逻辑] 合并模式 (update),防止覆盖掉 bound_iccid
|
|
||||||
# 先读取数据库里已有的 json_data
|
|
||||||
old_json = {}
|
old_json = {}
|
||||||
try:
|
try:
|
||||||
if device.json_data:
|
if device.json_data:
|
||||||
@ -454,16 +429,13 @@ def run_monitor():
|
|||||||
except:
|
except:
|
||||||
old_json = {}
|
old_json = {}
|
||||||
|
|
||||||
# 只有当 raw_json 是字典时才进行合并
|
|
||||||
new_json = d_raw if isinstance(d_raw, dict) else item.get('raw_json', {})
|
new_json = d_raw if isinstance(d_raw, dict) else item.get('raw_json', {})
|
||||||
if isinstance(new_json, dict):
|
if isinstance(new_json, dict):
|
||||||
# 使用 update 方法,这样 old_json 里存在的 bound_iccid 不会被删掉
|
|
||||||
old_json.update(new_json)
|
old_json.update(new_json)
|
||||||
|
|
||||||
device.json_data = json.dumps(old_json, ensure_ascii=False)
|
device.json_data = json.dumps(old_json, ensure_ascii=False)
|
||||||
device.offset = calculate_offset(device.latest_time)
|
device.offset = calculate_offset(device.latest_time)
|
||||||
|
|
||||||
# 写入历史记录
|
|
||||||
new_history = DeviceHistory(
|
new_history = DeviceHistory(
|
||||||
device_id=device.id,
|
device_id=device.id,
|
||||||
status=item.get('status'),
|
status=item.get('status'),
|
||||||
@ -481,7 +453,7 @@ def run_monitor():
|
|||||||
# --- B. 执行 IoT 同步 (写入数据库) ---
|
# --- B. 执行 IoT 同步 (写入数据库) ---
|
||||||
if sync_iot_data_service:
|
if sync_iot_data_service:
|
||||||
iot_list = 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)
|
c, e = save_iot_cards_to_db(iot_list)
|
||||||
if e:
|
if e:
|
||||||
msg_list.append(f"IoT错: {e}")
|
msg_list.append(f"IoT错: {e}")
|
||||||
@ -497,7 +469,7 @@ def run_monitor():
|
|||||||
|
|
||||||
|
|
||||||
# =========================================================
|
# =========================================================
|
||||||
# 4. 白名单、绑定与设备管理 (新功能)
|
# 4. 白名单、绑定与设备管理
|
||||||
# =========================================================
|
# =========================================================
|
||||||
|
|
||||||
@api_bp.route('/toggle_whitelist', methods=['POST'])
|
@api_bp.route('/toggle_whitelist', methods=['POST'])
|
||||||
|
|||||||
@ -173,29 +173,37 @@ def sync_iot_data_service():
|
|||||||
1. 登录
|
1. 登录
|
||||||
2. 遍历所有分页获取 ICCID
|
2. 遍历所有分页获取 ICCID
|
||||||
3. 批量查询详情
|
3. 批量查询详情
|
||||||
4. 返回完整数据列表 (List[Dict])
|
4. 解析 cardStatus 状态码
|
||||||
|
5. 返回完整数据列表 (List[Dict])
|
||||||
"""
|
"""
|
||||||
print("[IoT Service] 开始同步任务...")
|
print("[IoT Service] 开始同步任务...")
|
||||||
|
|
||||||
# 1. 登录
|
# ✅ 1. 定义状态码映射表 (根据提供的需求文档)
|
||||||
|
STATUS_MAP = {
|
||||||
|
"1": "测试期",
|
||||||
|
"2": "沉默期",
|
||||||
|
"3": "在使用",
|
||||||
|
"4": "停机",
|
||||||
|
"5": "停机保号",
|
||||||
|
"6": "销户"
|
||||||
|
}
|
||||||
|
|
||||||
token = get_access_token()
|
token = get_access_token()
|
||||||
if not token:
|
if not token:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# 2. 循环翻页获取所有 ICCID
|
|
||||||
all_iccids = []
|
all_iccids = []
|
||||||
page_no = 1
|
page_no = 1
|
||||||
page_size = 100
|
page_size = 100
|
||||||
|
|
||||||
|
# ✅ 2. 循环翻页获取所有 ICCID
|
||||||
while True:
|
while True:
|
||||||
res = get_iot_card_page(token, page_no, page_size)
|
res = get_iot_card_page(token, page_no, page_size)
|
||||||
|
|
||||||
# 校验响应
|
|
||||||
if not res or (res.get('code') != 0 and res.get('code') != 200):
|
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'}")
|
print(f"[IoT Service] 列表获取结束或中断: {res.get('msg') if res else 'No Response'}")
|
||||||
break
|
break
|
||||||
|
|
||||||
# 解析数据结构 (兼容 data 为 list 或 data.rows)
|
|
||||||
data_field = res.get('data', {})
|
data_field = res.get('data', {})
|
||||||
rows = []
|
rows = []
|
||||||
if isinstance(data_field, list):
|
if isinstance(data_field, list):
|
||||||
@ -206,43 +214,47 @@ def sync_iot_data_service():
|
|||||||
if not rows:
|
if not rows:
|
||||||
break
|
break
|
||||||
|
|
||||||
# 提取 ICCID
|
|
||||||
current_batch = [str(x.get('iccid')) for x in rows if x.get('iccid')]
|
current_batch = [str(x.get('iccid')) for x in rows if x.get('iccid')]
|
||||||
all_iccids.extend(current_batch)
|
all_iccids.extend(current_batch)
|
||||||
|
|
||||||
# print(f"DEBUG: page {page_no} done, items: {len(current_batch)}")
|
|
||||||
|
|
||||||
# 判断是否最后一页
|
|
||||||
if len(rows) < page_size:
|
if len(rows) < page_size:
|
||||||
break
|
break
|
||||||
|
|
||||||
page_no += 1
|
page_no += 1
|
||||||
time.sleep(0.2) # 避免请求过快
|
time.sleep(0.2)
|
||||||
|
|
||||||
total_count = len(all_iccids)
|
total_count = len(all_iccids)
|
||||||
if total_count == 0:
|
if total_count == 0:
|
||||||
print("[IoT Service] 未找到任何卡片")
|
print("[IoT Service] 未找到任何卡片")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# 3. 分批查询详情
|
# ✅ 3. 分批查询详情并处理状态
|
||||||
final_data_list = []
|
final_data_list = []
|
||||||
batch_size = 50
|
batch_size = 50
|
||||||
|
|
||||||
# print(f"DEBUG: 开始查询 {total_count} 张卡的详情...")
|
|
||||||
|
|
||||||
for i in range(0, total_count, batch_size):
|
for i in range(0, total_count, batch_size):
|
||||||
batch_iccids = all_iccids[i: i + batch_size]
|
batch_iccids = all_iccids[i: i + batch_size]
|
||||||
|
|
||||||
detail_res = get_iot_card_details_batch(token, batch_iccids)
|
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):
|
if detail_res and (detail_res.get('code') == 0 or detail_res.get('code') == 200):
|
||||||
details = detail_res.get('data', [])
|
details = detail_res.get('data', [])
|
||||||
if isinstance(details, list):
|
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)
|
time.sleep(0.2)
|
||||||
|
|
||||||
print(f"[IoT Service] 同步完成,共获取 {len(final_data_list)} 条详情数据")
|
print(f"[IoT Service] 同步完成,共获取 {len(final_data_list)} 条详情数据")
|
||||||
|
|
||||||
# 4. 返回列表供 api.py 写入数据库
|
|
||||||
return final_data_list
|
return final_data_list
|
||||||
@ -136,6 +136,22 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="卡状态" width="110" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div v-if="row.isBound">
|
||||||
|
<el-tag
|
||||||
|
:type="getCardStatusType(row.statusDesc)"
|
||||||
|
effect="light"
|
||||||
|
size="small"
|
||||||
|
style="font-weight: bold;"
|
||||||
|
>
|
||||||
|
{{ row.statusDesc || '未知' }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<span v-else style="color: #ccc;">--</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
<el-table-column label="服务截止" width="140">
|
<el-table-column label="服务截止" width="140">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div v-if="row.isBound && row.stopDate">
|
<div v-if="row.isBound && row.stopDate">
|
||||||
@ -277,6 +293,15 @@ const showAddDialog = ref(false)
|
|||||||
const isAdding = ref(false)
|
const isAdding = ref(false)
|
||||||
const newDeviceForm = reactive({ name: '', site: '' })
|
const newDeviceForm = reactive({ name: '', site: '' })
|
||||||
|
|
||||||
|
// === 辅助函数:根据中文状态返回 Tag 颜色 ===
|
||||||
|
const getCardStatusType = (status) => {
|
||||||
|
if (status === '在使用') return 'success' // 绿色
|
||||||
|
if (status === '停机' || status === '销户') return 'danger' // 红色
|
||||||
|
if (status === '停机保号' || status === '沉默期') return 'warning' // 黄色
|
||||||
|
if (status === '测试期') return 'info' // 灰色
|
||||||
|
return 'info' // 默认
|
||||||
|
}
|
||||||
|
|
||||||
// === 核心数据处理逻辑 ===
|
// === 核心数据处理逻辑 ===
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@ -367,7 +392,9 @@ const fetchData = async () => {
|
|||||||
if (isNaN(trafficNum)) trafficNum = 0
|
if (isNaN(trafficNum)) trafficNum = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === 修改处:恢复流量超标警告判断,用于标黄 ===
|
||||||
const trafficWarning = (trafficNum >= 500 && !isWhitelist)
|
const trafficWarning = (trafficNum >= 500 && !isWhitelist)
|
||||||
|
|
||||||
let expireWarning = false
|
let expireWarning = false
|
||||||
if (item.stopDate && item.stopDate !== 'N/A') {
|
if (item.stopDate && item.stopDate !== 'N/A') {
|
||||||
const stopD = new Date(item.stopDate.replace(/_/g, '-'))
|
const stopD = new Date(item.stopDate.replace(/_/g, '-'))
|
||||||
@ -395,10 +422,10 @@ const fetchData = async () => {
|
|||||||
} else if (diffHours > 24) {
|
} else if (diffHours > 24) {
|
||||||
statusLabel = '滞后'; statusColor = '#E6A23C'; statusType = 'warning';
|
statusLabel = '滞后'; statusColor = '#E6A23C'; statusType = 'warning';
|
||||||
statusReason = `滞后 ${Math.floor(diffDays)} 天`;
|
statusReason = `滞后 ${Math.floor(diffDays)} 天`;
|
||||||
} else if (trafficWarning) {
|
|
||||||
statusLabel = '流量警告'; statusColor = '#E6A23C'; statusType = 'warning';
|
// === 注意:这里没有把 trafficWarning 加入到 sortWeight 或 statusType 的改变逻辑中 ===
|
||||||
statusReason = `流量超标`;
|
// 从而实现了“只标黄文字,不改变行状态,不置顶”
|
||||||
sortWeight = 500;
|
|
||||||
} else if (expireWarning) {
|
} else if (expireWarning) {
|
||||||
statusLabel = '即将过期'; statusColor = '#E6A23C'; statusType = 'warning';
|
statusLabel = '即将过期'; statusColor = '#E6A23C'; statusType = 'warning';
|
||||||
statusReason = `即将过期`;
|
statusReason = `即将过期`;
|
||||||
@ -412,7 +439,7 @@ const fetchData = async () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
latest_time: displayTime, // <--- 这里使用了我们格式化好的漂亮时间
|
latest_time: displayTime,
|
||||||
is_hidden: isHidden,
|
is_hidden: isHidden,
|
||||||
isOrphanIoT,
|
isOrphanIoT,
|
||||||
isBound,
|
isBound,
|
||||||
|
|||||||
Reference in New Issue
Block a user