import os import sys import json import logging import mimetypes from datetime import datetime from flask import Flask, send_from_directory, jsonify from flask_cors import CORS from flask_apscheduler import APScheduler # ============================================================================== # ✅ 1. 核心模块引用 # ============================================================================== try: # 数据库实例 (在根目录 extensions.py 中) from extensions import db # 数据模型 (在根目录 models.py 中) from models import Device, DeviceHistory, MaintenanceLog # 核心业务逻辑 (在 services/core.py 中) from services.core import execute_monitor_task # 路由蓝图 (在 routes/api.py 中) try: from routes.api import api_bp as device_bp except ImportError: from routes.api import device_bp # 工具函数 (在 routes/api.py 中) from routes.api import calculate_offset except ImportError as e: print(f"❌ 严重错误: 模块导入失败。请检查文件名和变量名。详细信息: {e}") print(f"系统路径: {sys.path}") sys.exit(1) # ============================================================================== # 2. 路径计算模块 (兼容 PyInstaller 打包) # ============================================================================== def get_base_path(): """获取运行时基准路径,兼容开发环境和打包环境""" if getattr(sys, 'frozen', False): if hasattr(sys, '_MEIPASS'): return sys._MEIPASS # --onefile 模式 else: return os.path.dirname(os.path.abspath(sys.executable)) # --onedir 模式 else: return os.path.abspath(os.path.dirname(__file__)) BASE_DIR = get_base_path() STATIC_FOLDER = os.path.join(BASE_DIR, 'web_dist') INSTANCE_FOLDER = os.path.join(BASE_DIR, 'instance') DB_PATH = os.path.join(INSTANCE_FOLDER, 'devices.db') # 修复 Windows 下注册表 MIME 类型缺失导致网页白屏的问题 mimetypes.add_type('application/javascript', '.js') mimetypes.add_type('text/css', '.css') print(f"🚀 运行环境: {'Packaged' if getattr(sys, 'frozen', False) else 'Dev'}") print(f"📂 基准路径: {BASE_DIR}") print(f"💾 数据库路径: {DB_PATH}") # ============================================================================== # 3. 定时任务逻辑 # ============================================================================== def auto_monitor_job(app): """定时任务具体执行逻辑""" with app.app_context(): print(f"⏰ [定时任务] 启动时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") if not execute_monitor_task: print("❌ 错误: 无法加载爬虫核心模块 (execute_monitor_task is Missing)") return try: # 执行爬取 task_result = execute_monitor_task() if not task_result: print("⚠️ [定时任务] 爬虫未获取到数据") return 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 # 查找或创建设备 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() # 更新字段 device.status = item.get('status') device.current_value = item.get('value') device.latest_time = item.get('target_time') device.check_time = current_time device.json_data = json.dumps(item.get('raw_json', {}), ensure_ascii=False) device.offset = calculate_offset(item.get('target_time')) # 写入历史 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 db.session.commit() print(f"✅ [定时任务] 成功更新 {count} 台设备状态") except Exception as e: db.session.rollback() print(f"❌ [定时任务] 异常: {str(e)}") # ============================================================================== # 4. Flask 应用工厂 # ============================================================================== def create_app(): # 🔴 关键修复:移除了 static_url_path='' # 这样 Flask 就不会强制拦截所有根路径请求,让下面的 serve_static 有机会处理 /dashboard app = Flask(__name__, static_folder=STATIC_FOLDER) CORS(app) # 确保 instance 目录存在 if not os.path.exists(INSTANCE_FOLDER): os.makedirs(INSTANCE_FOLDER, exist_ok=True) app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{DB_PATH}' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['SCHEDULER_API_ENABLED'] = True # 初始化数据库 db.init_app(app) # 初始化定时任务 scheduler = APScheduler() scheduler.init_app(app) scheduler.start() # 添加定时任务 (每天 10:00) scheduler.add_job( id='daily_monitor_task', func=auto_monitor_job, args=[app], trigger='cron', hour=10, minute=0 ) # 注册蓝图 app.register_blueprint(device_bp) # ------------------------------------------------- # 前端路由支持 (Vue History Mode) # ------------------------------------------------- @app.route('/') def serve_index(): if not os.path.exists(os.path.join(app.static_folder, 'index.html')): return "❌ 错误: 前端文件丢失 (web_dist/index.html)", 404 return send_from_directory(app.static_folder, 'index.html') @app.route('/') def serve_static(path): # 1. 优先尝试直接返回实际存在的文件 (js, css, img等) file_path = os.path.join(app.static_folder, path) if os.path.exists(file_path): return send_from_directory(app.static_folder, path) # 2. 如果是 API 请求但没找到对应接口,返回 404 JSON (不返回 HTML) if path.startswith('api') or path.startswith('static'): return jsonify({'code': 404, 'message': 'Not Found'}), 404 # 3. 关键逻辑: # 访问 /dashboard 等前端路由时,文件系统中并没有 dashboard 这个文件 # 所以会走到这里,返回 index.html,让 Vue 及其 Router 接管页面渲染 return send_from_directory(app.static_folder, 'index.html') with app.app_context(): db.create_all() return app if __name__ == '__main__': app = create_app() # 生产环境/打包环境通常设为 False debug_mode = not getattr(sys, 'frozen', False) print("🚀 服务启动中...") app.run(host='0.0.0.0', port=5000, debug=debug_mode, use_reloader=False)