224 lines
7.8 KiB
Python
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) |