Files
ZDXX/2_3banben/app.py

224 lines
7.8 KiB
Python

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 <token>'\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('/<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)
# 如果不是静态文件请求,也不是 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)