import os from datetime import timedelta import json from pathlib import Path from typing import Any # 项目根目录 BASE_DIR = os.path.abspath(os.path.dirname(__file__)) def _get_bool_env(key: str, default: bool = False) -> bool: """从环境变量获取布尔值 Args: key: 环境变量名 default: 默认值 Returns: 布尔值 """ value = os.getenv(key) if value is None: return default return value.lower() in ("true", "1", "yes", "on") def _load_env_file(env_dir: str | os.PathLike | None = None) -> bool: """从项目目录加载 .env 文件。""" try: from dotenv import load_dotenv search_dir = Path(env_dir or os.getenv("ITI_ENV_DIR") or os.getcwd()).resolve() env_name = os.getenv("FLASK_ENV", "dev") env_files = [ ".env.local", f".env.{env_name}", ".env", ] for env_file in env_files: env_path = search_dir / env_file if env_path.exists(): load_dotenv(env_path, override=False) return True return False except ImportError: return False # 在定义配置类之前加载环境变量 _load_env_file() class BaseConfig: """基础配置类 - 所有环境共享的配置""" BASE_DIR = BASE_DIR # 应用配置 SECRET_KEY = os.getenv("SECRET_KEY", "dev-secret-key-change-in-production") # 数据库配置 SQLALCHEMY_ENGINE_OPTIONS = { "json_serializer": lambda obj: json.dumps(obj, ensure_ascii=False), # "json_deserializer": json.loads, } SQLALCHEMY_TRACK_MODIFICATIONS = False SQLALCHEMY_RECORD_QUERIES = True SQLALCHEMY_ECHO = False SQLALCHEMY_COMMIT_ON_TEARDOWN = True SQLALCHEMY_SHOW_ERROR_DETAILS = True SQLALCHEMY_DATABASE_URI = os.getenv( "DATABASE_URL", f"sqlite:///{BASE_DIR}/runtime/iti-flask.db" ) # JWT 配置 JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "dev-jwt-secret-key") JWT_TOKEN_LOCATION = ["headers"] JWT_HEADER_NAME = "Authorization" JWT_HEADER_TYPE = "Bearer" JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=1) JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=30) # 跨域配置 CORS_ORIGINS = ["*"] # 限流配置 RATELIMIT_ENABLED = True RATELIMIT_KEY_PREFIX = "iti_rate_limit_" RATELIMIT_STORAGE_URL = "memory://" RATELIMIT_STORAGE_URI = "memory://" RATELIMIT_DEFAULT = "200 per hour" # JSON 序列化配置 JSON_ENSURE_ASCII = False # 支持中文,不转义为 Unicode JSON_SORT_KEYS = False # 不对 key 排序 # 文件上传配置 MAX_CONTENT_LENGTH = 200 * 1024 * 1024 # 200MB,Flask 上传大小限制 # 缓存配置 CACHE_SIMPLE = { # 类型 NullCache | SimpleCache | FileSystemCache | RedisCache | RedisSentinelCache | RedisClusterCache | UWSGICache | MemcachedCache | SASLMemcachedCache | SpreadSASLMemcachedCache "ENABLED": True, "CACHE_TYPE": "SimpleCache", "CACHE_NO_NULL_WARNING": False, "CACHE_ARGS": [], "CACHE_OPTIONS": None, "CACHE_DEFAULT_TIMEOUT": 0, "CACHE_IGNORE_ERRORS": False, "CACHE_THRESHOLD": 500, "CACHE_KEY_PREFIX": "iti_cache_", "CACHE_SOURCE_CHECK": False, # # UWSGI 配置 # "CACHE_UWSGI_NAME": "mycache@localhost:3031", # # MEMCACHED 配置 # "CACHE_MEMCACHED_SERVERS": ["localhost:11211"], # "CACHE_MEMCACHED_USERNAME": "None", # "CACHE_MEMCACHED_PASSWORD": "None", # # REDIS 配置 # "CACHE_REDIS_URL": "redis://localhost:6379/2", # "CACHE_REDIS_HOST": "localhost", # "CACHE_REDIS_PORT": 6379, # "CACHE_REDIS_PASSWORD": None, # "CACHE_REDIS_DB": 0, # "CACHE_REDIS_SENTINELS": ["localhost:26379"], # "CACHE_REDIS_SENTINEL_MASTER": "mymaster", # "CACHE_REDIS_CLUSTER": "localhost:6379,localhost:6380,localhost:6381", # # FILE-SYSTEM 配置 # "CACHE_DIR": r"/tmp/iti_cache", } # Redis缓存配置 CACHE_REDIS = { "ENABLED": False, "CACHE_TYPE": "RedisCache", "CACHE_NO_NULL_WARNING": False, "CACHE_ARGS": [], "CACHE_OPTIONS": None, "CACHE_DEFAULT_TIMEOUT": 0, "CACHE_IGNORE_ERRORS": False, "CACHE_THRESHOLD": 500, "CACHE_KEY_PREFIX": "iti_cache_", "CACHE_SOURCE_CHECK": False, # REDIS 配置 # "CACHE_REDIS_URL": "redis://localhost:6379/0", "CACHE_REDIS_HOST": "localhost", "CACHE_REDIS_PORT": 6379, "CACHE_REDIS_PASSWORD": None, "CACHE_REDIS_DB": 0, # "CACHE_REDIS_SENTINELS": ["localhost:26379"], # "CACHE_REDIS_SENTINEL_MASTER": "mymaster", # "CACHE_REDIS_CLUSTER": "localhost:6379,localhost:6380,localhost:6381", } # 文件存储配置 FILE_STORAGE = { "DEFAULT_STORAGE_TYPE": "local", "MAX_FILE_SIZE": 100 * 1024 * 1024 * 1024, # 100GB "TUS_CHUNK_SIZE": 5 * 1024 * 1024, # 5MB # 本地存储配置 "LOCAL": { "base_path": os.path.join(BASE_DIR, "runtime", "uploads"), }, # 阿里云OSS配置 "ALIYUN_OSS": { "access_key_id": os.getenv("ALIYUN_OSS_ACCESS_KEY_ID"), "access_key_secret": os.getenv("ALIYUN_OSS_ACCESS_KEY_SECRET"), "endpoint": os.getenv( "ALIYUN_OSS_ENDPOINT" ), # 例如:oss-cn-hangzhou.aliyuncs.com "bucket": os.getenv("ALIYUN_OSS_BUCKET"), }, # 腾讯云COS配置 "TENCENT_COS": { "secret_id": os.getenv("TENCENT_COS_SECRET_ID"), "secret_key": os.getenv("TENCENT_COS_SECRET_KEY"), "region": os.getenv("TENCENT_COS_REGION"), # 例如:ap-guangzhou "bucket": os.getenv("TENCENT_COS_BUCKET"), }, # 七牛云Kodo配置 "QINIU_KODO": { "access_key": os.getenv("QINIU_KODO_ACCESS_KEY"), "secret_key": os.getenv("QINIU_KODO_SECRET_KEY"), "bucket": os.getenv("QINIU_KODO_BUCKET"), "domain": os.getenv("QINIU_KODO_DOMAIN"), # CDN域名 }, # 华为云OBS配置 "HUAWEI_OBS": { "access_key_id": os.getenv("HUAWEI_OBS_ACCESS_KEY_ID"), "secret_access_key": os.getenv("HUAWEI_OBS_SECRET_ACCESS_KEY"), "server": os.getenv( "HUAWEI_OBS_SERVER" ), # 例如:obs.cn-north-4.myhuaweicloud.com "bucket": os.getenv("HUAWEI_OBS_BUCKET"), }, } # 是否启用业务项目自带 SPA 静态文件承载。 # 框架不内置前端产物;启用后从业务项目 FRONTEND_PATH 读取文件。 FRONTEND_ENABLED = _get_bool_env("FRONTEND_ENABLED", False) FRONTEND_PATH = os.getenv("FRONTEND_PATH", "") # 服务调用配置。业务项目按需添加具体服务。 SERVICES: dict[str, dict[str, Any]] = {} # 轻量任务配置。多进程部署时只应在一个专用实例启用。 TASKS_ENABLED = False class DevConfig(BaseConfig): """开发环境配置""" DEBUG = True TESTING = False # 开发环境数据库 SQLALCHEMY_DATABASE_URI = os.getenv( "DATABASE_URL", f"sqlite:///{BASE_DIR}/runtime/iti-flask_dev.db" ) SQLALCHEMY_ECHO = True # 开发环境打印 SQL SQLALCHEMY_SHOW_ERROR_DETAILS = True # JSON 配置(开发环境:格式化输出,方便调试) JSON_INDENT = 2 # 缩进 2 个空格 JSON_SEPARATORS = (", ", ": ") # 使用空格分隔 # JWT 开发环境配置 JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=24) # 开发环境延长有效期 # JWT_ACCESS_TOKEN_EXPIRES = timedelta(seconds=5) # 测试过期 # 限流配置(开发环境较宽松) RATELIMIT_ENABLED = True RATELIMIT_DEFAULT = "1000 per hour" class TestConfig(BaseConfig): """测试环境配置""" DEBUG = False TESTING = True # 测试数据库(使用内存数据库) SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:" SQLALCHEMY_ECHO = False SQLALCHEMY_SHOW_ERROR_DETAILS = False # JSON 配置(测试环境:与生产一致) JSON_INDENT = None # 不缩进 JSON_SEPARATORS = (",", ":") # 紧凑分隔符 # 限流配置 RATELIMIT_ENABLED = False # 测试环境禁用限流 class ProdConfig(BaseConfig): """生产环境配置""" DEBUG = False TESTING = False # 生产环境必须使用环境变量 SECRET_KEY = os.getenv("SECRET_KEY", "") JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "") # 生产环境数据库 SQLALCHEMY_DATABASE_URI = os.getenv( "DATABASE_URL", f"sqlite:///{BASE_DIR}/runtime/iti-flask_prod.db" ) # 处理 PostgreSQL URL 格式 if SQLALCHEMY_DATABASE_URI and SQLALCHEMY_DATABASE_URI.startswith("postgres://"): SQLALCHEMY_DATABASE_URI = SQLALCHEMY_DATABASE_URI.replace( "postgres://", "postgresql://", 1 ) SQLALCHEMY_SHOW_ERROR_DETAILS = False # 限流配置(生产环境严格) RATELIMIT_STORAGE_URL = os.getenv("REDIS_URL", "memory://") RATELIMIT_DEFAULT = "100 per hour" # JSON 配置(生产环境:紧凑输出,减小体积) JSON_INDENT = None # 不缩进 JSON_SEPARATORS = (",", ":") # 紧凑分隔符 # 性能优化 SQLALCHEMY_POOL_SIZE = 10 SQLALCHEMY_POOL_TIMEOUT = 30 SQLALCHEMY_POOL_RECYCLE = 3600 # 配置字典 config = { "dev": DevConfig, "test": TestConfig, "prod": ProdConfig, "default": DevConfig, } def get_config(env_name=None): """ 根据环境名称获取配置类 Args: env_name: 环境名称 ('dev', 'test', 'prod') 如果为 None 则从环境变量 FLASK_ENV 读取 Returns: 配置类 """ if env_name is None: env_name = os.getenv("FLASK_ENV", "dev") return config.get(env_name, config["default"])