from __future__ import annotations import logging from flask import Flask, abort, jsonify, request from .config import Config, DATA_DIR, ensure_dirs from .extensions import db, login_manager from .models import AppSetting, User from .prediction import FEATURES, load_model from .security import csrf_token, validate_csrf_token from .time_utils import current_year_for_timezone, format_datetime_for_timezone def create_app(config_object: type[Config] = Config, *, load_model_on_start: bool = True) -> Flask: ensure_dirs() configure_logging() app = Flask(__name__, template_folder="../templates", static_folder="../static") app.config.from_object(config_object) if app.config.get("SECRET_KEY_GENERATED"): logging.warning("未设置 SECRET_KEY,已生成临时密钥;服务重启后登录会话将失效。") db.init_app(app) login_manager.init_app(app) login_manager.login_view = "main.login" login_manager.login_message = "请先登录后再访问该页面。" register_app_hooks(app) from .routes import bp app.register_blueprint(bp) with app.app_context(): db.create_all() init_admin_user(app) if load_model_on_start: try: app.config["RSF_MODEL"] = load_model(app.config["MODEL_PATH"]) logging.info("模型加载成功") except Exception as exc: app.config["RSF_MODEL"] = None logging.exception("模型加载失败: %s", exc) return app def configure_logging() -> None: logging.basicConfig( filename=str(DATA_DIR / "app.log"), level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s", ) def init_admin_user(app: Flask) -> None: admin_username = app.config["ADMIN_USERNAME"] admin_password = app.config["ADMIN_PASSWORD"] if admin_password: admin = User.query.filter_by(username=admin_username).first() if admin is None: admin = User(username=admin_username, is_admin=True) admin.set_password(admin_password) db.session.add(admin) else: admin.is_admin = True if not admin.check_password(admin_password): admin.set_password(admin_password) db.session.commit() elif not User.query.filter_by(is_admin=True).first(): logging.warning("未设置 ADMIN_PASSWORD,跳过自动创建管理员账号。") default_admin = User.query.filter_by(username="admin", is_admin=True).first() if default_admin and default_admin.check_password("admin123"): logging.warning("检测到默认管理员密码 admin123,请立即通过 ADMIN_PASSWORD 更新。") def register_app_hooks(app: Flask) -> None: @login_manager.user_loader def load_user(user_id: str): try: return db.session.get(User, int(user_id)) except (TypeError, ValueError): return None @app.context_processor def inject_helpers(): def now_year() -> int: return current_year_for_timezone(app.config["APP_TIMEZONE"]) def format_datetime(value) -> str: return format_datetime_for_timezone(value, app.config["APP_TIMEZONE"]) return { "feature_list": FEATURES, "now_year": now_year, "format_datetime": format_datetime, "csrf_token": csrf_token, "allow_registration": AppSetting.get_bool( "allow_registration", app.config["ALLOW_REGISTRATION"], ), } @app.errorhandler(413) def handle_file_too_large(_exc): max_mb = app.config["MAX_CONTENT_LENGTH"] // (1024 * 1024) if request.path == "/predict": return jsonify({"error": f"文件过大,请上传 {max_mb}MB 以内的文件。"}), 413 return f"文件过大,请上传 {max_mb}MB 以内的文件。", 413 @app.before_request def protect_csrf(): if request.method not in {"POST", "PUT", "PATCH", "DELETE"}: return None if validate_csrf_token(): return None if request.path == "/predict" or request.headers.get("X-Requested-With") == "XMLHttpRequest": return jsonify({"error": "CSRF 校验失败,请刷新页面后重试。"}), 400 abort(400)