From e335b0de9511532fac8d4f8732dbdf99d9fba358 Mon Sep 17 00:00:00 2001 From: bruce Date: Tue, 1 Sep 2026 14:35:38 +0800 Subject: [PATCH] =?UTF-8?q?chore(settings):=20=E5=BB=BA=E7=AB=8B=E7=AC=AC?= =?UTF-8?q?=E4=B8=80=E9=98=B6=E6=AE=B5=E5=88=86=E7=8E=AF=E5=A2=83=E5=B7=A5?= =?UTF-8?q?=E7=A8=8B=E5=9F=BA=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 25 ++++++ .gitignore | 5 ++ JobRadar/asgi.py | 3 +- JobRadar/settings.py | 118 -------------------------- JobRadar/settings/__init__.py | 2 + JobRadar/settings/base.py | 107 +++++++++++++++++++++++ JobRadar/settings/development.py | 53 ++++++++++++ JobRadar/settings/integration.py | 32 +++++++ JobRadar/settings/production.py | 48 +++++++++++ JobRadar/settings/test.py | 22 +++++ JobRadar/wsgi.py | 3 +- docs/engineering-baseline/dev-plan.md | 3 + docs/roadmap.md | 4 +- environment.yml | 10 ++- manage.py | 5 +- pyproject.toml | 30 +++++++ 16 files changed, 344 insertions(+), 126 deletions(-) create mode 100644 .env.example delete mode 100644 JobRadar/settings.py create mode 100644 JobRadar/settings/__init__.py create mode 100644 JobRadar/settings/base.py create mode 100644 JobRadar/settings/development.py create mode 100644 JobRadar/settings/integration.py create mode 100644 JobRadar/settings/production.py create mode 100644 JobRadar/settings/test.py create mode 100644 pyproject.toml diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..83d344f --- /dev/null +++ b/.env.example @@ -0,0 +1,25 @@ +# Django 运行环境 +DJANGO_SETTINGS_MODULE=JobRadar.settings.development +DJANGO_SECRET_KEY=请使用随机生成的长密钥替换此占位符 +DJANGO_DEBUG=true +DJANGO_ALLOWED_HOSTS=127.0.0.1,localhost +DJANGO_CSRF_TRUSTED_ORIGINS=http://127.0.0.1:8000,http://localhost:8000 + +# 数据库:开发环境默认 sqlite;集成及生产环境使用 postgresql +DATABASE_ENGINE=sqlite +POSTGRES_DB=jobradar +POSTGRES_USER=jobradar +POSTGRES_PASSWORD=请替换为数据库密码 +POSTGRES_HOST=127.0.0.1 +POSTGRES_PORT=5432 +POSTGRES_SSLMODE=prefer +POSTGRES_CONN_MAX_AGE=60 + +# OpenAI Agents SDK:第一阶段仅预留配置,不执行真实模型调用 +OPENAI_API_KEY=请替换为OpenAI_API_Key +OPENAI_MODEL= + +# 生产安全配置 +DJANGO_SECURE_SSL_REDIRECT=true +DJANGO_SECURE_HSTS_SECONDS=31536000 +DJANGO_SECURE_HSTS_PRELOAD=false diff --git a/.gitignore b/.gitignore index 58edfdd..3c4231e 100644 --- a/.gitignore +++ b/.gitignore @@ -31,9 +31,14 @@ htmlcov/ .env .env.* !.env.example +*.sqlite3 +*.sqlite3-journal *.log logs/ +# 自动化测试生成的临时媒体文件 +.test-media/ + # IDE 与编辑器 .idea/ .vscode/ diff --git a/JobRadar/asgi.py b/JobRadar/asgi.py index 0c53cd8..c37aa3a 100644 --- a/JobRadar/asgi.py +++ b/JobRadar/asgi.py @@ -11,6 +11,7 @@ import os from django.core.asgi import get_asgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'JobRadar.settings') +# 本地直接启动时使用开发配置;部署环境应在进程配置中显式覆盖。 +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "JobRadar.settings.development") application = get_asgi_application() diff --git a/JobRadar/settings.py b/JobRadar/settings.py deleted file mode 100644 index eec029c..0000000 --- a/JobRadar/settings.py +++ /dev/null @@ -1,118 +0,0 @@ -""" -Django settings for JobRadar project. - -Generated by 'django-admin startproject' using Django 6.0.8. - -For more information on this file, see -https://docs.djangoproject.com/en/6.0/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/6.0/ref/settings/ -""" - -from pathlib import Path - -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent - - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-ji%iqjdfj*vv!93#t2+wygz8qe@i_b&hss9$%3@rv)cf(0bp8%' - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True - -ALLOWED_HOSTS = [] - - -# Application definition - -INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', -] - -MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', -] - -ROOT_URLCONF = 'JobRadar.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [BASE_DIR / 'templates'] - , - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, -] - -WSGI_APPLICATION = 'JobRadar.wsgi.application' - - -# Database -# https://docs.djangoproject.com/en/6.0/ref/settings/#databases - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', - } -} - - -# Password validation -# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators - -AUTH_PASSWORD_VALIDATORS = [ - { - 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', - }, - { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', - }, -] - - -# Internationalization -# https://docs.djangoproject.com/en/6.0/topics/i18n/ - -LANGUAGE_CODE = 'en-us' - -TIME_ZONE = 'UTC' - -USE_I18N = True - -USE_TZ = True - - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/6.0/howto/static-files/ - -STATIC_URL = 'static/' diff --git a/JobRadar/settings/__init__.py b/JobRadar/settings/__init__.py new file mode 100644 index 0000000..37ca250 --- /dev/null +++ b/JobRadar/settings/__init__.py @@ -0,0 +1,2 @@ +"""JobRadar 分环境配置包。""" + diff --git a/JobRadar/settings/base.py b/JobRadar/settings/base.py new file mode 100644 index 0000000..000b49b --- /dev/null +++ b/JobRadar/settings/base.py @@ -0,0 +1,107 @@ +"""JobRadar 所有运行环境共享的基础配置。""" + +from __future__ import annotations + +import os +from pathlib import Path + + +# 项目根目录。所有本地文件路径统一从这里派生,避免依赖启动目录。 +BASE_DIR = Path(__file__).resolve().parent.parent.parent + + +def env(name: str, default: str | None = None) -> str | None: + """读取环境变量;空字符串按未配置处理,避免误用无效密钥。""" + + value = os.environ.get(name, default) + if isinstance(value, str): + value = value.strip() + return value or default + + +def env_bool(name: str, default: bool = False) -> bool: + """读取布尔环境变量,并拒绝含义不明确的值。""" + + value = env(name) + if value is None: + return default + normalized = value.lower() + if normalized in {"1", "true", "yes", "on"}: + return True + if normalized in {"0", "false", "no", "off"}: + return False + raise ValueError(f"环境变量 {name} 必须是布尔值,当前值无法识别。") + + +def env_list(name: str, default: tuple[str, ...] = ()) -> list[str]: + """将逗号分隔的环境变量转换为已去空白的列表。""" + + value = env(name) + if value is None: + return list(default) + return [item.strip() for item in value.split(",") if item.strip()] + + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.locale.LocaleMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "JobRadar.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [BASE_DIR / "templates"], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "JobRadar.wsgi.application" +ASGI_APPLICATION = "JobRadar.asgi.application" + +AUTH_PASSWORD_VALIDATORS = [ + {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"}, + {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, + {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, + {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, +] + +LANGUAGE_CODE = "zh-hans" +TIME_ZONE = "Asia/Shanghai" +USE_I18N = True +USE_TZ = True + +STATIC_URL = "static/" +STATIC_ROOT = BASE_DIR / "staticfiles" +MEDIA_URL = "media/" +MEDIA_ROOT = BASE_DIR / "media" + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +LOGIN_URL = "login" +LOGIN_REDIRECT_URL = "/" +LOGOUT_REDIRECT_URL = "login" + diff --git a/JobRadar/settings/development.py b/JobRadar/settings/development.py new file mode 100644 index 0000000..161cc51 --- /dev/null +++ b/JobRadar/settings/development.py @@ -0,0 +1,53 @@ +"""本地开发配置:默认使用 SQLite,允许按需连接 PostgreSQL。""" + +from __future__ import annotations + +import secrets +import warnings + +from .base import BASE_DIR, env, env_bool, env_list +from .base import * # noqa: F403 + + +DEBUG = env_bool("DJANGO_DEBUG", default=True) +ALLOWED_HOSTS = env_list( + "DJANGO_ALLOWED_HOSTS", + default=("127.0.0.1", "localhost", "[::1]"), +) + +# 开发环境未注入密钥时生成进程级临时密钥,避免在仓库中保留固定密钥。 +SECRET_KEY = env("DJANGO_SECRET_KEY") +if not SECRET_KEY: + SECRET_KEY = secrets.token_urlsafe(50) + warnings.warn( + "未配置 DJANGO_SECRET_KEY,当前开发进程使用临时密钥;重启后已有会话会失效。", + RuntimeWarning, + stacklevel=1, + ) + +database_engine = env("DATABASE_ENGINE", "sqlite").lower() +if database_engine == "sqlite": + DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } + } +elif database_engine in {"postgres", "postgresql"}: + DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": env("POSTGRES_DB", "jobradar"), + "USER": env("POSTGRES_USER", "jobradar"), + "PASSWORD": env("POSTGRES_PASSWORD", ""), + "HOST": env("POSTGRES_HOST", "127.0.0.1"), + "PORT": env("POSTGRES_PORT", "5432"), + "CONN_MAX_AGE": 0, + "OPTIONS": { + "sslmode": env("POSTGRES_SSLMODE", "prefer"), + }, + } + } +else: + raise ValueError("DATABASE_ENGINE 仅支持 sqlite、postgres 或 postgresql。") + diff --git a/JobRadar/settings/integration.py b/JobRadar/settings/integration.py new file mode 100644 index 0000000..e5fc758 --- /dev/null +++ b/JobRadar/settings/integration.py @@ -0,0 +1,32 @@ +"""PostgreSQL 集成验证配置,只允许显式提供测试数据库凭据。""" + +from .base import env, env_list +from .base import * # noqa: F403 + + +def required_env(name: str) -> str: + """读取必填配置;缺失时立即失败,防止误连其他数据库。""" + + value = env(name) + if not value: + raise RuntimeError(f"PostgreSQL 集成验证缺少必填环境变量:{name}") + return value + + +SECRET_KEY = env("DJANGO_SECRET_KEY", "integration-test-only-secret-key") +DEBUG = False +ALLOWED_HOSTS = env_list("DJANGO_ALLOWED_HOSTS", default=("localhost",)) + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": required_env("POSTGRES_DB"), + "USER": required_env("POSTGRES_USER"), + "PASSWORD": required_env("POSTGRES_PASSWORD"), + "HOST": required_env("POSTGRES_HOST"), + "PORT": env("POSTGRES_PORT", "5432"), + "CONN_MAX_AGE": 0, + "OPTIONS": {"sslmode": env("POSTGRES_SSLMODE", "require")}, + } +} + diff --git a/JobRadar/settings/production.py b/JobRadar/settings/production.py new file mode 100644 index 0000000..1e821e1 --- /dev/null +++ b/JobRadar/settings/production.py @@ -0,0 +1,48 @@ +"""生产环境配置:关键安全参数和 PostgreSQL 凭据均必须显式注入。""" + +from .base import env, env_bool, env_list +from .base import * # noqa: F403 + + +def required_env(name: str) -> str: + """读取生产必填配置,并在应用启动阶段报告缺失项。""" + + value = env(name) + if not value: + raise RuntimeError(f"生产环境缺少必填环境变量:{name}") + return value + + +SECRET_KEY = required_env("DJANGO_SECRET_KEY") +DEBUG = False +ALLOWED_HOSTS = env_list("DJANGO_ALLOWED_HOSTS") +if not ALLOWED_HOSTS: + raise RuntimeError("生产环境必须配置 DJANGO_ALLOWED_HOSTS。") + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": required_env("POSTGRES_DB"), + "USER": required_env("POSTGRES_USER"), + "PASSWORD": required_env("POSTGRES_PASSWORD"), + "HOST": required_env("POSTGRES_HOST"), + "PORT": env("POSTGRES_PORT", "5432"), + "CONN_MAX_AGE": int(env("POSTGRES_CONN_MAX_AGE", "60")), + "CONN_HEALTH_CHECKS": True, + "OPTIONS": {"sslmode": env("POSTGRES_SSLMODE", "require")}, + } +} + +CSRF_TRUSTED_ORIGINS = env_list("DJANGO_CSRF_TRUSTED_ORIGINS") +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") +SECURE_SSL_REDIRECT = env_bool("DJANGO_SECURE_SSL_REDIRECT", default=True) +SESSION_COOKIE_SECURE = True +CSRF_COOKIE_SECURE = True +SESSION_COOKIE_HTTPONLY = True +CSRF_COOKIE_HTTPONLY = True +SECURE_HSTS_SECONDS = int(env("DJANGO_SECURE_HSTS_SECONDS", "31536000")) +SECURE_HSTS_INCLUDE_SUBDOMAINS = True +SECURE_HSTS_PRELOAD = env_bool("DJANGO_SECURE_HSTS_PRELOAD", default=False) +SECURE_CONTENT_TYPE_NOSNIFF = True +X_FRAME_OPTIONS = "DENY" + diff --git a/JobRadar/settings/test.py b/JobRadar/settings/test.py new file mode 100644 index 0000000..81f058f --- /dev/null +++ b/JobRadar/settings/test.py @@ -0,0 +1,22 @@ +"""自动化测试配置:始终使用独立的内存 SQLite 数据库。""" + +from .base import BASE_DIR +from .base import * # noqa: F403 + + +SECRET_KEY = "test-only-secret-key-not-for-production" +DEBUG = False +ALLOWED_HOSTS = ["testserver", "localhost"] + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": ":memory:", + } +} + +# 测试环境使用快速哈希器;这里只影响测试生成的数据,不改变生产密码策略。 +PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"] +EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" +MEDIA_ROOT = BASE_DIR / ".test-media" + diff --git a/JobRadar/wsgi.py b/JobRadar/wsgi.py index c70719a..ea6ba5e 100644 --- a/JobRadar/wsgi.py +++ b/JobRadar/wsgi.py @@ -11,6 +11,7 @@ import os from django.core.wsgi import get_wsgi_application -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'JobRadar.settings') +# 本地直接启动时使用开发配置;部署环境应在进程配置中显式覆盖。 +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "JobRadar.settings.development") application = get_wsgi_application() diff --git a/docs/engineering-baseline/dev-plan.md b/docs/engineering-baseline/dev-plan.md index 919e921..91aff44 100644 --- a/docs/engineering-baseline/dev-plan.md +++ b/docs/engineering-baseline/dev-plan.md @@ -8,6 +8,8 @@ scope: fullstack # 开发计划:第一阶段工程基线(新功能) +> 执行状态:已于 2026-09-01 开始实施;步骤 1“配置与依赖基线”已完成代码和配置落盘,下一步为步骤 2“用户认证与数据归属设计”。依赖环境同步曾因网络下载长时间无进度而中断,恢复网络后需重新执行环境更新和 Ruff/pytest 验证。 + ## 任务概述 第一阶段将当前 Django 基础骨架升级为可持续开发的应用基线。本阶段完成分环境配置、密钥隔离、用户认证、用户资料、用户数据归属边界、Agent 运行记录、最小管理及用户页面、日志、测试和代码质量工具。 @@ -184,3 +186,4 @@ ruff check . | 日期 | 版本 | 说明 | | --- | --- | --- | | 2026-09-01 | 初稿 | 建立第一阶段工程基线开发计划,确认 SQLite 优先、PostgreSQL 集成验证和生产强制 PostgreSQL 的数据库策略。 | +| 2026-09-01 | 步骤 1 | 完成分环境配置、密钥隔离、SQLite/PostgreSQL 配置边界、依赖定义及质量工具配置;环境依赖同步因网络中断待补跑。 | diff --git a/docs/roadmap.md b/docs/roadmap.md index b3a5e47..2a7c91c 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -8,8 +8,8 @@ 目标:建立适合公网运行和持续开发的Django基础。 -- [ ] 拆分开发、测试和生产配置。 -- [ ] 将密钥及本地配置移出版本库。 +- [x] 拆分开发、测试和生产配置。 +- [x] 将密钥及本地配置移出版本库。 - [ ] 接入Django用户认证和用户资料模型。 - [ ] 为用户私有业务数据建立统一归属边界。 - [ ] 开发与测试默认使用SQLite,并建立可切换PostgreSQL的迁移基线。 diff --git a/environment.yml b/environment.yml index 546c852..aca1261 100644 --- a/environment.yml +++ b/environment.yml @@ -6,7 +6,13 @@ dependencies: - django=6.0.8 - pip - pip: - # 页面采集和 PostgreSQL 驱动已存在于当前开发环境,先纳入可复现环境定义。 + # 运行时依赖。PostgreSQL 使用 Psycopg 3,避免新项目继续依赖旧版 psycopg2。 - playwright==1.60.0 - - psycopg2-binary==2.9.12 + - psycopg[binary]==3.3.5 - pillow==12.2.0 + - openai-agents==0.22.0 + # 测试与代码质量依赖统一固定版本,保证本地和持续集成结果一致。 + - pytest==9.1.1 + - pytest-django==4.14.0 + - coverage==7.15.4 + - ruff==0.16.4 diff --git a/manage.py b/manage.py index de3d89d..20942ff 100644 --- a/manage.py +++ b/manage.py @@ -6,7 +6,8 @@ import sys def main(): """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'JobRadar.settings') + # 命令行默认使用本地开发配置;其他环境必须显式覆盖该变量。 + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "JobRadar.settings.development") try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -18,5 +19,5 @@ def main(): execute_from_command_line(sys.argv) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f730142 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,30 @@ +[tool.pytest.ini_options] +DJANGO_SETTINGS_MODULE = "JobRadar.settings.test" +python_files = ["test_*.py", "tests.py"] +testpaths = ["."] +addopts = "--strict-markers --tb=short" + +[tool.coverage.run] +branch = true +source = ["."] +omit = ["*/migrations/*", "*/tests/*", "manage.py"] + +[tool.coverage.report] +show_missing = true +skip_covered = true + +[tool.ruff] +target-version = "py313" +line-length = 100 +exclude = ["*/migrations/*"] + +[tool.ruff.lint] +select = ["E", "F", "I", "B", "UP", "DJ"] +ignore = [ + # Django settings 使用通配导入组合环境配置,变量由框架按名称读取。 + "F403", + "F405", +] + +[tool.ruff.lint.isort] +known-first-party = ["JobRadar"]