188 lines
7.1 KiB
Python
188 lines
7.1 KiB
Python
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):
|
||
"""将换行符转换为<br>标签的过滤器"""
|
||
if not text:
|
||
return ""
|
||
import re
|
||
# 转义HTML特殊字符,然后将换行符转换为<br>标签
|
||
escaped_text = text.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"').replace("'", ''')
|
||
return escaped_text.replace('\n', '<br>').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 |