import os from datetime import timedelta import logging from logging.handlers import TimedRotatingFileHandler class Config: """基础配置类""" # SECRET_KEY必须从环境变量获取,生产环境不允许使用默认值 SECRET_KEY = os.environ.get('SECRET_KEY') if not SECRET_KEY: import sys print("警告: SECRET_KEY未设置,使用不安全的默认值。生产环境必须设置SECRET_KEY环境变量!", file=sys.stderr) SECRET_KEY = 'dev-secret-key-change-in-production' # 数据库配置 - 优先使用环境变量中的DATABASE_URL import os # 使用绝对路径确保数据库文件能够正确创建 instance_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'instance') if not os.path.exists(instance_dir): os.makedirs(instance_dir) SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL', f'sqlite:///{os.path.join(instance_dir, "kamaxitong.db")}') SQLALCHEMY_TRACK_MODIFICATIONS = False # SQLAlchemy 2.0 兼容性设置和连接池优化 SQLALCHEMY_ENGINE_OPTIONS = { "future": True, "pool_pre_ping": True, # 连接前检查连接是否有效 "pool_size": int(os.environ.get('DB_POOL_SIZE', 10)), # 连接池大小 "max_overflow": int(os.environ.get('DB_MAX_OVERFLOW', 20)), # 最大溢出连接数 "pool_recycle": int(os.environ.get('DB_POOL_RECYCLE', 3600)), # 连接回收时间(秒) "pool_timeout": int(os.environ.get('DB_POOL_TIMEOUT', 30)), # 获取连接超时时间(秒) "echo": False, # 关闭SQL日志输出(生产环境) "connect_args": { "charset": "utf8mb4", "use_unicode": True, } if os.environ.get('DATABASE_URL', '').startswith('mysql') else {} } # 系统基本配置 SITE_NAME = os.environ.get('SITE_NAME') or '软件授权管理系统' ADMIN_EMAIL = os.environ.get('ADMIN_EMAIL') or '' # 前端域名配置 - 支持多种环境变量 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] # 验证器配置 - 生产环境必须设置 AUTH_SECRET_KEY = os.environ.get('AUTH_SECRET_KEY') if not AUTH_SECRET_KEY: import sys print("严重错误: AUTH_SECRET_KEY未设置!生产环境必须设置AUTH_SECRET_KEY环境变量!", file=sys.stderr) sys.exit(1) OFFLINE_CACHE_DAYS = int(os.environ.get('OFFLINE_CACHE_DAYS', 7)) # 离线缓存天数 MAX_FAILED_ATTEMPTS = int(os.environ.get('MAX_FAILED_ATTEMPTS', 5)) # 最大失败次数 LOCKOUT_MINUTES = int(os.environ.get('LOCKOUT_MINUTES', 10)) # 锁定时间(分钟) MAX_UNBIND_TIMES = int(os.environ.get('MAX_UNBIND_TIMES', 3)) # 最大解绑次数 # 卡密配置 LICENSE_KEY_LENGTH = int(os.environ.get('LICENSE_KEY_LENGTH', 32)) # 卡密长度 LICENSE_KEY_PREFIX = os.environ.get('LICENSE_KEY_PREFIX', '') # 卡密前缀 TRIAL_PREFIX = os.environ.get('TRIAL_PREFIX') or 'TRIAL_' # 试用卡密前缀 # API配置 API_VERSION = os.environ.get('API_VERSION') or 'v1' ITEMS_PER_PAGE = int(os.environ.get('ITEMS_PER_PAGE', 20)) # 文件上传配置 - 增加到500MB MAX_CONTENT_LENGTH = int(os.environ.get('MAX_CONTENT_LENGTH', 500 * 1024 * 1024)) # 500MB UPLOAD_FOLDER = os.environ.get('UPLOAD_FOLDER') or 'static/uploads' # 会话配置 - 增强的会话管理 PERMANENT_SESSION_LIFETIME = timedelta(hours=int(os.environ.get('SESSION_LIFETIME_HOURS', 168))) # 默认延长到7天 # 添加更多会话相关配置 - 支持域名访问 SESSION_COOKIE_SECURE = os.environ.get('SESSION_COOKIE_SECURE', 'False').lower() == 'true' # 在生产环境中设为True SESSION_COOKIE_HTTPONLY = os.environ.get('SESSION_COOKIE_HTTPONLY', 'True').lower() == 'true' SESSION_COOKIE_SAMESITE = os.environ.get('SESSION_COOKIE_SAMESITE', 'Lax') # 改为Lax以提高兼容性 # 如果配置了域名,设置Cookie域 if FRONTEND_DOMAIN: SESSION_COOKIE_DOMAIN = FRONTEND_DOMAIN # 记住我功能配置 REMEMBER_COOKIE_DURATION = timedelta(days=int(os.environ.get('REMEMBER_COOKIE_DURATION', 30))) # 记住我功能持续30天 REMEMBER_COOKIE_SECURE = os.environ.get('REMEMBER_COOKIE_SECURE', 'False').lower() == 'true' # 生产环境中设为True REMEMBER_COOKIE_HTTPONLY = os.environ.get('REMEMBER_COOKIE_HTTPONLY', 'True').lower() == 'true' REMEMBER_COOKIE_SAMESITE = os.environ.get('REMEMBER_COOKIE_SAMESITE', 'Lax') # 如果配置了域名,设置记住我Cookie域 if FRONTEND_DOMAIN: REMEMBER_COOKIE_DOMAIN = FRONTEND_DOMAIN # 日志配置 LOG_LEVEL = os.environ.get('LOG_LEVEL', 'INFO') LOG_FILE = os.environ.get('LOG_FILE', 'logs/kamaxitong.log') # 支付宝配置 ALIPAY_APP_ID = os.environ.get('ALIPAY_APP_ID', '') # 支付宝应用ID ALIPAY_PRIVATE_KEY = os.environ.get('ALIPAY_PRIVATE_KEY', '') # 应用私钥 ALIPAY_PUBLIC_KEY = os.environ.get('ALIPAY_PUBLIC_KEY', '') # 支付宝公钥 ALIPAY_ALIPAY_PUBLIC_KEY = os.environ.get('ALIPAY_ALIPAY_PUBLIC_KEY', '') # 支付宝平台公钥 ALIPAY_GATEWAY = os.environ.get('ALIPAY_GATEWAY', 'https://openapi.alipay.com/gateway.do') # 支付宝网关地址 ALIPAY_SIGN_TYPE = 'RSA2' # 签名算法 ALIPAY_CHARSET = 'utf-8' # 编码格式 ALIPAY_VERSION = '1.0' # API版本号 ALIPAY_NOTIFY_URL = os.environ.get('ALIPAY_NOTIFY_URL', '') # 异步通知URL ALIPAY_RETURN_URL = os.environ.get('ALIPAY_RETURN_URL', '') # 同步返回URL ALIPAY_TIMEOUT_EXPRESS = int(os.environ.get('ALIPAY_TIMEOUT_EXPRESS', 30)) # 支付超时时间(分钟) PAYMENT_ENABLED = os.environ.get('PAYMENT_ENABLED', 'False').lower() == 'true' # 是否启用支付功能 @staticmethod def init_app(app): """初始化应用配置""" # 确保日志目录存在 import os if not os.path.exists('logs'): os.mkdir('logs') # 配置日志 - 使用安全的日志处理器,避免Windows文件锁定问题 import logging from logging.handlers import TimedRotatingFileHandler import time class SafeTimedRotatingFileHandler(TimedRotatingFileHandler): """安全的日志轮转处理器,解决Windows文件锁定问题""" def doRollover(self): """重写轮转方法,添加错误处理""" try: super().doRollover() except PermissionError as e: # Windows文件锁定错误,静默处理 import sys print(f"日志轮转被跳过(文件被占用): {str(e)}", file=sys.stderr) except Exception as e: import sys print(f"日志轮转错误: {str(e)}", file=sys.stderr) # 使用安全的日志处理器 file_handler = SafeTimedRotatingFileHandler( 'logs/kamaxitong.log', when='midnight', interval=1, backupCount=10, encoding='utf-8' ) file_handler.setFormatter(logging.Formatter( '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]' )) file_handler.setLevel(logging.INFO) # 清除可能已存在的handler(避免重复) app.logger.handlers.clear() app.logger.addHandler(file_handler) app.logger.setLevel(logging.INFO) app.logger.info('KaMiXiTong startup') class DevelopmentConfig(Config): """开发环境配置""" DEBUG = True SQLALCHEMY_ECHO = False # 关闭SQL日志输出以提高性能,需要调试时可临时开启 class ProductionConfig(Config): """生产环境配置""" DEBUG = False SQLALCHEMY_ECHO = False @staticmethod def init_app(app): # 只调用父类的init_app,避免重复配置日志 Config.init_app(app) class TestingConfig(Config): """测试环境配置""" TESTING = True SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:' WTF_CSRF_ENABLED = False # 为内存数据库禁用连接池配置 SQLALCHEMY_ENGINE_OPTIONS = { "future": True, "echo": False, } # 配置字典 config = { 'development': DevelopmentConfig, 'production': ProductionConfig, 'testing': TestingConfig, 'default': DevelopmentConfig }