Files
SMANAGE/gvsds/settings.py
T

368 lines
14 KiB
Python

import os
from pathlib import Path
from datetime import timedelta
from gvsds.secrets import get as _secret_get, getint as _secret_getint
from gvsds.app_config import get as _app_get, getint as _app_getint, getboolean as _app_getbool, getlist as _app_getlist
BASE_DIR = Path(__file__).resolve().parent.parent
# 将 gvsdsdk 加入 Python 路径,便于子服务器 SDK 共享
import sys as _sys
import logging as _logging
_GVSDSDK = str(BASE_DIR / 'gvsdsdk')
if _GVSDSDK not in _sys.path:
_sys.path.insert(0, _GVSDSDK)
# 启动时校验 gvsdsdk 版本——主从必须一致
_REQUIRED_GVSDSDK = '1.0.0'
try:
import gvsdsdk
_sdk_info = gvsdsdk.get_version_info()
_logging.getLogger('gvsds.startup').info(
f'[SDK] 加载: v{_sdk_info["Version"]} @ {_sdk_info["Location"]}'
)
_ok, _msg = gvsdsdk.check_min_version(_REQUIRED_GVSDSDK)
if _ok:
_logging.getLogger('gvsds.startup').info(f'[SDK] {_msg}')
else:
_logging.getLogger('gvsds.startup').warning(
f'[SDK] 版本不符!{_msg}。双端 gvsdsdk 版本必须一致!'
)
except ImportError as e:
_logging.getLogger('gvsds.startup').error(f'[SDK] gvsdsdk 未安装: {e}')
SECRET_KEY = _secret_get('django', 'SECRET_KEY')
DEBUG = os.environ.get('DJANGO_DEBUG', '').lower() in ('true', '1', 'yes') or _app_getbool('server', 'DEBUG', fallback=False)
ALLOWED_HOSTS = _app_getlist('hosts', 'ALLOWED_HOSTS', fallback=[
"www.gvsds.com", "gvsds.com", "127.0.0.1", "localhost",
"gvsds.dev", "yc8z.org", "ycbz.org", "gvsds.org", "gvsds.studio",
".gvsds.com",
])
INSTALLED_APPS = _app_getlist('apps', 'INSTALLED_APPS', fallback=[
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'rest_framework_simplejwt',
'corsheaders',
'gvsds.apps.core',
'gvsds.apps.gerp',
'gvsds.apps.ugm',
'gvsds.apps.crb',
'gvsds.apps.invite',
'gvsds.apps.ccgo',
'gvsds.apps.sve',
'gvsds.apps.t4',
'gvsds.apps.mc',
'gvsds.apps.doc',
'gvsds.apps.api',
'gvsds.apps.msync',
'gvsds.apps.payment',
'gvsds.apps.commission',
'gvsds.apps.stats',
'gvsdsdk',
])
MIDDLEWARE = _app_getlist('middleware', 'MIDDLEWARE', fallback=[
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'gvsds.ip_filter.IPFilterMiddleware',
'gvsds.middleware.DomainMiddleware',
'gvsds.middleware.CurrentRoleMiddleware',
'gvsds.apps.msync.middleware.ServiceAuthMiddleware',
'gvsds.middleware.AttackDetectMiddleware',
'gvsds.middleware.ErrorLoggingMiddleware',
])
ROOT_URLCONF = _app_get('server', 'ROOT_URLCONF', fallback='gvsds.urls')
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / _app_get('template', 'DIRS', fallback='TempLate')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = _app_get('server', 'WSGI_APPLICATION', fallback='gvsds.wsgi.application')
SQL_IP = _secret_get('database', 'HOST', fallback='127.0.0.1')
DB_USER = _secret_get('database', 'USER', fallback='root')
DB_PASS = _secret_get('database', 'PASSWORD', fallback='')
DB_PORT = _secret_get('database', 'PORT', fallback='3306')
DB_CHARSET = _secret_get('database', 'CHARSET', fallback=_app_get('database', 'CHARSET', fallback='utf8mb4'))
DB_ENGINE = _app_get('database', 'ENGINE', fallback='django.db.backends.mysql')
DATABASES = {
'default': {
'ENGINE': DB_ENGINE,
'NAME': _app_get('database', 'DEFAULT_NAME', fallback='Users'),
'USER': DB_USER,
'PASSWORD': DB_PASS,
'HOST': SQL_IP,
'PORT': DB_PORT,
'CHARSET': DB_CHARSET,
'OPTIONS': {
'init_command': f"SET sql_mode='STRICT_TRANS_TABLES', character_set_connection={DB_CHARSET}",
},
},
'gerpcore': {
'ENGINE': DB_ENGINE,
'NAME': _app_get('database', 'GERPCORE_NAME', fallback='GerpCore'),
'USER': DB_USER,
'PASSWORD': DB_PASS,
'HOST': SQL_IP,
'PORT': DB_PORT,
'CHARSET': DB_CHARSET,
},
'plans': {
'ENGINE': DB_ENGINE,
'NAME': _app_get('database', 'PLANS_NAME', fallback='Plans'),
'USER': DB_USER,
'PASSWORD': DB_PASS,
'HOST': SQL_IP,
'PORT': DB_PORT,
'CHARSET': DB_CHARSET,
},
'tasks': {
'ENGINE': DB_ENGINE,
'NAME': _app_get('database', 'TASKS_NAME', fallback='Tasks'),
'USER': DB_USER,
'PASSWORD': DB_PASS,
'HOST': SQL_IP,
'PORT': DB_PORT,
'CHARSET': DB_CHARSET,
},
't4pc': {
'ENGINE': DB_ENGINE,
'NAME': _app_get('database', 'T4PC_NAME', fallback='T4PC'),
'USER': DB_USER,
'PASSWORD': DB_PASS,
'HOST': SQL_IP,
'PORT': DB_PORT,
'CHARSET': DB_CHARSET,
},
'document': {
'ENGINE': DB_ENGINE,
'NAME': _app_get('database', 'DOCUMENT_NAME', fallback='Document'),
'USER': DB_USER,
'PASSWORD': DB_PASS,
'HOST': SQL_IP,
'PORT': DB_PORT,
'CHARSET': DB_CHARSET,
},
'minecraft': {
'ENGINE': DB_ENGINE,
'NAME': _app_get('database', 'MINECRAFT_NAME', fallback='Minecraft'),
'USER': DB_USER,
'PASSWORD': DB_PASS,
'HOST': SQL_IP,
'PORT': DB_PORT,
'CHARSET': DB_CHARSET,
},
}
DATABASE_ROUTERS = ['gvsds.db_router.DatabaseRouter']
# === 缓存(Redis 主,locmem 降级) ===
# Redis 不可用时自动降级到本地内存缓存,保证开发环境零依赖启动。
REDIS_HOST = _secret_get('redis', 'HOST', fallback='127.0.0.1')
REDIS_PORT = _secret_getint('redis', 'PORT', fallback=6379)
REDIS_DB = _secret_getint('redis', 'DB', fallback=0)
REDIS_PASSWORD = _secret_get('redis', 'PASSWORD', fallback='')
if _app_getbool('redis', 'ENABLED', fallback=False):
_REDIS_KWARGS = {
'LOCATION': f'redis://{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}',
}
if REDIS_PASSWORD:
_REDIS_KWARGS['LOCATION'] = f'redis://:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}'
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': _REDIS_KWARGS['LOCATION'],
'TIMEOUT': _app_getint('cache', 'DEFAULT_TIMEOUT', fallback=300),
'OPTIONS': {},
},
}
else:
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'gvsds-default',
'TIMEOUT': _app_getint('cache', 'DEFAULT_TIMEOUT', fallback=300),
},
}
AUTH_PASSWORD_VALIDATORS = [
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
'OPTIONS': {'min_length': _app_getint('password', 'MIN_LENGTH', fallback=8)}},
]
LANGUAGE_CODE = _app_get('server', 'LANGUAGE_CODE', fallback='zh-hans')
TIME_ZONE = _app_get('server', 'TIME_ZONE', fallback='Asia/Shanghai')
USE_I18N = _app_getbool('server', 'USE_I18N', fallback=True)
USE_TZ = _app_getbool('server', 'USE_TZ', fallback=False)
STATIC_URL = _app_get('static', 'STATIC_URL', fallback='/static/')
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = [BASE_DIR / 'static']
MEDIA_URL = _app_get('static', 'MEDIA_URL', fallback='/media/')
MEDIA_ROOT = BASE_DIR / 'media'
DEFAULT_AUTO_FIELD = _app_get('server', 'DEFAULT_AUTO_FIELD', fallback='django.db.models.BigAutoField')
SESSION_COOKIE_DOMAIN = os.environ.get('SESSION_COOKIE_DOMAIN', _app_get('session', 'COOKIE_DOMAIN', fallback='.gvsds.com'))
SESSION_COOKIE_AGE = _app_getint('session', 'COOKIE_AGE', fallback=30 * 24 * 60 * 60)
CSRF_TRUSTED_ORIGINS = [
f'https://{h}' for h in ALLOWED_HOSTS if '*' not in h
]
CORS_ALLOW_ALL_ORIGINS = _app_getbool('cors', 'ALLOW_ALL_ORIGINS', fallback=False)
CORS_ALLOWED_ORIGINS = [
f'https://{h}' for h in ALLOWED_HOSTS if '*' not in h
]
CORS_ALLOW_CREDENTIALS = _app_getbool('cors', 'ALLOW_CREDENTIALS', fallback=True)
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'gvsds.auth.CustomJWTAuthentication',
'rest_framework.authentication.SessionAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_THROTTLE_CLASSES': (
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle',
),
'DEFAULT_THROTTLE_RATES': {
'anon': _app_get('throttle', 'ANON_RATE', fallback='100/min'),
'user': _app_get('throttle', 'USER_RATE', fallback='1000/min'),
},
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': _app_getint('rest_framework', 'PAGE_SIZE', fallback=50),
'DEFAULT_RENDERER_CLASSES': (
'gvsds.json_encoder.JSONRenderer',
),
'DEFAULT_PARSER_CLASSES': (
'rest_framework.parsers.JSONParser',
'rest_framework.parsers.FormParser',
'rest_framework.parsers.MultiPartParser',
),
'EXCEPTION_HANDLER': 'gvsds.exception_handler.custom_exception_handler',
}
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(hours=_app_getint('jwt', 'ACCESS_TOKEN_LIFETIME_HOURS', fallback=168)), # 7 天
'REFRESH_TOKEN_LIFETIME': timedelta(days=_app_getint('jwt', 'REFRESH_TOKEN_LIFETIME_DAYS', fallback=90)), # 90 天
'ROTATE_REFRESH_TOKENS': _app_getbool('jwt', 'ROTATE_REFRESH_TOKENS', fallback=True),
'BLACKLIST_AFTER_ROTATION': _app_getbool('jwt', 'BLACKLIST_AFTER_ROTATION', fallback=False),
'AUTH_HEADER_TYPES': tuple(_app_getlist('jwt', 'AUTH_HEADER_TYPES', fallback=['Bearer'])),
'USER_ID_FIELD': 'UserUUID',
'USER_ID_CLAIM': 'UserUUID',
}
APPEND_SLASH = _app_getbool('server', 'APPEND_SLASH', fallback=False)
# SSL重定向:生产环境通常由反向代理(nginx)处理SSL终止和HTTP→HTTPS重定向,
# 因此Django层面的SSL_REDIRECT默认关闭。若Django直接对外服务,应设为True。
SECURE_SSL_REDIRECT = _app_getbool('security', 'SSL_REDIRECT', fallback=False)
SECURE_HSTS_SECONDS = _app_getint('security', 'HSTS_SECONDS', fallback=31536000)
SECURE_HSTS_INCLUDE_SUBDOMAINS = _app_getbool('security', 'HSTS_INCLUDE_SUBDOMAINS', fallback=True)
SECURE_HSTS_PRELOAD = _app_getbool('security', 'HSTS_PRELOAD', fallback=True)
SESSION_COOKIE_SECURE = _app_getbool('security', 'SESSION_COOKIE_SECURE', fallback=True)
CSRF_COOKIE_SECURE = _app_getbool('security', 'CSRF_COOKIE_SECURE', fallback=True)
DATA_UPLOAD_MAX_MEMORY_SIZE = _secret_getint('upload', 'MAX_MEMORY_SIZE', fallback=10 * 1024 * 1024)
FILE_UPLOAD_MAX_MEMORY_SIZE = _secret_getint('upload', 'MAX_MEMORY_SIZE', fallback=10 * 1024 * 1024)
PAYMENT_ENCRYPTION_KEY = _secret_get('payment', 'ENCRYPTION_KEY', fallback=None)
if not PAYMENT_ENCRYPTION_KEY:
from django.core.exceptions import ImproperlyConfigured
raise ImproperlyConfigured('payment ENCRYPTION_KEY 未配置,请在 secrets.ini 或环境变量 PAYMENT_ENCRYPTION_KEY 中设置')
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': _app_get('logging', 'FORMAT', fallback='[{asctime}] [{levelname}] {message}'),
'style': '{',
},
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'verbose',
},
},
'root': {
'handlers': ['console'],
'level': _app_get('logging', 'LEVEL', fallback='INFO'),
},
}
# === Celery 异步任务(无 Celery 时降级为同步执行) ===
if _app_getbool('celery', 'ENABLED', fallback=False):
_CELERY_BROKER_URL = f'redis://{REDIS_HOST}:{REDIS_PORT}/{_secret_getint("redis", "CELERY_DB", fallback=1)}'
if REDIS_PASSWORD:
_CELERY_BROKER_URL = f'redis://:{REDIS_PASSWORD}@{REDIS_HOST}:{REDIS_PORT}/{_secret_getint("redis", "CELERY_DB", fallback=1)}'
CELERY_BROKER_URL = _app_get('celery', 'BROKER_URL', fallback=_CELERY_BROKER_URL)
CELERY_RESULT_BACKEND = _app_get('celery', 'RESULT_BACKEND', fallback=_CELERY_BROKER_URL)
CELERY_TIMEZONE = TIME_ZONE
CELERY_ENABLE_UTC = _app_getbool('celery', 'ENABLE_UTC', fallback=False)
CELERY_TASK_SERIALIZER = _app_get('celery', 'TASK_SERIALIZER', fallback='json')
CELERY_RESULT_SERIALIZER = _app_get('celery', 'RESULT_SERIALIZER', fallback='json')
CELERY_ACCEPT_CONTENT = ['json']
CELERY_TASK_TIME_LIMIT = _app_getint('celery', 'TASK_TIME_LIMIT', fallback=300)
CELERY_TASK_SOFT_TIME_LIMIT = _app_getint('celery', 'TASK_SOFT_TIME_LIMIT', fallback=240)
CELERY_TASK_ACKS_LATE = _app_getbool('celery', 'TASK_ACKS_LATE', fallback=True)
CELERY_WORKER_PREFETCH_MULTIPLIER = _app_getint('celery', 'WORKER_PREFETCH', fallback=1)
CELERY_WORKER_MAX_TASKS_PER_CHILD = _app_getint('celery', 'WORKER_MAX_TASKS', fallback=1000)
from celery.schedules import crontab as _crontab
CELERY_BEAT_SCHEDULE = {
# 每日 00:00 重置当日统计
'reset-daily-stats': {
'task': 'gvsds.apps.stats.tasks.ResetDailyStats',
'schedule': _crontab(hour=0, minute=0),
},
# 每月 1 日 00:00 重置月度统计
'reset-monthly-stats': {
'task': 'gvsds.apps.stats.tasks.ResetMonthlyStats',
'schedule': _crontab(day_of_month=1, hour=0, minute=0),
},
# 每日 03:00 清理过期统计(保留 90 天)
'purge-old-stats': {
'task': 'gvsds.apps.stats.tasks.PurgeOldStats',
'schedule': _crontab(hour=3, minute=0),
},
# 每日 23:50 构建当日排行榜
'build-daily-rankings': {
'task': 'gvsds.apps.stats.tasks.BuildDailyRankings',
'schedule': _crontab(hour=23, minute=50),
},
}