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', 'corsheaders', 'gvsds.apps.core', 'gvsds.apps.gerp.gerp_app.GerpConfig', '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.rollcall', '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=False), # 已禁用 blacklist(业务 User 非 Django auth 模型,OutstandingToken 无法落库) '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), }, }