202 lines
7.2 KiB
Python
202 lines
7.2 KiB
Python
|
|
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('/<path:path>')
|
|||
|
|
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)
|