refactor: split flask app structure

This commit is contained in:
2026-07-06 14:41:16 +08:00
parent c2198aad51
commit a75e857c71
18 changed files with 1871 additions and 1390 deletions
+116
View File
@@ -0,0 +1,116 @@
from __future__ import annotations
import logging
from datetime import datetime
from flask import Flask, abort, jsonify, request
from .config import Config, DATA_DIR, ensure_dirs
from .extensions import db, login_manager
from .models import User
from .prediction import FEATURES, load_model
from .security import csrf_token, validate_csrf_token
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 datetime.now().year
return {
"feature_list": FEATURES,
"now_year": now_year,
"csrf_token": csrf_token,
"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":
return jsonify({"error": "CSRF 校验失败,请刷新页面后重试。"}), 400
abort(400)