Files
ZDXX/2_1banben/app.py
2026-01-14 14:42:33 +08:00

239 lines
8.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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. 路径计算 (核心修复:区分资源路径和数据路径)
# ==============================================================================
def get_paths():
"""
计算关键路径:
1. resource_base: 用于存放 web_dist (打包后在临时目录)
2. data_base: 用于存放数据库 (打包后在 exe 旁边)
"""
if getattr(sys, 'frozen', False):
# --- 打包环境 (PyInstaller) ---
# 资源文件在临时解压目录 (sys._MEIPASS)
resource_base = sys._MEIPASS
# 数据文件(数据库)在 exe 所在目录 (sys.executable 的父目录)
data_base = os.path.dirname(sys.executable)
else:
# --- 开发环境 ---
base = os.path.abspath(os.path.dirname(__file__))
resource_base = base
data_base = base
return resource_base, data_base
# 获取路径
RESOURCE_BASE, DATA_BASE = get_paths()
# 定义具体文件夹路径
STATIC_FOLDER = os.path.join(RESOURCE_BASE, 'web_dist')
# ⚠️ 关键:强制将 instance 文件夹定位到数据目录 (exe旁边),而不是临时目录
INSTANCE_PATH = os.path.join(DATA_BASE, 'instance')
# 修复 Windows MIME 类型
mimetypes.add_type('application/javascript', '.js')
mimetypes.add_type('text/css', '.css')
# ==============================================================================
# 3. 定时任务逻辑 (保持不变)
# ==============================================================================
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():
# ⚠️ 关键修改:显式传入 instance_path告诉 Flask 去哪里找/存 数据库文件
app = Flask(__name__, static_folder=STATIC_FOLDER, instance_path=INSTANCE_PATH)
CORS(app)
# 1. 确保 instance 目录存在 (在 exe 旁边创建文件夹)
if not os.path.exists(app.instance_path):
try:
os.makedirs(app.instance_path, exist_ok=True)
print(f"📁 已创建数据目录: {app.instance_path}")
except OSError as e:
print(f"❌ 无法创建数据目录 {app.instance_path}: {e}")
# 2. 加载配置
app.config.from_object(Config)
# ⚠️ 关键修改:强制重写数据库 URI确保使用绝对路径
# 即使 Config 里写了,这里也要确保它指向我们刚才计算出的 INSTANCE_PATH
db_name = 'monitor_data.db' # 你的数据库文件名
db_path = os.path.join(app.instance_path, db_name)
# Windows下路径分隔符处理防止报错
if sys.platform.startswith('win'):
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}'
else:
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:////{db_path}'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
print(f"💾 数据库路径锁定为: {db_path}")
# 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():
index_path = os.path.join(app.static_folder, 'index.html')
if not os.path.exists(index_path):
return f"❌ 错误: 前端文件丢失 ({index_path})", 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("🚀 服务启动中...")
# use_reloader=False 防止定时任务执行两次
app.run(host='0.0.0.0', port=5000, debug=debug_mode, use_reloader=False)