chore: sync local project changes

This commit is contained in:
Shen Wei
2026-04-28 16:39:21 +08:00
parent 365caa800a
commit e4cf7f8485
27 changed files with 13691 additions and 1317 deletions

View File

@@ -1,4 +1,6 @@
# Fonrey 项目骨架搭建 — 工程执行提示词
> **版本**v2.22026-04-28v2.0 修复 P0×5+P1×4v2.1 修复 P0×9交叉比对 AGENTS.md / 测试规范.md / 系统管理技术文档.mdv2.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/` 子目录)
@@ -33,12 +35,13 @@ fonrey/
│ ├── property/ # 房源核心in TENANT_APPS
│ ├── client/ # 客源管理in TENANT_APPS
│ ├── setting/ # 系统设置in TENANT_APPS
│ └── release/ # 客户端发布管理in SHARED_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/
@@ -56,8 +59,10 @@ fonrey/
│ │ ├── __init__.py
│ │ ├── base.py # 基础配置
│ │ ├── development.py
│ │ ├── testing.py # 测试配置pytest
│ │ └── production.py
│ ├── urls.py
│ ├── urls.py # tenant schema 路由入口
│ ├── urls_public.py # public schema 路由入口
│ ├── asgi.py # ASGI 入口
│ └── wsgi.py
├── templates/
@@ -111,7 +116,7 @@ apps/property/
├── services/
│ ├── __init__.py
│ └── .gitkeep
├── tasks.py # Celery 任务骨架
├── tasks.py # Celery 任务骨架(见第九节模板)
├── views.py # HTMX/JSON 视图骨架
├── urls.py
├── templates/
@@ -120,8 +125,85 @@ apps/property/
├── __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/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
```python
# 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 分区
```python
# config/settings/base.py
@@ -139,6 +221,11 @@ SHARED_APPS = [
# 第三方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",
@@ -149,7 +236,6 @@ TENANT_APPS = [
"apps.property",
"apps.client",
"apps.setting",
"core",
]
INSTALLED_APPS = list(SHARED_APPS) + list(TENANT_APPS)
```
@@ -158,7 +244,7 @@ INSTALLED_APPS = list(SHARED_APPS) + list(TENANT_APPS)
# 多租户
TENANT_MODEL = "tenant.Tenant"
TENANT_DOMAIN_MODEL = "tenant.Domain"
DEFAULT_AUTO_FIELD = "django.db.models.UUIDField" # 全局 UUID PK
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" # 非 UUID 表的默认 PK业务表 UUID PK 靠 UUIDPrimaryKeyModel
# 数据库(从环境变量读取)
DATABASES = {
"default": {
@@ -206,7 +292,7 @@ ASGI_APPLICATION = "config.asgi.application"
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = "Lax"
CSRF_COOKIE_HTTPONLY = False # HTMX 需要读取
CSRF_COOKIE_HTTPONLY = False # ⚠️ HTMX 需要 JS 读取 CSRF token故意设为 False禁止"修复"此项
X_FRAME_OPTIONS = "DENY"
# 模板
TEMPLATES = [{
@@ -224,6 +310,15 @@ TEMPLATES = [{
}]
# 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": False, # 枚举说明由 ENUMS.md 权威维护
}
# 日志骨架production 扩展)
LOGGING = {
"version": 1,
@@ -244,6 +339,7 @@ MIDDLEWARE = [
"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", # 自定义审计(骨架)
]
```
@@ -266,6 +362,10 @@ class TimeStampedModel(UUIDPrimaryKeyModel):
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)
@@ -301,37 +401,125 @@ class AuditedModel(SoftDeleteModel):
)
class Meta:
abstract = True
class ActiveManager(models.Manager):
"""默认只返回未软删除的记录"""
def get_queryset(self):
return super().get_queryset().filter(deleted_at__isnull=True)
```
---
## 五、PII 加密core/encryption.py
骨架实现,接口固定(后续补充实现体),确保接口签名正确:
## 五、全局枚举骨架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
from cryptography.fernet import Fernet
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 哈希索引
存储字段phone_encrypted加密密文+ phone_hash哈希用于精确查询
显示:脱敏格式 138****1234
- 加密算法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 密文"""
... # TODO: 实现
"""加密手机号,返回 base64(nonce + ciphertext + tag)"""
... # TODO: AESGCM(key).encrypt(nonce, phone.encode(), None)
@staticmethod
def decrypt(ciphertext: str) -> str:
"""解密返回明文"""
... # TODO: 实现
... # TODO: AESGCM(key).decrypt(nonce, ciphertext_bytes, None).decode()
@staticmethod
def hash(phone: str) -> str:
"""返回 SHA-256 哈希(用于 DB 索引查询)"""
... # TODO: 实现
... # TODO: hashlib.sha256(phone.encode()).hexdigest()
@staticmethod
def mask(phone: str) -> str:
"""返回脱敏格式138****1234"""
@@ -340,7 +528,7 @@ class PhoneEncryption:
return phone[:3] + "****" + phone[-4:]
```
---
## 、Heroicons Templatetagcore/templatetags/heroicons.py
## 、Heroicons Templatetagcore/templatetags/heroicons.py
```python
from django import template
from django.utils.safestring import mark_safe
@@ -356,11 +544,61 @@ def heroicon(name: str, size: str = "24", style: str = "outline", css_class: str
# 骨架:实际从 heroicons vendor 文件读取 SVG
# size 可选: 12, 16, 20, 24
# style 可选: outline, solid, mini
css = f'class="w-{size//4 if isinstance(size, int) else size} h-{size//4 if isinstance(size, int) else size} {css_class}"'
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 %}` — 页面标题
@@ -369,7 +607,7 @@ def heroicon(name: str, size: str = "24", style: str = "outline", css_class: str
- `{% block content %}` — 页面主内容
- `{% block extra_js %}` — 页面级 JS
引入资源顺序:
1. Tailwind CSS编译后的 `main.css`
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 之后)
@@ -453,6 +691,10 @@ 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
```
@@ -461,7 +703,47 @@ 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 快捷命令
@@ -553,9 +835,9 @@ target-version = ["py312"]
profile = "black"
line_length = 100
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "config.settings.development"
DJANGO_SETTINGS_MODULE = "config.settings.testing"
python_files = ["test_*.py", "*_test.py"]
addopts = "--reuse-db"
addopts = "--reuse-db --cov=apps --cov=core --cov-report=term-missing -n auto"
```
---
## 十五、执行顺序与验证清单
@@ -565,12 +847,13 @@ addopts = "--reuse-db"
[ ] 2. 创建 pyproject.toml / .gitignore / .env.example / Makefile
[ ] 3. 创建 requirements/ 三个文件
[ ] 4. 创建 config/settings/base.py完整配置
[ ] 5. 创建 config/settings/development.py 和 production.py
[ ] 6. 创建 config/urls.py骨架路由,含 django-tenants URL routing
[ ] 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 工具骨架)
[ ] 10. 创建 core/cache.pyRedis 工具骨架,含 get_redis_key见第八节
[ ] 11. 创建 core/htmx.pyhtmx_response 工具)
[ ] 12. 创建 core/templatetags/heroicons.py
[ ] 13. 创建 core/middleware/audit.py骨架
@@ -584,7 +867,7 @@ addopts = "--reuse-db"
[ ] 21. 创建 tailwind.config.js完整色彩/字体规范)
[ ] 22. 创建 package.json
[ ] 23. 创建 Dockerfile
[ ] 24. 创建 docker-compose.yml5 个服务)
[ ] 24. 创建 docker-compose.yml6 个服务web/db/redis/celery/celery-beat/tailwind
[ ] 25. 创建 manage.py
[ ] 26. 验证python manage.py check --deploy 无致命错误
[ ] 27. 验证:项目目录树与第二节规范 100% 匹配