Files
SMANAGE/gvsds/settings.py
T
TermiNexus e76a5b359e security(jwt): 收敛 token 生命周期(168h→24h)并启用 refresh 轮换黑名单
- ACCESS_TOKEN_LIFETIME 默认 24h(缩小泄露窗口)
- BLACKLIST_AFTER_ROTATION 默认开启,注册 token_blacklist app
- LOGGING 增加 RotatingFileHandler 落盘(django.log)
- USE_TZ=False 决策文档化(存量 naive UTC 时间戳无法安全切换)
2026-08-13 17:55:54 +08:00

394 lines
16 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.
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.1.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',
'rest_framework_simplejwt.token_blacklist', # refresh 轮换后旧 token 拉黑(BLACKLIST_AFTER_ROTATION)
'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.community',
'gvsdsdk.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',
'gvsdsdk.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')
# ---------------------------------------------------------------------------
# 用户模型(AUTH_USER_MODEL)——架构决策说明
# 业务用户模型为 gvsds.apps.core.models.User(db_table='User',主键 UserUUID,
# bcrypt 密码 UserPassword)。**不设置 AUTH_USER_MODEL**,原因:
# 1. core.User 继承 QModel 而非 Django AbstractBaseUser,无 USERNAME_FIELD 等
# 框架必需接口,设置后 Django SystemCheckError 启动失败;
# 2. 改 AUTH_USER_MODEL 要求重建 contenttypes/auth_permission 框架,
# 现有 MySQL 业务库无法承受(非新建项目);
# 3. 认证链路已收敛:CustomJWTAuthentication 显式绑定 core.User
# (gvsds/auth.py),admin/session 依赖的 auth_user 表未被业务使用。
# 若未来要接入 Django auth 框架,需先重构 core.User 为 AbstractBaseUser 子类。
# ---------------------------------------------------------------------------
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 保持 False:全项目时间戳以 datetime.utcnow() 存 naive UTC,
# 改 True 会把存量 naive 时间解释为本地时区,导致 stats 周期键/跨时区比较错乱。
# 若未来统一时区,需先迁移存量数据为 aware UTC(见架构债治理清单 2.7)。
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=24)), # 24 小时(收敛自 168h,缩小泄露窗口)
'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=True), # 轮换后旧 refresh 立即失效
'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',
},
'file': {
'class': 'logging.handlers.RotatingFileHandler',
'filename': str(BASE_DIR / 'logs' / 'django.log'),
'maxBytes': 10 * 1024 * 1024,
'backupCount': 5,
'formatter': 'verbose',
'encoding': 'utf-8',
},
},
'root': {
'handlers': ['console', 'file'],
'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),
},
}