368 lines
14 KiB
Python
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),
|
|
},
|
|
}
|