Files
nexus/Project/fonrey/prompt/提示词模板/创建项目骨架提示词_v2.3.md
2026-04-30 06:33:50 +08:00

904 lines
38 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Fonrey 项目骨架搭建 — 工程执行提示词
> **版本**v2.32026-04-29v2.0 修复 P0×5+P1×4v2.1 修复 P0×9交叉比对 AGENTS.md / 测试规范.md / 系统管理技术文档.mdv2.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/` 子目录)
**所有文档目录**`/mnt/d/Workspace/nexus/Project/fonrey/`
**执行方式**:逐步创建,每创建一个文件/目录后立即验证,遇到冲突停下来询问而不是自行决策。
---
## 一、技术栈约束(必读,不得违反)
| 层级 | 技术 | 版本约束 |
|------|------|----------|
| Backend | Django | 4.2 LTSASGI 模式) |
| Multi-tenant | django-tenants | latest stable |
| Database | PostgreSQL | 16 |
| Cache | Redis | latest stable |
| Tasks | Celery + Celery Beat | latest stable |
| Storage | django-storages + boto3 | Cloudflare R2S3 兼容) |
| 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/CSShtmx.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 # 全局 fixtureDB、租户、认证
├── integration/
│ ├── __init__.py
│ ├── property/
│ │ └── .gitkeep
│ ├── client/
│ │ └── .gitkeep
│ └── release/
│ └── test_client_update_api.py # schemathesis 契约测试骨架
└── e2e/
├── __init__.py
└── .gitkeep # playwright E2Epytest-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 includepublic 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/系统管理技术文档.mdDjango 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", # DRFdrf-spectacular 依赖)
"drf_spectacular", # OpenAPI schemaAPI_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"]
# RedisCache + 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"
# Sentryproduction 环境激活)
# 见 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-spectacularOpenAPIAPI_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 §11Schema 中展开枚举说明
}
# 日志骨架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", # HTMXrequest.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禁止 FernetFernet 是 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-GCMAGENTS.md §4.4,对照 TECH_STACK/系统管理技术文档.md
- 存储字段phone_encryptedbase64 密文)+ phone_hashSHA-256用于精确查询
- 显示:脱敏格式 138****1234
密钥来源settings.PHONE_ENCRYPTION_KEY32字节base64 encoded从 .env 注入)
"""
@staticmethod
def _get_key() -> bytes:
key_b64 = settings.PHONE_ENCRYPTION_KEY
return base64.b64decode(key_b64) # 必须 32 bytesAES-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 Templatetagcore/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'<!-- heroicon:{style}/{name} -->') # TODO: 替换为实际 SVG
```
---
## 八、Redis 工具core/cache.py
**Redis Key 格式规范**(所有 Redis 操作必须遵守):`{tenant_schema}:{module}:{key}`
禁止裸字符串拼接 Keypublic 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/<mod>/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`,高 56pxsticky top-0 z-20
-Logo 150px 区
-8 个主导航 Tab主页/房源/客源/营销/交易/数据/人事/系统)+ 全局搜索
- 右:通知铃 + 设置齿轮 + 头像菜单
- Sidebar固定展开 240px / 收起 64pxAlpine `$persist` 记忆状态z-20
- 主内容区(`ml-60``ml-16``px-6 py-4`
- Toast 容器fixed bottom-rightz-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 ASGIUvicorn |
| `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 # DRFdrf-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://<account_id>.r2.cloudflarestorage.com
R2_ACCESS_KEY_ID=
R2_SECRET_ACCESS_KEY=<your_r2_secret_access_key>
R2_BUCKET_NAME=media
R2_CUSTOM_DOMAIN=
# Sentryproduction 填写)
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-70Toast 层)
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.pyrelease API + OpenAPI
[ ] 7. 创建 config/asgi.pyASGI 入口)
[ ] 8. 创建 core/models/base.py四个抽象基类
[ ] 8b. 创建 core/enums.py枚举骨架见第五节与 ENUMS.md v2.2 对齐)
[ ] 9. 创建 core/encryption.pyPhoneEncryption 骨架)
[ ] 10. 创建 core/cache.pyRedis 工具骨架,含 get_redis_key见第八节
[ ] 11. 创建 core/htmx.pyhtmx_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.pyTenant、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.cssTailwind 入口)
[ ] 20. 创建 static/js/main.jsAlpine 初始化 + HTMX 全局事件)
[ ] 21. 创建 tailwind.config.js完整色彩/字体规范)
[ ] 22. 创建 package.json
[ ] 23. 创建 Dockerfile
[ ] 24. 创建 docker-compose.yml6 个服务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 DOM4 秒自动消失。
9. **小屏拦截**`layouts/app.html` 中内嵌 JS`window.innerWidth < 1280` 时显示全屏遮罩,文案:"Fonrey 当前仅支持桌面端≥1280px请在电脑上访问"。
10. **所有密码、密钥、Tenant ID** 禁止出现在任何 Python 文件中,统一从 `python-decouple``env()` 读取。