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 config import Config # 引入扩展 from extensions import db, jwt, scheduler # 引入模型 (确保 create_all 能扫描到) from models import Device, DeviceHistory, User # 引入 API 蓝图和工具 try: from routes.api import api_bp, calculate_offset except ImportError: api_bp = None calculate_offset = None # 引入爬虫服务 try: from services.core import execute_monitor_task except ImportError: execute_monitor_task = None # 注册 MIME 类型 (防止前端 JS/CSS 加载报 404 或类型错误) mimetypes.add_type('application/javascript', '.js') mimetypes.add_type('text/css', '.css') # --- 定时任务逻辑 (保持不变) --- 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("❌ 错误: 爬虫模块未加载") 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) if calculate_offset: 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)}") # --- App 工厂 --- def create_app(): app = Flask(__name__, static_folder=Config.STATIC_FOLDER) # 1. 加载配置 (包含数据库、JWT、爬虫配置) app.config.from_object(Config) # 2. 初始化扩展 db.init_app(app) jwt.init_app(app) scheduler.init_app(app) # 3. 配置 CORS (允许 Authorization 头,解决 401/422 的关键) # 允许所有来源,允许凭证,允许关键 Header CORS(app, resources={r"/*": {"origins": "*"}}, supports_credentials=True, allow_headers=["Content-Type", "Authorization", "X-Requested-With"]) # 4. 注册蓝图 if api_bp: app.register_blueprint(api_bp) # ========================================== # 5. JWT 详细错误处理 (调试核心部分) # ========================================== # A. 没带 Token 或者 Header 格式不对 @jwt.unauthorized_loader def missing_token(error_string): print(f"\n🔴 [JWT ERROR] 请求被拒绝: 缺少 Token 或格式错误") print(f" 原因详情: {error_string}") print(f" 提示: 前端 header 必须是 'Authorization: Bearer '\n") return jsonify({ "code": 401, "message": "Missing Authorization Header", "detail": error_string }), 401 # B. Token 是坏的 (签名不对,或者被篡改,或者密钥不匹配) @jwt.invalid_token_loader def invalid_token(error_string): print(f"\n🔴 [JWT ERROR] 请求被拒绝: Token 无效 (Invalid)") print(f" 原因详情: {error_string}") print(f" 排查: 1. 后端密钥可能变了 2. Token 是旧的 3. 复制粘贴错了\n") return jsonify({ "code": 401, "message": "Invalid Token", "detail": error_string }), 401 # C. Token 过期了 @jwt.expired_token_loader def expired_token(jwt_header, jwt_payload): print(f"\n🔴 [JWT ERROR] 请求被拒绝: Token 已过期 (Expired)") print(f" 过期 Token 内容: {jwt_payload}") print(f" 当前服务器时间: {datetime.now()}") print(f" 提示: 请检查 config.py 里的有效期设置,或校准服务器时间\n") return jsonify({ "code": 401, "message": "Token has expired", "detail": "token_expired" }), 401 # ========================================== # 6. 启动定时任务 if execute_monitor_task: # 防止重复添加任务 if not scheduler.get_job('daily_monitor_task'): scheduler.add_job( id='daily_monitor_task', func=auto_monitor_job, args=[app], trigger='cron', hour=12, minute=0 ) if not scheduler.running: scheduler.start() # 7. 静态文件路由 @app.route('/') def serve_index(): if not os.path.exists(os.path.join(app.static_folder, 'index.html')): return "Web files not found. Please build frontend first.", 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) # 如果不是静态文件请求,也不是 api 请求,就返回 index.html (前端路由) if path.startswith('api'): return jsonify({'code': 404, 'msg': 'API endpoint not found'}), 404 return send_from_directory(app.static_folder, 'index.html') # 8. 初始化数据库和默认管理员 with app.app_context(): # db.create_all() 会根据 binds 配置自动创建 users.db 和 devices.db db.create_all() try: # 检查是否有管理员,没有则创建 if not User.query.filter_by(username='admin').first(): print("🛠️ 正在创建默认管理员账号...") admin = User(username='admin', role='admin') admin.set_password('licahk') db.session.add(admin) db.session.commit() print("✅ 初始管理员已创建: admin / licahk") except Exception as e: # 捕获数据库连接错误等 print(f"⚠️ 初始化数据警告 (可能是首次运行或表结构变更): {e}") return app if __name__ == '__main__': # 确保在主程序块中运行 app = create_app() # 判断是否为打包后的环境 debug_mode = not getattr(sys, 'frozen', False) print(f"\n🚀 服务启动中...") print(f" 模式: {'Debug (开发)' if debug_mode else 'Production (生产)'}") print(f" 端口: 5000") print(f" 密钥检查: {app.config.get('JWT_SECRET_KEY')[:5]}*** (请确保重启后这里不变)\n") app.run(host='0.0.0.0', port=5000, debug=debug_mode, use_reloader=False)