Files

205 lines
6.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
基础配置:所有环境共享的部分。
具体环境差异:
- 开发: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 = 不限