Files
ZDXX/2_1banben/app.py

241 lines
8.8 KiB
Python
Raw Normal View History

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-14 14:42:33 +08:00
# 导入配置类
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-14 14:42:33 +08:00
# 核心业务逻辑 (IoT) - 用于定时任务
2026-01-13 14:50:23 +08:00
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-14 14:42:33 +08:00
# 导入保存逻辑,供定时任务复用
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-14 14:42:33 +08:00
# 2. 路径计算 (核心修复:区分资源路径和数据路径)
2026-01-09 12:48:50 +08:00
# ==============================================================================
2026-01-14 14:42:33 +08:00
def get_paths():
"""
计算关键路径:
1. resource_base: 用于存放 web_dist (打包后在临时目录)
2. data_base: 用于存放数据库 (打包后在 exe 旁边)
"""
2026-01-09 12:48:50 +08:00
if getattr(sys, 'frozen', False):
2026-01-14 14:42:33 +08:00
# --- 打包环境 (PyInstaller) ---
# 资源文件在临时解压目录 (sys._MEIPASS)
resource_base = sys._MEIPASS
# 数据文件(数据库)在 exe 所在目录 (sys.executable 的父目录)
data_base = os.path.dirname(sys.executable)
2026-01-09 12:48:50 +08:00
else:
2026-01-14 14:42:33 +08:00
# --- 开发环境 ---
base = os.path.abspath(os.path.dirname(__file__))
resource_base = base
data_base = base
return resource_base, data_base
2026-01-09 12:48:50 +08:00
2026-01-14 14:42:33 +08:00
# 获取路径
RESOURCE_BASE, DATA_BASE = get_paths()
2026-01-09 12:48:50 +08:00
2026-01-14 14:42:33 +08:00
# 定义具体文件夹路径
STATIC_FOLDER = os.path.join(RESOURCE_BASE, 'web_dist')
# ⚠️ 关键:强制将 instance 文件夹定位到数据目录 (exe旁边),而不是临时目录
INSTANCE_PATH = os.path.join(DATA_BASE, 'instance')
2026-01-09 12:48:50 +08:00
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-14 14:42:33 +08:00
# 3. 定时任务逻辑 (保持不变)
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
# =========== ✅ 核心修复开始:防止丢失 bound_iccid ===========
# 1. 准备容器:先读取数据库里现有的 JSON 数据
old_json = {}
try:
if device.json_data:
old_json = json.loads(device.json_data)
except Exception:
old_json = {}
# 2. 获取爬虫新数据
new_json = item.get('raw_json', {})
# 3. 合并数据:只更新新爬取到的字段,保留 old_json 里的 bound_iccid
if isinstance(new_json, dict):
old_json.update(new_json)
# 4. 存回数据库
device.json_data = json.dumps(old_json, ensure_ascii=False)
# =========== ✅ 核心修复结束 ===========
2026-01-13 14:50:23 +08:00
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}")
import traceback
traceback.print_exc()
2026-01-09 12:48:50 +08:00
# ==============================================================================
# 4. Flask 应用工厂
# ==============================================================================
def create_app():
2026-01-14 14:42:33 +08:00
# ⚠️ 关键修改:显式传入 instance_path,告诉 Flask 去哪里找/存 数据库文件
app = Flask(__name__, static_folder=STATIC_FOLDER, instance_path=INSTANCE_PATH)
2026-01-09 12:48:50 +08:00
CORS(app)
2026-01-14 14:42:33 +08:00
# 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}")
2026-01-09 12:48:50 +08:00
2026-01-14 14:42:33 +08:00
# 2. 加载配置
2026-01-13 14:50:23 +08:00
app.config.from_object(Config)
2026-01-14 14:42:33 +08:00
# ⚠️ 关键修改:强制重写数据库 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}")
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():
2026-01-14 14:42:33 +08:00
index_path = os.path.join(app.static_folder, 'index.html')
if not os.path.exists(index_path):
return f"❌ 错误: 前端文件丢失 ({index_path})", 404
2026-01-09 12:48:50 +08:00
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():
2026-01-14 14:42:33 +08:00
# 自动创建表结构
2026-01-09 12:48:50 +08:00
db.create_all()
return app
if __name__ == '__main__':
app = create_app()
debug_mode = not getattr(sys, 'frozen', False)
print("🚀 服务启动中...")
2026-01-14 14:42:33 +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)