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 # ============================================================================== # ✅ 1. 核心模块引用 # ============================================================================== try: from config import Config from extensions import db from models import Device, DeviceHistory from services.core import execute_monitor_task try: from services.iot_api import sync_iot_data_service except ImportError: sync_iot_data_service = None try: from routes.api import api_bp as device_bp from routes.api import calculate_offset except ImportError: from routes.api import device_bp, calculate_offset except ImportError as e: print(f"❌ 严重错误: 模块导入失败。详细信息: {e}") sys.exit(1) # ============================================================================== # 2. 智能路径配置 (适配 PyInstaller 的 _internal 和 _MEIPASS) # ============================================================================== RESOURCE_BASE = Config.BASE_DIR INSTANCE_PATH = Config.INSTANCE_DIR def find_static_folder(base_path): """ 全能路径搜寻逻辑,按优先级查找 web_dist """ # 1. PyInstaller 打包后的特殊路径 if getattr(sys, 'frozen', False): if hasattr(sys, '_MEIPASS'): mei_path = os.path.join(sys._MEIPASS, 'web_dist') if os.path.exists(os.path.join(mei_path, 'index.html')): return mei_path internal_path = os.path.join(base_path, '_internal', 'web_dist') if os.path.exists(os.path.join(internal_path, 'index.html')): return internal_path # 2. 当前目录 (exe 同级) path = os.path.join(base_path, 'web_dist') if os.path.exists(os.path.join(path, 'index.html')): return path # 3. 开发环境上一级 parent_path = os.path.join(os.path.dirname(base_path), 'web_dist') if os.path.exists(os.path.join(parent_path, 'index.html')): return parent_path return path STATIC_FOLDER = find_static_folder(RESOURCE_BASE) mimetypes.add_type('application/javascript', '.js') mimetypes.add_type('text/css', '.css') # ============================================================================== # 3. 核心定时任务逻辑 # ============================================================================== def auto_monitor_job(app): """ 每天 17:00 触发的爬虫任务。 修复:移除不匹配的 create_time 字段,并确保 Session 清理。 """ with app.app_context(): tz = pytz.timezone('Asia/Shanghai') now_str = datetime.now(tz).strftime('%Y-%m-%d %H:%M:%S') print(f"\n{'=' * 50}") print(f"⏰ [定时任务触发] 北京时间: {now_str}") if not execute_monitor_task: return try: task_result = execute_monitor_task() if not task_result: print("⚠️ [警告] 爬虫执行完毕,但返回空数据") return scraped_list = task_result.get('device_list', []) print(f"📦 [数据获取] 爬取到 {len(scraped_list)} 条设备数据") current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") stats = {'new_device': 0, 'history_added': 0} for item in scraped_list: d_name = item.get('name') if not d_name: continue f_count = item.get('num_files', 0) target_date = item.get('target_time') # A. 更新 Device 表 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() stats['new_device'] += 1 device.status = item.get('status') device.current_value = item.get('value') device.latest_time = target_date device.check_time = current_time device.file_count = f_count device.offset = calculate_offset(target_date) # 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) # B. 新增 History 记录 # [修复点] 移除了 create_time 参数,防止报错 history_entry = DeviceHistory( device_id=device.id, status=item.get('status'), result_data=item.get('value'), data_time=target_date, json_data=device.json_data, file_count=f_count # create_time=datetime.now() # 已删除:你的 models.py 中没有定义这个字段 ) db.session.add(history_entry) stats['history_added'] += 1 db.session.flush() db.session.commit() print(f"✅ [入库成功] 新增设备: {stats['new_device']} | 新增历史: {stats['history_added']}") except Exception as e: db.session.rollback() print(f"❌ [严重异常] 数据写入失败: {e}") finally: db.session.remove() print(f"{'=' * 50}\n") # ============================================================================== # 4. Flask 应用工厂 # ============================================================================== def create_app(): # 调试路径 print(f"🔍 [前端路径锁定] {STATIC_FOLDER}") if not os.path.exists(os.path.join(STATIC_FOLDER, 'index.html')): print(f"❌ [严重警告] 仍然无法找到 index.html,请检查 PyInstaller 是否将 web_dist 打包进了 _internal 目录。") app = Flask(__name__, static_folder=STATIC_FOLDER, instance_path=INSTANCE_PATH) CORS(app) if not os.path.exists(app.instance_path): os.makedirs(app.instance_path, exist_ok=True) app.config.from_object(Config) db.init_app(app) scheduler = APScheduler() scheduler.init_app(app) scheduler.start() scheduler.add_job( id='daily_monitor_task', func=auto_monitor_job, args=[app], trigger='cron', hour=17, minute=0, second=0, misfire_grace_time=3600, timezone=pytz.timezone('Asia/Shanghai') ) print(f"📅 定时任务已锁定: 每天北京时间 17: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(): try: return send_from_directory(app.static_folder, 'index.html') except Exception: return "

错误:找不到前端文件

", 404 @app.route('/') def serve_static(path): 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'): return jsonify({'code': 404, 'message': 'API endpoint not found'}), 404 try: return send_from_directory(app.static_folder, 'index.html') except Exception: return "Frontend not found", 404 with app.app_context(): db.create_all() return app if __name__ == '__main__': app = create_app() debug_mode = not getattr(sys, 'frozen', False) print(f"🚀 服务启动中... 数据库: {app.config['SQLALCHEMY_DATABASE_URI']}") app.run(host='0.0.0.0', port=5000, debug=debug_mode, use_reloader=False)