Files
nexus/Project/fonrey/TECH_STACK/TECH_STACK.md
2026-05-02 11:35:20 +08:00

463 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
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 技术栈总纲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.xASGI 模式) | 支持异步能力 |
| **Multi-tenant** | `django-tenants` | PostgreSQL Schema 隔离,租户数据物理安全 |
| **Database** | PostgreSQL 16 + PgBouncer | 连接池优化,支撑高并发 |
| **Cache** | Redis | 缓存、限流、Token、权限快照 |
| **Tasks** | Celery + Celery Beat | 异步导出、智能配房、邮件、图片转码 |
| **Storage** | Cloudflare R2S3 兼容) | 房源图片、附件、客户端安装包 |
| **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_idUUID前缀必须按 §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: '(?<!_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 ✅ |