chore(settings): 建立第一阶段分环境工程基线

This commit is contained in:
bruce
2026-09-01 14:35:38 +08:00
parent 3db6d83e04
commit e335b0de95
16 changed files with 344 additions and 126 deletions
+25
View File
@@ -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
+5
View File
@@ -31,9 +31,14 @@ htmlcov/
.env .env
.env.* .env.*
!.env.example !.env.example
*.sqlite3
*.sqlite3-journal
*.log *.log
logs/ logs/
# 自动化测试生成的临时媒体文件
.test-media/
# IDE 与编辑器 # IDE 与编辑器
.idea/ .idea/
.vscode/ .vscode/
+2 -1
View File
@@ -11,6 +11,7 @@ import os
from django.core.asgi import get_asgi_application 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() application = get_asgi_application()
-118
View File
@@ -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/'
+2
View File
@@ -0,0 +1,2 @@
"""JobRadar 分环境配置包。"""
+107
View File
@@ -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"
+53
View File
@@ -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。")
+32
View File
@@ -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")},
}
}
+48
View File
@@ -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"
+22
View File
@@ -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"
+2 -1
View File
@@ -11,6 +11,7 @@ import os
from django.core.wsgi import get_wsgi_application 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() application = get_wsgi_application()
+3
View File
@@ -8,6 +8,8 @@ scope: fullstack
# 开发计划:第一阶段工程基线(新功能) # 开发计划:第一阶段工程基线(新功能)
> 执行状态:已于 2026-09-01 开始实施;步骤 1“配置与依赖基线”已完成代码和配置落盘,下一步为步骤 2“用户认证与数据归属设计”。依赖环境同步曾因网络下载长时间无进度而中断,恢复网络后需重新执行环境更新和 Ruff/pytest 验证。
## 任务概述 ## 任务概述
第一阶段将当前 Django 基础骨架升级为可持续开发的应用基线。本阶段完成分环境配置、密钥隔离、用户认证、用户资料、用户数据归属边界、Agent 运行记录、最小管理及用户页面、日志、测试和代码质量工具。 第一阶段将当前 Django 基础骨架升级为可持续开发的应用基线。本阶段完成分环境配置、密钥隔离、用户认证、用户资料、用户数据归属边界、Agent 运行记录、最小管理及用户页面、日志、测试和代码质量工具。
@@ -184,3 +186,4 @@ ruff check .
| 日期 | 版本 | 说明 | | 日期 | 版本 | 说明 |
| --- | --- | --- | | --- | --- | --- |
| 2026-09-01 | 初稿 | 建立第一阶段工程基线开发计划,确认 SQLite 优先、PostgreSQL 集成验证和生产强制 PostgreSQL 的数据库策略。 | | 2026-09-01 | 初稿 | 建立第一阶段工程基线开发计划,确认 SQLite 优先、PostgreSQL 集成验证和生产强制 PostgreSQL 的数据库策略。 |
| 2026-09-01 | 步骤 1 | 完成分环境配置、密钥隔离、SQLite/PostgreSQL 配置边界、依赖定义及质量工具配置;环境依赖同步因网络中断待补跑。 |
+2 -2
View File
@@ -8,8 +8,8 @@
目标:建立适合公网运行和持续开发的Django基础。 目标:建立适合公网运行和持续开发的Django基础。
- [ ] 拆分开发、测试和生产配置。 - [x] 拆分开发、测试和生产配置。
- [ ] 将密钥及本地配置移出版本库。 - [x] 将密钥及本地配置移出版本库。
- [ ] 接入Django用户认证和用户资料模型。 - [ ] 接入Django用户认证和用户资料模型。
- [ ] 为用户私有业务数据建立统一归属边界。 - [ ] 为用户私有业务数据建立统一归属边界。
- [ ] 开发与测试默认使用SQLite,并建立可切换PostgreSQL的迁移基线。 - [ ] 开发与测试默认使用SQLite,并建立可切换PostgreSQL的迁移基线。
+8 -2
View File
@@ -6,7 +6,13 @@ dependencies:
- django=6.0.8 - django=6.0.8
- pip - pip
- pip: - pip:
# 页面采集和 PostgreSQL 驱动已存在于当前开发环境,先纳入可复现环境定义。 # 运行时依赖。PostgreSQL 使用 Psycopg 3,避免新项目继续依赖旧版 psycopg2。
- playwright==1.60.0 - playwright==1.60.0
- psycopg2-binary==2.9.12 - psycopg[binary]==3.3.5
- pillow==12.2.0 - 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
+3 -2
View File
@@ -6,7 +6,8 @@ import sys
def main(): def main():
"""Run administrative tasks.""" """Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'JobRadar.settings') # 命令行默认使用本地开发配置;其他环境必须显式覆盖该变量。
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "JobRadar.settings.development")
try: try:
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
except ImportError as exc: except ImportError as exc:
@@ -18,5 +19,5 @@ def main():
execute_from_command_line(sys.argv) execute_from_command_line(sys.argv)
if __name__ == '__main__': if __name__ == "__main__":
main() main()
+30
View File
@@ -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"]