From 6dbe26af0b4de5918bffbb6cdde0bec0451fb4c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Tue, 15 Sep 2026 11:16:01 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20Django=20=E5=B7=A5=E7=A8=8B=E9=AA=A8?= =?UTF-8?q?=E6=9E=B6=EF=BC=88config+tracker=20app+=E5=B8=B8=E9=87=8F?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Code --- .gitignore | 4 +++ server/config/__init__.py | 5 +++ server/config/settings.py | 62 ++++++++++++++++++++++++++++++++++ server/config/settings_test.py | 2 ++ server/config/urls.py | 1 + server/config/wsgi.py | 7 ++++ server/manage.py | 21 ++++++++++++ server/pytest.ini | 4 +++ server/requirements.lock.txt | 17 ++++++++++ server/requirements.txt | 7 ++++ server/tracker/__init__.py | 0 server/tracker/apps.py | 6 ++++ server/tracker/constants.py | 19 +++++++++++ server/tracker/data/.gitkeep | 0 14 files changed, 155 insertions(+) create mode 100644 server/config/__init__.py create mode 100644 server/config/settings.py create mode 100644 server/config/settings_test.py create mode 100644 server/config/urls.py create mode 100644 server/config/wsgi.py create mode 100644 server/manage.py create mode 100644 server/pytest.ini create mode 100644 server/requirements.lock.txt create mode 100644 server/requirements.txt create mode 100644 server/tracker/__init__.py create mode 100644 server/tracker/apps.py create mode 100644 server/tracker/constants.py create mode 100644 server/tracker/data/.gitkeep diff --git a/.gitignore b/.gitignore index a51f6fc..9e2f590 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,7 @@ __pycache__/ *.pyc node_modules/ .env +server/.venv/ +server/db.sqlite3 +server/.cache/ +server/tracker/data/*.xdb diff --git a/server/config/__init__.py b/server/config/__init__.py new file mode 100644 index 0000000..bb60ef9 --- /dev/null +++ b/server/config/__init__.py @@ -0,0 +1,5 @@ +try: + import pymysql + pymysql.install_as_MySQLdb() +except ImportError: + pass diff --git a/server/config/settings.py b/server/config/settings.py new file mode 100644 index 0000000..dba7863 --- /dev/null +++ b/server/config/settings.py @@ -0,0 +1,62 @@ +from datetime import timedelta +from pathlib import Path +import os + +BASE_DIR = Path(__file__).resolve().parent.parent +SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "dev-insecure") +DEBUG = os.environ.get("DJANGO_DEBUG", "0" if os.environ.get("DJANGO_ENV") == "prod" else "1") == "1" +ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "*").split(",") +if os.environ.get("DJANGO_ENV") == "prod": # 生产 fail-fast:漏配即启动崩溃,不带病上线 + assert SECRET_KEY != "dev-insecure", "生产必须配置 DJANGO_SECRET_KEY" + assert os.environ.get("DJANGO_ALLOWED_HOSTS"), "生产必须显式配置 DJANGO_ALLOWED_HOSTS" + +INSTALLED_APPS = [ + "django.contrib.contenttypes", + "django.contrib.auth", + "corsheaders", + "rest_framework", + "tracker", +] +MIDDLEWARE = [ + "corsheaders.middleware.CorsMiddleware", + "django.middleware.common.CommonMiddleware", +] +ROOT_URLCONF = "config.urls" +WSGI_APPLICATION = "config.wsgi.application" +TEMPLATES = [{ + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], "APP_DIRS": True, "OPTIONS": {"context_processors": []}, +}] + +if os.environ.get("DB_ENGINE", "mysql") == "sqlite": # 本地无 MySQL 的开发模式 + DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3"}} +else: + DATABASES = { + "default": { + "ENGINE": "django.db.backends.mysql", + "NAME": os.environ.get("DB_NAME", "tracker"), + "USER": os.environ.get("DB_USER", "root"), + "PASSWORD": os.environ.get("DB_PASSWORD", ""), + "HOST": os.environ.get("DB_HOST", "127.0.0.1"), + "PORT": os.environ.get("DB_PORT", "3306"), + "OPTIONS": {"charset": "utf8mb4"}, + } + } + +REST_FRAMEWORK = { + "DEFAULT_AUTHENTICATION_CLASSES": ("rest_framework_simplejwt.authentication.JWTAuthentication",), + "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",), + "DEFAULT_THROTTLE_RATES": {"collect": "120/min", "admin": "600/min", "login": "10/min"}, + "NUM_PROXIES": 1, +} +SIMPLE_JWT = {"ACCESS_TOKEN_LIFETIME": timedelta(hours=12), "REFRESH_TOKEN_LIFETIME": timedelta(hours=24)} +CORS_ALLOW_ALL_ORIGINS = False +CACHES = {"default": {"BACKEND": "django.core.cache.backends.filebased.FileBasedCache", + "LOCATION": str(BASE_DIR / ".cache")}} # 限流计数跨 gunicorn worker 共享(不共享则限额×3) +DATA_UPLOAD_MAX_MEMORY_SIZE = 262144 # 256KB 应用层请求体兜底 +TIME_ZONE = "Asia/Shanghai" +USE_TZ = True +LANGUAGE_CODE = "zh-hans" +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" +IP2REGION_XDB = str(BASE_DIR / "tracker" / "data" / "ip2region.xdb") diff --git a/server/config/settings_test.py b/server/config/settings_test.py new file mode 100644 index 0000000..efdcc6d --- /dev/null +++ b/server/config/settings_test.py @@ -0,0 +1,2 @@ +from .settings import * +DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}} diff --git a/server/config/urls.py b/server/config/urls.py new file mode 100644 index 0000000..865a87e --- /dev/null +++ b/server/config/urls.py @@ -0,0 +1 @@ +urlpatterns = [] # Task 7 起逐任务填充 diff --git a/server/config/wsgi.py b/server/config/wsgi.py new file mode 100644 index 0000000..8509335 --- /dev/null +++ b/server/config/wsgi.py @@ -0,0 +1,7 @@ +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") + +application = get_wsgi_application() diff --git a/server/manage.py b/server/manage.py new file mode 100644 index 0000000..38fc221 --- /dev/null +++ b/server/manage.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +"""Django 命令行入口(标准模板)。""" +import os +import sys + + +def main(): + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/server/pytest.ini b/server/pytest.ini new file mode 100644 index 0000000..8ffc6f1 --- /dev/null +++ b/server/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +DJANGO_SETTINGS_MODULE = config.settings_test +python_files = test_*.py +addopts = -q diff --git a/server/requirements.lock.txt b/server/requirements.lock.txt new file mode 100644 index 0000000..ae21340 --- /dev/null +++ b/server/requirements.lock.txt @@ -0,0 +1,17 @@ +# 由 pip freeze 生成,剔除测试依赖;部署用 requirements.txt +asgiref==3.12.1 +colorama==0.4.6 +Django==5.2.17 +django-cors-headers==4.9.0 +djangorestframework==3.18.1 +djangorestframework_simplejwt==5.5.1 +gunicorn==26.2.0 +iniconfig==2.3.0 +packaging==26.3 +pluggy==1.6.0 +py-ip2region==3.0.4 +Pygments==2.21.0 +PyJWT==2.14.0 +PyMySQL==1.2.0 +sqlparse==0.6.0 +tzdata==2026.4 diff --git a/server/requirements.txt b/server/requirements.txt new file mode 100644 index 0000000..b7c845a --- /dev/null +++ b/server/requirements.txt @@ -0,0 +1,7 @@ +Django>=5.1,<5.3 +djangorestframework>=3.15 +djangorestframework-simplejwt>=5.3 +django-cors-headers>=4.4 +PyMySQL>=1.1 +py-ip2region>=3.0 +gunicorn>=22.0 diff --git a/server/tracker/__init__.py b/server/tracker/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/tracker/apps.py b/server/tracker/apps.py new file mode 100644 index 0000000..37951d5 --- /dev/null +++ b/server/tracker/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class TrackerConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "tracker" diff --git a/server/tracker/constants.py b/server/tracker/constants.py new file mode 100644 index 0000000..e2f160d --- /dev/null +++ b/server/tracker/constants.py @@ -0,0 +1,19 @@ +EVENT_NAMES = {"page_view", "page_stay", "sdk_loaded"} +MAX_EVENTS_PER_BATCH = 50 +MAX_PROP_KEYS = 32 +MAX_STR_LEN = 500 +MAX_PATH_LEN = 500 +MAX_TITLE_LEN = 200 +CLOCK_SKEW_LATE_SEC = 120 # 超过即标 late +CLOCK_SKEW_REJECT_SEC = 86400 # 超过即拒绝 +SENSITIVE_KEY_PATTERNS = ("password", "passwd", "token", "secret", "card", "verif", "otp", "idcard") +URL_PARAM_DROP_PATTERNS = ("email", "phone", "mobile", "token", "key", "password", "verif") +SESSION_IDLE_MS = 30 * 60 * 1000 +BOT_UA_KEYWORDS = ("bot", "spider", "crawl", "slurp", "bingpreview", "headless", "lighthouse") +EVENT_PROP_WHITELIST = { # spec §9.1:未知字段拒绝。免费文本/任意属性不得入库 + "page_view": {"referrer", "referrer_domain", "utm"}, + "page_stay": {"active_ms", "path"}, + "sdk_loaded": {"sdk_version"}, +} +MAX_PROP_DEPTH = 2 +MAX_PROP_JSON_BYTES = 4096 diff --git a/server/tracker/data/.gitkeep b/server/tracker/data/.gitkeep new file mode 100644 index 0000000..e69de29