37 KiB
Fonrey 项目骨架搭建 — 工程执行提示词
版本:v2.2(2026-04-28)|v2.0 修复 P0×5+P1×4;v2.1 修复 P0×9(交叉比对 AGENTS.md / 测试规范.md / 系统管理技术文档.md);v2.2 收口剩余一致性问题(URL 分离、Admin 弃用、密钥变量统一、测试 settings 一致性、环境变量占位修复) v2.2 主要变更:统一
config/urls.py/config/urls_public.py职责并修正执行清单;移除 Django Admin 路由引用(对齐系统管理技术文档);PII 密钥统一为PHONE_ENCRYPTION_KEY;pyproject.toml测试 settings 对齐config.settings.testing并新增testing.py生成要求;修复 AWS/R2 示例占位符与.env.example断行问题;修正 docker-compose 服务数量描述
你的角色与约束
你是一名资深 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/<name>/ 内部结构如下(以 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/<mod>/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 路由文件职责(强制分离,禁止合并)
# 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")),
]
# 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/updates/latest/", include("apps.release.urls")),
# 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):
# config/settings/base.py — 多租户 URL 分离配置(必须显式声明)
ROOT_URLCONF = "config.urls" # tenant schema 路由入口
PUBLIC_SCHEMA_URLCONF = "config.urls_public" # public schema 路由入口
config/urls.py仅包含urlpatterns(tenant 路由),config/urls_public.py包含urlpatterns(public 路由)。两个文件分开维护,不得合并。
3.1 INSTALLED_APPS 分区
# 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 必须包含以下所有项)
# 多租户
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,
"OPTIONS": {"pool_size": 10}, # PgBouncer 协同
}
}
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": False, # 枚举说明由 ENUMS.md 权威维护
}
# 日志(骨架,production 扩展)
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {"console": {"class": "logging.StreamHandler"}},
"root": {"handlers": ["console"], "level": "INFO"},
}
3.3 中间件顺序(严格按此,不得调整)
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)
严格按以下规范实现,不得修改字段名、类型、顺序:
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 中硬编码字符串枚举值,必须从此文件导入。
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。
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)
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'<!-- heroicon:{style}/{name} -->') # TODO: 替换为实际 SVG
八、Redis 工具(core/cache.py)
Redis Key 格式规范(所有 Redis 操作必须遵守):{tenant_schema}:{module}:{key}
禁止裸字符串拼接 Key;public schema 操作传 "public" 作为 tenant_schema。
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 上下文。
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 引入资源顺序:
- Tailwind CSS(编译后的
output.css,即tailwindcss -o ./static/css/output.css的产出;main.css是 Tailwind 入口源文件,不直接加载) - Flatpickr CSS(条件加载)
- HTMX
htmx.min.js - Alpine.js
alpine.min.js(defer,必须在 HTMX 之后) - 全局
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(所有需要通知用户的操作必须返回此头):
# 工具函数骨架(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
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 失败或测试体系不合规:
# 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 而非完整页面
# 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 快捷命令
.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 模板
# 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://<account_id>.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 的规范实现,包含:
- Primary 色(Teal):
primary-50(#F0FDFA) 到primary-800(#134E4A),primary-600(#0F766E) 为主色 - Neutral 色(Slate):
neutral-50(#F8FAFC) 到neutral-900(#0F172A) - 语义色:
success-600(#16A34A),warning-600(#D97706),danger-600(#DC2626),info-600(#2563EB) - 字体栈:Inter, PingFang SC, Microsoft YaHei, sans-serif
- 自定义 z-index:z-60, z-70(Toast 层)
- 自定义 boxShadow:xs
- 动画:
slide-in-right(Drawer 进场) - content 扫描路径:
./templates/**/*.html,./apps/**/templates/**/*.html,./static/js/**/*.js
十三、package.json(仅 Tailwind 构建)
{
"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(代码质量工具)
[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"
十五、执行顺序与验证清单
按以下顺序执行,每步完成后打 ✅:
[ ] 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.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% 匹配
十六、关键注意事项
- django-tenants
apps/tenant/models.py必须定义Tenant(继承TenantMixin)和Domain(继承DomainMixin),且Tenant的auto_create_schema = True。 shared/App 的apps.py中name = "shared",用于公共 Schema 的跨租户共享数据(如 PermissionDef 等)。- 所有 App 的
apps.py必须包含正确的name(含包路径,如apps.property)和verbose_name(中文)。 config/urls.py使用django-tenants的 URL 路由模式,区分 public schema 路由和 tenant schema 路由。apps/release/放在SHARED_APPS(所有租户共享一张版本表),其余业务 App 放TENANT_APPS。.gitignore必须包含:.env、*.pyc、__pycache__/、.DS_Store、node_modules/、static/css/output.css、media/、dist/。- 模板中所有异步 HTMX 请求在骨架阶段只需占位,但必须包含正确的
hx-属性结构,不可省略hx-target和hx-swap。 - Toast 系统:前端监听
htmx:afterRequest事件,检查响应头HX-Trigger中的fonrey:toast,动态插入 Toast DOM,4 秒自动消失。 - 小屏拦截:
layouts/app.html中内嵌 JS,window.innerWidth < 1280时显示全屏遮罩,文案:"Fonrey 当前仅支持桌面端(≥1280px),请在电脑上访问"。 - 所有密码、密钥、Tenant ID 禁止出现在任何 Python 文件中,统一从
python-decouple的env()读取。