""" Django settings for flower project. Generated by 'django-admin startproject' using Django 5.2.7. For more information on this file, see https://docs.djangoproject.com/en/5.2/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/5.2/ref/settings/ """ # 兼容旧版 django-qiniu-storage try: from django.utils.encoding import force_text except ImportError: from django.utils.encoding import force_str as force_text import django.utils.encoding django.utils.encoding.force_text = force_text from datetime import timedelta from pathlib import Path from environ import Env # 应用七牛云 SDK 补丁(必须在导入其他模块之前) from flower.qiniu_patch import apply_all_patches apply_all_patches() env = Env( DEBUG=(bool, False), ) # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent # Initialize environment variables Env.read_env(str(BASE_DIR / '.env')) # 允许一次请求携带更多表单字段,避免 admin 批量操作时报 TooManyFieldsSent # Django 默认 DATA_UPLOAD_MAX_NUMBER_FIELDS=1000,当一次 POST/GET 的字段(含重复 key)过多会直接抛异常。 DATA_UPLOAD_MAX_NUMBER_FIELDS = env.int('DATA_UPLOAD_MAX_NUMBER_FIELDS', default=20000) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = env('SECRET_KEY', default='weak_secret_key_for_dev_only') # SECURITY WARNING: don't run with debug turned on in production! DEBUG = env('DEBUG', default=False) ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=[ 'yuwenerp.yuwen.cloud', ]) CSRF_TRUSTED_ORIGINS = env.list('CSRF_TRUSTED_ORIGINS', default=[ 'https://yuwenerp.yuwen.cloud', ]) # 明道云同步配置 MDY_MERCHANT_ID = env.int('MDY_MERCHANT_ID', default=1) MDY_PRODUCT_CATEGORY_ID = env.int('MDY_PRODUCT_CATEGORY_ID', default=1) MDY_SYNC_PAGE_SIZE = env.int('MDY_SYNC_PAGE_SIZE', default=100) MDY_SYNC_MAX_PAGES = env.int('MDY_SYNC_MAX_PAGES', default=2) MDY_SYNC_MAX_RECORDS = env.int('MDY_SYNC_MAX_RECORDS', default=200) # CORS 配置 CORS_ALLOW_ALL_ORIGINS = DEBUG # 开发环境允许所有源,生产环境需要配置白名单 CORS_ALLOWED_ORIGINS = env.list('CORS_ALLOWED_ORIGINS', default=[ 'http://localhost:8000', 'http://127.0.0.1:5173', 'http://127.0.0.1:5174', 'http://127.0.0.1:5179', 'https://yuwenerp.yuwen.cloud', ]) CORS_ALLOW_CREDENTIALS = True # 允许携带凭证(如 Cookie、认证头) # SSE 需要的特殊 CORS 配置 CORS_ALLOW_HEADERS = [ 'accept', 'accept-encoding', 'authorization', 'content-type', 'dnt', 'origin', 'user-agent', 'x-csrftoken', 'x-requested-with', 'cache-control', # SSE 需要 'x-accel-buffering', # SSE 需要 'last-event-id', # SSE 重连需要 # 注意:不要添加 'connection',这是 hop-by-hop 头部,会被代理过滤 ] # SSE 需要暴露的响应头 CORS_EXPOSE_HEADERS = [ 'content-type', 'cache-control', 'x-accel-buffering', ] # 预检请求缓存时间(秒) CORS_PREFLIGHT_MAX_AGE = 86400 # 24小时 # Application definition INSTALLED_APPS = [ 'simpleui', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', # 使用七牛云存储,不需要staticfiles app 'corsheaders', # CORS 支持 'rest_framework', 'drf_spectacular', 'basic_info', 'business', 'stock', 'api_v1', 'api_man', 'printing', 'stateflow', 'sse', 'api_v2', ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'corsheaders.middleware.CorsMiddleware', # CORS 中间件,必须放在 CommonMiddleware 之前 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'flower.middleware.ApiAuditLogMiddleware', # API审计日志中间件,需在AuthenticationMiddleware之后 ] REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework_simplejwt.authentication.JWTAuthentication', ), 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.IsAuthenticated', ], 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', } # JWT 配置 SIMPLE_JWT = { 'ACCESS_TOKEN_LIFETIME': timedelta(days=7), 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), 'ROTATE_REFRESH_TOKENS': True, 'UPDATE_LAST_LOGIN': True, } SPECTACULAR_SETTINGS = { 'TITLE': 'Flower API', 'DESCRIPTION': 'API documentation for the Flower project', 'VERSION': '1.0.0', 'SERVE_INCLUDE_SCHEMA': False, } ROOT_URLCONF = 'flower.urls' 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', ], }, }, ] WSGI_APPLICATION = 'flower.wsgi.application' # Database # https://docs.djangoproject.com/en/5.2/ref/settings/#databases DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': env('DB_NAME'), 'USER': env('DB_USER'), 'PASSWORD': env('DB_PASSWORD'), 'HOST': env('DB_HOST', default='localhost'), 'PORT': env('DB_PORT', default='6432'), 'CONN_HEALTH_CHECK': True, 'CONN_MAX_AGE': env.int('CONN_MAX_AGE', default=0), } } # Cache configuration # https://docs.djangoproject.com/en/5.2/topics/cache/ # Django 5.x 内置 Redis 缓存后端,使用 redis 包(已安装) # Redis 连接配置:优先使用 CACHE_URL,否则从 CACHE_HOST/CACHE_PORT/CACHE_DB 构建 # 默认 localhost:6379(适合宿主机运行),Docker Compose 可通过环境变量指定 CACHE_HOST=redis # 检测是否在测试环境中 import sys TESTING = 'test' in sys.argv if TESTING: # 测试环境使用内存缓存,避免 Redis 连接问题 CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 'LOCATION': 'unique-testing-cache', } } else: # 生产/开发环境使用 Redis # 优先使用 CACHE_URL(完整 URL),否则从 host/port/db 构建 cache_url = env('CACHE_URL', default=None) if cache_url is None: cache_host = env('CACHE_HOST', default='localhost') cache_port = env.int('CACHE_PORT', default=6379) cache_db = env.int('CACHE_DB', default=0) cache_url = f'redis://{cache_host}:{cache_port}/{cache_db}' CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.redis.RedisCache', 'LOCATION': cache_url, 'OPTIONS': { # Redis 连接选项 'socket_connect_timeout': 5, # 连接超时(秒) 'socket_timeout': 5, # 操作超时(秒) 'retry_on_timeout': True, # 超时后重试 'health_check_interval': 30, # 健康检查间隔(秒) }, # 键前缀,避免不同项目/环境之间的键冲突 'KEY_PREFIX': env('CACHE_KEY_PREFIX', default='flower'), # 默认过期时间(秒),None 表示永不过期(由具体使用场景决定) 'TIMEOUT': env.int('CACHE_DEFAULT_TIMEOUT', default=300), # 默认 5 分钟 } } # Logging configuration # 目标: # - 生产环境出现 500 时,能在 stdout 日志里看到完整 traceback # - 所有日志都带时间戳(含毫秒),便于快速定位与检索 LOG_LEVEL = env('LOG_LEVEL', default='INFO').upper() DJANGO_LOG_LEVEL = env('DJANGO_LOG_LEVEL', default=LOG_LEVEL).upper() UVICORN_LOG_LEVEL = env('UVICORN_LOG_LEVEL', default=LOG_LEVEL).upper() UVICORN_ACCESS_LOG_LEVEL = env('UVICORN_ACCESS_LOG_LEVEL', default='INFO').upper() LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'default': { # 注意:Python logging 的 datefmt 基于 time.strftime,不支持 %f; # 这里用 %(msecs)03d 输出毫秒。 'format': '%(asctime)s.%(msecs)03d [%(levelname)s] %(name)s: %(message)s', 'datefmt': '%Y-%m-%d %H:%M:%S', }, }, 'handlers': { 'console': { 'class': 'logging.StreamHandler', 'formatter': 'default', }, }, 'root': { 'handlers': ['console'], 'level': LOG_LEVEL, }, 'loggers': { # Django 在发生未捕获异常(500)时,会通过 django.request 输出错误日志, # record.exc_info 会携带 traceback,Formatter 会自动把 traceback 打到日志中。 'django.request': { 'handlers': ['console'], 'level': 'ERROR', 'propagate': False, }, 'django': { 'handlers': ['console'], 'level': DJANGO_LOG_LEVEL, 'propagate': False, }, # Uvicorn 日志(access/error)统一走我们的 console formatter,确保有时间戳 'uvicorn': { 'handlers': ['console'], 'level': UVICORN_LOG_LEVEL, 'propagate': False, }, 'uvicorn.error': { 'handlers': ['console'], 'level': UVICORN_LOG_LEVEL, 'propagate': False, }, 'uvicorn.access': { 'handlers': ['console'], 'level': UVICORN_ACCESS_LOG_LEVEL, 'propagate': False, }, }, } # Password validation # https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators 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', # }, ] # Internationalization # https://docs.djangoproject.com/en/5.2/topics/i18n/ LANGUAGE_CODE = 'zh-hans' TIME_ZONE = 'Asia/Shanghai' USE_I18N = True USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/5.2/howto/static-files/ # 七牛云存储配置 QINIU_ACCESS_KEY = 'IySZff0zEN7jHeDuxj5PrQNMv0qjKoVqBRCuFMzi' QINIU_SECRET_KEY = 'Mph5XkjUQO3VHFPT1JulYHY442uMOya2bXjbSZp8' QINIU_BUCKET_NAME = 'yunwenerp' QINIU_BUCKET_DOMAIN = 'image.yuwen.cloud' QINIU_SECURE_URL = True STORAGES = { 'default': { 'BACKEND': 'qiniustorage.backends.QiniuStorage', }, 'staticfiles': { 'BACKEND': 'qiniustorage.backends.QiniuStorage', }, } DEFAULT_FILE_STORAGE = 'qiniustorage.backends.QiniuStorage' STATICFILES_STORAGE = 'qiniustorage.backends.QiniuStorage' # 静态文件URL(直接从七牛云访问) STATIC_URL = f'http://{QINIU_BUCKET_DOMAIN}/static/' STATIC_ROOT = 'static' MEDIA_URL = f'http://{QINIU_BUCKET_DOMAIN}/media/' # Default primary key field type # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' # Stateflow 配置 # ------------------------------------------------------------------------------ # STATEFLOW_CURRENT_STATE_MODE: 控制 current_state 的语义 # # 可选值: # 'NEXT' (默认): current_state 返回"下一个待执行的节点" # - 配合 progress 判断:"节点名" / "已完成" # - 适合显示"接下来要做什么" # - 原有默认行为 # # 'LAST': current_state 返回"最后完成的节点" # - 显示"刚做完了什么" # - 适合审计和历史查看 # - 新增行为模式 # # 注意: # - 修改此配置会影响 status 属性的显示内容 # - API 接口保持兼容,但返回的 status 和 status_id 语义会变化 # - 推荐在新代码中直接使用 get_last_completed_state() 或 get_next_pending_state() # 以避免歧义 # - 已废除"未开始"的显示,直接显示第一个节点名称 # ------------------------------------------------------------------------------ STATEFLOW_CURRENT_STATE_MODE = 'NEXT' # STATEFLOW_CURRENT_STATE_MODE = 'LAST' # Printing module settings PRINTING_DEFAULT_PROCESS_ID = 1 # 默认印染流程ID PLATE_ORDER_DEFAULT_PROCESS_ID = 2 # 默认开版流程ID # API 审计日志配置 # ------------------------------------------------------------------------------ # 用于记录特定模块POST请求的历史现场,便于追踪"创建"操作。 # 通过Celery异步写入,不影响API响应性能。 # ------------------------------------------------------------------------------ API_AUDIT_LOG_ENABLED = True API_AUDIT_LOG_URL_PREFIXES = [ '/api/v1/printing', '/api/v1/plate', ] # Celery 配置 # RabbitMQ Broker 配置:优先使用 CELERY_BROKER_URL,否则从各组件构建 # 默认 localhost(适合宿主机运行),Docker Compose 可通过环境变量指定 CELERY_BROKER_HOST=rabbitmq celery_broker_url = env('CELERY_BROKER_URL', default=None) if celery_broker_url is None: celery_broker_host = env('CELERY_BROKER_HOST', default='localhost') celery_broker_port = env.int('CELERY_BROKER_PORT', default=5672) celery_broker_user = env('CELERY_BROKER_USER', default='guest') celery_broker_password = env('CELERY_BROKER_PASSWORD', default='guest') celery_broker_vhost = env('CELERY_BROKER_VHOST', default='/') celery_broker_url = f'amqp://{celery_broker_user}:{celery_broker_password}@{celery_broker_host}:{celery_broker_port}/{celery_broker_vhost}' CELERY_BROKER_URL = celery_broker_url # Celery Result Backend 配置:优先使用 CELERY_RESULT_BACKEND,否则从各组件构建 # 默认使用与 Cache 相同的 Redis 实例,Docker Compose 可通过环境变量指定 CELERY_RESULT_HOST=redis celery_result_url = env('CELERY_RESULT_BACKEND', default=None) if celery_result_url is None: celery_result_host = env('CELERY_RESULT_HOST', default='localhost') celery_result_port = env.int('CELERY_RESULT_PORT', default=6379) celery_result_db = env.int('CELERY_RESULT_DB', default=1) # 使用 db=1,与 Cache 的 db=0 区分 celery_result_url = f'redis://{celery_result_host}:{celery_result_port}/{celery_result_db}' CELERY_RESULT_BACKEND = celery_result_url CELERY_ACCEPT_CONTENT = ['json'] CELERY_TASK_SERIALIZER = 'json' CELERY_RESULT_SERIALIZER = 'json' CELERY_TIMEZONE = TIME_ZONE from celery.schedules import crontab CELERY_BEAT_SCHEDULE = { 'daily_database_backup': { 'task': 'api_v1.tasks.backup_database', 'schedule': crontab(hour=3, minute=0), 'kwargs': { 'output_dir': str(BASE_DIR / 'data-bak'), 'filename_prefix': 'db-backup', }, }, 'mdy_product_sync': { 'task': 'api_v1.tasks.sync_mdy_products', 'schedule': crontab(minute='*/10'), 'kwargs': { 'page_size': MDY_SYNC_PAGE_SIZE, 'max_pages': MDY_SYNC_MAX_PAGES, 'max_records': MDY_SYNC_MAX_RECORDS, }, }, 'mdy_customer_sync': { 'task': 'api_v1.tasks.sync_mdy_customers', 'schedule': crontab(minute='*/5'), 'kwargs': { 'page_size': MDY_SYNC_PAGE_SIZE, 'max_pages': MDY_SYNC_MAX_PAGES, 'max_records': MDY_SYNC_MAX_RECORDS, }, }, }