""" 基础配置:所有环境共享的部分。 具体环境差异: - 开发:config.settings.dev(继承 base,DEBUG=True,SQLite) - 生产:config.settings.prod(继承 base,DEBUG=False,PostgreSQL + Granian) - 测试:config.settings.test(继承 base,内存 SQLite) """ from pathlib import Path import environ BASE_DIR = Path(__file__).resolve().parent.parent.parent # backend/ env = environ.Env() # 读取 backend/.env(若存在) env_file = BASE_DIR / ".env" if env_file.exists(): environ.Env.read_env(str(env_file)) # === 核心 === SECRET_KEY = env( "DJANGO_SECRET_KEY", default="dev-insecure-change-me-IN-PROD-PLEASE", ) DEBUG = False # 必须在子模块覆盖 ALLOWED_HOSTS = ["*"] # 子模块覆盖 # === 应用 === INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", # 第三方 "rest_framework", "rest_framework_simplejwt", # 业务应用 "apps.core", "apps.catalog", "apps.partner", "apps.inventory", "apps.finance", "apps.purchase", "apps.sales", "apps.report", "apps.channel", "apps.notify", "apps.openapi", "apps.printing", "apps.ai", "apps.billing", "apps.website", "apps.storefront", ] MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "whitenoise.middleware.WhiteNoiseMiddleware", # 前端静态资源同源托管 "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", # 多租户识别 "apps.core.context.RequestContextMiddleware", # 请求上下文(审计用) "apps.core.middleware.TenantMiddleware", "apps.core.middleware.DemoReadOnlyMiddleware", ] # Vue3 构建产物(dist)由 Django 同源服务:/static/* 托管资源,/ 返回 index.html FRONTEND_DIST = BASE_DIR / "frontend_dist" # 容器内:/app/frontend_dist if not FRONTEND_DIST.exists(): FRONTEND_DIST = BASE_DIR.parent / "frontend" / "dist" # 本机开发:dealerhub/frontend/dist STATICFILES_DIRS = [FRONTEND_DIST] if FRONTEND_DIST.exists() else [] ROOT_URLCONF = "config.urls" ASGI_APPLICATION = "config.asgi.application" WSGI_APPLICATION = "config.wsgi.application" TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [], "APP_DIRS": True, "OPTIONS": { "context_processors": [ "django.template.context_processors.request", "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", ], }, }, ] # === 数据库(默认 SQLite,子模块可覆盖) === DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": BASE_DIR / "db.sqlite3", } } # === 密码校验 === AUTH_PASSWORD_VALIDATORS = [ {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"}, {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, ] AUTH_USER_MODEL = "auth.User" # 阶段 1 后改为 apps.authx.User # === 本地化 === LANGUAGE_CODE = "zh-hans" TIME_ZONE = "Asia/Shanghai" USE_I18N = True USE_TZ = True # === 静态文件 === STATIC_URL = "static/" STATIC_ROOT = BASE_DIR / "staticfiles" MEDIA_URL = "media/" MEDIA_ROOT = BASE_DIR / "media" DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" # === DRF === REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": ( "rest_framework_simplejwt.authentication.JWTAuthentication", "rest_framework.authentication.SessionAuthentication", # admin 用 ), "DEFAULT_PERMISSION_CLASSES": ( "rest_framework.permissions.IsAuthenticated", "apps.core.permissions.TenantMembershipPermission", ), "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", "PAGE_SIZE": 20, "DATETIME_FORMAT": "%Y-%m-%d %H:%M:%S", # 统一业务异常 → HTTP 语义(迭代第 2 轮);见 apps/core/exceptions.py "EXCEPTION_HANDLER": "apps.core.exceptions.api_exception_handler", } # === JWT === from datetime import timedelta # noqa: E402 SIMPLE_JWT = { "ACCESS_TOKEN_LIFETIME": timedelta(hours=1), "REFRESH_TOKEN_LIFETIME": timedelta(days=30), "AUTH_HEADER_TYPES": ("Bearer",), } # === 日志 === LOGGING = { "version": 1, "disable_existing_loggers": False, "formatters": { "verbose": { "format": "[{asctime}] {levelname} {name}: {message}", "style": "{", }, }, "handlers": { "console": { "class": "logging.StreamHandler", "formatter": "verbose", }, }, "root": { "handlers": ["console"], "level": "INFO", }, "loggers": { "django": {"handlers": ["console"], "level": "INFO", "propagate": False}, "dealerhub": {"handlers": ["console"], "level": "DEBUG", "propagate": False}, }, } # === 多租户配置 === TENANT_HEADER = "X-Tenant-Id" TENANT_DEFAULT = env("TENANT_DEFAULT", default="default") # === AI 配置(批次 B) === # 无 KEY 时全部功能降级为纯统计版,不阻塞测试与演示。 AI_PROVIDER = env("AI_PROVIDER", default="") # 空 = 未接 LLM,只跑统计引擎 AI_BASE_URL = env("AI_BASE_URL", default="https://api.openai.com/v1") AI_API_KEY = env("AI_API_KEY", default="") AI_MODEL = env("AI_MODEL", default="gpt-4o-mini") AI_TIMEOUT = env.int("AI_TIMEOUT", default=20) AI_FREE_MONTHLY_QUOTA = env.int("AI_FREE_MONTHLY_QUOTA", default=10) # 0 = 不限