23 KiB
Fonrey 技术栈总纲(TECH_STACK)
For AI assistants: Read this entire file before writing any code. All decisions here are final. Do not suggest alternatives unless asked.
版本: 2.5 | 最后更新: 2026-04-30 定位: 本文档是 Fonrey 项目技术栈的总索引。所有跨模块的技术决策、版本约束、目录规范、禁止项在此定稿;单一模块的具体技术方案(数据模型、服务层、HTMX 交互、Celery 任务等)见各自子文档(见 §9 索引)。
变更历史
| 日期 | 变更人 | 变更内容 |
|---|---|---|
| 2026-04-30 | Atlas | 新增 ADR 导航入口与变更治理规则(README 缺失场景下接入 TECH_STACK / DATA_MODEL / AGENTS) |
| 2026-04-30 | Atlas | 补充“变更历史”章节(文档治理) |
1. 项目概览
Fonrey(房睿)房产经纪管理系统 —— 面向房地产经纪公司的 B2B SaaS 平台,解决房源/客源信息散乱、跟进缺失、重复录入等痛点,支撑单租户 89,000+ 房源数据量级下的高效匹配。
- 核心模块:房源管理、客源管理、楼盘管理、组织人事、权限管理、登录管理、系统设置、客户端发布
- 目标用户:一线经纪人(高频)、店长/经理(每日)、运营/行政(每日)、系统管理员(不定期)
- 形态:Web 端为主 + Electron 桌面客户端(壳应用);移动端为 v2 规划
- 设计哲学:数据一致性 > 录入/筛选速度 > UI 简洁高效。优先保障多租户数据物理隔离与极速响应。
2. 核心技术栈
| 层级 | 技术选型 | 说明 |
|---|---|---|
| Frontend | HTMX + Alpine.js + Tailwind CSS | 无重前端框架;HTMX 局刷、Alpine 管状态、Tailwind 样式 |
| Backend | Django 4.x(ASGI 模式) | 支持异步能力 |
| Multi-tenant | django-tenants |
PostgreSQL Schema 隔离,租户数据物理安全 |
| Database | PostgreSQL 16 + PgBouncer | 连接池优化,支撑高并发 |
| Cache | Redis | 缓存、限流、Token、权限快照 |
| Tasks | Celery + Celery Beat | 异步导出、智能配房、邮件、图片转码 |
| Storage | Cloudflare R2(S3 兼容) | 房源图片、附件、客户端安装包 |
| CDN | Cloudflare | 静态资源 + 客户端更新包加速 |
| Server | Gunicorn + Uvicorn workers + Nginx | ASGI 服务部署 |
| Monitoring | Sentry + Grafana | 错误追踪 + 指标监控 |
| Deployment | Docker Compose | 容器化部署 |
| Desktop Client | Electron + electron-updater | 壳应用,渲染层复用 Web 技术栈,详见 §7 |
3. 关键约定
- 多租户隔离:所有数据库查询必须基于当前租户 Schema;严禁跨租户访问。
shared_apps仅放平台基础数据(Tenant、ClientRelease、PermissionDef 等)。 - UI 交互:HTMX 处理局部 DOM 刷新(分页、筛选、联想);Alpine.js 处理前端状态(弹窗、多选、字数统计);禁止编写复杂原生 JS。
- 异步处理:所有耗时 > 500ms 的任务必须经 Celery 异步执行(Excel 导出、图片处理、智能配房、邮件发送)。
- 错误处理:后端 API 返回标准 JSON 错误格式;HTMX 请求失败触发全局 Toast 提示。
- 文件命名:Django App 用
snake_case;前端模板组件用kebab-case。 - 敏感数据:手机号等 PII 通过
core/encryption.py加密存储。 - 配置:环境变量统一通过
.env注入,禁止硬编码。
4. 目录结构
fonrey/
├── apps/
│ ├── tenant/ # django-tenants 配置(shared_apps)
│ ├── account/ # 登录认证(详见 登录管理技术方案.md)
│ ├── permission/ # 权限管理(详见 权限管理系统技术方案.md)
│ ├── org/ # 组织人事(org_units, staff)
│ ├── region/ # 区域管理(districts, business_areas, metro)
│ ├── complex/ # 楼盘管理(complexes, buildings, schools)
│ ├── property/ # 房源核心(含 models/services/tasks 三层)
│ ├── client/ # 客源管理
│ ├── setting/ # 系统设置(lookup, tags)
│ └── release/ # 客户端发布管理(shared_apps)
├── shared/ # 公共 Schema App
└── core/
├── models/base.py # 抽象基类
├── encryption.py # PII 加密
└── cache.py # Redis 工具
Django App 内部分层规范(以 property 为典型,其他模块参照执行):
apps/property/
├── models/ # 一表一文件,避免单文件膨胀
├── services/ # 业务逻辑(完成度计算、重复检测、搜索等)
├── tasks.py # Celery 异步任务
├── views.py # HTMX/JSON 视图
└── urls.py
5. 禁止项(Do NOT)
- ❌ React / Vue / Angular 等重前端框架
- ❌ 在请求线程中处理耗时 > 500ms 的任务(必须用 Celery)
- ❌ 传统页面全刷方案
- ❌ 复杂原生 JavaScript(优先 HTMX/Alpine 指令)
- ❌ Electron 渲染进程开启
nodeIntegration: true - ❌ 客户端内嵌业务逻辑或本地数据库(壳应用原则)
- ❌ 跨租户 SQL 查询(必须经
django-tenants中间件切换 Schema) - ❌ 在代码中硬编码密钥、Tenant ID、URL
- ❌ Celery 任务内手写
connection.set_schema(...)(必须用@tenant_task装饰器,见 §12) - ❌ 业务视图/服务层直接调用
<Model>.objects.filter/get/all(...)(必须用Model.scoped(staff),见 §14) - ❌ R2 对象 key 使用原始文件名或 tenant_id(UUID)前缀(必须按 §13 路径模板)
6. 外部服务
| 服务 | 用途 | 配置位置 |
|---|---|---|
| Sentry | 错误追踪 | 已配置 |
| Cloudflare R2 | 房源/客源图片、附件、客户端安装包 | bucket: media、releases |
| Cloudflare CDN | 静态资源 + 客户端更新包加速 | 复用现有账号 |
| 邮件服务 | 找回密码、通知 | 待选型(详见 登录管理技术方案) |
| 代码签名 | EV 证书(DigiCert / Sectigo) | CI/CD 阶段使用 |
| 地图服务 | v2 规划,本期不涉及 | — |
7. 客户端发布技术栈(Desktop Client)
完整方案见:
TECH_STACK/客户端发布管理技术方案.md(实现口径)与PRD/发布管理/客户端发布管理模块PRD.md(需求口径)。本节仅列最终结论。
- 框架:Electron(稳定版) + Chromium 内核(随版本固定,不依赖系统浏览器)
- 渲染层:直接加载 Fonrey Web URL,100% 复用 HTMX + Alpine + Tailwind,渲染层零新增框架
- 自动更新:
electron-updater;更新包存 R2 / 经 CDN 分发;后端检测端点GET /api/release/updates/latest/(公开);启动时 + 每 4h 轮询;后台静默下载,下载完成提示重启;服务端可标记强制更新 - 构建:
electron-builder输出 NSIS.exe+ 便携版.zip;目标 Windows x64(优先),ARM64 按需 - 代码签名:EV 证书,CI/CD 自动签名,消除 SmartScreen 警告
- 完整性校验:下载后必须校验 SHA256 与服务端返回一致才能安装
- 后端模型:
apps/release/ClientRelease(shared_apps,所有租户共享版本表)
8. 模块技术方案索引
每个模块的具体技术决策(模型字段、服务层、缓存策略、HTMX/Celery 集成等)见对应子文档:
| 模块 | 技术方案文档 | PRD | 数据模型 | 测试文件 | 最近版本 |
|---|---|---|---|---|---|
| 登录认证 | 登录管理技术方案.md |
PRD/登录管理/ |
DATA_MODEL/DATA_MODEL_LOGIN.md |
tests/integration/account/test_us_account.py |
v3.1 |
| 权限管理 | 权限管理系统技术方案.md |
PRD/权限管理/ |
DATA_MODEL/DATA_MODEL_PERMISSION.md |
tests/integration/permission/test_us_permission.py |
v2.1 |
| 房源管理 | 房源管理技术方案.md |
PRD/房源管理/ |
DATA_MODEL/DATA_MODEL_PROPERTY.md |
tests/integration/property/test_us_property.py |
v1.0 |
| 客源管理 | 客源管理技术方案.md |
PRD/客源管理/ |
DATA_MODEL/DATA_MODEL_CLIENT.md |
tests/integration/client/test_us_client.py |
v1.0 |
| 楼盘管理 | 楼盘管理技术方案.md |
PRD/房源管理/(含楼盘) |
DATA_MODEL/DATA_MODEL_COMPLEX.md |
tests/integration/complex/test_us_complex.py |
v1.0 |
| 组织人事 | 组织人事技术方案.md |
PRD/组织人事管理/ |
DATA_MODEL/DATA_MODEL_ORG.md |
tests/integration/org/test_us_org.py |
v1.0 |
| 系统设置 | 系统设置技术方案.md |
PRD/系统配置/、PRD/系统管理/ |
DATA_MODEL/DATA_MODEL_SETTING.md |
tests/integration/setting/test_us_setting.py |
v1.2 |
| 客户端发布 | 客户端发布管理技术方案.md |
PRD/发布管理/客户端发布管理模块PRD.md |
DATA_MODEL/DATA_MODEL_PUBLIC.md(client_releases / client_heartbeats) |
tests/integration/release/test_us_release.py |
v1.0 |
总览数据模型:DATA_MODEL/DATA_MODEL.md
全局 API 契约:API_CONTRACT.md
MVP 范围与产品总览:PRD/PRD_MVP.md
ADR 动态决策记录:ADR.md
9. 模块技术方案一致性矩阵(15 标准章节)
目的:确保各模块技术方案采用同构模板,便于 AI Agent 与开发同学横向查阅、执行与回归。
9.1 标准章节骨架(统一实现标准)
| 标准章节编号 | 标准章节名 |
|---|---|
| 1 | 文档定位与边界 |
| 2 | 范围定义 |
| 3 | 模块架构边界 |
| 4 | API 设计原则 |
| 5 | 端点清单(核心) |
| 6 | 关键 API 规范(请求/响应) |
| 7 | HTMX 交互约定 |
| 8 | 权限与数据范围 |
| 9 | 异步任务与缓存策略 |
| 10 | 性能与可靠性约束 |
| 11 | 安全与合规 |
| 12 | 错误码建议 |
| 13 | 测试映射 |
| 14 | 落地顺序建议 |
| 15 | 文档同步规则 |
9.2 模块覆盖情况(2026-04-30)
| 模块 | 技术方案文档 | 15 章节覆盖 | 备注 |
|---|---|---|---|
| 登录认证 | 登录管理技术方案.md |
15/15 | 完全覆盖 |
| 权限管理 | 权限管理系统技术方案.md |
15/15 | 完全覆盖 |
| 房源管理 | 房源管理技术方案.md |
15/15 | 完全覆盖 |
| 客源管理 | 客源管理技术方案.md |
15/15 | 完全覆盖 |
| 楼盘管理 | 楼盘管理技术方案.md |
15/15 | 完全覆盖 |
| 组织人事 | 组织人事技术方案.md |
15/15 | 完全覆盖 |
| 系统设置 | 系统设置技术方案.md |
15/15 | 完全覆盖 |
| 客户端发布 | 客户端发布管理技术方案.md |
15/15 | 新增,已覆盖 Electron/EV/Heartbeat/自动升级/R2/官网下载/便携版 |
9.3 使用规则(对 AI Agent 生效)
- 新增模块技术方案时,必须按上表 15 章节骨架创建,不得自定义主结构。
- 若模块存在特殊子节,可在对应主章节下扩展
x.y,但不得删除主章节。 - PRD/TASK 范围变化后,先更新模块文档,再回填本矩阵覆盖状态。
10. 测试策略
完整测试规范见:
测试规范.md。本节仅列关键结论。
Fonrey 采用 AI vibe coding 模式开发,测试是保证每日迭代质量的唯一安全网。每个 P0 User Story 完成后,对应测试必须同步产出,不允许欠测试债。
测试分层
| 层级 | 工具 | 覆盖目标 | 运行频率 |
|---|---|---|---|
| 单元测试 | pytest-django + factory_boy |
core/、services/、tasks.py |
每次 push |
| 集成测试 | pytest-django TenantClient |
所有 P0 User Story 的 HTTP 接口 | 每次 push |
| E2E 测试 | playwright (Python) |
5 条核心用户旅程 | 每日定时 |
关键约定
- 所有集成测试必须使用
django-tenants的TenantClient,禁止使用 Django 原生Client() - HTMX 局部请求测试须携带
HTTP_HX_REQUEST: trueheader,并验证返回局部 HTML 而非完整页面 - Celery 任务测试使用
CELERY_TASK_ALWAYS_EAGER = True同步执行 - 外部服务(R2、Redis、邮件)在测试中全部 Mock,禁止真实调用
- 每个受权限保护的 View,必须覆盖:有权限(200)、无权限(403)、未登录(302)三个场景
覆盖率基准
| 模块 | 最低目标 |
|---|---|
core/ 核心基础模块 |
≥ 90% |
apps/*/services/ 业务逻辑层 |
≥ 80% |
apps/*/views.py 视图层 |
≥ 70% |
| E2E 核心用户旅程(5 条) | 100% 通过 |
CI 自动化
- 每次 push 到
main/develop自动运行单元测试 + 集成测试 - 每日北京时间凌晨 2 点自动运行全量套件(含 E2E)
- 配置文件:
.github/workflows/daily-test.yml
11. 文档维护原则
- 本文档仅记录跨模块共识与模块索引,不展开模块细节
- 模块技术方案在子文档中维护,并通过 §8 表格回链
- API 契约总则在
API_CONTRACT.md维护;模块文档只做模块特化,不得覆盖全局契约 - 任何技术栈变更(替换组件、升级主版本、新增外部服务)须同步更新本文档 §2、§5、§6
- 新增模块时,先在 §4 目录结构补位,再在 §8 索引登记子文档
- 涉及跨模块规则、接口口径、测试治理、范围边界的决策,必须先写入
ADR.md(新增 accepted 记录)再改 TECH_STACK/PRD/DATA_MODEL/TEST_CASES - 若已有决策被替代,必须在
ADR.md新增 superseded 记录并显式关联新 ADR;禁止静默改文档覆盖历史 - 提交 PR 时,若变更命中上述决策域,必须在描述中附 ADR ID(如
ADR-20260430-011) - 测试规范变更须同步更新 §10 关键结论,完整细节在
测试规范.md中维护 - 15 章节统一模板发生变更时,须先更新 §9 标准章节骨架,再同步各模块文档
12. Celery 多租户规范(M-12)
For AI assistants: Every Celery task that touches tenant data MUST use the
@tenant_taskdecorator defined here. No exceptions.
12.1 背景与风险
多模块技术方案均声明 Celery 任务签名带 tenant_schema_name: str,但 缺乏统一封装。
Celery worker 复用进程池,相邻任务若未正确切换 search_path,会产生 跨租户脏读/脏写,且不报错。
12.2 @tenant_task 装饰器规范
位置:core/celery_utils.py(由架构师统一提供,禁止各模块自己实现)
# core/celery_utils.py
import functools
import structlog
from django_tenants.utils import schema_context
from celery import current_task
logger = structlog.get_logger(__name__)
def tenant_task(schema_arg: str = "tenant_schema_name"):
"""
装饰器:在 Celery 任务执行前自动切换租户 Schema,结束/异常后还原为 public。
使用方式:
@shared_task
@tenant_task(schema_arg="tenant_schema_name")
def export_properties(tenant_schema_name: str, ...):
... # 此处 search_path 已切换到目标 schema
"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
schema = kwargs.get(schema_arg)
if not schema:
# 位置参数兼容:尝试从 args 中读取(按函数签名顺序)
import inspect
sig = inspect.signature(func)
params = list(sig.parameters.keys())
if schema_arg in params:
idx = params.index(schema_arg)
schema = args[idx] if idx < len(args) else None
if not schema:
raise ValueError(
f"[tenant_task] '{schema_arg}' 参数缺失,任务 {func.__name__} 拒绝执行"
)
log = logger.bind(task=func.__name__, task_id=current_task.request.id, schema=schema)
log.info("tenant_task.start")
try:
with schema_context(schema):
result = func(*args, **kwargs)
log.info("tenant_task.success")
return result
except Exception as exc:
log.error("tenant_task.error", exc=str(exc))
raise
return wrapper
return decorator
12.3 强制约束
| 约束 | 说明 |
|---|---|
所有业务 Celery 任务 必须使用 @tenant_task |
包括导出、图片处理、智能配房、分区维护任务 |
任务签名 必须含 tenant_schema_name: str 形参 |
位置或关键字均可,schema_arg 参数可自定义名称 |
装饰器顺序:先 @shared_task,后 @tenant_task |
确保 Celery 正常注册任务名 |
禁止 在任务内部手写 connection.set_schema(...) |
统一走装饰器,禁止散落手写 |
平台级无租户任务(如 partition_maintenance_task) |
直接 with schema_context(target_schema): 循环,不需要此装饰器 |
12.4 测试补充规范
- Celery 任务测试(
CELERY_TASK_ALWAYS_EAGER = True)必须断言schema_context被以目标 schema 调用 - 可用
unittest.mock.patch("core.celery_utils.schema_context")拦截,断言call_args - 反例测试:传入空
tenant_schema_name时,任务必须抛出ValueError,不得静默执行
13. R2 对象存储路径规范(M-13)
For AI assistants: All R2 object keys MUST follow the template table below. Never invent custom prefixes.
13.1 路径模板表
| 资源类型 | Key 模板 | 访问方式 | TTL / 生命周期 |
|---|---|---|---|
| 客户端发布包 | releases/system/{version}/{filename} |
public-read | 永久(不自动删除) |
| 租户备份 | backups/{tenant_schema}/{record_id}.tar.gz |
signed URL only | 90 天自动删除 |
| 租户导出 | exports/{tenant_schema}/{task_id}.zip |
signed URL 24h | 7 天自动删除 |
| 房源图片 | media/{tenant_schema}/property/{property_id}/{photo_id}.{ext} |
signed URL | 永久(随物理删除清理) |
| 跟进附件 | media/{tenant_schema}/follow/{log_id}/{idx}.{ext} |
signed URL | 90 天自动删除 |
| 客源附件 | media/{tenant_schema}/client/{client_id}/{idx}.{ext} |
signed URL | 永久(随物理删除清理) |
| 审计归档 | exports/audit/{task_id}.csv |
signed URL only | 2 年(合规保留) |
13.2 关键约束
- 路径中 禁止使用
tenant_id(UUID),统一用tenant_schema_name(字符串),便于跨环境迁移与 bucket policy 配置 {ext}统一小写(jpg/png/webp),禁止.JPG- 文件名仅用 UUID / 整数索引,禁止使用原始文件名(防路径注入)
- Signed URL 生成统一通过
core/storage.py的generate_presigned_url(key, expires_in)封装
13.3 Bucket Policy 摘要
| Prefix | Policy | 说明 |
|---|---|---|
releases/system/ |
public-read | 客户端更新包,CDN 加速 |
media/ |
无公开读,仅 signed | 所有租户媒体文件必须签名访问 |
backups/ |
无公开读,仅 signed | 备份包,严禁公开 |
exports/ |
无公开读,仅 signed | 导出包,签名 URL 有效期按上表 |
13.4 生命周期规则(R2 Object Lifecycle)
在 Cloudflare R2 控制台/API 配置以下规则:
Rule 1: backups/ prefix → Delete after 90 days
Rule 2: exports/{tenant}/ → Delete after 7 days
Rule 3: follow/ in media/ → Delete after 90 days (与 follow_logs 分区归档对齐)
Rule 4: exports/audit/ → Delete after 730 days (2 年合规保留)
14. ORM 数据范围强制规范(M-14)
For AI assistants: Never use
Model.objects.filter(...)directly in views or services. Always go throughscoped(staff).
14.1 背景
DATA_MODEL/DATA_MODEL_PERMISSION.md 已实现 ScopeQueryBuilder,但未强制约束使用入口。
模块技术方案 view 示例直接使用 Property.objects.filter(...),可绕过权限控制。
14.2 强制规范
所有业务 Model 必须暴露 scoped(staff) 入口,隐藏 objects Manager。
# core/models/scoped.py
from django.db import models
class ScopedManager(models.Manager):
"""
业务 Model 统一 Manager。
直接调用 Model.objects 将报错,强制使用 Model.scoped(staff)。
"""
def get_queryset(self):
raise RuntimeError(
f"[ScopedManager] 禁止直接调用 {self.model.__name__}.objects。"
f"请使用 {self.model.__name__}.scoped(staff) 经权限范围过滤。"
)
def scoped(self, staff):
"""
返回经 ScopeQueryBuilder 过滤后的 QuerySet。
staff: 当前登录员工实例(含角色、org_unit、权限范围)
"""
from apps.permission.scope import ScopeQueryBuilder
return ScopeQueryBuilder(staff).apply(super().get_queryset())
class ScopedModel(models.Model):
"""
所有业务 Model 继承此基类(而非 models.Model)。
"""
objects = ScopedManager()
class Meta:
abstract = True
14.3 豁免场景
| 场景 | 豁免条件 | 写法 |
|---|---|---|
| 系统/平台级操作(如分区维护) | 无租户身份,有明确运维场景 | Model._default_manager.filter(...) + 代码注释说明 |
| 迁移脚本 / seed factory | RunPython 或测试工厂 |
Model._default_manager.all() |
| 测试内部 assert | 纯验证数据存在,非业务查询 | Model._default_manager.get(pk=...) |
14.4 Lint 规则(pre-commit)
在 .pre-commit-config.yaml 增加以下规则,阻断直接 objects 调用:
- repo: local
hooks:
- id: no-raw-objects
name: "禁止直接使用 Model.objects(业务代码)"
language: pygrep
entry: '(?<!_default_manager)\.\bObjects\b|(?<!\._default_manager)\.objects\.(filter|all|get|exclude|create|update|delete)\b'
files: ^apps/.*\.py$
exclude: (tests/|migrations/|factories/)
types: [python]
14.5 权限边界测试矩阵
每个受权限保护的 Model,集成测试必须覆盖以下 3 case:
| Case | 说明 | 预期结果 |
|---|---|---|
| own | 员工查自己负责的数据 | 返回数据 ✅ |
| department | 店长查本部门数据 | 按角色返回 ✅ |
| cross_department_denied | 普通员工跨部门查询 | 空集或 403 ✅ |