Files
nexus/Project/fonrey/prompt/提示词模板/创建项目骨架提示词_v2.3.md
2026-04-29 15:43:49 +08:00

38 KiB
Raw Blame History

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/ 子目录) 执行方式:逐步创建,每创建一个文件/目录后立即验证,遇到冲突停下来询问而不是自行决策。

一、技术栈约束(必读,不得违反)

层级 技术 版本约束
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 路由文件职责(强制分离,禁止合并)

# 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/系统管理技术文档.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

# 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 仅包含 urlpatternstenant 路由),config/urls_public.py 包含 urlpatternspublic 路由)。两个文件分开维护,不得合并

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",         # 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 必须包含以下所有项)

# 多租户
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 中间件顺序(严格按此,不得调整)

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

严格按以下规范实现,不得修改字段名、类型、顺序:

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禁止 FernetFernet 是 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-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

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

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 引入资源顺序:
  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.jsdefer必须在 HTMX 之后)
  5. 全局 main.js(初始化 Toast 监听、HTMX 事件、CSP nonce 等)

7.2 layouts/app.html主应用布局

继承 base.html,包含:

  • Topbarbg-primary-800,高 56pxsticky top-0 z-20
    • Logo 150px 区
    • 8 个主导航 Tab主页/房源/客源/营销/交易/数据/人事/系统)+ 全局搜索
    • 右:通知铃 + 设置齿轮 + 头像菜单
  • Sidebar固定展开 240px / 收起 64pxAlpine $persist 记忆状态z-20
  • 主内容区(ml-60ml-16px-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所有需要通知用户的操作必须返回此头

# 工具函数骨架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)。
dbredis 必须配置 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  # 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 失败或测试体系不合规:

# 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=<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 色Tealprimary-50 (#F0FDFA) 到 primary-800 (#134E4A)primary-600 (#0F766E) 为主色
  2. Neutral 色Slateneutral-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-indexz-60, z-70Toast 层)
  6. 自定义 boxShadowxs
  7. 动画slide-in-rightDrawer 进场)
  8. 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"

十五、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),且 Tenantauto_create_schema = True
  2. shared/ Appapps.pyname = "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_Storenode_modules/static/css/output.cssmedia/dist/
  7. 模板中所有异步 HTMX 请求在骨架阶段只需占位,但必须包含正确的 hx- 属性结构,不可省略 hx-targethx-swap
  8. Toast 系统:前端监听 htmx:afterRequest 事件,检查响应头 HX-Trigger 中的 fonrey:toast,动态插入 Toast DOM4 秒自动消失。
  9. 小屏拦截layouts/app.html 中内嵌 JSwindow.innerWidth < 1280 时显示全屏遮罩,文案:"Fonrey 当前仅支持桌面端≥1280px请在电脑上访问"。
  10. 所有密码、密钥、Tenant ID 禁止出现在任何 Python 文件中,统一从 python-decoupleenv() 读取。