import os # 尝试加载.env文件(在配置之前加载,确保环境变量生效) try: from dotenv import load_dotenv if load_dotenv(): print("成功加载.env文件") else: print("未找到或无法加载.env文件") except ImportError: print("python-dotenv未安装,跳过.env文件加载") from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_login import LoginManager from flask_wtf.csrf import CSRFProtect from flask_cors import CORS from flask_migrate import Migrate from config import config import logging from logging.handlers import RotatingFileHandler # 初始化扩展 db = SQLAlchemy() login_manager = LoginManager() csrf = CSRFProtect() cors = CORS() migrate = Migrate() def nl2br_filter(text): """将换行符转换为
标签的过滤器""" if not text: return "" import re # 转义HTML特殊字符,然后将换行符转换为
标签 escaped_text = text.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"').replace("'", ''') return escaped_text.replace('\n', '
').replace('\r', '') def format_date_filter(date_string): """格式化日期时间的过滤器""" if not date_string: return "-" try: from datetime import datetime import re # 支持多种日期格式 if isinstance(date_string, str): # 尝试解析常见的日期时间格式 formats = [ "%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M:%S.%f", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M:%S.%f", "%Y-%m-%d" ] parsed_date = None for fmt in formats: try: parsed_date = datetime.strptime(date_string, fmt) break except ValueError: continue if parsed_date is None: # 如果所有格式都无法解析,尝试使用 dateutil 解析 try: from dateutil import parser parsed_date = parser.parse(date_string) except: return date_string # 返回原始字符串 else: parsed_date = date_string # 格式化为中文日期时间格式 return parsed_date.strftime("%Y年%m月%d日 %H:%M:%S") except Exception as e: # 出现任何错误都返回原始值 return str(date_string) def create_app(config_name=None): # 如果没有指定配置名称,从环境变量获取或使用默认值 if config_name is None: config_name = os.environ.get('FLASK_ENV', 'default') app = Flask(__name__, template_folder=os.path.join(os.path.dirname(os.path.dirname(__file__)), 'app', 'web', 'templates'), static_folder=os.path.join(os.path.dirname(os.path.dirname(__file__)), 'static')) # 注册自定义过滤器 app.jinja_env.filters['nl2br'] = nl2br_filter app.jinja_env.filters['format_date'] = format_date_filter # 先应用配置对象,再初始化配置 app.config.from_object(config[config_name]) config[config_name].init_app(app) # 特别确保MAX_CONTENT_LENGTH配置正确应用 max_content_length = app.config.get('MAX_CONTENT_LENGTH', 16 * 1024 * 1024) app.config['MAX_CONTENT_LENGTH'] = max_content_length print(f"Setting MAX_CONTENT_LENGTH to: {max_content_length} bytes ({max_content_length / (1024*1024)} MB)") # 动态设置前端域名配置(支持多种环境变量) frontend_domain = os.environ.get('FRONTEND_DOMAIN') or os.environ.get('DOMAIN_NAME') or os.environ.get('SERVER_NAME') or '' # 确保域名不包含协议部分 if frontend_domain.startswith(('http://', 'https://')): frontend_domain = frontend_domain.split('://', 1)[1] app.config['FRONTEND_DOMAIN'] = frontend_domain print(f"Frontend domain set to: {frontend_domain}") # 初始化扩展 db.init_app(app) login_manager.init_app(app) migrate.init_app(app, db) csrf.init_app(app) # 配置CORS以支持域名访问 - 包括登录页面 cors.init_app(app, resources={ r"/api/*": { "origins": ["https://km.taisan.online", "http://km.taisan.online", "http://localhost:5088", "http://127.0.0.1:5088"], "methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"], "allow_headers": ["Content-Type", "Authorization"] }, r"/login": { "origins": ["https://km.taisan.online", "http://km.taisan.online", "http://localhost:5088", "http://127.0.0.1:5088"], "methods": ["GET", "POST", "OPTIONS"], "allow_headers": ["Content-Type", "X-Requested-With"], "supports_credentials": True } }) # 配置登录管理器 login_manager.login_view = 'web.login' # type: ignore login_manager.login_message = '请先登录' login_manager.login_message_category = 'info' login_manager.id_attribute = 'get_id' # 使用 get_id 方法获取用户ID login_manager.session_protection = 'strong' # 启用强会话保护 # 注册蓝图 from app.api import api_bp app.register_blueprint(api_bp, url_prefix=f'/api/{app.config["API_VERSION"]}') # 对API蓝图豁免CSRF保护,因为API有其他认证机制 csrf.exempt(api_bp) from app.web import web_bp, user_bp app.register_blueprint(web_bp) app.register_blueprint(user_bp) # 注册错误处理器 from app.web.views import register_error_handlers register_error_handlers(app) # 配置日志 if not app.debug and not app.testing: # 确保日志目录存在 if not os.path.exists('logs'): os.mkdir('logs') # 配置文件日志处理器 file_handler = RotatingFileHandler('logs/kamaxitong.log', maxBytes=10240, backupCount=10) file_handler.setFormatter(logging.Formatter( '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]' )) file_handler.setLevel(logging.INFO) app.logger.addHandler(file_handler) app.logger.setLevel(logging.INFO) app.logger.info('KaMiXiTong startup') # 初始化后台任务调度器 try: from app.utils.scheduler import init_scheduler, start_scheduler # 初始化调度器(在应用上下文中) with app.app_context(): scheduler = init_scheduler(app) # 仅在非测试环境中启动调度器 if not app.testing and not os.environ.get('DISABLE_SCHEDULER', '').lower() == 'true': start_scheduler() app.logger.info("后台定时任务调度器已启动") else: app.logger.info("后台定时任务调度器已初始化但未启动(测试环境或已禁用)") except Exception as e: # 调度器初始化失败不应该阻止应用启动 app.logger.error(f"初始化定时任务调度器失败: {str(e)}", exc_info=True) return app