import os import sys import json 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: # [新增] 导入配置类 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 # 路由蓝图 try: from routes.api import api_bp as device_bp # [新增] 导入保存逻辑,供定时任务复用 from routes.api import save_iot_cards_to_db, calculate_offset except ImportError: from routes.api import device_bp, save_iot_cards_to_db, calculate_offset except ImportError as e: print(f"❌ 严重错误: 模块导入失败。请检查文件名和变量名。详细信息: {e}") sys.exit(1) # ============================================================================== # 2. 路径计算 (辅助静态文件服务) # ============================================================================== # 注意:Config 类中已经处理了数据库路径,这里主要处理 web_dist 静态资源路径 def get_base_path(): if getattr(sys, 'frozen', False): if hasattr(sys, '_MEIPASS'): return sys._MEIPASS else: return os.path.dirname(os.path.abspath(sys.executable)) 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') # 修复 Windows MIME 类型 mimetypes.add_type('application/javascript', '.js') mimetypes.add_type('text/css', '.css') # ============================================================================== # 3. 定时任务逻辑 (同时运行 爬虫 + IoT同步) # ============================================================================== def auto_monitor_job(app): """定时任务具体执行逻辑""" with app.app_context(): print(f"⏰ [定时任务] 启动时间: {datetime.now().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 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 print(f"✅ [定时任务-爬虫] 更新 {count} 台") else: print("⚠️ [定时任务-爬虫] 未获取到数据") except Exception as e: print(f"❌ [定时任务-爬虫] 异常: {e}") # --- 任务 B: IoT 同步 (新增) --- if sync_iot_data_service: try: # 1. 获取数据 iot_list = sync_iot_data_service() # 2. 保存入库 (复用 api.py 中的逻辑) count_iot, err = save_iot_cards_to_db(iot_list) if err: print(f"❌ [定时任务-IoT] 错误: {err}") else: print(f"✅ [定时任务-IoT] 更新 {count_iot} 张") except Exception as e: print(f"❌ [定时任务-IoT] 异常: {e}") # 统一提交事务 try: db.session.commit() print("💾 [定时任务] 数据库事务提交完成") except Exception as e: db.session.rollback() print(f"❌ [定时任务] 提交失败: {e}") # ============================================================================== # 4. Flask 应用工厂 # ============================================================================== def create_app(): # 指定静态文件夹 app = Flask(__name__, static_folder=STATIC_FOLDER) CORS(app) # 1. 确保 instance 目录存在 if not os.path.exists(INSTANCE_FOLDER): os.makedirs(INSTANCE_FOLDER, exist_ok=True) # ========================================================== # ✅ 2. 核心修复:加载 config.py 中的配置 # ========================================================== app.config.from_object(Config) # 打印一下关键配置,确保 IoT 配置已加载 (调试用) # print(f"DEBUG Config Loaded: IOT_APP_ID={app.config.get('IOT_APP_ID')}") # 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 ) # 5. 注册路由蓝图 app.register_blueprint(device_bp) # ------------------------------------------------- # 前端路由支持 # ------------------------------------------------- @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): 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'): 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 if __name__ == '__main__': app = create_app() debug_mode = not getattr(sys, 'frozen', False) print("🚀 服务启动中...") # 注意:use_reloader=False 防止定时任务执行两次 app.run(host='0.0.0.0', port=5000, debug=debug_mode, use_reloader=False)