1
0
forked from erp-dev/erp
Files
erpnew/flower/settings.py

571 lines
21 KiB
Python
Raw 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.
"""
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)
# 明道云“开版暂存”同步:每次请求之间的最小间隔(用于限流)
# - 0.02 ~ 50 qps理论值实际应更保守以预留重试/关联查询)
MDY_PLATE_ORDER_SYNC_REQUEST_INTERVAL_SECONDS = env.float('MDY_PLATE_ORDER_SYNC_REQUEST_INTERVAL_SECONDS', default=0.05)
# Tencent Cloud (TIIA) 配置:用于通过 ImageUrl 上传图片到图库CreateImage
# 注意:不要把 secret 写死在代码里,建议通过 .env / 环境变量注入。
TENCENTCLOUD_SECRET_ID = env('TENCENTCLOUD_SECRET_ID', default='AKID2wtgIhYCcprkS0QfiZCbopetxpW1KSPE')
TENCENTCLOUD_SECRET_KEY = env('TENCENTCLOUD_SECRET_KEY', default='oSwGOJ0XzJRODrIXZWIrEPNAmHdppdHh')
TENCENTCLOUD_TOKEN = env('TENCENTCLOUD_TOKEN', default='') # STS 临时票据(可选)
TENCENTCLOUD_TIIA_GROUP_ID = env('TENCENTCLOUD_TIIA_GROUP_ID', default='168')
TENCENTCLOUD_TIIA_REGION = env('TENCENTCLOUD_TIIA_REGION', default='ap-guangzhou')
TENCENTCLOUD_TIIA_ENDPOINT = env('TENCENTCLOUD_TIIA_ENDPOINT', default='tiia.tencentcloudapi.com')
TENCENTCLOUD_TIIA_PIC_NAME_PREFIX = env('TENCENTCLOUD_TIIA_PIC_NAME_PREFIX', default='plate_order')
TENCENTCLOUD_TIIA_QPS = env.int('TENCENTCLOUD_TIIA_QPS', default=10) # Tencent API 限速:每秒最多 10 次
# 企业微信机器人 Webhook目前按需求直接写入 settings不走 .env
# 使用https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxx
WECOM_WEBHOOK_BASE_URL = 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send'
WECOM_WEBHOOK_KEY = env('WECOM_WEBHOOK_KEY', default='')
# 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',
'shipment',
]
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 分钟
}
}
# PrintingJob状态推进时的企业微信通知markdown
# - 默认:测试环境关闭(避免单测出网/刷屏),非测试环境开启
PRINTING_JOB_STATE_ADVANCED_WECOM_NOTIFY_ENABLED = (not TESTING)
# PrintingJob 跟进地址(用于企业微信通知里展示/点击)
# orderId 使用 PrintingOrder.id
PRINTING_JOB_STATE_ADVANCED_FOLLOWUP_URL_TEMPLATE = env('PRINTING_JOB_STATE_ADVANCED_FOLLOWUP_URL_TEMPLATE', default='')
PRINTING_JOB_STATE_ADVANCED_WECOM_MARKDOWN_TEMPLATE = (
"### 印染任务状态推进\n"
"\n"
"- **印染订单ID**`{printing_order_id}`\n"
"- **印染任务ID**`{printing_job_id}`\n"
"- **推进状态****{state_name}**\n"
"- **推进时间**`{advanced_at}`\n"
"- **发送者**{sender}\n"
"{followup_line}"
"\n"
"**工序参数**\n"
"{process_params_markdown}\n"
)
# PrintingOrder创建时的企业微信通知markdown
# - 默认:测试环境关闭(避免单测出网/刷屏),非测试环境开启
PRINTING_ORDER_CREATED_WECOM_NOTIFY_ENABLED = env.bool(
"PRINTING_ORDER_CREATED_WECOM_NOTIFY_ENABLED",
default=(not TESTING),
)
PRINTING_ORDER_CREATED_WECOM_MARKDOWN_TEMPLATE = (
"### 印染订单创建\n"
"\n"
"- **印染订单ID**`{printing_order_id}`\n"
"- **订单编号**`{printing_order_human_id}`\n"
"- **创建时间**`{created_at}`\n"
"- **发送者**{sender}\n"
"- **客户**{customer_name}\n"
"- **面料**{fabric}\n"
"- **出货日期**`{outgoing_date}`\n"
)
# 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 会携带 tracebackFormatter 会自动把 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
# 销售品自动创建配置PrintingJob 流程完成时)
# ------------------------------------------------------------------------------
# 指定从哪个流程节点获取数量参数,单位固定为"米"
# 如果该节点未完成或参数不存在,则不创建销售品
# ------------------------------------------------------------------------------
AUTO_CREATE_SALESITEM_FROM_PRINT_ORDER = env.bool('AUTO_CREATE_SALESITEM_FROM_PRINT_ORDER', default=False)
PRINTING_SALES_ITEM_SOURCE_STATE_ID = env.int('PRINTING_SALES_ITEM_SOURCE_STATE_ID') # 必须在 .env 中配置
PRINTING_SALES_ITEM_QUANTITY_KEY = '米数' # 数量参数的 key
# 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',
},
},
# 'daily_plate_order_tiia_image_upload': {
# 'task': 'printing.tasks.upload_yesterday_plate_order_images_to_tencent_tiia',
# 'schedule': crontab(hour=3, minute=0),
# },
# 'daily_mdy_plate_order_staging_tiia_image_upload': {
# 'task': 'api_v1.tasks.upload_mdy_plate_order_staging_images_to_tencent_tiia',
# 'schedule': crontab(hour=3, minute=20),
# 'kwargs': {
# 'batch_size': 500,
# 'dry_run': False,
# },
# },
'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,
},
},
# 明道云开版:同步到“暂存表”
# - 仅在 02:00-08:00 时间窗内持续运行(每 N 分钟触发一次)
# - 通过 request_interval_seconds 控制单次任务对明道云 API 的请求节奏,避免超 QPS
'mdy_plate_order_staging_sync': {
'task': 'api_v1.tasks.sync_mdy_plate_orders',
# 02:00 - 07:59不包含 08:xx
'schedule': crontab(minute='*/2', hour='2-7'),
'kwargs': {
'page_size': max(1, int(MDY_SYNC_PAGE_SIZE)),
'max_pages': max(1, int(MDY_SYNC_MAX_PAGES)),
'max_records': max(1, int(MDY_SYNC_MAX_RECORDS)),
'with_related': True,
'max_related_per_type': 5,
'request_interval_seconds': float(MDY_PLATE_ORDER_SYNC_REQUEST_INTERVAL_SECONDS),
},
},
}