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

23 KiB
Raw Blame History

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: mediareleases
Cloudflare CDN 静态资源 + 客户端更新包加速 复用现有账号
邮件服务 找回密码、通知 待选型(详见 登录管理技术方案)
代码签名 EV 证书DigiCert / Sectigo CI/CD 阶段使用
地图服务 v2 规划,本期不涉及

7. 客户端发布技术栈Desktop Client

完整方案见:TECH_STACK/平台管理后台技术方案.md(实现口径)与 PRD/平台管理后台/平台管理后台PRD.md(需求口径)。本节仅列最终结论。

  • 框架Electron稳定版 + Chromium 内核(随版本固定,不依赖系统浏览器)
  • 渲染层:直接加载 Fonrey Web URL100% 复用 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/ClientReleaseshared_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/系统配置/ DATA_MODEL/DATA_MODEL_SETTING.md tests/integration/setting/test_us_setting.py v1.2
平台管理后台 平台管理后台技术方案.md PRD/平台管理后台/平台管理后台PRD.md DATA_MODEL/DATA_MODEL_PUBLIC.mdtenants / 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 全局 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 已合并入「平台管理后台技术方案」(ADR-20260502-002),覆盖 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-tenantsTenantClient,禁止使用 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 IDADR-20260430-011
  • 测试规范变更须同步更新 §10 关键结论,完整细节在 测试规范.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(由架构师统一提供,禁止各模块自己实现)

# 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_idUUID,统一用 tenant_schema_name(字符串),便于跨环境迁移与 bucket policy 配置
  • {ext} 统一小写(jpg / png / webp),禁止 .JPG
  • 文件名仅用 UUID / 整数索引,禁止使用原始文件名(防路径注入)
  • Signed URL 生成统一通过 core/storage.pygenerate_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。

# 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