# 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) - ❌ 业务视图/服务层直接调用 `.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`](./登录管理技术方案.md) | `PRD/登录管理/` | `DATA_MODEL/DATA_MODEL_LOGIN.md` | `tests/integration/account/test_us_account.py` | `v3.1` | | 权限管理 | [`权限管理系统技术方案.md`](./权限管理系统技术方案.md) | `PRD/权限管理/` | `DATA_MODEL/DATA_MODEL_PERMISSION.md` | `tests/integration/permission/test_us_permission.py` | `v2.1` | | 房源管理 | [`房源管理技术方案.md`](./房源管理技术方案.md) | `PRD/房源管理/` | `DATA_MODEL/DATA_MODEL_PROPERTY.md` | `tests/integration/property/test_us_property.py` | `v1.0` | | 客源管理 | [`客源管理技术方案.md`](./客源管理技术方案.md) | `PRD/客源管理/` | `DATA_MODEL/DATA_MODEL_CLIENT.md` | `tests/integration/client/test_us_client.py` | `v1.0` | | 楼盘管理 | [`楼盘管理技术方案.md`](./楼盘管理技术方案.md) | `PRD/房源管理/`(含楼盘) | `DATA_MODEL/DATA_MODEL_COMPLEX.md` | `tests/integration/complex/test_us_complex.py` | `v1.0` | | 组织人事 | [`组织人事技术方案.md`](./组织人事技术方案.md) | `PRD/组织人事管理/` | `DATA_MODEL/DATA_MODEL_ORG.md` | `tests/integration/org/test_us_org.py` | `v1.0` | | 系统设置 | [`系统设置技术方案.md`](./系统设置技术方案.md) | `PRD/系统配置/` | `DATA_MODEL/DATA_MODEL_SETTING.md` | `tests/integration/setting/test_us_setting.py` | `v1.2` | | 平台管理后台 | [`平台管理后台技术方案.md`](./平台管理后台技术方案.md) | `PRD/平台管理后台/平台管理后台PRD.md` | `DATA_MODEL/DATA_MODEL_PUBLIC.md`(`tenants` / `platform_admins` / `client_releases` / `client_heartbeats` / `tenant_backups` / `tenant_data_exports` / `audit_logs` / `feature_flags`) | `tests/integration/admin_console/`、`tests/integration/release/test_us_release.py` | `v1.0` | **总览数据模型**:[`DATA_MODEL/DATA_MODEL.md`](../DATA_MODEL/DATA_MODEL.md) **全局 API 契约**:[`API_CONTRACT.md`](./API_CONTRACT.md) **MVP 范围与产品总览**:[`PRD/PRD_MVP.md`](../PRD/PRD_MVP.md) **ADR 动态决策记录**:[`ADR.md`](../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 | 已合并入「平台管理后台技术方案」(`ADR-20260502-002`),覆盖 Electron/EV/Heartbeat/自动升级/R2/官网下载/便携版 | ### 9.3 使用规则(对 AI Agent 生效) - 新增模块技术方案时,必须按上表 15 章节骨架创建,不得自定义主结构。 - 若模块存在特殊子节,可在对应主章节下扩展 `x.y`,但不得删除主章节。 - PRD/TASK 范围变化后,先更新模块文档,再回填本矩阵覆盖状态。 --- ## 10. 测试策略 > **完整测试规范**见:[`测试规范.md`](./测试规范.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: true` header,并验证返回局部 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`](./测试规范.md) 中维护 - 15 章节统一模板发生变更时,须先更新 §9 标准章节骨架,再同步各模块文档 --- ## 12. Celery 多租户规范(M-12) > **For AI assistants**: Every Celery task that touches tenant data MUST use the `@tenant_task` decorator defined here. No exceptions. ### 12.1 背景与风险 多模块技术方案均声明 Celery 任务签名带 `tenant_schema_name: str`,但 **缺乏统一封装**。 Celery worker 复用进程池,相邻任务若未正确切换 `search_path`,会产生 **跨租户脏读/脏写**,且不报错。 ### 12.2 `@tenant_task` 装饰器规范 位置:`core/celery_utils.py`(由架构师统一提供,禁止各模块自己实现) ```python # 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 through `scoped(staff)`. ### 14.1 背景 `DATA_MODEL/DATA_MODEL_PERMISSION.md` 已实现 `ScopeQueryBuilder`,但未强制约束使用入口。 模块技术方案 view 示例直接使用 `Property.objects.filter(...)`,可绕过权限控制。 ### 14.2 强制规范 **所有业务 Model 必须暴露 `scoped(staff)` 入口,隐藏 `objects` Manager。** ```python # 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` 调用: ```yaml - repo: local hooks: - id: no-raw-objects name: "禁止直接使用 Model.objects(业务代码)" language: pygrep entry: '(?