Files
ZDXX/2_1banben/services/time_utils.py

98 lines
3.4 KiB
Python
Raw Normal View History

refactor: 抽出统一入库管道,合并定时与手动两条重复路径 问题:app.py:auto_monitor_job(定时)和 routes/api.py:run_monitor(手动) 各自维护了一份几乎相同但细节不一致的写入逻辑,同一条数据经不同入口落库后 latest_time / source / offset / file_count 可能不同。 - 新增 services/time_utils.py:calculate_offset 下沉到无依赖模块,避免 db_ingest 与 routes.api 互相 import 形成循环。routes/api.py 里 re-export 一次,保证 app.py 原有的 from routes.api import calculate_offset 不失效。 - 新增 services/db_ingest.py:ingest_device_data(),承载全部入库细节,只 add/flush 不 commit,事务边界交给调用方。 - routes/api.py:run_monitor 瘦身为「触发爬虫 -> 调管道 -> 提交」。 - models.py:to_dict() 的 offset 改为读取时用 calculate_offset(latest_time) 实时计算,不再读 offset 列。该列是写入时刻的快照,采集一停摆就整体失真 (库里停在 2026-02-06,offset 却仍显示“当天”)。列保留但已废弃, 不执行 ALTER TABLE DROP COLUMN。 入库语义(db_ingest): - 只有爬虫拿到真实业务时间才更新主表 latest_time,拿不到就保留上一个有效值 (冻结),不再回退到 current_time。 - DeviceHistory 单独用 history_time:主表回答“数据到什么时候”,历史回答 “什么时候采过”。 - 历史表 json_data 改存本次增量切片,不再复制主表那份越滚越大的累积 JSON。 - 主表 json_data 改为覆盖式更新以切断无限膨胀,但保留 APP_OWNED_KEYS (bound_iccid / is_whitelist)—— 这两个键由 /bind_device_card 和 /toggle_whitelist 写入,按原方案直接覆盖会清空所有设备-流量卡绑定。
2026-09-15 17:05:38 +08:00
# services/time_utils.py
"""
数据时效判定的唯一实现。
此前「滞后几天」这件事在后端(calculate_offset)和前端(Dashboard.vue 里
自算 diffDays/diffHours)各算了一套,阈值和粒度都不同,跨日必然打架:
昨天 23:00 的数据在次日 01:00 查看,后端说「滞后 1 天」,前端标「昨日数据」。
现在统一由这里判定,前端只消费 Device.to_dict() 暴露的
offset / stale_level / stale_days 三个字段做渲染。
refactor: 抽出统一入库管道,合并定时与手动两条重复路径 问题:app.py:auto_monitor_job(定时)和 routes/api.py:run_monitor(手动) 各自维护了一份几乎相同但细节不一致的写入逻辑,同一条数据经不同入口落库后 latest_time / source / offset / file_count 可能不同。 - 新增 services/time_utils.py:calculate_offset 下沉到无依赖模块,避免 db_ingest 与 routes.api 互相 import 形成循环。routes/api.py 里 re-export 一次,保证 app.py 原有的 from routes.api import calculate_offset 不失效。 - 新增 services/db_ingest.py:ingest_device_data(),承载全部入库细节,只 add/flush 不 commit,事务边界交给调用方。 - routes/api.py:run_monitor 瘦身为「触发爬虫 -> 调管道 -> 提交」。 - models.py:to_dict() 的 offset 改为读取时用 calculate_offset(latest_time) 实时计算,不再读 offset 列。该列是写入时刻的快照,采集一停摆就整体失真 (库里停在 2026-02-06,offset 却仍显示“当天”)。列保留但已废弃, 不执行 ALTER TABLE DROP COLUMN。 入库语义(db_ingest): - 只有爬虫拿到真实业务时间才更新主表 latest_time,拿不到就保留上一个有效值 (冻结),不再回退到 current_time。 - DeviceHistory 单独用 history_time:主表回答“数据到什么时候”,历史回答 “什么时候采过”。 - 历史表 json_data 改存本次增量切片,不再复制主表那份越滚越大的累积 JSON。 - 主表 json_data 改为覆盖式更新以切断无限膨胀,但保留 APP_OWNED_KEYS (bound_iccid / is_whitelist)—— 这两个键由 /bind_device_card 和 /toggle_whitelist 写入,按原方案直接覆盖会清空所有设备-流量卡绑定。
2026-09-15 17:05:38 +08:00
刻意不依赖 flask / db / models,避免被 services.db_ingest 和 routes.api
互相引用时产生循环导入。
"""
import re
from datetime import datetime, date
refactor: 抽出统一入库管道,合并定时与手动两条重复路径 问题:app.py:auto_monitor_job(定时)和 routes/api.py:run_monitor(手动) 各自维护了一份几乎相同但细节不一致的写入逻辑,同一条数据经不同入口落库后 latest_time / source / offset / file_count 可能不同。 - 新增 services/time_utils.py:calculate_offset 下沉到无依赖模块,避免 db_ingest 与 routes.api 互相 import 形成循环。routes/api.py 里 re-export 一次,保证 app.py 原有的 from routes.api import calculate_offset 不失效。 - 新增 services/db_ingest.py:ingest_device_data(),承载全部入库细节,只 add/flush 不 commit,事务边界交给调用方。 - routes/api.py:run_monitor 瘦身为「触发爬虫 -> 调管道 -> 提交」。 - models.py:to_dict() 的 offset 改为读取时用 calculate_offset(latest_time) 实时计算,不再读 offset 列。该列是写入时刻的快照,采集一停摆就整体失真 (库里停在 2026-02-06,offset 却仍显示“当天”)。列保留但已废弃, 不执行 ALTER TABLE DROP COLUMN。 入库语义(db_ingest): - 只有爬虫拿到真实业务时间才更新主表 latest_time,拿不到就保留上一个有效值 (冻结),不再回退到 current_time。 - DeviceHistory 单独用 history_time:主表回答“数据到什么时候”,历史回答 “什么时候采过”。 - 历史表 json_data 改存本次增量切片,不再复制主表那份越滚越大的累积 JSON。 - 主表 json_data 改为覆盖式更新以切断无限膨胀,但保留 APP_OWNED_KEYS (bound_iccid / is_whitelist)—— 这两个键由 /bind_device_card 和 /toggle_whitelist 写入,按原方案直接覆盖会清空所有设备-流量卡绑定。
2026-09-15 17:05:38 +08:00
# 视为「从未同步」的占位值
_NEVER_SYNCED = ('', 'N/A', 'NA', 'NONE', 'NULL', 'NAN')
refactor: 抽出统一入库管道,合并定时与手动两条重复路径 问题:app.py:auto_monitor_job(定时)和 routes/api.py:run_monitor(手动) 各自维护了一份几乎相同但细节不一致的写入逻辑,同一条数据经不同入口落库后 latest_time / source / offset / file_count 可能不同。 - 新增 services/time_utils.py:calculate_offset 下沉到无依赖模块,避免 db_ingest 与 routes.api 互相 import 形成循环。routes/api.py 里 re-export 一次,保证 app.py 原有的 from routes.api import calculate_offset 不失效。 - 新增 services/db_ingest.py:ingest_device_data(),承载全部入库细节,只 add/flush 不 commit,事务边界交给调用方。 - routes/api.py:run_monitor 瘦身为「触发爬虫 -> 调管道 -> 提交」。 - models.py:to_dict() 的 offset 改为读取时用 calculate_offset(latest_time) 实时计算,不再读 offset 列。该列是写入时刻的快照,采集一停摆就整体失真 (库里停在 2026-02-06,offset 却仍显示“当天”)。列保留但已废弃, 不执行 ALTER TABLE DROP COLUMN。 入库语义(db_ingest): - 只有爬虫拿到真实业务时间才更新主表 latest_time,拿不到就保留上一个有效值 (冻结),不再回退到 current_time。 - DeviceHistory 单独用 history_time:主表回答“数据到什么时候”,历史回答 “什么时候采过”。 - 历史表 json_data 改存本次增量切片,不再复制主表那份越滚越大的累积 JSON。 - 主表 json_data 改为覆盖式更新以切断无限膨胀,但保留 APP_OWNED_KEYS (bound_iccid / is_whitelist)—— 这两个键由 /bind_device_card 和 /toggle_whitelist 写入,按原方案直接覆盖会清空所有设备-流量卡绑定。
2026-09-15 17:05:38 +08:00
# 从任意时间字符串里抠出 YYYY-MM-DD。分隔符兼容 - / _ ,
# 因此 ISO 的 2026-09-15T08:30:00Z 和 2026_09_15 都能命中。
# 滞后判定按自然日,时分秒不影响结果,所以不做完整的时间解析。
_DATE_RE = re.compile(r'(\d{4})[-/_](\d{1,2})[-/_](\d{1,2})')
# 超过这个自然日数算「严重滞后」
SEVERE_THRESHOLD_DAYS = 7
# 判定级别
LEVEL_UNKNOWN = 'unknown' # 从未同步 / 时间无法解析
LEVEL_OK = 'ok' # 当天
LEVEL_YESTERDAY = 'yesterday' # 昨天
LEVEL_LAGGING = 'lagging' # 滞后 2~7 天
LEVEL_SEVERE = 'severe' # 滞后 7 天以上
def parse_record_date(value):
refactor: 抽出统一入库管道,合并定时与手动两条重复路径 问题:app.py:auto_monitor_job(定时)和 routes/api.py:run_monitor(手动) 各自维护了一份几乎相同但细节不一致的写入逻辑,同一条数据经不同入口落库后 latest_time / source / offset / file_count 可能不同。 - 新增 services/time_utils.py:calculate_offset 下沉到无依赖模块,避免 db_ingest 与 routes.api 互相 import 形成循环。routes/api.py 里 re-export 一次,保证 app.py 原有的 from routes.api import calculate_offset 不失效。 - 新增 services/db_ingest.py:ingest_device_data(),承载全部入库细节,只 add/flush 不 commit,事务边界交给调用方。 - routes/api.py:run_monitor 瘦身为「触发爬虫 -> 调管道 -> 提交」。 - models.py:to_dict() 的 offset 改为读取时用 calculate_offset(latest_time) 实时计算,不再读 offset 列。该列是写入时刻的快照,采集一停摆就整体失真 (库里停在 2026-02-06,offset 却仍显示“当天”)。列保留但已废弃, 不执行 ALTER TABLE DROP COLUMN。 入库语义(db_ingest): - 只有爬虫拿到真实业务时间才更新主表 latest_time,拿不到就保留上一个有效值 (冻结),不再回退到 current_time。 - DeviceHistory 单独用 history_time:主表回答“数据到什么时候”,历史回答 “什么时候采过”。 - 历史表 json_data 改存本次增量切片,不再复制主表那份越滚越大的累积 JSON。 - 主表 json_data 改为覆盖式更新以切断无限膨胀,但保留 APP_OWNED_KEYS (bound_iccid / is_whitelist)—— 这两个键由 /bind_device_card 和 /toggle_whitelist 写入,按原方案直接覆盖会清空所有设备-流量卡绑定。
2026-09-15 17:05:38 +08:00
"""
把各种格式的记录时间解析成 date,失败返回 None。
兼容:2026-09-15 08:30:00 / 2026_09_15 / 2026-09-15T08:30:00Z /
2026/09/15 08:30 / 2026-09-15 08:30 / 2026-09-15
refactor: 抽出统一入库管道,合并定时与手动两条重复路径 问题:app.py:auto_monitor_job(定时)和 routes/api.py:run_monitor(手动) 各自维护了一份几乎相同但细节不一致的写入逻辑,同一条数据经不同入口落库后 latest_time / source / offset / file_count 可能不同。 - 新增 services/time_utils.py:calculate_offset 下沉到无依赖模块,避免 db_ingest 与 routes.api 互相 import 形成循环。routes/api.py 里 re-export 一次,保证 app.py 原有的 from routes.api import calculate_offset 不失效。 - 新增 services/db_ingest.py:ingest_device_data(),承载全部入库细节,只 add/flush 不 commit,事务边界交给调用方。 - routes/api.py:run_monitor 瘦身为「触发爬虫 -> 调管道 -> 提交」。 - models.py:to_dict() 的 offset 改为读取时用 calculate_offset(latest_time) 实时计算,不再读 offset 列。该列是写入时刻的快照,采集一停摆就整体失真 (库里停在 2026-02-06,offset 却仍显示“当天”)。列保留但已废弃, 不执行 ALTER TABLE DROP COLUMN。 入库语义(db_ingest): - 只有爬虫拿到真实业务时间才更新主表 latest_time,拿不到就保留上一个有效值 (冻结),不再回退到 current_time。 - DeviceHistory 单独用 history_time:主表回答“数据到什么时候”,历史回答 “什么时候采过”。 - 历史表 json_data 改存本次增量切片,不再复制主表那份越滚越大的累积 JSON。 - 主表 json_data 改为覆盖式更新以切断无限膨胀,但保留 APP_OWNED_KEYS (bound_iccid / is_whitelist)—— 这两个键由 /bind_device_card 和 /toggle_whitelist 写入,按原方案直接覆盖会清空所有设备-流量卡绑定。
2026-09-15 17:05:38 +08:00
"""
if value is None:
return None
text = str(value).strip()
if not text or text.upper() in _NEVER_SYNCED:
return None
match = _DATE_RE.search(text)
if not match:
return None
refactor: 抽出统一入库管道,合并定时与手动两条重复路径 问题:app.py:auto_monitor_job(定时)和 routes/api.py:run_monitor(手动) 各自维护了一份几乎相同但细节不一致的写入逻辑,同一条数据经不同入口落库后 latest_time / source / offset / file_count 可能不同。 - 新增 services/time_utils.py:calculate_offset 下沉到无依赖模块,避免 db_ingest 与 routes.api 互相 import 形成循环。routes/api.py 里 re-export 一次,保证 app.py 原有的 from routes.api import calculate_offset 不失效。 - 新增 services/db_ingest.py:ingest_device_data(),承载全部入库细节,只 add/flush 不 commit,事务边界交给调用方。 - routes/api.py:run_monitor 瘦身为「触发爬虫 -> 调管道 -> 提交」。 - models.py:to_dict() 的 offset 改为读取时用 calculate_offset(latest_time) 实时计算,不再读 offset 列。该列是写入时刻的快照,采集一停摆就整体失真 (库里停在 2026-02-06,offset 却仍显示“当天”)。列保留但已废弃, 不执行 ALTER TABLE DROP COLUMN。 入库语义(db_ingest): - 只有爬虫拿到真实业务时间才更新主表 latest_time,拿不到就保留上一个有效值 (冻结),不再回退到 current_time。 - DeviceHistory 单独用 history_time:主表回答“数据到什么时候”,历史回答 “什么时候采过”。 - 历史表 json_data 改存本次增量切片,不再复制主表那份越滚越大的累积 JSON。 - 主表 json_data 改为覆盖式更新以切断无限膨胀,但保留 APP_OWNED_KEYS (bound_iccid / is_whitelist)—— 这两个键由 /bind_device_card 和 /toggle_whitelist 写入,按原方案直接覆盖会清空所有设备-流量卡绑定。
2026-09-15 17:05:38 +08:00
try:
return date(int(match.group(1)), int(match.group(2)), int(match.group(3)))
except ValueError:
# 例如 2026-13-45 这种越界日期
return None
def staleness_of(value):
"""
统一的滞后判定。
返回 {'level': str, 'days': int|None, 'text': str}
level —— unknown / ok / yesterday / lagging / severe
days —— 滞后自然日数,unknown 时为 None
text —— 中文描述,可直接展示
"""
if value is None or str(value).strip().upper() in _NEVER_SYNCED:
return {'level': LEVEL_UNKNOWN, 'days': None, 'text': '从未同步'}
record_date = parse_record_date(value)
if record_date is None:
return {'level': LEVEL_UNKNOWN, 'days': None, 'text': '时间解析失败'}
days = (datetime.now().date() - record_date).days
if days < 0:
# 数据时间在未来(设备/服务端时钟超前),按当天处理,不显示负数
days = 0
if days == 0:
return {'level': LEVEL_OK, 'days': 0, 'text': '当天'}
if days == 1:
return {'level': LEVEL_YESTERDAY, 'days': 1, 'text': '滞后 1 天'}
if days <= SEVERE_THRESHOLD_DAYS:
return {'level': LEVEL_LAGGING, 'days': days, 'text': f'滞后 {days} 天'}
return {'level': LEVEL_SEVERE, 'days': days, 'text': f'滞后 {days} 天'}
def calculate_offset(latest_time_str):
"""
(兼容保留) 返回滞后天数的中文描述文本。
新代码请直接用 staleness_of(),需要分级时不要再从这段文本里反解。
"""
return staleness_of(latest_time_str)['text']