修改新增加文件数量的查询功能
This commit is contained in:
236
2_1banben/app.py
236
2_1banben/app.py
@ -2,7 +2,10 @@ import os
|
||||
import sys
|
||||
import json
|
||||
import mimetypes
|
||||
import logging
|
||||
from datetime import datetime
|
||||
import pytz # ✅ 必须引入:用于强制指定北京时间
|
||||
|
||||
from flask import Flask, send_from_directory, jsonify
|
||||
from flask_cors import CORS
|
||||
from flask_apscheduler import APScheduler
|
||||
@ -11,171 +14,161 @@ from flask_apscheduler import APScheduler
|
||||
# ✅ 1. 核心模块引用
|
||||
# ==============================================================================
|
||||
try:
|
||||
# 导入配置类
|
||||
from config import Config
|
||||
|
||||
# 数据库实例
|
||||
from extensions import db
|
||||
|
||||
# 数据模型
|
||||
from models import Device, DeviceHistory
|
||||
|
||||
# 核心业务逻辑 (爬虫)
|
||||
from services.core import execute_monitor_task
|
||||
|
||||
# 核心业务逻辑 (IoT) - 用于定时任务
|
||||
from services.iot_api import sync_iot_data_service
|
||||
# from services.iot_api import sync_iot_data_service # 如果不需要IoT可以注释
|
||||
|
||||
# 路由蓝图
|
||||
try:
|
||||
from routes.api import api_bp as device_bp
|
||||
# 导入保存逻辑,供定时任务复用
|
||||
from routes.api import save_iot_cards_to_db, calculate_offset
|
||||
from routes.api import calculate_offset
|
||||
except ImportError:
|
||||
from routes.api import device_bp, save_iot_cards_to_db, calculate_offset
|
||||
from routes.api import device_bp, calculate_offset
|
||||
|
||||
except ImportError as e:
|
||||
print(f"❌ 严重错误: 模块导入失败。请检查文件名和变量名。详细信息: {e}")
|
||||
print(f"❌ 严重错误: 模块导入失败。详细信息: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 2. 路径计算 (核心修复:区分资源路径和数据路径)
|
||||
# 2. 路径配置
|
||||
# ==============================================================================
|
||||
|
||||
def get_paths():
|
||||
"""
|
||||
计算关键路径:
|
||||
1. resource_base: 用于存放 web_dist (打包后在临时目录)
|
||||
2. data_base: 用于存放数据库 (打包后在 exe 旁边)
|
||||
"""
|
||||
if getattr(sys, 'frozen', False):
|
||||
# --- 打包环境 (PyInstaller) ---
|
||||
|
||||
# 资源文件在临时解压目录 (sys._MEIPASS)
|
||||
resource_base = sys._MEIPASS
|
||||
|
||||
# 数据文件(数据库)在 exe 所在目录 (sys.executable 的父目录)
|
||||
data_base = os.path.dirname(sys.executable)
|
||||
else:
|
||||
# --- 开发环境 ---
|
||||
base = os.path.abspath(os.path.dirname(__file__))
|
||||
resource_base = base
|
||||
data_base = base
|
||||
|
||||
return resource_base, data_base
|
||||
|
||||
|
||||
# 获取路径
|
||||
RESOURCE_BASE, DATA_BASE = get_paths()
|
||||
|
||||
# 定义具体文件夹路径
|
||||
STATIC_FOLDER = os.path.join(RESOURCE_BASE, 'web_dist')
|
||||
# ⚠️ 关键:强制将 instance 文件夹定位到数据目录 (exe旁边),而不是临时目录
|
||||
INSTANCE_PATH = os.path.join(DATA_BASE, 'instance')
|
||||
|
||||
# 修复 Windows MIME 类型
|
||||
mimetypes.add_type('application/javascript', '.js')
|
||||
mimetypes.add_type('text/css', '.css')
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 3. 定时任务逻辑 (保持不变)
|
||||
# 3. 核心定时任务逻辑 (加强版)
|
||||
# ==============================================================================
|
||||
def auto_monitor_job(app):
|
||||
"""定时任务具体执行逻辑"""
|
||||
"""
|
||||
每天 12:00 触发的爬虫任务
|
||||
"""
|
||||
# ✅ 强制使用应用上下文,确保数据库连接有效
|
||||
with app.app_context():
|
||||
print(f"⏰ [定时任务] 启动时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
# 获取当前北京时间用于日志
|
||||
tz = pytz.timezone('Asia/Shanghai')
|
||||
now_str = datetime.now(tz).strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
# --- 任务 A: 爬虫更新 ---
|
||||
if execute_monitor_task:
|
||||
try:
|
||||
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 = 0
|
||||
for item in scraped_list:
|
||||
d_name = item.get('name')
|
||||
if not d_name: continue
|
||||
print(f"\n{'=' * 50}")
|
||||
print(f"⏰ [定时任务触发] 北京时间: {now_str}")
|
||||
print(f"🚀 正在开始执行爬虫逻辑...")
|
||||
|
||||
device = Device.query.filter_by(name=d_name).first()
|
||||
if not device:
|
||||
device = Device(name=d_name, source=item.get('source'), install_site="")
|
||||
db.session.add(device)
|
||||
db.session.flush()
|
||||
if not execute_monitor_task:
|
||||
print("❌ 错误: 未找到爬虫执行函数 (execute_monitor_task)")
|
||||
return
|
||||
|
||||
device.status = item.get('status')
|
||||
device.current_value = item.get('value')
|
||||
device.latest_time = item.get('target_time')
|
||||
device.check_time = current_time
|
||||
try:
|
||||
# 1. 执行爬取
|
||||
task_result = execute_monitor_task()
|
||||
|
||||
# =========== ✅ 核心修复开始:防止丢失 bound_iccid ===========
|
||||
if not task_result:
|
||||
print("⚠️ [警告] 爬虫执行完毕,但返回空数据 (None)")
|
||||
return
|
||||
|
||||
# 1. 准备容器:先读取数据库里现有的 JSON 数据
|
||||
old_json = {}
|
||||
try:
|
||||
if device.json_data:
|
||||
old_json = json.loads(device.json_data)
|
||||
except Exception:
|
||||
old_json = {}
|
||||
scraped_list = task_result.get('device_list', [])
|
||||
print(f"📦 [数据获取] 爬虫返回了 {len(scraped_list)} 条设备数据")
|
||||
|
||||
# 2. 获取爬虫新数据
|
||||
new_json = item.get('raw_json', {})
|
||||
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
success_count = 0
|
||||
|
||||
# 3. 合并数据:只更新新爬取到的字段,保留 old_json 里的 bound_iccid
|
||||
if isinstance(new_json, dict):
|
||||
old_json.update(new_json)
|
||||
# 2. 遍历入库
|
||||
for item in scraped_list:
|
||||
d_name = item.get('name')
|
||||
if not d_name: continue
|
||||
|
||||
# 4. 存回数据库
|
||||
device.json_data = json.dumps(old_json, ensure_ascii=False)
|
||||
# 查找或新建设备
|
||||
device = Device.query.filter_by(name=d_name).first()
|
||||
if not device:
|
||||
print(f"🆕 发现新设备: {d_name},正在创建...")
|
||||
device = Device(name=d_name, source=item.get('source'), install_site="")
|
||||
db.session.add(device)
|
||||
db.session.flush() # 立即获取 ID
|
||||
|
||||
# =========== ✅ 核心修复结束 ===========
|
||||
# 更新设备状态表
|
||||
device.status = item.get('status')
|
||||
device.current_value = item.get('value')
|
||||
device.latest_time = item.get('target_time')
|
||||
device.check_time = current_time # 更新检查时间证明爬过了
|
||||
|
||||
device.offset = calculate_offset(item.get('target_time'))
|
||||
f_count = item.get('num_files', 0)
|
||||
device.file_count = f_count
|
||||
|
||||
db.session.add(DeviceHistory(
|
||||
device_id=device.id,
|
||||
status=device.status,
|
||||
result_data=device.current_value,
|
||||
data_time=item.get('target_time'),
|
||||
json_data=device.json_data
|
||||
))
|
||||
count += 1
|
||||
print(f"✅ [定时任务-爬虫] 更新 {count} 台")
|
||||
else:
|
||||
print("⚠️ [定时任务-爬虫] 未获取到数据")
|
||||
except Exception as e:
|
||||
print(f"❌ [定时任务-爬虫] 异常: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
# 计算 offset
|
||||
device.offset = calculate_offset(item.get('target_time'))
|
||||
|
||||
# JSON 字段合并逻辑
|
||||
old_json = {}
|
||||
try:
|
||||
if device.json_data:
|
||||
old_json = json.loads(device.json_data)
|
||||
except:
|
||||
old_json = {}
|
||||
|
||||
new_json = item.get('raw_json', {})
|
||||
if isinstance(new_json, dict):
|
||||
old_json.update(new_json)
|
||||
device.json_data = json.dumps(old_json, ensure_ascii=False)
|
||||
|
||||
# ✅ 3. 写入历史记录 (这是数据留存的关键)
|
||||
history_entry = DeviceHistory(
|
||||
device_id=device.id,
|
||||
status=device.status,
|
||||
result_data=device.current_value,
|
||||
data_time=item.get('target_time'), # 文件的时间
|
||||
json_data=device.json_data,
|
||||
file_count=f_count,
|
||||
create_time=datetime.now() # 记录入库时的系统时间
|
||||
)
|
||||
db.session.add(history_entry)
|
||||
success_count += 1
|
||||
|
||||
# ✅ 4. 显式提交事务
|
||||
print(f"💾 正在提交事务到数据库...")
|
||||
db.session.commit()
|
||||
print(f"✅ [成功] 已更新 {success_count} 台设备,并写入历史记录。")
|
||||
print(f"{'=' * 50}\n")
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback() # 出错回滚
|
||||
print(f"❌ [严重异常] 定时任务执行失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 4. Flask 应用工厂
|
||||
# ==============================================================================
|
||||
def create_app():
|
||||
# ⚠️ 关键修改:显式传入 instance_path,告诉 Flask 去哪里找/存 数据库文件
|
||||
app = Flask(__name__, static_folder=STATIC_FOLDER, instance_path=INSTANCE_PATH)
|
||||
CORS(app)
|
||||
|
||||
# 1. 确保 instance 目录存在 (在 exe 旁边创建文件夹)
|
||||
# 数据库路径配置
|
||||
if not os.path.exists(app.instance_path):
|
||||
try:
|
||||
os.makedirs(app.instance_path, exist_ok=True)
|
||||
print(f"📁 已创建数据目录: {app.instance_path}")
|
||||
except OSError as e:
|
||||
print(f"❌ 无法创建数据目录 {app.instance_path}: {e}")
|
||||
os.makedirs(app.instance_path, exist_ok=True)
|
||||
|
||||
# 2. 加载配置
|
||||
app.config.from_object(Config)
|
||||
|
||||
# ⚠️ 关键修改:强制重写数据库 URI,确保使用绝对路径
|
||||
# 即使 Config 里写了,这里也要确保它指向我们刚才计算出的 INSTANCE_PATH
|
||||
db_name = 'monitor_data.db' # 你的数据库文件名
|
||||
db_name = 'monitor_data.db'
|
||||
db_path = os.path.join(app.instance_path, db_name)
|
||||
|
||||
# Windows下路径分隔符处理,防止报错
|
||||
if sys.platform.startswith('win'):
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}'
|
||||
else:
|
||||
@ -183,36 +176,43 @@ def create_app():
|
||||
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
|
||||
print(f"💾 数据库路径锁定为: {db_path}")
|
||||
# ✅ APScheduler 配置
|
||||
app.config['SCHEDULER_API_ENABLED'] = True
|
||||
app.config['SCHEDULER_TIMEZONE'] = "Asia/Shanghai" # 全局时区设置
|
||||
|
||||
# 3. 初始化扩展
|
||||
db.init_app(app)
|
||||
|
||||
scheduler = APScheduler()
|
||||
scheduler.init_app(app)
|
||||
scheduler.start()
|
||||
|
||||
# 4. 添加定时任务 (每天 12:00)
|
||||
# ✅ 添加定时任务 (针对常开机环境的最稳配置)
|
||||
scheduler.add_job(
|
||||
id='daily_monitor_task',
|
||||
func=auto_monitor_job,
|
||||
args=[app],
|
||||
trigger='cron',
|
||||
hour=12,
|
||||
minute=0
|
||||
hour=12, # 每天 12 点
|
||||
minute=0,
|
||||
second=0,
|
||||
misfire_grace_time=3600, # 允许延迟1小时执行
|
||||
timezone=pytz.timezone('Asia/Shanghai') # 再次强制指定时区
|
||||
)
|
||||
|
||||
# 5. 注册路由蓝图
|
||||
# 打印一下确认任务已添加
|
||||
print(f"📅 定时任务已锁定: 每天北京时间 12:00 执行")
|
||||
|
||||
app.register_blueprint(device_bp)
|
||||
|
||||
# -------------------------------------------------
|
||||
# 前端路由支持
|
||||
# -------------------------------------------------
|
||||
# 手动触发测试接口 (保留以备不时之需)
|
||||
@app.route('/api/force_run')
|
||||
def force_run_task():
|
||||
auto_monitor_job(app)
|
||||
return jsonify({'code': 200, 'msg': '手动触发成功'})
|
||||
|
||||
# 前端路由
|
||||
@app.route('/')
|
||||
def serve_index():
|
||||
index_path = os.path.join(app.static_folder, 'index.html')
|
||||
if not os.path.exists(index_path):
|
||||
return f"❌ 错误: 前端文件丢失 ({index_path})", 404
|
||||
return send_from_directory(app.static_folder, 'index.html')
|
||||
|
||||
@app.route('/<path:path>')
|
||||
@ -220,14 +220,11 @@ def create_app():
|
||||
file_path = os.path.join(app.static_folder, path)
|
||||
if os.path.exists(file_path):
|
||||
return send_from_directory(app.static_folder, path)
|
||||
|
||||
if path.startswith('api') or path.startswith('static'):
|
||||
if path.startswith('api'):
|
||||
return jsonify({'code': 404, 'message': 'Not Found'}), 404
|
||||
|
||||
return send_from_directory(app.static_folder, 'index.html')
|
||||
|
||||
with app.app_context():
|
||||
# 自动创建表结构
|
||||
db.create_all()
|
||||
|
||||
return app
|
||||
@ -236,6 +233,9 @@ def create_app():
|
||||
if __name__ == '__main__':
|
||||
app = create_app()
|
||||
debug_mode = not getattr(sys, 'frozen', False)
|
||||
print("🚀 服务启动中...")
|
||||
# use_reloader=False 防止定时任务执行两次
|
||||
|
||||
print("🚀 服务启动中 (24小时常驻模式)...")
|
||||
|
||||
# ✅ 关键设置: use_reloader=False
|
||||
# 防止 Flask 的热重载功能启动两个进程,导致定时任务跑两遍或者被意外杀掉
|
||||
app.run(host='0.0.0.0', port=5000, debug=debug_mode, use_reloader=False)
|
||||
Reference in New Issue
Block a user