2026-01-09 12:48:50 +08:00
|
|
|
|
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:
|
2026-01-13 14:50:23 +08:00
|
|
|
|
# [新增] 导入配置类
|
|
|
|
|
|
from config import Config
|
|
|
|
|
|
|
|
|
|
|
|
# 数据库实例
|
2026-01-09 12:48:50 +08:00
|
|
|
|
from extensions import db
|
|
|
|
|
|
|
2026-01-13 14:50:23 +08:00
|
|
|
|
# 数据模型
|
|
|
|
|
|
from models import Device, DeviceHistory
|
2026-01-09 12:48:50 +08:00
|
|
|
|
|
2026-01-13 14:50:23 +08:00
|
|
|
|
# 核心业务逻辑 (爬虫)
|
2026-01-09 12:48:50 +08:00
|
|
|
|
from services.core import execute_monitor_task
|
|
|
|
|
|
|
2026-01-13 14:50:23 +08:00
|
|
|
|
# [新增] 核心业务逻辑 (IoT) - 用于定时任务
|
|
|
|
|
|
from services.iot_api import sync_iot_data_service
|
|
|
|
|
|
|
|
|
|
|
|
# 路由蓝图
|
2026-01-09 12:48:50 +08:00
|
|
|
|
try:
|
|
|
|
|
|
from routes.api import api_bp as device_bp
|
2026-01-13 14:50:23 +08:00
|
|
|
|
# [新增] 导入保存逻辑,供定时任务复用
|
|
|
|
|
|
from routes.api import save_iot_cards_to_db, calculate_offset
|
2026-01-09 12:48:50 +08:00
|
|
|
|
except ImportError:
|
2026-01-13 14:50:23 +08:00
|
|
|
|
from routes.api import device_bp, save_iot_cards_to_db, calculate_offset
|
2026-01-09 12:48:50 +08:00
|
|
|
|
|
|
|
|
|
|
except ImportError as e:
|
|
|
|
|
|
print(f"❌ 严重错误: 模块导入失败。请检查文件名和变量名。详细信息: {e}")
|
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ==============================================================================
|
2026-01-13 14:50:23 +08:00
|
|
|
|
# 2. 路径计算 (辅助静态文件服务)
|
2026-01-09 12:48:50 +08:00
|
|
|
|
# ==============================================================================
|
2026-01-13 14:50:23 +08:00
|
|
|
|
# 注意:Config 类中已经处理了数据库路径,这里主要处理 web_dist 静态资源路径
|
2026-01-09 12:48:50 +08:00
|
|
|
|
def get_base_path():
|
|
|
|
|
|
if getattr(sys, 'frozen', False):
|
|
|
|
|
|
if hasattr(sys, '_MEIPASS'):
|
2026-01-13 14:50:23 +08:00
|
|
|
|
return sys._MEIPASS
|
2026-01-09 12:48:50 +08:00
|
|
|
|
else:
|
2026-01-13 14:50:23 +08:00
|
|
|
|
return os.path.dirname(os.path.abspath(sys.executable))
|
2026-01-09 12:48:50 +08:00
|
|
|
|
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')
|
|
|
|
|
|
|
2026-01-13 14:50:23 +08:00
|
|
|
|
# 修复 Windows MIME 类型
|
2026-01-09 12:48:50 +08:00
|
|
|
|
mimetypes.add_type('application/javascript', '.js')
|
|
|
|
|
|
mimetypes.add_type('text/css', '.css')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ==============================================================================
|
2026-01-13 14:50:23 +08:00
|
|
|
|
# 3. 定时任务逻辑 (同时运行 爬虫 + IoT同步)
|
2026-01-09 12:48:50 +08:00
|
|
|
|
# ==============================================================================
|
|
|
|
|
|
def auto_monitor_job(app):
|
|
|
|
|
|
"""定时任务具体执行逻辑"""
|
|
|
|
|
|
with app.app_context():
|
|
|
|
|
|
print(f"⏰ [定时任务] 启动时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
|
|
|
|
|
|
2026-01-13 14:50:23 +08:00
|
|
|
|
# --- 任务 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}")
|
|
|
|
|
|
|
|
|
|
|
|
# 统一提交事务
|
2026-01-09 12:48:50 +08:00
|
|
|
|
try:
|
|
|
|
|
|
db.session.commit()
|
2026-01-13 14:50:23 +08:00
|
|
|
|
print("💾 [定时任务] 数据库事务提交完成")
|
2026-01-09 12:48:50 +08:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
db.session.rollback()
|
2026-01-13 14:50:23 +08:00
|
|
|
|
print(f"❌ [定时任务] 提交失败: {e}")
|
2026-01-09 12:48:50 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ==============================================================================
|
|
|
|
|
|
# 4. Flask 应用工厂
|
|
|
|
|
|
# ==============================================================================
|
|
|
|
|
|
def create_app():
|
2026-01-13 14:50:23 +08:00
|
|
|
|
# 指定静态文件夹
|
2026-01-09 12:48:50 +08:00
|
|
|
|
app = Flask(__name__, static_folder=STATIC_FOLDER)
|
|
|
|
|
|
CORS(app)
|
|
|
|
|
|
|
2026-01-13 14:50:23 +08:00
|
|
|
|
# 1. 确保 instance 目录存在
|
2026-01-09 12:48:50 +08:00
|
|
|
|
if not os.path.exists(INSTANCE_FOLDER):
|
|
|
|
|
|
os.makedirs(INSTANCE_FOLDER, exist_ok=True)
|
|
|
|
|
|
|
2026-01-13 14:50:23 +08:00
|
|
|
|
# ==========================================================
|
|
|
|
|
|
# ✅ 2. 核心修复:加载 config.py 中的配置
|
|
|
|
|
|
# ==========================================================
|
|
|
|
|
|
app.config.from_object(Config)
|
|
|
|
|
|
|
|
|
|
|
|
# 打印一下关键配置,确保 IoT 配置已加载 (调试用)
|
|
|
|
|
|
# print(f"DEBUG Config Loaded: IOT_APP_ID={app.config.get('IOT_APP_ID')}")
|
2026-01-09 12:48:50 +08:00
|
|
|
|
|
2026-01-13 14:50:23 +08:00
|
|
|
|
# 3. 初始化扩展
|
2026-01-09 12:48:50 +08:00
|
|
|
|
db.init_app(app)
|
|
|
|
|
|
|
|
|
|
|
|
scheduler = APScheduler()
|
|
|
|
|
|
scheduler.init_app(app)
|
|
|
|
|
|
scheduler.start()
|
|
|
|
|
|
|
2026-01-13 14:50:23 +08:00
|
|
|
|
# 4. 添加定时任务 (每天 12:00)
|
2026-01-09 12:48:50 +08:00
|
|
|
|
scheduler.add_job(
|
|
|
|
|
|
id='daily_monitor_task',
|
|
|
|
|
|
func=auto_monitor_job,
|
|
|
|
|
|
args=[app],
|
|
|
|
|
|
trigger='cron',
|
2026-01-12 15:57:34 +08:00
|
|
|
|
hour=12,
|
2026-01-09 12:48:50 +08:00
|
|
|
|
minute=0
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-13 14:50:23 +08:00
|
|
|
|
# 5. 注册路由蓝图
|
2026-01-09 12:48:50 +08:00
|
|
|
|
app.register_blueprint(device_bp)
|
|
|
|
|
|
|
|
|
|
|
|
# -------------------------------------------------
|
2026-01-13 14:50:23 +08:00
|
|
|
|
# 前端路由支持
|
2026-01-09 12:48:50 +08:00
|
|
|
|
# -------------------------------------------------
|
|
|
|
|
|
@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):
|
|
|
|
|
|
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("🚀 服务启动中...")
|
2026-01-13 14:50:23 +08:00
|
|
|
|
# 注意:use_reloader=False 防止定时任务执行两次
|
2026-01-09 12:48:50 +08:00
|
|
|
|
app.run(host='0.0.0.0', port=5000, debug=debug_mode, use_reloader=False)
|