# Fonrey 项目骨架搭建 — 工程执行提示词 > **版本**:v2.3(2026-04-29)|v2.0 修复 P0×5+P1×4;v2.1 修复 P0×9(交叉比对 AGENTS.md / 测试规范.md / 系统管理技术文档.md);v2.2 收口剩余一致性问题(URL 分离、Admin 弃用、密钥变量统一、测试 settings 一致性、环境变量占位修复);v2.3 修复开工阻塞(R2 密钥变量名、release 结构冲突、DB 连接参数)并补齐 API_CONTRACT 核对清单 > **v2.3 主要变更**:修复 `AWS_SECRET_ACCESS_KEY` 环境变量占位错误(统一为 `R2_SECRET_ACCESS_KEY`);移除 `DATABASES.OPTIONS.pool_size` 非标准参数;修正执行清单中 `apps/release` 误要求 `services/` 与 `tasks.py` 的冲突;补充 API_CONTRACT 强制核对清单(路径/方法/参数/响应 envelope/错误码/@extend_schema/openapi.json/schemathesis);统一 URL 命名口径说明(`config.urls` 对应系统文档 `config.urls_tenant`) ## 你的角色与约束 你是一名资深 Django 后端工程师。你的任务是**严格按照规范**搭建 Fonrey 项目骨架,不得自行发明技术方案,不得引入文档未授权的第三方库。每一步操作后必须验证结果。 **项目工作目录**:`/mnt/c/Project/`(在此目录下创建 `fonrey/` 子目录) **执行方式**:逐步创建,每创建一个文件/目录后立即验证,遇到冲突停下来询问而不是自行决策。 --- ## 一、技术栈约束(必读,不得违反) | 层级 | 技术 | 版本约束 | |------|------|----------| | Backend | Django | 4.2 LTS(ASGI 模式) | | Multi-tenant | django-tenants | latest stable | | Database | PostgreSQL | 16 | | Cache | Redis | latest stable | | Tasks | Celery + Celery Beat | latest stable | | Storage | django-storages + boto3 | Cloudflare R2(S3 兼容) | | Frontend | HTMX + Alpine.js + Tailwind CSS | HTMX 2.x, Alpine 3.x, Tailwind 3.x | | Icons | Heroicons v2 | inline SVG via templatetag | | Server | Gunicorn + Uvicorn workers | ASGI | | Container | Docker + Docker Compose | — | | Monitoring | Sentry SDK | — | **绝对禁止**:React / Vue / Angular;任何 JS 框架;`nodeIntegration: true`;硬编码密钥/ID;跨租户 SQL 查询。 --- ## 二、目录结构(严格按此创建,不得增减顶层结构) ``` fonrey/ ├── apps/ │ ├── tenant/ # django-tenants 配置(in SHARED_APPS) │ ├── account/ # 登录认证(in TENANT_APPS) │ ├── permission/ # 权限管理(in TENANT_APPS) │ ├── org/ # 组织人事(in TENANT_APPS) │ ├── region/ # 区域管理(in TENANT_APPS) │ ├── complex/ # 楼盘管理(in TENANT_APPS) │ ├── property/ # 房源核心(in TENANT_APPS) │ ├── client/ # 客源管理(in TENANT_APPS) │ ├── setting/ # 系统设置(in TENANT_APPS) │ └── release/ # 客户端发布管理(in SHARED_APPS,⚠️ 无 services/,不做多租户隔离) ├── core/ │ ├── __init__.py │ ├── models/ │ │ ├── __init__.py │ │ └── base.py # 抽象基类(见第四节规范) │ ├── enums.py # 全局枚举类(与 ENUMS.md 严格对齐,供 models/serializers 导入) │ ├── encryption.py # PII 加密(AES-256-GCM) │ ├── cache.py # Redis 工具 │ ├── templatetags/ │ │ ├── __init__.py │ │ └── heroicons.py # {% heroicon 'plus' %} templatetag │ └── middleware/ │ ├── __init__.py │ └── audit.py # 审计日志中间件骨架 ├── shared/ │ ├── __init__.py │ └── apps.py # 公共 Schema App 配置 ├── config/ │ ├── __init__.py │ ├── settings/ │ │ ├── __init__.py │ │ ├── base.py # 基础配置 │ │ ├── development.py │ │ ├── testing.py # 测试配置(pytest) │ │ └── production.py │ ├── urls.py # tenant schema 路由入口 │ ├── urls_public.py # public schema 路由入口 │ ├── asgi.py # ASGI 入口 │ └── wsgi.py ├── templates/ │ ├── base.html # 全局基础模板 │ ├── layouts/ │ │ ├── auth.html # 认证页独立布局 │ │ └── app.html # 主应用布局(含 Topbar + Sidebar) │ ├── components/ # 可复用组件片段 │ │ ├── topbar.html │ │ ├── sidebar.html │ │ ├── pagination.html │ │ ├── toast.html │ │ ├── modal.html │ │ └── empty-state.html │ └── errors/ │ ├── 403.html │ ├── 404.html │ └── 500.html ├── static/ │ ├── css/ │ │ └── main.css # Tailwind 入口(@tailwind directives) │ ├── js/ │ │ └── main.js # Alpine.js 初始化 + 全局 HTMX 事件 │ └── vendor/ # 第三方 JS/CSS(htmx.min.js, alpine.min.js 等) ├── locale/ # 预留国际化(v2),当前仅中文 ├── .env.example ├── .env # 不入 git ├── .gitignore ├── manage.py ├── requirements/ │ ├── base.txt │ ├── development.txt │ └── production.txt ├── docker-compose.yml ├── docker-compose.prod.yml ├── Dockerfile ├── Makefile # 常用命令快捷方式 ├── tailwind.config.js ├── package.json # 仅用于 Tailwind 构建,不含业务 JS └── pyproject.toml # ruff + black + isort 配置 ``` 每个 `apps//` 内部结构如下(以 `property` 为典型,其他 App 骨架相同): ``` apps/property/ ├── __init__.py ├── apps.py ├── admin.py ├── models/ │ ├── __init__.py # 统一 re-export │ └── .gitkeep # 骨架阶段留空,后续一表一文件 ├── services/ │ ├── __init__.py │ └── .gitkeep ├── tasks.py # Celery 任务骨架(见第九节模板) ├── views.py # HTMX/JSON 视图骨架 ├── urls.py ├── templates/ │ └── property/ # App 级模板(可覆盖全局同名模板) └── tests/ ├── __init__.py └── .gitkeep ``` **⚠️ `apps/release/` 的内部结构特殊**(in SHARED_APPS,不做多租户数据隔离): ``` └── tests/ ├── __init__.py └── .gitkeep # App 内单元测试(纯 model/service 逻辑,无 HTTP) ``` **整体 tests/ 分层规范**(P1-10): - `apps//tests/` — 单元测试(该 App 内部逻辑,不跨 App) - 项目根 `tests/` — 集成测试 / E2E(跨 App 或依赖外部服务) ``` tests/ # 项目根集成/E2E 测试 ├── __init__.py ├── conftest.py # 全局 fixture(DB、租户、认证) ├── integration/ │ ├── __init__.py │ ├── property/ │ │ └── .gitkeep │ ├── client/ │ │ └── .gitkeep │ └── release/ │ └── test_client_update_api.py # schemathesis 契约测试骨架 └── e2e/ ├── __init__.py └── .gitkeep # playwright E2E(pytest-playwright) ``` ⚠️ `release/` **无** `services/`、`tasks.py`;不引用 `schema_context`。 ``` apps/release/ ├── __init__.py ├── apps.py ├── admin.py ├── models/ │ ├── __init__.py │ └── .gitkeep # ClientRelease 模型 ├── views.py # 公开 API(无需登录),返回 JSON ├── urls.py # 被 config/urls_public.py include(public schema) └── serializers.py # DRF Serializer(配合 drf-spectacular) ``` --- ## 三、Django 配置规范 ### 3.0 URL 路由文件职责(强制分离,禁止合并) ```python # config/urls.py — Tenant schema 路由入口(仅 tenant) from django.urls import path, include urlpatterns = [ path("", include("apps.account.urls")), path("", include("apps.property.urls")), path("", include("apps.client.urls")), path("", include("apps.complex.urls")), path("", include("apps.org.urls")), path("", include("apps.permission.urls")), path("", include("apps.setting.urls")), ] ``` ```python # config/urls_public.py — Public schema 专用路由(管理后台、release API、OpenAPI) # ⚠️ 对齐 TECH_STACK/系统管理技术文档.md:Django Admin 全环境弃用,不注册任何管理后台路由 from django.urls import path, include from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView urlpatterns = [ path("api/client/", include("apps.release.urls")), # apps.release.urls 内定义 updates/latest/ # OpenAPI — 仅 DEBUG 暴露,production 通过 nginx ACL 限制 path("api/schema/", SpectacularAPIView.as_view(), name="schema"), path("api/docs/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui"), ] ``` ⚠️ django-tenants URL routing 强制规范(系统管理技术文档.md:207): ```python # config/settings/base.py — 多租户 URL 分离配置(必须显式声明) ROOT_URLCONF = "config.urls" # tenant schema 路由入口 PUBLIC_SCHEMA_URLCONF = "config.urls_public" # public schema 路由入口 # 口径说明:系统管理技术文档中的 `config.urls_tenant` 在本骨架中命名为 `config.urls`,语义等价。 ``` > `config/urls.py` 仅包含 `urlpatterns`(tenant 路由),`config/urls_public.py` 包含 `urlpatterns`(public 路由)。两个文件分开维护,**不得合并**。 ### 3.1 INSTALLED_APPS 分区 ```python # config/settings/base.py SHARED_APPS = [ "django_tenants", # 必须第一位 "apps.tenant", # Tenant / Domain 模型 "apps.release", # ClientRelease 模型 "shared", # 公共 Schema App # Django 内置 "django.contrib.contenttypes", "django.contrib.auth", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", # 第三方(shared) "django_celery_beat", "django_celery_results", "rest_framework", # DRF(drf-spectacular 依赖) "drf_spectacular", # OpenAPI schema(API_CONTRACT.md §11 MUST) "core", # 基础工具层(非业务 App,放 shared 确保迁移可见) "django_htmx", # HTMX 中间件(request.htmx 语义支持,系统管理技术文档.md:431) "django_extensions", # shell_plus 等开发辅助命令 ] TENANT_APPS = [ "apps.account", "apps.permission", "apps.org", "apps.region", "apps.complex", "apps.property", "apps.client", "apps.setting", ] INSTALLED_APPS = list(SHARED_APPS) + list(TENANT_APPS) ``` ### 3.2 核心配置项(base.py 必须包含以下所有项) ```python # 多租户 TENANT_MODEL = "tenant.Tenant" TENANT_DOMAIN_MODEL = "tenant.Domain" DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" # 非 UUID 表的默认 PK;业务表 UUID PK 靠 UUIDPrimaryKeyModel # 数据库(从环境变量读取) DATABASES = { "default": { "ENGINE": "django_tenants.postgresql_backend", "NAME": env("DB_NAME"), "USER": env("DB_USER"), "PASSWORD": env("DB_PASSWORD"), "HOST": env("DB_HOST", default="localhost"), "PORT": env("DB_PORT", default="5432"), "CONN_MAX_AGE": 60, # PgBouncer 连接池在 DB/Proxy 层管理;此处不注入非标准 DSN 参数,避免驱动报错 } } DATABASE_ROUTERS = ["django_tenants.routers.TenantSyncRouter"] # Redis(Cache + Session + Celery Broker) CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": env("REDIS_URL", default="redis://127.0.0.1:6379/0"), "OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"}, "KEY_PREFIX": "fonrey", } } SESSION_ENGINE = "django.contrib.sessions.backends.cache" SESSION_CACHE_ALIAS = "default" # Celery CELERY_BROKER_URL = env("CELERY_BROKER_URL", default="redis://127.0.0.1:6379/1") CELERY_RESULT_BACKEND = "django-db" CELERY_TASK_ALWAYS_EAGER = False CELERY_TASK_TIME_LIMIT = 300 CELERY_TASK_SOFT_TIME_LIMIT = 270 # 存储(Cloudflare R2) DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage" AWS_S3_ENDPOINT_URL = env("R2_ENDPOINT_URL") AWS_ACCESS_KEY_ID = env("R2_ACCESS_KEY_ID") AWS_SECRET_ACCESS_KEY = env("R2_SECRET_ACCESS_KEY") AWS_STORAGE_BUCKET_NAME = env("R2_BUCKET_NAME", default="media") AWS_S3_CUSTOM_DOMAIN = env("R2_CUSTOM_DOMAIN", default=None) AWS_DEFAULT_ACL = "private" # ASGI ASGI_APPLICATION = "config.asgi.application" # Sentry(production 环境激活) # 见 production.py # 安全 SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_SAMESITE = "Lax" CSRF_COOKIE_HTTPONLY = False # ⚠️ HTMX 需要 JS 读取 CSRF token,故意设为 False,禁止"修复"此项 X_FRAME_OPTIONS = "DENY" # 模板 TEMPLATES = [{ "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [BASE_DIR / "templates"], "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", ] }, }] # HTMX HTMX_GLOBAL_CSRF = True # 全局 CSRF 注入 # drf-spectacular(OpenAPI,API_CONTRACT.md §11 MUST) SPECTACULAR_SETTINGS = { "TITLE": "Fonrey API", "DESCRIPTION": "Fonrey 房产经纪管理系统 OpenAPI 3.1", "VERSION": "1.0.0", "SERVE_INCLUDE_SCHEMA": False, "COMPONENT_SPLIT_REQUEST": True, "ENUM_GENERATE_CHOICE_DESCRIPTION": True, # 对齐 API_CONTRACT.md §11,Schema 中展开枚举说明 } # 日志(骨架,production 扩展) LOGGING = { "version": 1, "disable_existing_loggers": False, "handlers": {"console": {"class": "logging.StreamHandler"}}, "root": {"handlers": ["console"], "level": "INFO"}, } ``` ### 3.3 中间件顺序(严格按此,不得调整) ```python MIDDLEWARE = [ "django_tenants.middleware.main.TenantMainMiddleware", # 必须第一位 "django.middleware.security.SecurityMiddleware", "whitenoise.middleware.WhiteNoiseMiddleware", # 静态文件 "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", "django_htmx.middleware.HtmxMiddleware", # HTMX:request.htmx(系统管理技术文档.md:431) "core.middleware.audit.AuditMiddleware", # 自定义审计(骨架) ] ``` --- ## 四、核心抽象基类(core/models/base.py) 严格按以下规范实现,不得修改字段名、类型、顺序: ```python import uuid from django.db import models from django.utils import timezone class UUIDPrimaryKeyModel(models.Model): """所有业务模型的根基类:UUID v4 主键""" id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) class Meta: abstract = True class TimeStampedModel(UUIDPrimaryKeyModel): """追加创建/更新时间(TIMESTAMPTZ)""" created_at = models.DateTimeField(auto_now_add=True, db_index=True) updated_at = models.DateTimeField(auto_now=True) class Meta: abstract = True ordering = ["-created_at"] class ActiveManager(models.Manager): """默认只返回未软删除的记录""" def get_queryset(self): return super().get_queryset().filter(deleted_at__isnull=True) class SoftDeleteModel(TimeStampedModel): """软删除:deleted_at=NULL 表示未删除""" deleted_at = models.DateTimeField(null=True, blank=True, db_index=True) objects = ActiveManager() # 默认过滤已删除 all_objects = models.Manager() # 包含已删除记录 def delete(self, using=None, keep_parents=False): self.deleted_at = timezone.now() self.save(update_fields=["deleted_at"]) def hard_delete(self): super().delete() def restore(self): self.deleted_at = None self.save(update_fields=["deleted_at"]) @property def is_deleted(self): return self.deleted_at is not None class Meta: abstract = True class AuditedModel(SoftDeleteModel): """审计字段:操作人(FK to Staff,允许 NULL 表示系统操作)""" created_by = models.ForeignKey( "org.Staff", null=True, blank=True, on_delete=models.SET_NULL, related_name="%(app_label)s_%(class)s_created", db_index=True, ) updated_by = models.ForeignKey( "org.Staff", null=True, blank=True, on_delete=models.SET_NULL, related_name="%(app_label)s_%(class)s_updated", ) class Meta: abstract = True ``` --- ## 五、全局枚举骨架(core/enums.py) **权威来源**:`DATA_MODEL/ENUMS.md v2.2`。本文件是 ENUMS.md 的 Python 镜像,每次 ENUMS.md 更新后必须同步。 **规范**:所有枚举值 `lower_snake_case`,禁止在 models/serializers 中硬编码字符串枚举值,必须从此文件导入。 ```python from django.db import models # ────────────────────────────────────────────── # public schema 枚举 # ────────────────────────────────────────────── class PlatformAuditResult(models.TextChoices): SUCCESS = "success", "通过" FAILED = "failed", "未通过" class UpgradeType(models.TextChoices): APP = "app", "应用升级" SCHEMA = "schema", "数据库 Schema 升级" FEATURE = "feature", "功能开关升级" class ExportTaskStatus(models.TextChoices): PENDING = "pending", "排队中" PROCESSING = "processing", "处理中" SUCCESS = "success", "成功" FAILED = "failed", "失败" # ────────────────────────────────────────────── # property 枚举(骨架,值见 ENUMS.md §property) # ────────────────────────────────────────────── class PropertyGrade(models.TextChoices): A = "a", "A(急迫)" B = "b", "B(优先)" C = "c", "C(普通)" D = "d", "D(搁置)" # ────────────────────────────────────────────── # client 枚举(骨架,值见 ENUMS.md §client) # ────────────────────────────────────────────── class ClientGrade(models.TextChoices): A = "a", "A(急迫)" B = "b", "B(优先)" C = "c", "C(普通)" D = "d", "D(搁置)" E = "e", "E(冷冻)" class ClientType(models.TextChoices): PRIVATE = "private", "私客" PUBLIC = "public", "公客" TRANSACTED = "transacted", "成交客" INVALID = "invalid", "无效客" class ClientStatus(models.TextChoices): BUYING = "buying", "买房中" RENTING = "renting", "租房中" BUY_OR_RENT = "buy_or_rent", "买租均可" SUSPENDED = "suspended", "暂停" BOUGHT = "bought", "已购" RENTED_DONE = "rented_done", "已租" PUBLIC = "public", "已转公客" INVALID = "invalid", "已无效" # ────────────────────────────────────────────── # permission 枚举(骨架,值见 ENUMS.md §permission) # ────────────────────────────────────────────── class PermissionValueType(models.TextChoices): BOOLEAN = "boolean", "布尔" SCOPE = "scope", "数据范围" INTEGER = "integer", "整数" class PermissionOverrideMode(models.TextChoices): REPLACE = "replace", "替换" RESTRICT = "restrict", "限制(取更严格)" GRANT = "grant", "授权(取更宽松)" # TODO:其余枚举按 ENUMS.md 顺序补全(setting、org、complex、account...) ``` --- ## 六、PII 加密(core/encryption.py) 骨架实现,接口固定(后续补充实现体),确保接口签名正确: > ⚠️ **算法强制规范(AGENTS.md §4.4):必须用 AES-256-GCM,禁止 Fernet(Fernet 是 AES-128-CBC)。** > `cryptography` 库使用 `cryptography.hazmat.primitives.ciphers.aead.AESGCM`。 ```python import hashlib import os import base64 from django.conf import settings from cryptography.hazmat.primitives.ciphers.aead import AESGCM class PhoneEncryption: """ 手机号 AES-256-GCM 加密存储 + SHA-256 哈希索引 - 加密算法:AES-256-GCM(AGENTS.md §4.4,对照 TECH_STACK/系统管理技术文档.md) - 存储字段:phone_encrypted(base64 密文)+ phone_hash(SHA-256,用于精确查询) - 显示:脱敏格式 138****1234 密钥来源:settings.PHONE_ENCRYPTION_KEY(32字节,base64 encoded,从 .env 注入) """ @staticmethod def _get_key() -> bytes: key_b64 = settings.PHONE_ENCRYPTION_KEY return base64.b64decode(key_b64) # 必须 32 bytes(AES-256) @staticmethod def encrypt(phone: str) -> str: """加密手机号,返回 base64(nonce + ciphertext + tag)""" ... # TODO: AESGCM(key).encrypt(nonce, phone.encode(), None) @staticmethod def decrypt(ciphertext: str) -> str: """解密返回明文""" ... # TODO: AESGCM(key).decrypt(nonce, ciphertext_bytes, None).decode() @staticmethod def hash(phone: str) -> str: """返回 SHA-256 哈希(用于 DB 索引查询)""" ... # TODO: hashlib.sha256(phone.encode()).hexdigest() @staticmethod def mask(phone: str) -> str: """返回脱敏格式:138****1234""" if not phone or len(phone) < 7: return "***" return phone[:3] + "****" + phone[-4:] ``` --- ## 七、Heroicons Templatetag(core/templatetags/heroicons.py) ```python from django import template from django.utils.safestring import mark_safe import os register = template.Library() ICONS_PATH = os.path.join(os.path.dirname(__file__), "..", "static", "icons") @register.simple_tag def heroicon(name: str, size: str = "24", style: str = "outline", css_class: str = "") -> str: """ 用法: {% heroicon 'plus' %} {% heroicon 'trash' size='20' style='solid' css_class='text-danger-600' %} """ # 骨架:实际从 heroicons vendor 文件读取 SVG # size 可选: 12, 16, 20, 24 # style 可选: outline, solid, mini size_int = int(size) css = f'class="w-{size_int // 4} h-{size_int // 4} {css_class}"' return mark_safe(f'') # TODO: 替换为实际 SVG ``` --- ## 八、Redis 工具(core/cache.py) **Redis Key 格式规范**(所有 Redis 操作必须遵守):`{tenant_schema}:{module}:{key}` 禁止裸字符串拼接 Key;public schema 操作传 `"public"` 作为 `tenant_schema`。 ```python from django.core.cache import cache def get_redis_key(tenant_schema: str, module: str, key: str) -> str: """ 构造带租户前缀的 Redis Key。 示例:get_redis_key("acme", "permission", "staff:uuid-xxx") → "acme:permission:staff:uuid-xxx" """ return f"{tenant_schema}:{module}:{key}" def cache_get(tenant_schema: str, module: str, key: str): return cache.get(get_redis_key(tenant_schema, module, key)) def cache_set(tenant_schema: str, module: str, key: str, value, timeout: int = 300): cache.set(get_redis_key(tenant_schema, module, key), value, timeout=timeout) def cache_delete(tenant_schema: str, module: str, key: str): cache.delete(get_redis_key(tenant_schema, module, key)) ``` --- ## 九、Celery 多租户任务模板(apps//tasks.py) 所有 Celery 任务必须接收 `tenant_schema_name` 参数并在任务入口处 `schema_context()`,**禁止依赖调用时的线程 schema 上下文**。 ```python from celery import shared_task from django_tenants.utils import schema_context @shared_task(bind=True, max_retries=3, default_retry_delay=60) def example_tenant_task(self, tenant_schema_name: str, **kwargs): """ 多租户 Celery 任务模板。 调用方示例: example_tenant_task.delay(tenant_schema_name=connection.schema_name, ...) """ try: with schema_context(tenant_schema_name): # --- 业务逻辑 --- pass except Exception as exc: raise self.retry(exc=exc) ``` --- ## 十、模板体系 ### 7.1 base.html(全局根模板) 包含以下 block 定义(骨架,后续填充): - `{% block title %}` — 页面标题 - `{% block extra_head %}` — 额外 CSS/meta - `{% block body_class %}` — body class 注入 - `{% block content %}` — 页面主内容 - `{% block extra_js %}` — 页面级 JS 引入资源顺序: 1. Tailwind CSS(编译后的 `output.css`,即 `tailwindcss -o ./static/css/output.css` 的产出;`main.css` 是 Tailwind 入口源文件,**不直接加载**) 2. Flatpickr CSS(条件加载) 3. HTMX `htmx.min.js` 4. Alpine.js `alpine.min.js`(defer,必须在 HTMX 之后) 5. 全局 `main.js`(初始化 Toast 监听、HTMX 事件、CSP nonce 等) ### 7.2 layouts/app.html(主应用布局) 继承 `base.html`,包含: - Topbar(`bg-primary-800`,高 56px,sticky top-0 z-20) - 左:Logo 150px 区 - 中:8 个主导航 Tab(主页/房源/客源/营销/交易/数据/人事/系统)+ 全局搜索 - 右:通知铃 + 设置齿轮 + 头像菜单 - Sidebar(固定,展开 240px / 收起 64px,Alpine `$persist` 记忆状态,z-20) - 主内容区(`ml-60` 或 `ml-16`,`px-6 py-4`) - Toast 容器(fixed bottom-right,z-70) - 小屏拦截门(`window.innerWidth < 1280` 时显示全屏提示) ### 7.3 layouts/auth.html(认证页布局) 独立布局,无 Sidebar/Topbar,居中卡片 `max-w-md`。 ### 7.4 HTMX Toast 约定 后端响应头触发 Toast(所有需要通知用户的操作必须返回此头): ```python # 工具函数骨架(core/htmx.py) from django.http import HttpResponse def htmx_response(content="", status=200, toast=None, redirect=None): """ toast: {"type": "success|error|warning|info", "message": "..."} """ response = HttpResponse(content, status=status) if toast: import json response["HX-Trigger"] = json.dumps({"fonrey:toast": toast}) if redirect: response["HX-Redirect"] = redirect return response ``` --- ## 八、Docker Compose 规范 ### docker-compose.yml(开发环境) 包含以下服务,网络统一使用 `fonrey_net`: | 服务 | 镜像 | 端口 | 说明 | |------|------|------|------| | `web` | 本地 Dockerfile | 8000:8000 | Django ASGI(Uvicorn) | | `db` | postgres:16-alpine | 5432:5432 | PostgreSQL | | `redis` | redis:7-alpine | 6379:6379 | Cache + Broker | | `celery` | 同 web 镜像 | — | `celery -A config worker` | | `celery-beat` | 同 web 镜像 | — | `celery -A config beat` | | `tailwind` | node:20-alpine | — | `npm run watch`(开发热重载) | 所有服务通过环境变量从 `.env` 文件读取配置(`env_file: .env`)。 `db` 和 `redis` 必须配置 `volumes` 持久化数据。 ### Dockerfile ```dockerfile FROM python:3.12-slim WORKDIR /app # 系统依赖(PostgreSQL 客户端、构建工具) RUN apt-get update && apt-get install -y \ libpq-dev gcc \ && rm -rf /var/lib/apt/lists/* COPY requirements/base.txt requirements/base.txt RUN pip install --no-cache-dir -r requirements/base.txt COPY . . # 收集静态文件(production 阶段) # RUN python manage.py collectstatic --noinput EXPOSE 8000 CMD ["uvicorn", "config.asgi:application", "--host", "0.0.0.0", "--port", "8000"] ``` --- ## 九、requirements 规范 ### requirements/base.txt(精确版本锁定) ``` Django==4.2.16 django-tenants==3.7.0 psycopg2-binary==2.9.9 django-redis==5.4.0 celery==5.4.0 django-celery-beat==2.7.0 django-celery-results==2.5.1 django-storages[s3]==1.14.4 boto3==1.35.0 cryptography==43.0.0 whitenoise==6.8.2 gunicorn==23.0.0 uvicorn[standard]==0.32.0 sentry-sdk[django]==2.18.0 python-decouple==3.8 # .env 读取 Pillow==11.0.0 # 图片处理 djangorestframework==3.15.2 # DRF(drf-spectacular 依赖) drf-spectacular==0.27.2 # OpenAPI 3.1 schema 自动生成(API_CONTRACT.md §11 MUST) django-htmx==1.21.0 # request.htmx 语义(AGENTS.md §4.2 / 系统管理技术文档.md:431) django-extensions==3.2.3 # shell_plus、runscript 等开发辅助命令 ``` ### requirements/development.txt ``` -r base.txt ruff==0.7.0 black==24.10.0 pytest-django==4.9.0 factory-boy==3.3.1 pytest-mock==3.14.0 # Mock 打桩(测试规范.md:86) responses==0.25.3 # 三方 HTTP 隔离(测试规范.md:87) pytest-cov==5.0.0 # 覆盖率报告(测试规范.md:88) pytest-xdist==3.6.1 # 并行加速(测试规范.md:89) django-debug-toolbar==4.4.6 schemathesis==3.36.0 # OpenAPI 契约测试(API_CONTRACT.md §11 MUST) pytest-playwright==0.5.0 # E2E 测试(TECH_STACK.md §10 / AGENTS.md §6) playwright==1.47.0 django-htmx==1.21.0 # request.htmx 支持(系统管理技术文档.md:431) django-extensions==3.2.3 # shell_plus、runscript 等(Makefile 依赖) ``` ### 测试关键约定(对齐 AGENTS.md §6 / 测试规范.md) > ⚠️ 以下约定**不可省略**,缺少任何一条将导致 CI 失败或测试体系不合规: ```python # conftest.py — 必须包含 TenantClient fixture import pytest from django_tenants.test.client import TenantClient from django_tenants.utils import schema_context @pytest.fixture def tenant_client(db, tenant): """所有集成测试必须使用此 client,禁止 Django 原生 Client()""" with schema_context(tenant.schema_name): yield TenantClient(tenant) # HTMX 局部请求测试示例(测试规范.md:206) def test_property_list_htmx(tenant_client): response = tenant_client.get( "/property/", HTTP_HX_REQUEST="true", # 必须携带,触发 partial 模板逻辑 ) assert response.status_code == 200 assert "全局布局标签" not in response.content.decode() # 返回 partial 而非完整页面 ``` ```ini # pytest.ini / pyproject.toml [tool.pytest.ini_options] DJANGO_SETTINGS_MODULE = config.settings.testing addopts = --cov=apps --cov=core --cov-report=term-missing -n auto ``` --- ## 十、Makefile 快捷命令 ```makefile .PHONY: dev migrate shell createsuperuser test lint dev: docker compose up migrate: docker compose exec web python manage.py migrate_schemas --shared docker compose exec web python manage.py migrate_schemas shell: docker compose exec web python manage.py shell_plus test: docker compose exec web pytest apps/ -v lint: ruff check . && black --check . tailwind-build: npm run build createsuperuser: docker compose exec web python manage.py create_tenant_superuser ``` --- ## 十一、.env.example 模板 ```bash # Django SECRET_KEY=your-secret-key-here DEBUG=True DJANGO_SETTINGS_MODULE=config.settings.development ALLOWED_HOSTS=localhost,127.0.0.1 # Database DB_NAME=fonrey DB_USER=fonrey DB_PASSWORD=fonrey DB_HOST=db DB_PORT=5432 # Redis REDIS_URL=redis://redis:6379/0 CELERY_BROKER_URL=redis://redis:6379/1 # Cloudflare R2 R2_ENDPOINT_URL=https://.r2.cloudflarestorage.com R2_ACCESS_KEY_ID= R2_SECRET_ACCESS_KEY= R2_BUCKET_NAME=media R2_CUSTOM_DOMAIN= # Sentry(production 填写) SENTRY_DSN= # PII 加密密钥(AES-256,生产环境必须替换) PHONE_ENCRYPTION_KEY= ``` --- ## 十二、tailwind.config.js(完整规范) 严格按照 UI_SYSTEM.md §2.7 和 §10.1 的规范实现,包含: 1. **Primary 色(Teal)**:`primary-50` (#F0FDFA) 到 `primary-800` (#134E4A),`primary-600` (#0F766E) 为主色 2. **Neutral 色(Slate)**:`neutral-50` (#F8FAFC) 到 `neutral-900` (#0F172A) 3. **语义色**:`success-600` (#16A34A), `warning-600` (#D97706), `danger-600` (#DC2626), `info-600` (#2563EB) 4. **字体栈**:Inter, PingFang SC, Microsoft YaHei, sans-serif 5. **自定义 z-index**:z-60, z-70(Toast 层) 6. **自定义 boxShadow**:xs 7. **动画**:`slide-in-right`(Drawer 进场) 8. **content 扫描路径**:`./templates/**/*.html`, `./apps/**/templates/**/*.html`, `./static/js/**/*.js` --- ## 十三、package.json(仅 Tailwind 构建) ```json { "name": "fonrey-frontend", "version": "1.0.0", "private": true, "scripts": { "build": "tailwindcss -i ./static/css/main.css -o ./static/css/output.css --minify", "watch": "tailwindcss -i ./static/css/main.css -o ./static/css/output.css --watch" }, "devDependencies": { "tailwindcss": "^3.4.0" } } ``` --- ## 十四、pyproject.toml(代码质量工具) ```toml [tool.ruff] line-length = 100 select = ["E", "F", "I", "N", "W", "UP"] ignore = ["E501"] target-version = "py312" [tool.black] line-length = 100 target-version = ["py312"] [tool.isort] profile = "black" line_length = 100 [tool.pytest.ini_options] DJANGO_SETTINGS_MODULE = "config.settings.testing" python_files = ["test_*.py", "*_test.py"] addopts = "--reuse-db --cov=apps --cov=core --cov-report=term-missing -n auto" ``` --- ## 十五、API_CONTRACT 契约核对清单(强制) 在生成任何 API 相关骨架(含 `apps/release`、OpenAPI 路由、契约测试占位)时,必须逐项核对: - [ ] **路径与方法**:端点路径/HTTP Method 与 `TECH_STACK/API_CONTRACT.md` 及模块文档一致 - [ ] **请求参数**:query/path/body 字段名、类型、必填/可选与契约一致 - [ ] **响应 envelope**:成功返回 `ok=true` + `data` + `meta`;失败返回 `ok=false` + `error` + `code` + `details` + `meta` - [ ] **错误码**:`code` 使用稳定 `UPPER_SNAKE_CASE`,并与模块前缀语义一致 - [ ] **OpenAPI 注解**:视图补齐 `@extend_schema`(或 `@extend_schema_view`) - [ ] **Schema 文件**:可执行 `python manage.py spectacular --file openapi.json` 成功生成 - [ ] **契约测试**:`schemathesis` 命令可运行(至少保留 Positive 路径骨架) 交付时必须附带“API 契约核对结果”小节,按以上 7 项逐项标注 ✅/❌ 与证据(文件路径 + 行号)。 --- ## 十六、执行顺序与验证清单 按以下顺序执行,每步完成后打 ✅: ``` [ ] 1. 创建根目录 fonrey/ 及上述完整目录树(含所有 __init__.py) [ ] 2. 创建 pyproject.toml / .gitignore / .env.example / Makefile [ ] 3. 创建 requirements/ 三个文件 [ ] 4. 创建 config/settings/base.py(完整配置) [ ] 5. 创建 config/settings/development.py、testing.py 和 production.py [ ] 6. 创建 config/urls.py(仅 tenant 路由)与 config/urls_public.py(release API + OpenAPI) [ ] 7. 创建 config/asgi.py(ASGI 入口) [ ] 8. 创建 core/models/base.py(四个抽象基类) [ ] 8b. 创建 core/enums.py(枚举骨架,见第五节;与 ENUMS.md v2.2 对齐) [ ] 9. 创建 core/encryption.py(PhoneEncryption 骨架) [ ] 10. 创建 core/cache.py(Redis 工具骨架,含 get_redis_key,见第八节) [ ] 11. 创建 core/htmx.py(htmx_response 工具) [ ] 12. 创建 core/templatetags/heroicons.py [ ] 13. 创建 core/middleware/audit.py(骨架) [ ] 14. 为每个 App 创建目录结构(`apps/release` 除外;其余含 apps.py、models/__init__.py、services/__init__.py、tasks.py 骨架、views.py 骨架、urls.py 骨架) [ ] 15. 创建 apps/tenant/models.py(Tenant、Domain 模型,django-tenants 规范) [ ] 16. 创建 templates/ 完整目录树及 base.html、layouts/app.html、layouts/auth.html 骨架 [ ] 17. 创建 components/ 模板骨架(topbar, sidebar, pagination, toast, modal, empty-state) [ ] 18. 创建 templates/errors/ 三个错误页骨架 [ ] 19. 创建 static/css/main.css(Tailwind 入口) [ ] 20. 创建 static/js/main.js(Alpine 初始化 + HTMX 全局事件) [ ] 21. 创建 tailwind.config.js(完整色彩/字体规范) [ ] 22. 创建 package.json [ ] 23. 创建 Dockerfile [ ] 24. 创建 docker-compose.yml(6 个服务:web/db/redis/celery/celery-beat/tailwind) [ ] 25. 创建 manage.py [ ] 26. 验证:python manage.py check --deploy 无致命错误 [ ] 27. 验证:项目目录树与第二节规范 100% 匹配 [ ] 28. 验证:API_CONTRACT 核对清单 7 项全部完成(含 openapi.json 生成与 schemathesis 骨架) ``` --- ## 十七、关键注意事项 1. **django-tenants `apps/tenant/models.py`** 必须定义 `Tenant`(继承 `TenantMixin`)和 `Domain`(继承 `DomainMixin`),且 `Tenant` 的 `auto_create_schema = True`。 2. **`shared/` App** 的 `apps.py` 中 `name = "shared"`,用于公共 Schema 的跨租户共享数据(如 PermissionDef 等)。 3. **所有 App 的 `apps.py`** 必须包含正确的 `name`(含包路径,如 `apps.property`)和 `verbose_name`(中文)。 4. **`config/urls.py`** 使用 `django-tenants` 的 URL 路由模式,区分 public schema 路由和 tenant schema 路由。 5. **`apps/release/`** 放在 `SHARED_APPS`(所有租户共享一张版本表),其余业务 App 放 `TENANT_APPS`。 6. **`.gitignore`** 必须包含:`.env`、`*.pyc`、`__pycache__/`、`.DS_Store`、`node_modules/`、`static/css/output.css`、`media/`、`dist/`。 7. **模板中所有异步 HTMX 请求**在骨架阶段只需占位,但必须包含正确的 `hx-` 属性结构,不可省略 `hx-target` 和 `hx-swap`。 8. **Toast 系统**:前端监听 `htmx:afterRequest` 事件,检查响应头 `HX-Trigger` 中的 `fonrey:toast`,动态插入 Toast DOM,4 秒自动消失。 9. **小屏拦截**:`layouts/app.html` 中内嵌 JS,`window.innerWidth < 1280` 时显示全屏遮罩,文案:"Fonrey 当前仅支持桌面端(≥1280px),请在电脑上访问"。 10. **所有密码、密钥、Tenant ID** 禁止出现在任何 Python 文件中,统一从 `python-decouple` 的 `env()` 读取。