docs: 新增系统配置模块PRD及数据模型文档,更新TASK.md

- 新增 PRD/系统配置/系统配置模块PRD.md(v0.1 Draft)
  - MVP 范围:US-SETTING-001-A(Lookup Items)、B(房源字段必填规则)、C(客源录入规则)
- 新增 PRD/系统配置/系统配置数据模型设计说明_for_Atlas.md
- 新增 PRD/系统配置/系统配置参数数据.md(竞品参数数据)
- 删除旧版 PRD/系统配置/系统配置.md(已被新PRD替代)
- 新增 DATA_MODEL/DATA_MODEL_SETTING.md(系统配置数据模型)
- 新增 DATA_MODEL/ENUMS.md(枚举定义与约定)
- 新增 AGENTS.md(AI Agent 开发规范)
- 更新 PRD/TASK.md:US-SETTING-001 拆分为 A/B/C 三个子任务,修正参考文档路径与验收标准
- 新增 VIBE_CODING_开工前缺失清单.md
- 新增 TECH_STACK/房源管理技术方案.md
- 更新 DATA_MODEL/DATA_MODEL.md、DATA_MODEL_CLIENT.md、DATA_MODEL_LOGIN.md
- 更新 PRD/PRD_MVP.md、PRD/权限管理/权限管理模块PRD.md
- 更新 TECH_STACK/TECH_STACK.md、权限管理系统技术方案.md
- 更新 UI_DESIGN/preview.html、UI_SYSTEM/UI_SYSTEM.md
- 新增 prompt/PRD - 为系统设置生成PRD设计文档.md、更新 prompt 模板
This commit is contained in:
Shen Wei
2026-04-27 15:31:48 +08:00
parent 4422c0eac8
commit 712a33fbac
21 changed files with 6466 additions and 2387 deletions

317
Project/fonrey/AGENTS.md Normal file
View File

@@ -0,0 +1,317 @@
> **For AI assistants**: Read this entire file before writing any code. All decisions here are final. Do not suggest alternatives unless asked. Every new feature or User Story implementation must be accompanied by corresponding tests as defined in this document.
# Fonrey房睿— AGENTS.md
**适用对象**:所有 AI Coding Agentvibe coding 模式)
**文档定位**:开发启动前的强制阅读清单,定义架构约定、禁止项和文档导航
**最后更新**2026-04-27
---
## 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 技术栈 |
---
## 3. 目录结构
代码库标准目录结构如下,**严格遵循**,不得自行创建新的顶层目录:
```
fonrey/
├── apps/
│ ├── tenant/ # django-tenants 配置shared_apps
│ ├── account/ # 登录认证
│ ├── permission/ # 权限管理
│ ├── org/ # 组织人事org_units, staff
│ ├── region/ # 区域管理districts, business_areas
│ ├── complex/ # 楼盘管理complexes, buildings, schools
│ ├── property/ # 房源核心(含 models/services/tasks 三层)
│ ├── client/ # 客源管理
│ ├── setting/ # 系统设置lookup, tags
│ └── release/ # 客户端发布管理shared_apps
├── shared/ # 公共 Schema Apppublic schema 模型)
└── core/
├── models/base.py # 抽象基类(所有业务表继承)
├── encryption.py # PII 加密AES-256-GCM
└── cache.py # Redis 工具(含 tenant 前缀)
```
**Django App 内部分层规范**(以 `property` 为典型,所有 App 参照执行):
```
apps/property/
├── models/ # 一表一文件,禁止全部堆在 models.py
├── services/ # 业务逻辑(完成度计算、重复检测、匹配等)
├── tasks.py # Celery 异步任务
├── views.py # HTMX/JSON 视图(薄视图,调 services
└── urls.py
```
---
## 4. 关键开发约定
### 4.1 多租户隔离(最高优先级)
- 所有数据库查询**必须**基于当前租户 Schema`django-tenants` 中间件已自动切换,不得绕过
- **严禁**跨租户 SQL 查询(包括 raw SQL 和 ORM 的 `using()` 指定 public schema
- `shared_apps` 仅放平台基础数据:`Tenant``ClientRelease``PermissionDef``PlatformAdmin`
- Celery 任务必须在任务参数中传入 `tenant_schema_name`,任务开头调用 `connection.set_schema(schema_name)` 切换到正确 Schema**不得依赖外部上下文传递**
### 4.2 前端交互约定
- **HTMX** 处理局部 DOM 刷新:分页、筛选、联想搜索、表单提交
- **Alpine.js** 处理前端状态:弹窗开关、多选状态、字数统计、条件显示
- **禁止**编写复杂原生 JavaScript逻辑无法用 HTMX/Alpine 覆盖时,优先提出问题而不是自行引入 JS 库
- HTMX 请求失败4xx/5xx必须触发全局 Toast 提示(通过 `HX-Trigger` 响应头)
- 所有 HTMX 局部请求后端视图必须校验 `HX-Request` header防止直接访问返回残缺 HTML
### 4.3 异步任务约定
- 所有耗时 **> 500ms** 的操作**必须**经 Celery 异步执行Excel 导出、图片处理、智能配房、邮件发送、完成度重算
- Celery Beat 定时任务:私客自动转公客(每小时)、重复客源检测(每日)
- 任务状态存 Redis前端通过轮询 HTMX 端点展示进度
### 4.4 数据模型约定
| 约定 | 规则 |
|------|------|
| 主键类型 | `UUID v4``gen_random_uuid()`),禁止自增整数 |
| 软删除 | 所有核心表含 `deleted_at TIMESTAMPTZ`,查询默认加 `WHERE deleted_at IS NULL` |
| 时间戳 | 全部使用 `TIMESTAMPTZ`(含时区),禁止 naive datetime |
| 手机号存储 | AES-256-GCM 加密,建立 SHA-256 哈希索引,通过 `core/encryption.py` 操作 |
| 审计字段 | `created_by UUID``updated_by UUID` 全表覆盖 |
| 枚举值 | 业务枚举用 `VARCHAR + CHECK CONSTRAINT`;不得在代码中硬编码中文枚举值 |
| 金额 | `NUMERIC(12,2)` 万元精度,禁止 FLOAT |
| 大文本 | `TEXT` 类型,不设长度限制 |
| 不可删除记录 | `listing_histories``price_changes``deleted_at`append-only禁止物理删除 |
### 4.5 查询性能约定
- 列表查询目标89,000 条房源 / 200 万条跟进日志下p95 响应 **< 2 秒**
- **所有列表查询必须使用 Keyset 分页**`WHERE id > :last_id ORDER BY id`),禁止 OFFSET 分页用于大数据量场景
- 新增表必须在 migration 中同步创建必要索引,不得事后补建
- 高写入表(`follow_logs``property_photos``permission_change_logs``login_attempts`)必须按月分区(`PARTITION BY RANGE`
### 4.6 安全约定
- 手机号等 PII 数据统一通过 `core/encryption.py` 加密存储,**禁止明文入库**
- 所有配置密钥、Bucket 名、外部服务 URL通过 `.env` 注入,**禁止硬编码**
- 每个受权限保护的 View 必须覆盖三个测试场景有权限200、无权限403、未登录302
- Redis Key 必须携带租户 Schema 前缀,格式:`{tenant_schema}:{module}:{key}`
### 4.7 错误处理约定
- 后端 API 返回标准 JSON 错误格式:`{"error": "...", "code": "SNAKE_CASE_CODE"}`
- View 层禁止直接抛出异常,必须捕获并返回对应 HTTP 状态码
- Celery 任务失败必须记录到 Sentry并更新任务状态表
### 4.8 文件命名约定
- Django App`snake_case`(如 `property``follow_log`
- 前端模板组件:`kebab-case`(如 `property-card.html``client-form.html`
- Migration 文件:不得手动重命名,保留 Django 自动生成的序号
---
## 5. 禁止项Do NOT — 违反视为 Bug
-**React / Vue / Angular** 等重前端框架
- ❌ 在请求线程中处理耗时 **> 500ms** 的任务(必须用 Celery
- ❌ 传统页面全刷方案(除初始页面加载外)
- ❌ 复杂原生 JavaScript优先 HTMX/Alpine 指令)
- ❌ Electron 渲染进程开启 `nodeIntegration: true`
- ❌ 客户端内嵌业务逻辑或本地数据库(壳应用原则,渲染层只加载 Web URL
- ❌ 跨租户 SQL 查询(必须经 `django-tenants` 中间件切换 Schema
- ❌ 代码中硬编码密钥、Tenant ID、URL、枚举中文字符串
- ❌ 使用 OFFSET 分页处理 1000 条以上数据集
- ❌ 手机号/身份证号明文存储
- ❌ 使用 Django 原生 `Client()` 做集成测试(必须用 `TenantClient`
- ❌ 在 MVP 阶段实现 `PRD_MVP.md §3` 中标注的 Out-of-Scope 功能(移动端、合同、财务、新房、三网发布等)
---
## 6. 测试要求
**核心原则**:每个 P0 User Story 完成后,对应测试必须同步产出,**不允许欠测试债**。
### 测试分层
| 层级 | 工具 | 覆盖目标 | 运行频率 |
|------|------|---------|---------|
| **单元测试** | `pytest-django` + `factory_boy` | `core/``services/``tasks.py` | 每次 push |
| **集成测试** | `pytest-django` + `TenantClient` | 所有 P0 User Story 的 HTTP 接口 | 每次 push |
| **E2E 测试** | `playwright` (Python) | 5 条核心用户旅程 | 每日定时 |
### 测试关键约定
- 集成测试**必须**使用 `TenantClient`,禁止使用 Django 原生 `Client()`
- HTMX 局部请求测试须携带 `HTTP_HX_REQUEST: true` header验证返回局部 HTML 而非完整页面
- Celery 任务测试使用 `CELERY_TASK_ALWAYS_EAGER = True` 同步执行
- 外部服务R2、Redis、邮件在测试中全部 Mock禁止真实调用
- 权限测试必须覆盖有权限200、无权限403、未登录302三场景
### 覆盖率基准
| 模块 | 最低目标 |
|------|---------|
| `core/` 核心基础模块 | ≥ 90% |
| `apps/*/services/` 业务逻辑层 | ≥ 80% |
| `apps/*/views.py` 视图层 | ≥ 70% |
| E2E 核心用户旅程5 条) | 100% 通过 |
---
## 7. MVP 范围Phase 1
**当前阶段**MVP Phase 1P0 功能)
实现任何功能前,先对照 `PRD/PRD_MVP.md` 确认是否在 P0 范围。**P0 范围以外的功能在 MVP 阶段禁止实现**,包括但不限于:
- 移动端适配v2 规划)
- 新房模块、合同、财务、三网发布
- 公客管理P2
- 门店分布地图P2
**当前 Phase 1 P0 Task 列表**:见 `PRD/TASK.md §Phase 1`US-ACCOUNT-001~003、US-COMPLEX-001~003、US-PROPERTY-001~008、US-CLIENT-001~017、US-ORG-001~003、US-PERMISSION-001~005、US-SETTING-001
---
## 8. 数据模型参考(实现前必读)
所有数据模型的**权威来源**如下,开发前必须阅读对应文档,不得凭印象实现:
| 模块 | 数据模型权威文档 |
|------|---------------|
| 总览 & 架构决策 | `DATA_MODEL/DATA_MODEL.md` |
| 房源管理 | `DATA_MODEL/DATA_MODEL_PROPERTY.md` |
| 客源管理 | `DATA_MODEL/DATA_MODEL_CLIENT.md` |
| 楼盘/小区/区域 | `DATA_MODEL/DATA_MODEL_COMPLEX.md` |
| 组织人事 | `DATA_MODEL/DATA_MODEL_ORG.md` |
| 权限管理 | `DATA_MODEL/DATA_MODEL_PERMISSION.md` |
| 登录认证 | `DATA_MODEL/DATA_MODEL_LOGIN.md` |
| Public Schema平台运营层 | `DATA_MODEL/DATA_MODEL_PUBLIC.md` |
| 系统配置lookup/setting | `DATA_MODEL/DATA_MODEL_SETTING.md` |
### 核心领域关系速览
```
[区域/商圈] ──────────────────────────────┐
│ │
[学校管理] │
│ ▼
[楼盘/小区] ── [楼栋] ──────────► [房源] ◄──── [挂牌历史]
│ │
│ ┌────────┼────────┐
│ │ │ │
│ [联系人] [跟进日志] [维护完成度]
[客源] ──── [配对记录] ──── [带看记录]
[员工/组织] ──── [权限]
```
---
## 9. 技术方案文档导航
| 模块 | 技术方案文档 | PRD |
|------|------------|-----|
| 总纲 & 禁止项 | `TECH_STACK/TECH_STACK.md` | `PRD/PRD_MVP.md` |
| 登录认证 | `TECH_STACK/登录管理技术方案.md` | `PRD/登录管理/` |
| 权限管理 | `TECH_STACK/权限管理系统技术方案.md` | `PRD/权限管理/` |
| 测试规范 | `TECH_STACK/测试规范.md` | — |
| 房源管理 | _待补充_ | `PRD/房源管理/` |
| 客源管理 | _待补充_ | `PRD/客源管理/` |
| 楼盘管理 | _待补充_ | `PRD/房源管理/(含楼盘)` |
| 组织人事 | _待补充_ | `PRD/组织人事管理/` |
| 系统设置 | _待补充_ | `PRD/系统配置/``PRD/系统管理/` |
| 客户端发布 | `TECH_STACK/TECH_STACK.md §7` | `PRD/发布管理/客户端发布管理模块PRD.md` |
**设计 Review 记录**(了解当前已知技术债与阻塞项):
- `REVIEW/REVIEW_全局_2026-04-26.md`(最新)
- `REVIEW/REVIEW_全局_2026-04-25.md`
---
## 10. 已知待解决问题(编码前必须确认)
以下问题在开始编码前需先确认当前状态,若未解决应在实现对应模块前暂停并上报:
| 编号 | 级别 | 问题描述 | 影响模块 |
|------|------|---------|---------|
| **B-01** | 🔴 Blocker | 系统配置 PRD`PRD/系统配置/系统配置.md`为空骨架US-SETTING-001 无法启动 | 系统配置 |
| **B-02** | 🔴 Blocker | 核心枚举三方不一致PRD ↔ DDL ↔ TASK AC客源 status/grade、房源 status 互相矛盾,需先冻结 `DATA_MODEL/ENUMS.md` | 房源、客源 |
| **B-03** | 🔴 Blocker | 权限数据范围档位冲突§3 非目标"三档" vs §5.6"五档"DataScope 实现方式未统一 | 权限管理 |
| **B-04** | 🔴 Blocker | Keyset 分页规范完全缺位89k 房源列表设计错误 | 房源、客源 |
| **M-02** | 🟠 Major | 主表 `properties`/`clients` 缺乏乐观锁字段 `version` | 房源、客源 |
| **M-03** | 🟠 Major | 高写入表分区 DDL 未落地(`follow_logs` 等) | 跟进日志 |
| **M-04** | 🟠 Major | Celery 多租户 schema 切换规范、R2 文件路径前缀规范、查询索引矩阵未补充 | 全局 |
> 详细内容见 `REVIEW/REVIEW_全局_2026-04-26.md`
---
## 11. 文档根目录
所有项目文档位于:`/mnt/d/Workspace/nexus/Project/fonrey/`
```
fonrey/
├── AGENTS.md # 本文件AI Agent 开发指南)
├── PRD/
│ ├── PRD_MVP.md # MVP 范围书(必读)
│ ├── TASK.md # 全量 Task BoardUS 编号)
│ ├── 房源管理/
│ ├── 客源管理/
│ ├── 楼盘管理/
│ ├── 组织人事管理/
│ ├── 权限管理/
│ ├── 登录管理/
│ ├── 系统配置/
│ ├── 系统管理/
│ └── 发布管理/
├── DATA_MODEL/
│ ├── DATA_MODEL.md # 数据模型总览(必读)
│ ├── DATA_MODEL_PROPERTY.md
│ ├── DATA_MODEL_CLIENT.md
│ ├── DATA_MODEL_COMPLEX.md
│ ├── DATA_MODEL_ORG.md
│ ├── DATA_MODEL_PERMISSION.md
│ ├── DATA_MODEL_LOGIN.md
│ └── DATA_MODEL_PUBLIC.md
├── TECH_STACK/
│ ├── TECH_STACK.md # 技术栈总纲(必读)
│ ├── 登录管理技术方案.md
│ ├── 权限管理系统技术方案.md
│ └── 测试规范.md
└── REVIEW/
├── REVIEW_全局_2026-04-26.md # 最新 Review含阻塞问题
└── REVIEW_全局_2026-04-25.md
```

View File

@@ -2,8 +2,8 @@
# Fonrey 房产经纪管理系统 — DATA MODEL 设计文档
> **作者**: Backend Architect
> **版本**: v1.3
> **日期**: 2026-04-24v1.1 修复 S1/S2/S4v1.2 扩展 public schemav1.3 §三 DDL 迁至 DATA_MODEL_PUBLIC.md本文改为索引
> **版本**: v1.4
> **日期**: 2026-04-24v1.1 修复 S1/S2/S4v1.2 扩展 public schemav1.3 §三 DDL 迁至 DATA_MODEL_PUBLIC.md本文改为索引v1.4 补充 LOGIN/PERMISSION 子文档引用、领域对象、租户 Schema 章节、Redis 缓存策略
> **技术栈**: Django 4.x + PostgreSQL + django-tenants + Redis
> **设计目标**: 支撑 89,000+ 房源、多租户隔离、sub-100ms 查询、合规审计
@@ -17,23 +17,24 @@
┌──────────────────────────────────────────────────────────────────────┐
│ PostgreSQL Instance │
│ │
│ ┌─────────────────────────┐ ┌──────────────┐ ┌──────────────┐
│ │ public schema │ │ tenant_abc │ │ tenant_xyz
│ │ (平台运营层) │ │ schema │ │ schema
│ │ │ │ │ │
│ │ - tenants │ │ - properties │ │ - properties │
│ │ - domains │ │ - clients │ │ - clients
│ │ - tenant_status_logs │ │ - complexes │ │ - complexes
│ │ - platform_admins │ │ - staff │ │ - staff
│ │ - admin_mfa_devices │ │ - org_units │ │ - org_units
│ │ - admin_sessions │ │ - ... │ │ - ...
│ │ - ip_whitelist │ └──────────────┘ └──────────────┘
│ │ - platform_audit_logs │
│ │ - backup_schedules │
│ │ - backup_records │
│ │ - export_tasks │
│ │ - system_versions │
│ │ - upgrade_events │
│ ┌─────────────────────────┐ ┌──────────────────┐ ┌────────────
│ │ public schema │ │ tenant_abc │ │ tenant_xyz
│ │ (平台运营层) │ │ schema │ │ schema
│ │ │ │ │ │
│ │ - tenants │ │ - org_units │ │ (同左)
│ │ - domains │ │ - staff │ │ │ │
│ │ - tenant_status_logs │ │ - complexes │ │
│ │ - platform_admins │ │ - properties │ │ │ │
│ │ - admin_mfa_devices │ │ - clients │ │
│ │ - admin_sessions │ │ - user_accounts │ │ │ │
│ │ - ip_whitelist │ │ - login_attempts │ │ │
│ │ - platform_audit_logs │ │ - permission_defs│ │
│ │ - backup_schedules │ │ - roles
│ │ - backup_records │ │ - staff_roles │ │
│ │ - export_tasks │ │ - lookup_items │ │
│ │ - system_versions │ │ - ...
│ │ - upgrade_events │ └──────────────────┘ └────────────┘
│ │ - enum_labels │ │
│ └─────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
```
@@ -104,6 +105,7 @@
| **ExportTask** | `public.export_tasks` | 数据导出异步任务CSV/JSON/SQL Dump24h 下载链接) |
| **SystemVersion** | `public.system_versions` | 平台版本历史,唯一 current 版本约束 |
| **UpgradeEvent** | `public.upgrade_events` | 升级/回滚事件,含灰度租户维度进度快照 |
| **EnumLabel** | `public.enum_labels` | 固定枚举字典(英文 Key → 中文标签),所有租户共享,供前端下拉渲染、导出报表中文标签、日志快照使用 |
#### Tenant Schema租户业务层
@@ -121,6 +123,9 @@
| **Client客源** | `clients` → [DATA_MODEL_CLIENT.md](./DATA_MODEL_CLIENT.md) | 买家/租客档案,分私客/公客/成交客,含活跃度评分与自动公客转换机制 |
| **Viewing带看** | `client_viewings` → [DATA_MODEL_CLIENT.md](./DATA_MODEL_CLIENT.md) | 经纪人带客户看房的完整记录 |
| **Match配对** | `client_property_matches` → [DATA_MODEL_CLIENT.md](./DATA_MODEL_CLIENT.md) | 系统/人工推荐的客源↔房源配对 |
| **UserAccount用户账号** | `user_accounts` → [DATA_MODEL_LOGIN.md](./DATA_MODEL_LOGIN.md) | 系统登录主体,与员工档案 1:1 绑定,含账号锁定/密码历史/登录审计 |
| **PermissionDef权限定义** | `permission_defs` → [DATA_MODEL_PERMISSION.md](./DATA_MODEL_PERMISSION.md) | 权限目录(约 300 条),驱动 Hybrid RBAC + Override 权限模型 |
| **Role业务角色** | `roles` → [DATA_MODEL_PERMISSION.md](./DATA_MODEL_PERMISSION.md) | 权限模板,含 4 大类别(置业顾问/店管/总经/运营/自定义) |
### 领域关系快速导航
@@ -149,6 +154,9 @@ OrgUnit (组织架构)
| [DATA_MODEL_COMPLEX.md](./DATA_MODEL_COMPLEX.md) | 楼盘/区域districts, business_areas, complexes, buildings, room_units, schools 等) | ✅ 完成 |
| [DATA_MODEL_CLIENT.md](./DATA_MODEL_CLIENT.md) | 客源管理clients, requirements, follow_logs, viewings, matches 等) | ✅ 完成 |
| [DATA_MODEL_PROPERTY.md](./DATA_MODEL_PROPERTY.md) | 房源管理properties 及配套 22 张表,含跟进/钥匙/委托/实勘/营销/产证/完成度/标签/收藏/保护/号码方审批等) | ✅ 完成 |
| [DATA_MODEL_LOGIN.md](./DATA_MODEL_LOGIN.md) | 登录与账号认证user_accounts, login_attempts, password_reset_tokens, password_histories + Redis 登录缓存) | ✅ 完成 |
| [DATA_MODEL_PERMISSION.md](./DATA_MODEL_PERMISSION.md) | 权限管理permission_defs, roles, role_permissions, staff_roles, staff_permission_overrides, staff_data_scopes, permission_change_logs + Redis 权限缓存) | ✅ 完成 |
| [ENUMS.md](./ENUMS.md) | 枚举字典(`public.enum_labels` 表设计 + 所有模块枚举定义 + 种子数据 SQL | ✅ 完成 |
---
@@ -175,6 +183,7 @@ OrgUnit (组织架构)
| `public.export_tasks` | 数据导出异步任务CSV/JSON/SQL Dump24h 下载链接) | §2.4 |
| `public.system_versions` | 平台版本历史,部分唯一索引保证唯一 current | §2.5 |
| `public.upgrade_events` | 升级/回滚事件,`tenant_progress` JSONB 快照各租户状态 | §2.5 |
| `public.enum_labels` | 固定枚举字典(英文 Key → 中文标签),所有租户共享 | §2.6 |
**关键约束提示**
- `tenant_status_logs` / `platform_audit_logs` **无 deleted_at**,禁止 UPDATE/DELETEappend-only
@@ -257,31 +266,31 @@ OrgUnit (组织架构)
**核心表概览**(开发时以 DATA_MODEL_PROPERTY.md 为准):
| 表名 | 说明 | 关键字段 |
|------|------|----------|
| `properties` | 房源主表系统核心89,000+ 数据量) | `status`, `attribute`, `property_type`, `complex_id`, `sale_price`, `area`, `grade`, `completeness_score`, `search_vector` |
| `property_contacts` | 业主/联系人(手机号 AES 加密+哈希索引) | `property_id`, `phone_enc`, `phone_hash`, `identity`, `is_number_holder` |
| `listing_histories` | 挂牌历史快照(不可删除) | `property_id`, `listing_type`, `status`, `sale_price`, `seller_agent_snapshot` |
| `price_changes` | 调价记录(不可删除) | `property_id`, `old_sale_price`, `new_sale_price`, `change_reason`, `changed_by` |
| `follow_logs` | 跟进日志6种类型最高写入频率 | `property_id`, `log_type`, `content`, `is_deletable`, `operator_id` |
| `follow_log_attachments` | 跟进附件(图片) | `follow_log_id`, `file_key`, `file_type` |
| `follow_log_recordings` | 跟进录音 | `follow_log_id`, `file_key`, `duration_seconds` |
| `property_keys` | 钥匙管理(机械钥匙/密码) | `property_id`, `key_type`, `holder_id`, `is_active` |
| `key_attachments` | 钥匙附件 | `key_id`, `file_key` |
| `commissions` | 委托管理(独家/非独家) | `property_id`, `commission_type`, `period_start`, `status` |
| `commission_attachments` | 委托附件(身份证/产证/委托书) | `commission_id`, `category`, `file_key` |
| `field_surveys` | 实勘管理GPS 打卡) | `property_id`, `status`, `gps_latitude`, `gps_longitude`, `created_by` |
| `survey_photos` | 实勘照片(按空间分类) | `survey_id`, `category`, `file_key`, `is_vr_screenshot` |
| `property_photos` | 房源图片(经纪人管理,封面唯一约束) | `property_id`, `category`, `is_cover`, `file_key` |
| `property_attachments` | 房源附件 | `property_id`, `category`, `file_key` |
| `property_marketing` | 营销信息1:1卖点/业主心态/介绍) | `property_id`, `marketing_title`, `core_selling_points` |
| `property_certificates` | 产证信息1:1 | `property_id`, `cert_no`, `owner_name`, `land_nature` |
| `property_completeness` | 维护完成度快照1:1Celery 异步计算) | `property_id`, `total_score`, `score_survey`, `score_commission`, ... |
| `property_tags` | 标签字典(系统预置+运营自定义) | `name`, `color`, `is_system` |
| `property_tag_relations` | 房源↔标签多对多 | `property_id`, `tag_id` |
| `property_favorites` | 经纪人收藏房源 | `staff_id`, `property_id` |
| `property_protections` | 保护房设置1:1 | `property_id`, `is_protected`, `start_at`, `end_at` |
| `number_holder_approvals` | 号码方变更审批 | `property_id`, `applicant_id`, `status` |
| 表名 | 说明 | 关键字段 |
| ------------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------- |
| `properties` | 房源主表系统核心89,000+ 数据量) | `status`, `attribute`, `property_type`, `complex_id`, `sale_price`, `area`, `grade`, `completeness_score`, `search_vector` |
| `property_contacts` | 业主/联系人(手机号 AES 加密+哈希索引) | `property_id`, `phone_enc`, `phone_hash`, `identity`, `is_number_holder` |
| `listing_histories` | 挂牌历史快照(不可删除) | `property_id`, `listing_type`, `status`, `sale_price`, `seller_agent_snapshot` |
| `price_changes` | 调价记录(不可删除) | `property_id`, `old_sale_price`, `new_sale_price`, `change_reason`, `changed_by` |
| `follow_logs` | 跟进日志6种类型最高写入频率 | `property_id`, `log_type`, `content`, `is_deletable`, `operator_id` |
| `follow_log_attachments` | 跟进附件(图片) | `follow_log_id`, `file_key`, `file_type` |
| `follow_log_recordings` | 跟进录音 | `follow_log_id`, `file_key`, `duration_seconds` |
| `property_keys` | 钥匙管理(机械钥匙/密码) | `property_id`, `key_type`, `holder_id`, `is_active` |
| `key_attachments` | 钥匙附件 | `key_id`, `file_key` |
| `commissions` | 委托管理(独家/非独家) | `property_id`, `commission_type`, `period_start`, `status` |
| `commission_attachments` | 委托附件(身份证/产证/委托书) | `commission_id`, `category`, `file_key` |
| `field_surveys` | 实勘管理GPS 打卡) | `property_id`, `status`, `gps_latitude`, `gps_longitude`, `created_by` |
| `survey_photos` | 实勘照片(按空间分类) | `survey_id`, `category`, `file_key`, `is_vr_screenshot` |
| `property_photos` | 房源图片(经纪人管理,封面唯一约束) | `property_id`, `category`, `is_cover`, `file_key` |
| `property_attachments` | 房源附件 | `property_id`, `category`, `file_key` |
| `property_marketing` | 营销信息1:1卖点/业主心态/介绍) | `property_id`, `marketing_title`, `core_selling_points` |
| `property_certificates` | 产证信息1:1 | `property_id`, `cert_no`, `owner_name`, `land_nature` |
| `property_completeness` | 维护完成度快照1:1Celery 异步计算) | `property_id`, `total_score`, `score_survey`, `score_commission`, ... |
| `property_tags` | 标签字典(系统预置+运营自定义) | `name`, `color`, `is_system` |
| `property_tag_relations` | 房源↔标签多对多 | `property_id`, `tag_id` |
| `property_favorites` | 经纪人收藏房源 | `staff_id`, `property_id` |
| `property_protections` | 保护房设置1:1 | `property_id`, `is_protected`, `start_at`, `end_at` |
| `number_holder_approvals` | 号码方变更审批 | `property_id`, `applicant_id`, `status` |
**关键约束提示**
- `property_contacts.phone_hash` 是重复房源检测的主要依据,录入前必须查重
@@ -293,6 +302,83 @@ OrgUnit (组织架构)
---
### 3.4 登录与账号认证Login & Account
> **详细模型** → 见 [`DATA_MODEL_LOGIN.md`](./DATA_MODEL_LOGIN.md)
> 该文件为权威定义包含完整字段、状态机、Redis 缓存结构和禁止操作。
**核心表概览**(开发时以 DATA_MODEL_LOGIN.md 为准):
| 表名 | 说明 |
|------|------|
| `user_accounts` | 账号主表1:1 绑定 `org.Staff`),含加密手机号/哈希、状态机active/locked/disabled、初始密码标识 |
| `login_attempts` | 登录审计日志append-only成功/失败均记录,无 FK 冗余存 username 保证历史完整) |
| `password_reset_tokens` | 密码重置 Token有效期 30 分钟,使用后立即标记 `is_used` |
| `password_histories` | 历史密码记录(保留最近 3 条,含初始密码,防止重复使用) |
**关键约束提示**
- `user_accounts` 主键用 `BIGSERIAL`(非 UUID登录审计场景 BigInt 更高效
- `user_accounts.phone_enc` AES-256-GCM 加密,`phone_hash` SHA-256 用于唯一索引
- **禁止物理删除** `user_accounts`,离职员工只能 `status=disabled`
- 账号锁定5 次密码错误)→ `status=locked``locked_until=NOW()+30min`Redis 仅计数,实际锁定以 DB 为准
- Tenant Admin 的 `staff_id` 可为空(可无员工档案);普通员工 `staff_id` 必填且关联 active Staff
- 员工离职(`org.Staff.status→resigned`)→ 应用层 Service 调用触发账号 `status→disabled`**禁止循环 FK**
- `password_reset_tokens` / `login_attempts` **无 deleted_at**,不可修改/删除
**Redis 辅助状态**(非持久化):
| Key 格式 | TTL | 说明 |
|----------|-----|------|
| `captcha_token:{uuid}` | 3 分钟 | 滑块验证会话 Token |
| `captcha_pass:{uuid}` | 3 分钟 | 一次性通过凭证(验证后立即删除) |
| `login_fail:{tenant_id}:{username}` | 30 分钟 | 连续密码错误计数≥5 触发锁定 |
| `recover_email:{email}` | 1 小时 | 找回邮件发送次数上限 3 次 |
| `tenant_verify_ip:{ip}` | 1 分钟 | Tenant 验证接口 IP 限流≥10 次拒绝 |
---
### 3.5 权限管理Permission & RBAC
> **详细模型** → 见 [`DATA_MODEL_PERMISSION.md`](./DATA_MODEL_PERMISSION.md)
> 该文件为权威定义,包含完整字段、权限解析算法、`ScopeQueryBuilder` 实现和禁止操作。
**权限模型概述**Hybrid RBAC + Individual Override支持 `BOOLEAN / SCOPE / INTEGER` 三类权限值,多角色合并规则 OR / MAX。
**核心表概览**(开发时以 DATA_MODEL_PERMISSION.md 为准):
| 表名 | 说明 |
|------|------|
| `permission_defs` | 权限目录(约 300 条,`PUBLIC Schema``shared_apps` 存储,所有租户共享),含模块/分组/值类型/默认值/上限类别 |
| `roles` | 业务角色每租户独立5 种类别:`agent/store_manager/director/operator/custom`,含系统内置标识 |
| `role_permissions` | 角色↔权限值(稀疏存储,仅存与 default_value 不同的项) |
| `staff_roles` | 员工↔角色分配N:M含主角色标识 `is_primary`、有效期) |
| `staff_permission_overrides` | 员工个人权限覆盖稀疏存储仅存与角色合并值不同的项3 种 override_modeREPLACE / RESTRICT / GRANT |
| `staff_data_scopes` | 员工数据范围扩展(补充 SCOPE 权限之外的额外可读范围,如特殊跨门店授权) |
| `permission_change_logs` | 权限变更不可变审计日志append-only禁止 UPDATE/DELETE |
**关键约束提示**
- `permission_defs` 位于 **Public Schema**`shared_apps`),所有租户共享;`roles` 及其余表属租户 Schema
- **禁止硬删除** `permission_defs`,改用 `is_active=FALSE` 下线;`code` 字段不可修改
- **禁止直接构造 Q 对象绕过 `ScopeQueryBuilder`**,会导致越权漏洞
- `permission_change_logs` **无 deleted_at**,禁止 UPDATE/DELETE
- 员工权限解析:`is_system_admin=TRUE` → 短路返回全权限;否则多角色 OR/MAX 合并后叠加 Override
- `StaffPermissionOverride` 保存前必须做差异对比,**禁止存与角色合并值相同的冗余记录**(稀疏存储)
- `staff_roles.is_primary` 唯一约束通过 Signal 维护,**禁止绕过**
**权限解析缓存**
| Cache Key | TTL | 失效触发 |
|-----------|-----|---------|
| `perm:v{VER}:{schema}:{staff_id}` | 3600s | Override / StaffRole 变更 |
| `perm:v{VER}:{schema}:role:{role_id}:staff_ids` | 3600s | 角色权限变更 → Pipeline 批量失效 |
| `perm:inconsistent:{schema}:{staff_id}` | 300s | 同上 |
| `perm:defs:{schema}` | 86400s | PermissionDef 变更(低频) |
| `perm:role_applied_count:{schema}:{role_id}` | 600s | StaffRole 变更 |
> **版本号机制**`CACHE_VERSION` 在 Django settings 中,升级 PermissionDef 结构时 bump一键全局失效。
---
### 3.17 客源管理Client Management
> **详细模型** → 见 [`DATA_MODEL_CLIENT.md`](./DATA_MODEL_CLIENT.md)
@@ -381,6 +467,73 @@ CREATE INDEX idx_saved_filters_staff ON saved_filters(staff_id, module);
---
### 3.19 枚举字典Enum Labels
> **权威定义** → 见 [`DATA_MODEL/ENUMS.md`](./ENUMS.md)
> 本节为概览,开发时以 ENUMS.md 为准。
#### 表归属
`enum_labels` 位于 **Public Schema**`shared_apps`),所有租户共享,**不属于任何租户 Schema**。
#### 核心表设计
```sql
CREATE TABLE enum_labels (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
domain VARCHAR(60) NOT NULL, -- 枚举域,格式:{模块}.{字段},如 client.status
value VARCHAR(60) NOT NULL, -- 英文 Key与数据库 CHECK 约束一致)
label_zh VARCHAR(60) NOT NULL, -- 中文标签(前端展示用)
sort_order SMALLINT NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
remark TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX idx_enum_labels_domain_value ON enum_labels(domain, value);
CREATE INDEX idx_enum_labels_domain ON enum_labels(domain, sort_order);
```
#### 覆盖的枚举域domain 清单)
| domain | 说明 | 对应表字段 |
|--------|------|-----------|
| `client.status` | 客源状态7 态) | `clients.status` |
| `client.grade` | 客源等级5 档 + E | `clients.grade` |
| `client.purpose_type` | 需求类型 | `client_requirements.purpose_type` |
| `client.usage` | 房源用途偏好 | `client_requirements.usage` |
| `client.orientation` | 朝向偏好 | `client_requirements.orientation` |
| `client.payment_method` | 付款方式 | `clients.payment_method` |
| `property.status` | 房源状态 | `properties.status` |
| `property.attribute` | 房源属性(公/私/保护) | `properties.attribute` |
| `property.usage` | 房源用途 | `properties.usage` |
| `property.grade` | 房源等级5 档) | `properties.grade` |
| `property.listing_type` | 挂牌类型 | `properties.listing_type` |
| `property.decoration` | 装修程度 | `properties.decoration` |
| `property.orientation` | 朝向 | `properties.orientation` |
| `commission.type` | 委托类型 | `commissions.commission_type` |
| `field_survey.status` | 实勘状态 | `field_surveys.status` |
| `follow_log.log_type` | 跟进日志类型 | `follow_logs.log_type` |
#### 重要约定
- `enum_labels.value` 必须与对应表的 `CHECK` 约束完全一致,**两者必须同步修改**
- 新增枚举值流程:① 修改 DDL `CHECK` 约束 → ② 插入 `enum_labels` 种子数据 → ③ 更新 `ENUMS.md`
- `is_active = FALSE` 仅停用前端展示,**不得修改或删除已有 `value`**(历史数据引用不可破坏)
- 前端下拉渲染**统一从 `enum_labels` 读取**,禁止在前端代码中硬编码中文标签
#### 与 `lookup_items` 的区别
| 对比维度 | `enum_labels` | `lookup_items` |
|---------|---------------|----------------|
| 用途 | 固定枚举的中文标签映射 | 运营可配置的动态选项(如跟进目的、来源渠道) |
| 修改权限 | 仅开发/DBA | 运营人员后台配置 |
| Schema 位置 | Public Schema共享 | Tenant Schema每租户独立 |
| 典型示例 | 客源状态、房源等级 | 跟进目的、客户来源渠道 |
---
## 五、关键索引汇总与查询优化策略
### 4.1 房源列表页核心查询分析
@@ -489,6 +642,21 @@ CREATE TRIGGER trg_update_last_followed
# 枚举值/lookup几乎不变
{schema}:lookup:{category_code} TTL: 86400 (24小时)
# 登录模块(详见 DATA_MODEL_LOGIN.md §四)
captcha_token:{uuid} TTL: 180 (3分钟)
captcha_pass:{uuid} TTL: 180 (3分钟)
login_fail:{tenant_id}:{username} TTL: 1800 (30分钟连续失败计数)
recover_email:{email} TTL: 3600 (1小时发送次数限流)
recover_reset:{account_id} TTL: 3600 (1小时Token 生成次数限流)
tenant_verify_ip:{ip} TTL: 60 (1分钟IP 限流)
# 权限模块(详见 DATA_MODEL_PERMISSION.md §六)
perm:v{VER}:{schema}:{staff_id} TTL: 3600 (员工完整权限快照)
perm:v{VER}:{schema}:role:{role_id}:staff_ids TTL: 3600 (角色→员工 ID 列表,批量失效用)
perm:inconsistent:{schema}:{staff_id} TTL: 300 (权限不一致标记)
perm:defs:{schema} TTL: 86400 (权限定义全量缓存)
perm:role_applied_count:{schema}:{role_id} TTL: 600 (角色应用人数)
# 标签列表
{schema}:tags:property TTL: 3600

View File

@@ -357,12 +357,12 @@ CREATE UNIQUE INDEX idx_favorite_folders_default ON client_favorite_folders(staf
### 3.10 client_folder_items — 收藏夹中的客源
| 字段 | 类型 | 约束 | 业务说明 |
|------|------|------|----------|
| folder_id | UUID | NOT NULL, FK→client_favorite_folders, CASCADE | |
| client_id | UUID | NOT NULL, FK→clients, CASCADE | |
| added_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
| PRIMARY KEY | (folder_id, client_id) | | |
| 字段 | 类型 | 约束 | 业务说明 |
| ----------- | ---------------------- | --------------------------------------------- | ---- |
| folder_id | UUID | NOT NULL, FK→client_favorite_folders, CASCADE | |
| client_id | UUID | NOT NULL, FK→clients, CASCADE | |
| added_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
| PRIMARY KEY | (folder_id, client_id) | | |
```sql
CREATE INDEX idx_folder_items_client ON client_folder_items(client_id);
@@ -415,8 +415,7 @@ buying/renting/buy_or_rent
### clients.grade等级
```
A_urgent = A(急迫)
A = A
A = A(急迫)
B = B(较强)
C = C(一般,默认值)
D = D(较弱)
@@ -438,15 +437,15 @@ merge = 合并客源(被合并的记录保留日志)
### clients.activity_level活跃度分层系统计算
| 值 | 含义 | 触发条件(示例,以运营配置为准) |
|----|------|------|
| `new_matched` | 新配偶 | 录入后 3 天内 |
| `active_7d` | 7日活跃 | 最后跟进在 7 天内 |
| `active_30d` | 30日活跃 | 最后跟进在 30 天内 |
| `active_90d` | 90日活跃 | 最后跟进在 90 天内 |
| `expiring` | 即将过期 | 距自动转公还有 N 天 |
| `frozen` | 冻结(暂缓) | status = suspended |
| `invalid` | 无效 | status = invalid |
| 值 | 含义 | 触发条件(示例,以运营配置为准) |
| ------------- | ------ | ------------------ |
| `new_matched` | 新匹配 | 录入后 3 天内 |
| `active_7d` | 7日活跃 | 最后跟进在 7 天内 |
| `active_30d` | 30日活跃 | 最后跟进在 30 天内 |
| `active_90d` | 90日活跃 | 最后跟进在 90 天内 |
| `expiring` | 即将过期 | 距自动转公还有 N 天 |
| `frozen` | 冻结(暂缓) | status = suspended |
| `invalid` | 无效 | status = invalid |
---

View File

@@ -1,470 +1,470 @@
# Fonrey — 登录与账号认证数据模型DATA_MODEL_LOGIN
> **所属系统**: Fonrey 房产经纪管理系统
> **版本**: v1.0
> **日期**: 2026-04-24
> **关联模块**: `apps/accounts/` — 账号认证、登录安全、密码管理
> **关联 PRD**: `Project/fonrey/PRD/登录管理/用户登录管理模块PRD.md` (v1.3)
> **关联技术方案**: `Project/fonrey/TECH_STACK/登录管理技术方案.md`
---
## 一、领域概览Domain Overview
### 核心概念
- **UserAccount用户账号**:系统登录主体,必须与员工档案(`org.Staff`1:1 绑定。分为 Tenant Admin超级管理账号每租户唯一和普通员工账号username 固定为手机号)。
- **LoginAttempt登录尝试记录**:记录每次登录行为(成功/失败),用于安全审计和账号锁定判断,保留 ≥ 90 天。
- **PasswordResetToken密码重置令牌**:通过邮件找回密码时生成的一次性令牌,有效期 30 分钟,使用后立即失效。
- **PasswordHistory历史密码记录**:保存最近 3 次密码哈希,用于防止重复使用历史密码。
### 关键业务规则
1. **账号与员工强绑定**:每个登录账号 **必须**`org.Staff` 中的员工档案 1:1 绑定Tenant Admin 例外,可不绑定)。
2. **用户名规则差异化**
- Tenant Admin由平台运营自定义字母开头6~30 位,含字母/数字/下划线)
- 普通员工:**固定为员工手机号**11 位数字),创建后不可变更
3. **初始密码强制修改**:新账号及密码重置后,`is_initial_password = True`,首次登录必须修改密码,不可跳过。
4. **账号锁定机制**:同一账号连续密码错误 ≥ 5 次,状态置为 `locked`30 分钟后自动恢复Tenant Admin 可提前手动解锁。
5. **员工离职联动**:员工离职时,对应账号的 `status` 自动置为 `disabled`,不可登录,历史操作记录保留。
6. **不支持自助注册**:所有账号由有权限的管理角色创建,普通员工账号在新增员工时由系统自动生成。
---
## 二、实体关系
```
UserAccount
├── 1:1 ── org.Staff (实名绑定,普通员工必须)
├── 1:N ── LoginAttempt (登录审计记录)
├── 1:N ── PasswordResetToken (密码重置令牌)
├── 1:N ── PasswordHistory (历史密码记录)
└── M:1 ── UserAccount.created_by (创建人自引用)
```
### Schema 归属
| 表 | Schema 位置 | 说明 |
|----|------------|------|
| `user_accounts` | 租户 Schema | 账号数据按租户隔离username 唯一性在 Schema 维度生效 |
| `login_attempts` | 租户 Schema | 审计记录属于租户,跨租户不可见 |
| `password_reset_tokens` | 租户 Schema | 令牌与租户账号绑定 |
| `password_histories` | 租户 Schema | 历史密码与账号绑定 |
> **注意**Tenant ID 验证相关逻辑在 **Public Schema**`shared_apps`),使用 `django-tenants` 的 `TenantModel`,不在本文档范围内,详见 `DATA_MODEL.md` §四(公共 Schema
---
## 三、Schema 定义
### 3.1 `user_accounts` — 账号主表(租户 Schema
**表说明**:系统登录主体,每个租户内独立隔离,`username` 唯一性约束在 Schema 维度生效。
#### 字段定义
| 字段名 | 类型 | 约束 | 默认值 | 说明 |
|--------|------|------|--------|------|
| `id` | `BIGSERIAL` | `PRIMARY KEY` | — | 自增主键(审计场景下 BigInt 更直观;跨环境引用使用 UUID 扩展字段见下) |
| `username` | `VARCHAR(30)` | `NOT NULL` | — | 登录名普通员工为手机号11 位数字Tenant Admin 为自定义字符串;创建后不可更改 |
| `password` | `VARCHAR(128)` | `NOT NULL` | — | PBKDF2+SHA256 哈希存储,使用 Django `make_password` |
| `email` | `VARCHAR(254)` | `NULL` | `NULL` | 绑定邮箱;用于找回密码/用户名;为空则无法自助找回;同租户唯一 |
| `phone_enc` | `TEXT` | `NULL` | `NULL` | 手机号 AES-256-GCM 加密密文(`core.encryption`);普通员工必填 |
| `phone_hash` | `VARCHAR(64)` | `NULL` | `NULL` | 手机号 SHA-256 哈希;用于唯一性校验和查询;不可反推原文 |
| `staff_id` | `BIGINT` | `FK → org_staff.id`, `NULL`, `UNIQUE` | `NULL` | 员工档案绑定1:1普通员工必须有值Tenant Admin 可为空 |
| `is_tenant_admin` | `BOOLEAN` | `NOT NULL` | `FALSE` | 是否为该租户的超级管理账号;每个租户最多 1 个(应用层约束) |
| `status` | `VARCHAR(10)` | `NOT NULL`, `CHECK(status IN ('active','disabled','locked'))` | `'active'` | 账号状态;`locked` 为密码错误锁定30 分钟自动恢复 |
| `is_initial_password` | `BOOLEAN` | `NOT NULL` | `TRUE` | 初始密码标记True 时登录成功后强制跳转修改密码页,不可跳过 |
| `last_login` | `TIMESTAMPTZ` | `NULL` | `NULL` | 最后登录时间 |
| `locked_until` | `TIMESTAMPTZ` | `NULL` | `NULL` | 锁定到期时间;到期后应用层将 status 恢复 active |
| `created_at` | `TIMESTAMPTZ` | `NOT NULL` | `NOW()` | 账号创建时间 |
| `updated_at` | `TIMESTAMPTZ` | `NOT NULL` | `NOW()` | 最后更新时间(触发器维护) |
| `created_by` | `BIGINT` | `FK → user_accounts.id`, `NULL` | `NULL` | 创建人;普通员工由 Tenant Admin 创建Tenant Admin 由平台运营创建(可为 NULL |
#### 唯一性约束
```sql
UNIQUE (username) -- Schema 内唯一跨租户不冲突django-tenants 机制保障)
UNIQUE (email) -- 同租户内邮箱唯一(可为 NULLNULL 不参与唯一性校验)
UNIQUE (phone_hash) -- 同租户内手机号唯一(通过 hash 实现,不暴露原文)
UNIQUE (staff_id) -- 员工档案 1:1 绑定
```
#### 索引
```sql
CREATE UNIQUE INDEX uq_user_accounts_username ON user_accounts (username);
CREATE UNIQUE INDEX uq_user_accounts_email ON user_accounts (email) WHERE email IS NOT NULL;
CREATE UNIQUE INDEX uq_user_accounts_phone ON user_accounts (phone_hash) WHERE phone_hash IS NOT NULL;
CREATE INDEX idx_user_accounts_status ON user_accounts (status);
CREATE INDEX idx_user_accounts_staff ON user_accounts (staff_id);
```
#### Django Model 定义
```python
# apps/accounts/models.py
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
from django.db import models
class UserAccountManager(BaseUserManager):
def create_user(self, username, password, **extra_fields):
if not username:
raise ValueError("username 不能为空")
user = self.model(username=username, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
class UserAccount(AbstractBaseUser):
"""
租户级用户账号。
- 普通员工username 固定为手机号11 位数字)
- Tenant Adminusername 由平台运营自定义字母开头6~30 位)
注意:此表位于租户 Schemausername 唯一性约束在 Schema 维度生效。
"""
username = models.CharField(max_length=30)
email = models.EmailField(null=True, blank=True)
phone_enc = models.TextField(null=True, blank=True) # AES-256-GCM 加密密文
phone_hash = models.CharField(max_length=64, null=True, blank=True) # SHA-256 哈希索引
staff = models.OneToOneField(
'org.Staff',
null=True, blank=True,
on_delete=models.SET_NULL,
related_name='account',
)
is_tenant_admin = models.BooleanField(default=False)
status = models.CharField(
max_length=10,
choices=[('active', 'Active'), ('disabled', 'Disabled'), ('locked', 'Locked')],
default='active',
)
is_initial_password = models.BooleanField(default=True)
last_login = models.DateTimeField(null=True, blank=True)
locked_until = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
created_by = models.ForeignKey(
'self',
null=True, blank=True,
on_delete=models.SET_NULL,
related_name='created_accounts',
)
USERNAME_FIELD = 'username'
REQUIRED_FIELDS = []
objects = UserAccountManager()
class Meta:
db_table = 'user_accounts'
# Schema 内唯一约束
constraints = [
models.UniqueConstraint(fields=['username'], name='uq_user_accounts_username'),
]
def __str__(self):
return f"{self.username} ({'admin' if self.is_tenant_admin else 'staff'})"
def is_locked(self) -> bool:
"""检查账号是否处于锁定状态(含自动过期判断)"""
from django.utils import timezone
if self.status == 'locked':
if self.locked_until and timezone.now() >= self.locked_until:
# 锁定已到期,应用层自动恢复(实际由 service 层处理)
return False
return True
return False
```
---
### 3.2 `login_attempts` — 登录尝试审计表(租户 Schema
**表说明**:记录每次登录请求(成功/失败),用于安全审计和锁定判断。数据保留 ≥ 90 天,不得提前清理。
#### 字段定义
| 字段名 | 类型 | 约束 | 默认值 | 说明 |
|--------|------|------|--------|------|
| `id` | `BIGSERIAL` | `PRIMARY KEY` | — | 自增主键 |
| `username` | `VARCHAR(30)` | `NOT NULL` | — | 尝试登录的用户名(冗余存储,即使账号不存在也记录) |
| `ip_address` | `INET` | `NOT NULL` | — | 来源 IP 地址(支持 IPv4/IPv6 |
| `user_agent` | `TEXT` | `NULL` | `NULL` | 客户端 User-AgentElectron 版本信息) |
| `success` | `BOOLEAN` | `NOT NULL` | — | 是否登录成功 |
| `failure_reason` | `VARCHAR(30)` | `NULL` | `NULL` | 失败原因;可选值见下方枚举 |
| `attempted_at` | `TIMESTAMPTZ` | `NOT NULL` | `NOW()` | 尝试时间 |
**`failure_reason` 枚举值**
| 值 | 含义 |
|----|------|
| `wrong_password` | 用户名或密码错误 |
| `wrong_captcha` | 行为验证码失败 |
| `account_locked` | 账号已锁定 |
| `account_disabled` | 账号已停用 |
| `tenant_not_found` | 租户不存在(理论上不应出现,防御性记录) |
#### 索引
```sql
CREATE INDEX idx_login_attempts_username ON login_attempts (username);
CREATE INDEX idx_login_attempts_ip ON login_attempts (ip_address);
CREATE INDEX idx_login_attempts_time ON login_attempts (attempted_at DESC);
-- 复合索引:按账号查询最近失败记录(锁定判断场景)
CREATE INDEX idx_login_attempts_fail_check ON login_attempts (username, success, attempted_at DESC);
```
#### Django Model 定义
```python
class LoginAttempt(models.Model):
"""
登录尝试审计记录。
- 合规保留周期:≥ 90 天
- 注意failure_reason 不得存储密码明文(含错误密码)
"""
FAILURE_REASONS = [
('wrong_password', '用户名或密码错误'),
('wrong_captcha', '行为验证码失败'),
('account_locked', '账号已锁定'),
('account_disabled', '账号已停用'),
('tenant_not_found', '租户不存在'),
]
username = models.CharField(max_length=30)
ip_address = models.GenericIPAddressField()
user_agent = models.TextField(null=True, blank=True)
success = models.BooleanField()
failure_reason = models.CharField(max_length=30, null=True, blank=True, choices=FAILURE_REASONS)
attempted_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'login_attempts'
indexes = [
models.Index(fields=['username']),
models.Index(fields=['ip_address']),
models.Index(fields=['-attempted_at']),
models.Index(fields=['username', 'success', '-attempted_at'],
name='idx_login_attempts_fail_check'),
]
def __str__(self):
return f"{self.username} @ {self.attempted_at} - {'OK' if self.success else self.failure_reason}"
```
---
### 3.3 `password_reset_tokens` — 密码重置令牌表(租户 Schema
**表说明**用于通过邮件找回密码的一次性令牌。单次有效30 分钟过期。
#### 字段定义
| 字段名 | 类型 | 约束 | 默认值 | 说明 |
|--------|------|------|--------|------|
| `id` | `BIGSERIAL` | `PRIMARY KEY` | — | 自增主键 |
| `user_id` | `BIGINT` | `FK → user_accounts.id`, `NOT NULL` | — | 关联账号 |
| `token` | `VARCHAR(86)` | `NOT NULL`, `UNIQUE` | — | `secrets.token_urlsafe(64)` 生成86 字符),全局唯一 |
| `expires_at` | `TIMESTAMPTZ` | `NOT NULL` | — | 过期时间(`created_at + 30 分钟` |
| `is_used` | `BOOLEAN` | `NOT NULL` | `FALSE` | 是否已使用;使用后立即置 True防止重放攻击 |
| `created_at` | `TIMESTAMPTZ` | `NOT NULL` | `NOW()` | 创建时间 |
#### 索引
```sql
CREATE UNIQUE INDEX uq_password_reset_tokens_token ON password_reset_tokens (token);
CREATE INDEX idx_password_reset_tokens_user ON password_reset_tokens (user_id);
CREATE INDEX idx_password_reset_tokens_expiry ON password_reset_tokens (expires_at) WHERE is_used = FALSE;
```
#### Django Model 定义
```python
class PasswordResetToken(models.Model):
"""
密码重置令牌。
安全约束:
- Token 单次有效is_used=True 后立即失效)
- 有效期 30 分钟
- 同一账号 1 小时内最多生成 3 个服务层限频Redis 计数)
"""
user = models.ForeignKey(UserAccount, on_delete=models.CASCADE, related_name='reset_tokens')
token = models.CharField(max_length=86, unique=True) # secrets.token_urlsafe(64)
expires_at = models.DateTimeField()
is_used = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'password_reset_tokens'
indexes = [
models.Index(fields=['user_id']),
]
def is_valid(self) -> bool:
from django.utils import timezone
return not self.is_used and timezone.now() < self.expires_at
```
---
### 3.4 `password_histories` — 历史密码记录表(租户 Schema
**表说明**:保存账号最近 3 次密码哈希,用于防止重复使用历史密码(含初始密码)。
#### 字段定义
| 字段名 | 类型 | 约束 | 默认值 | 说明 |
|--------|------|------|--------|------|
| `id` | `BIGSERIAL` | `PRIMARY KEY` | — | 自增主键 |
| `user_id` | `BIGINT` | `FK → user_accounts.id`, `NOT NULL` | — | 关联账号 |
| `password_hash` | `VARCHAR(128)` | `NOT NULL` | — | PBKDF2+SHA256 哈希值 |
| `created_at` | `TIMESTAMPTZ` | `NOT NULL` | `NOW()` | 记录时间(密码修改时间) |
#### 索引
```sql
CREATE INDEX idx_password_histories_user ON password_histories (user_id, created_at DESC);
```
#### Django Model 定义
```python
class PasswordHistory(models.Model):
"""
历史密码记录,每个账号保留最近 N 条(默认 3 条)。
新密码不得与最近 3 条历史记录相同(含系统初始密码 Fonrey@2025
"""
user = models.ForeignKey(UserAccount, on_delete=models.CASCADE, related_name='password_histories')
password_hash = models.CharField(max_length=128)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'password_histories'
ordering = ['-created_at']
indexes = [
models.Index(fields=['user', '-created_at']),
]
```
---
## 四、Redis 缓存结构(辅助状态,非持久化)
以下 Redis Key 不存入 PostgreSQL属于运行时状态需与数据库状态保持最终一致
| Key 格式 | 类型 | TTL | 说明 |
|----------|------|-----|------|
| `captcha_token:{uuid}` | STRING | 3 分钟 | 滑块验证会话 Token验证通过后生成 `captcha_pass_token` |
| `captcha_pass:{uuid}` | STRING | 3 分钟 | 一次性通过凭证;登录提交时校验后立即删除 |
| `login_fail:{tenant_id}:{username}` | STRING计数 | 30 分钟 | 连续密码错误次数;≥ 5 触发锁定TTL 30 分钟自动清零 |
| `recover_email:{email}` | STRING计数 | 1 小时 | 找回邮件发送次数;上限 3 次/小时 |
| `recover_reset:{account_id}` | STRING计数 | 1 小时 | 同一账号密码重置 Token 生成次数;上限 3 次/小时 |
| `tenant_verify_ip:{ip}` | STRING计数 | 1 分钟 | Tenant 验证接口 IP 限流;≥ 10 次拒绝请求 |
> **一致性说明**:账号锁定状态由 `user_accounts.status` 持久化Redis 仅做计数触发器。当 Redis 数据丢失(如 Redis 重启),应用层通过 `locked_until` 字段恢复锁定状态判断。
---
## 五、账号创建流程与状态机
### 5.1 账号状态机
```
┌─────────────────────────────────────┐
│ 账号生命周期状态机 │
└─────────────────────────────────────┘
[创建账号]
│ is_initial_password=True, status=active
[初始密码态] ─── 使用初始密码登录成功 ───► [强制修改密码页]
│ │ 修改成功
│ ▼
│ [正常使用态]
│ status=active
│ is_initial_password=False
├── 密码错误 ≥ 5 次 ──────────────────► [锁定态]
│ status=locked
│ locked_until = now+30min
│ │
│ ┌───────────────┤
│ │ 30分钟到期 │ 管理员手动解锁
│ ▼ ▼
│ [正常使用态] ◄─── [管理员操作]
└── 员工离职 / 管理员停用 ──► [停用态]
status=disabled
员工复职/管理员恢复
[正常使用态]
```
### 5.2 账号创建触发时机
| 账号类型 | 触发时机 | 创建者 | username 规则 | 初始密码 |
|----------|----------|--------|--------------|---------|
| Tenant Admin | 平台运营在系统后台开通租户时 | 平台运营 | 自定义字母开头6~30 位) | 平台运营自定义 |
| 普通员工 | Tenant Admin 在「新增员工」时系统自动生成 | 系统Tenant Admin 触发) | 固定为员工手机号11 位) | 系统统一初始密码(部署配置) |
---
## 六、关联约束与数据完整性
### 6.1 与 `org.Staff` 的关联规则
```
org_staff (1) ──── (0..1) user_accounts
```
- 普通员工账号:`staff_id` **必须**有值,且在 `org.Staff` 中对应记录的 `status` 为 active
- Tenant Admin`staff_id` **可为空**(平台运营账号可不绑定实名档案)
- 员工离职时(`org.Staff.status``resigned`),触发账号 `status``disabled`(由 `org` App Service 层调用 `accounts` 服务执行,避免循环依赖)
- 账号删除:**不允许物理删除**,仅允许 `status=disabled`,审计记录永久保留
### 6.2 跨 App 依赖方向
```
accounts ──► org (单向依赖accounts.UserAccount.staff_id → org.Staff)
org ──► accounts (反向触发,通过 Service 层调用,不通过 FK 反查)
```
> **设计原则**:避免循环 FK 依赖,跨 App 的状态联动通过 Service 层的显式调用完成,不在 Model 层建立反向 FK。
---
## 七、迁移说明Django Migrations
### 初始迁移顺序
```
0001_initial_user_accounts.py # UserAccount 表(依赖 org.Staff 表已存在)
0002_login_attempts.py # LoginAttempt 表
0003_password_reset_tokens.py # PasswordResetToken 表
0004_password_histories.py # PasswordHistory 表
```
### 注意事项
- `accounts` App 的迁移依赖 `org` App`org.Staff` 表须先创建),需在 `INSTALLED_APPS` 中确保 `org``accounts` 之前
- 所有迁移均在**租户 Schema** 下执行(`django-tenants``migrate_schemas` 命令)
- 不得为 `email` 字段设置 `NOT NULL` 约束(允许为空,是否绑定邮箱属于用户选择)
---
## 八、设计决策说明ADR
| 决策 | 选择 | 理由 |
|------|------|------|
| 主键类型 | `BIGSERIAL` (BigInt) | 登录审计场景下 BigInt 主键更简洁高效;跨环境引用场景少,无需 UUID 的随机性 |
| `phone` 字段拆分为 `phone_enc` + `phone_hash` | 是 | 与 `org.Staff` 保持一致;`phone_enc` 保存原文用于展示,`phone_hash` 用于唯一性校验和快速查询,避免加密字段全表扫描 |
| 不扩展 Django `User` | 使用 `AbstractBaseUser` | 避免 `django.contrib.auth.User` 的全局唯一性限制(多租户下同一 username 在不同租户是允许的) |
| `LoginAttempt` 不设外键到 `UserAccount` | 是(冗余存储 username | 即使账号被删除(停用),审计记录仍需保留;使用 username 字符串字段保证审计完整性 |
| 历史密码单独建表 | `PasswordHistory` 独立表 | 而非在 `UserAccount` 中存 JSON 数组,便于查询和维护,支持未来扩展保留次数 |
| 锁定到期时间持久化 | `locked_until` 字段 | Redis 可能重启丢失数据,持久化 `locked_until` 保证 Redis 故障时锁定状态不丢失 |
# Fonrey — 登录与账号认证数据模型DATA_MODEL_LOGIN
> **所属系统**: Fonrey 房产经纪管理系统
> **版本**: v1.0
> **日期**: 2026-04-24
> **关联模块**: `apps/accounts/` — 账号认证、登录安全、密码管理
> **关联 PRD**: `Project/fonrey/PRD/登录管理/用户登录管理模块PRD.md` (v1.3)
> **关联技术方案**: `Project/fonrey/TECH_STACK/登录管理技术方案.md`
---
## 一、领域概览Domain Overview
### 核心概念
- **UserAccount用户账号**:系统登录主体,必须与员工档案(`org.Staff`1:1 绑定。分为 Tenant Admin超级管理账号每租户唯一和普通员工账号username 固定为手机号)。
- **LoginAttempt登录尝试记录**:记录每次登录行为(成功/失败),用于安全审计和账号锁定判断,保留 ≥ 90 天。
- **PasswordResetToken密码重置令牌**:通过邮件找回密码时生成的一次性令牌,有效期 30 分钟,使用后立即失效。
- **PasswordHistory历史密码记录**:保存最近 3 次密码哈希,用于防止重复使用历史密码。
### 关键业务规则
1. **账号与员工强绑定**:每个登录账号 **必须**`org.Staff` 中的员工档案 1:1 绑定Tenant Admin 例外,可不绑定)。
2. **用户名规则差异化**
- Tenant Admin由平台运营自定义字母开头6~30 位,含字母/数字/下划线)
- 普通员工:**固定为员工手机号**11 位数字),创建后不可变更
3. **初始密码强制修改**:新账号及密码重置后,`is_initial_password = True`,首次登录必须修改密码,不可跳过。
4. **账号锁定机制**:同一账号连续密码错误 ≥ 5 次,状态置为 `locked`30 分钟后自动恢复Tenant Admin 可提前手动解锁。
5. **员工离职联动**:员工离职时,对应账号的 `status` 自动置为 `disabled`,不可登录,历史操作记录保留。
6. **不支持自助注册**:所有账号由有权限的管理角色创建,普通员工账号在新增员工时由系统自动生成。
---
## 二、实体关系
```
UserAccount
├── 1:1 ── org.Staff (实名绑定,普通员工必须)
├── 1:N ── LoginAttempt (登录审计记录)
├── 1:N ── PasswordResetToken (密码重置令牌)
├── 1:N ── PasswordHistory (历史密码记录)
└── M:1 ── UserAccount.created_by (创建人自引用)
```
### Schema 归属
| 表 | Schema 位置 | 说明 |
|----|------------|------|
| `user_accounts` | 租户 Schema | 账号数据按租户隔离username 唯一性在 Schema 维度生效 |
| `login_attempts` | 租户 Schema | 审计记录属于租户,跨租户不可见 |
| `password_reset_tokens` | 租户 Schema | 令牌与租户账号绑定 |
| `password_histories` | 租户 Schema | 历史密码与账号绑定 |
> **注意**Tenant ID 验证相关逻辑在 **Public Schema**`shared_apps`),使用 `django-tenants` 的 `TenantModel`,不在本文档范围内,详见 `DATA_MODEL.md` §四(公共 Schema
---
## 三、Schema 定义
### 3.1 `user_accounts` — 账号主表(租户 Schema
**表说明**:系统登录主体,每个租户内独立隔离,`username` 唯一性约束在 Schema 维度生效。
#### 字段定义
| 字段名 | 类型 | 约束 | 默认值 | 说明 |
|--------|------|------|--------|------|
| `id` | `BIGSERIAL` | `PRIMARY KEY` | — | 自增主键(审计场景下 BigInt 更直观;跨环境引用使用 UUID 扩展字段见下) |
| `username` | `VARCHAR(30)` | `NOT NULL` | — | 登录名普通员工为手机号11 位数字Tenant Admin 为自定义字符串;创建后不可更改 |
| `password` | `VARCHAR(128)` | `NOT NULL` | — | PBKDF2+SHA256 哈希存储,使用 Django `make_password` |
| `email` | `VARCHAR(254)` | `NULL` | `NULL` | 绑定邮箱;用于找回密码/用户名;为空则无法自助找回;同租户唯一 |
| `phone_enc` | `TEXT` | `NULL` | `NULL` | 手机号 AES-256-GCM 加密密文(`core.encryption`);普通员工必填 |
| `phone_hash` | `VARCHAR(64)` | `NULL` | `NULL` | 手机号 SHA-256 哈希;用于唯一性校验和查询;不可反推原文 |
| `staff_id` | `BIGINT` | `FK → org_staff.id`, `NULL`, `UNIQUE` | `NULL` | 员工档案绑定1:1普通员工必须有值Tenant Admin 可为空 |
| `is_tenant_admin` | `BOOLEAN` | `NOT NULL` | `FALSE` | 是否为该租户的超级管理账号;每个租户最多 1 个(应用层约束) |
| `status` | `VARCHAR(10)` | `NOT NULL`, `CHECK(status IN ('active','disabled','locked'))` | `'active'` | 账号状态;`locked` 为密码错误锁定30 分钟自动恢复 |
| `is_initial_password` | `BOOLEAN` | `NOT NULL` | `TRUE` | 初始密码标记True 时登录成功后强制跳转修改密码页,不可跳过 |
| `last_login` | `TIMESTAMPTZ` | `NULL` | `NULL` | 最后登录时间 |
| `locked_until` | `TIMESTAMPTZ` | `NULL` | `NULL` | 锁定到期时间;到期后应用层将 status 恢复 active |
| `created_at` | `TIMESTAMPTZ` | `NOT NULL` | `NOW()` | 账号创建时间 |
| `updated_at` | `TIMESTAMPTZ` | `NOT NULL` | `NOW()` | 最后更新时间(触发器维护) |
| `created_by` | `BIGINT` | `FK → user_accounts.id`, `NULL` | `NULL` | 创建人;普通员工由 Tenant Admin 创建Tenant Admin 由平台运营创建(可为 NULL |
#### 唯一性约束
```sql
UNIQUE (username) -- Schema 内唯一跨租户不冲突django-tenants 机制保障)
UNIQUE (email) -- 同租户内邮箱唯一(可为 NULLNULL 不参与唯一性校验)
UNIQUE (phone_hash) -- 同租户内手机号唯一(通过 hash 实现,不暴露原文)
UNIQUE (staff_id) -- 员工档案 1:1 绑定
```
#### 索引
```sql
CREATE UNIQUE INDEX uq_user_accounts_username ON user_accounts (username);
CREATE UNIQUE INDEX uq_user_accounts_email ON user_accounts (email) WHERE email IS NOT NULL;
CREATE UNIQUE INDEX uq_user_accounts_phone ON user_accounts (phone_hash) WHERE phone_hash IS NOT NULL;
CREATE INDEX idx_user_accounts_status ON user_accounts (status);
CREATE INDEX idx_user_accounts_staff ON user_accounts (staff_id);
```
#### Django Model 定义
```python
# apps/accounts/models.py
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
from django.db import models
class UserAccountManager(BaseUserManager):
def create_user(self, username, password, **extra_fields):
if not username:
raise ValueError("username 不能为空")
user = self.model(username=username, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
class UserAccount(AbstractBaseUser):
"""
租户级用户账号。
- 普通员工username 固定为手机号11 位数字)
- Tenant Adminusername 由平台运营自定义字母开头6~30 位)
注意:此表位于租户 Schemausername 唯一性约束在 Schema 维度生效。
"""
username = models.CharField(max_length=30)
email = models.EmailField(null=True, blank=True)
phone_enc = models.TextField(null=True, blank=True) # AES-256-GCM 加密密文
phone_hash = models.CharField(max_length=64, null=True, blank=True) # SHA-256 哈希索引
staff = models.OneToOneField(
'org.Staff',
null=True, blank=True,
on_delete=models.SET_NULL,
related_name='account',
)
is_tenant_admin = models.BooleanField(default=False)
status = models.CharField(
max_length=10,
choices=[('active', 'Active'), ('disabled', 'Disabled'), ('locked', 'Locked')],
default='active',
)
is_initial_password = models.BooleanField(default=True)
last_login = models.DateTimeField(null=True, blank=True)
locked_until = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
created_by = models.ForeignKey(
'self',
null=True, blank=True,
on_delete=models.SET_NULL,
related_name='created_accounts',
)
USERNAME_FIELD = 'username'
REQUIRED_FIELDS = []
objects = UserAccountManager()
class Meta:
db_table = 'user_accounts'
# Schema 内唯一约束
constraints = [
models.UniqueConstraint(fields=['username'], name='uq_user_accounts_username'),
]
def __str__(self):
return f"{self.username} ({'admin' if self.is_tenant_admin else 'staff'})"
def is_locked(self) -> bool:
"""检查账号是否处于锁定状态(含自动过期判断)"""
from django.utils import timezone
if self.status == 'locked':
if self.locked_until and timezone.now() >= self.locked_until:
# 锁定已到期,应用层自动恢复(实际由 service 层处理)
return False
return True
return False
```
---
### 3.2 `login_attempts` — 登录尝试审计表(租户 Schema
**表说明**:记录每次登录请求(成功/失败),用于安全审计和锁定判断。数据保留 ≥ 90 天,不得提前清理。
#### 字段定义
| 字段名 | 类型 | 约束 | 默认值 | 说明 |
|--------|------|------|--------|------|
| `id` | `BIGSERIAL` | `PRIMARY KEY` | — | 自增主键 |
| `username` | `VARCHAR(30)` | `NOT NULL` | — | 尝试登录的用户名(冗余存储,即使账号不存在也记录) |
| `ip_address` | `INET` | `NOT NULL` | — | 来源 IP 地址(支持 IPv4/IPv6 |
| `user_agent` | `TEXT` | `NULL` | `NULL` | 客户端 User-AgentElectron 版本信息) |
| `success` | `BOOLEAN` | `NOT NULL` | — | 是否登录成功 |
| `failure_reason` | `VARCHAR(30)` | `NULL` | `NULL` | 失败原因;可选值见下方枚举 |
| `attempted_at` | `TIMESTAMPTZ` | `NOT NULL` | `NOW()` | 尝试时间 |
**`failure_reason` 枚举值**
| 值 | 含义 |
|----|------|
| `wrong_password` | 用户名或密码错误 |
| `wrong_captcha` | 行为验证码失败 |
| `account_locked` | 账号已锁定 |
| `account_disabled` | 账号已停用 |
| `tenant_not_found` | 租户不存在(理论上不应出现,防御性记录) |
#### 索引
```sql
CREATE INDEX idx_login_attempts_username ON login_attempts (username);
CREATE INDEX idx_login_attempts_ip ON login_attempts (ip_address);
CREATE INDEX idx_login_attempts_time ON login_attempts (attempted_at DESC);
-- 复合索引:按账号查询最近失败记录(锁定判断场景)
CREATE INDEX idx_login_attempts_fail_check ON login_attempts (username, success, attempted_at DESC);
```
#### Django Model 定义
```python
class LoginAttempt(models.Model):
"""
登录尝试审计记录。
- 合规保留周期:≥ 90 天
- 注意failure_reason 不得存储密码明文(含错误密码)
"""
FAILURE_REASONS = [
('wrong_password', '用户名或密码错误'),
('wrong_captcha', '行为验证码失败'),
('account_locked', '账号已锁定'),
('account_disabled', '账号已停用'),
('tenant_not_found', '租户不存在'),
]
username = models.CharField(max_length=30)
ip_address = models.GenericIPAddressField()
user_agent = models.TextField(null=True, blank=True)
success = models.BooleanField()
failure_reason = models.CharField(max_length=30, null=True, blank=True, choices=FAILURE_REASONS)
attempted_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'login_attempts'
indexes = [
models.Index(fields=['username']),
models.Index(fields=['ip_address']),
models.Index(fields=['-attempted_at']),
models.Index(fields=['username', 'success', '-attempted_at'],
name='idx_login_attempts_fail_check'),
]
def __str__(self):
return f"{self.username} @ {self.attempted_at} - {'OK' if self.success else self.failure_reason}"
```
---
### 3.3 `password_reset_tokens` — 密码重置令牌表(租户 Schema
**表说明**用于通过邮件找回密码的一次性令牌。单次有效30 分钟过期。
#### 字段定义
| 字段名 | 类型 | 约束 | 默认值 | 说明 |
|--------|------|------|--------|------|
| `id` | `BIGSERIAL` | `PRIMARY KEY` | — | 自增主键 |
| `user_id` | `BIGINT` | `FK → user_accounts.id`, `NOT NULL` | — | 关联账号 |
| `token` | `VARCHAR(86)` | `NOT NULL`, `UNIQUE` | — | `secrets.token_urlsafe(64)` 生成86 字符),全局唯一 |
| `expires_at` | `TIMESTAMPTZ` | `NOT NULL` | — | 过期时间(`created_at + 30 分钟` |
| `is_used` | `BOOLEAN` | `NOT NULL` | `FALSE` | 是否已使用;使用后立即置 True防止重放攻击 |
| `created_at` | `TIMESTAMPTZ` | `NOT NULL` | `NOW()` | 创建时间 |
#### 索引
```sql
CREATE UNIQUE INDEX uq_password_reset_tokens_token ON password_reset_tokens (token);
CREATE INDEX idx_password_reset_tokens_user ON password_reset_tokens (user_id);
CREATE INDEX idx_password_reset_tokens_expiry ON password_reset_tokens (expires_at) WHERE is_used = FALSE;
```
#### Django Model 定义
```python
class PasswordResetToken(models.Model):
"""
密码重置令牌。
安全约束:
- Token 单次有效is_used=True 后立即失效)
- 有效期 30 分钟
- 同一账号 1 小时内最多生成 3 个服务层限频Redis 计数)
"""
user = models.ForeignKey(UserAccount, on_delete=models.CASCADE, related_name='reset_tokens')
token = models.CharField(max_length=86, unique=True) # secrets.token_urlsafe(64)
expires_at = models.DateTimeField()
is_used = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'password_reset_tokens'
indexes = [
models.Index(fields=['user_id']),
]
def is_valid(self) -> bool:
from django.utils import timezone
return not self.is_used and timezone.now() < self.expires_at
```
---
### 3.4 `password_histories` — 历史密码记录表(租户 Schema
**表说明**:保存账号最近 3 次密码哈希,用于防止重复使用历史密码(含初始密码)。
#### 字段定义
| 字段名 | 类型 | 约束 | 默认值 | 说明 |
|--------|------|------|--------|------|
| `id` | `BIGSERIAL` | `PRIMARY KEY` | — | 自增主键 |
| `user_id` | `BIGINT` | `FK → user_accounts.id`, `NOT NULL` | — | 关联账号 |
| `password_hash` | `VARCHAR(128)` | `NOT NULL` | — | PBKDF2+SHA256 哈希值 |
| `created_at` | `TIMESTAMPTZ` | `NOT NULL` | `NOW()` | 记录时间(密码修改时间) |
#### 索引
```sql
CREATE INDEX idx_password_histories_user ON password_histories (user_id, created_at DESC);
```
#### Django Model 定义
```python
class PasswordHistory(models.Model):
"""
历史密码记录,每个账号保留最近 N 条(默认 3 条)。
新密码不得与最近 3 条历史记录相同(含系统初始密码 Fonrey@2025
"""
user = models.ForeignKey(UserAccount, on_delete=models.CASCADE, related_name='password_histories')
password_hash = models.CharField(max_length=128)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'password_histories'
ordering = ['-created_at']
indexes = [
models.Index(fields=['user', '-created_at']),
]
```
---
## 四、Redis 缓存结构(辅助状态,非持久化)
以下 Redis Key 不存入 PostgreSQL属于运行时状态需与数据库状态保持最终一致
| Key 格式 | 类型 | TTL | 说明 |
|----------|------|-----|------|
| `captcha_token:{uuid}` | STRING | 3 分钟 | 滑块验证会话 Token验证通过后生成 `captcha_pass_token` |
| `captcha_pass:{uuid}` | STRING | 3 分钟 | 一次性通过凭证;登录提交时校验后立即删除 |
| `login_fail:{tenant_id}:{username}` | STRING计数 | 30 分钟 | 连续密码错误次数;≥ 5 触发锁定TTL 30 分钟自动清零 |
| `recover_email:{email}` | STRING计数 | 1 小时 | 找回邮件发送次数;上限 3 次/小时 |
| `recover_reset:{account_id}` | STRING计数 | 1 小时 | 同一账号密码重置 Token 生成次数;上限 3 次/小时 |
| `tenant_verify_ip:{ip}` | STRING计数 | 1 分钟 | Tenant 验证接口 IP 限流;≥ 10 次拒绝请求 |
> **一致性说明**:账号锁定状态由 `user_accounts.status` 持久化Redis 仅做计数触发器。当 Redis 数据丢失(如 Redis 重启),应用层通过 `locked_until` 字段恢复锁定状态判断。
---
## 五、账号创建流程与状态机
### 5.1 账号状态机
```
┌─────────────────────────────────────┐
│ 账号生命周期状态机 │
└─────────────────────────────────────┘
[创建账号]
│ is_initial_password=True, status=active
[初始密码态] ─── 使用初始密码登录成功 ───► [强制修改密码页]
│ │ 修改成功
│ ▼
│ [正常使用态]
│ status=active
│ is_initial_password=False
├── 密码错误 ≥ 5 次 ──────────────────► [锁定态]
│ status=locked
│ locked_until = now+30min
│ │
│ ┌───────────────┤
│ │ 30分钟到期 │ 管理员手动解锁
│ ▼ ▼
│ [正常使用态] ◄─── [管理员操作]
└── 员工离职 / 管理员停用 ──► [停用态]
status=disabled
员工复职/管理员恢复
[正常使用态]
```
### 5.2 账号创建触发时机
| 账号类型 | 触发时机 | 创建者 | username 规则 | 初始密码 |
|----------|----------|--------|--------------|---------|
| Tenant Admin | 平台运营在系统后台开通租户时 | 平台运营 | 自定义字母开头6~30 位) | 平台运营自定义 |
| 普通员工 | Tenant Admin 在「新增员工」时系统自动生成 | 系统Tenant Admin 触发) | 固定为员工手机号11 位) | 系统统一初始密码(部署配置) |
---
## 六、关联约束与数据完整性
### 6.1 与 `org.Staff` 的关联规则
```
org_staff (1) ──── (0..1) user_accounts
```
- 普通员工账号:`staff_id` **必须**有值,且在 `org.Staff` 中对应记录的 `status` 为 active
- Tenant Admin`staff_id` **可为空**(平台运营账号可不绑定实名档案)
- 员工离职时(`org.Staff.status``resigned`),触发账号 `status``disabled`(由 `org` App Service 层调用 `accounts` 服务执行,避免循环依赖)
- 账号删除:**不允许物理删除**,仅允许 `status=disabled`,审计记录永久保留
### 6.2 跨 App 依赖方向
```
accounts ──► org (单向依赖accounts.UserAccount.staff_id → org.Staff)
org ──► accounts (反向触发,通过 Service 层调用,不通过 FK 反查)
```
> **设计原则**:避免循环 FK 依赖,跨 App 的状态联动通过 Service 层的显式调用完成,不在 Model 层建立反向 FK。
---
## 七、迁移说明Django Migrations
### 初始迁移顺序
```
0001_initial_user_accounts.py # UserAccount 表(依赖 org.Staff 表已存在)
0002_login_attempts.py # LoginAttempt 表
0003_password_reset_tokens.py # PasswordResetToken 表
0004_password_histories.py # PasswordHistory 表
```
### 注意事项
- `accounts` App 的迁移依赖 `org` App`org.Staff` 表须先创建),需在 `INSTALLED_APPS` 中确保 `org``accounts` 之前
- 所有迁移均在**租户 Schema** 下执行(`django-tenants``migrate_schemas` 命令)
- 不得为 `email` 字段设置 `NOT NULL` 约束(允许为空,是否绑定邮箱属于用户选择)
---
## 八、设计决策说明ADR
| 决策 | 选择 | 理由 |
|------|------|------|
| 主键类型 | `BIGSERIAL` (BigInt) | 登录审计场景下 BigInt 主键更简洁高效;跨环境引用场景少,无需 UUID 的随机性 |
| `phone` 字段拆分为 `phone_enc` + `phone_hash` | 是 | 与 `org.Staff` 保持一致;`phone_enc` 保存原文用于展示,`phone_hash` 用于唯一性校验和快速查询,避免加密字段全表扫描 |
| 不扩展 Django `User` | 使用 `AbstractBaseUser` | 避免 `django.contrib.auth.User` 的全局唯一性限制(多租户下同一 username 在不同租户是允许的) |
| `LoginAttempt` 不设外键到 `UserAccount` | 是(冗余存储 username | 即使账号被删除(停用),审计记录仍需保留;使用 username 字符串字段保证审计完整性 |
| 历史密码单独建表 | `PasswordHistory` 独立表 | 而非在 `UserAccount` 中存 JSON 数组,便于查询和维护,支持未来扩展保留次数 |
| 锁定到期时间持久化 | `locked_until` 字段 | Redis 可能重启丢失数据,持久化 `locked_until` 保证 Redis 故障时锁定状态不丢失 |

View File

@@ -0,0 +1,391 @@
> **For AI assistants**: Read this entire file before writing any code. All decisions here are final. Do not suggest alternatives unless asked.
# Fonrey — 系统配置模块数据模型DATA_MODEL_SETTING
> **定位**:本文件是 `apps/setting/` 模块的数据模型权威来源。
> **版本**v1.0 | **日期**2026-04-27
> **关联 PRD**`PRD/系统配置/系统配置模块PRD.md`
> **关联文档**`DATA_MODEL/ENUMS.md`、`DATA_MODEL/DATA_MODEL.md`
---
## 一、模块定位与架构边界
### 1.1 系统配置模块职责
系统配置模块(`apps/setting/`)负责管理三类性质不同的配置数据:
| 类型 | 说明 | 表 | Schema |
| -------------- | ---------------- | --------------------------------------------- | -------------- |
| **A. 固定系统枚举** | 平台级固定值域,所有租户共享 | `enum_labels` | Publicshared |
| **B. 可配置枚举** | 各租户选项不同,管理员可增删排序 | `lookup_groups` + `lookup_items` | Tenant |
| **C. 行为规则与开关** | 标量配置开关 + 字段必填规则 | `tenant_settings` + `field_requirement_rules` | Tenant |
> **重要区分**
> - 类型 A (`enum_labels`) 已在 `DATA_MODEL/ENUMS.md` 完整定义,**本文件不重复**
> - 类型 B/C 均存于 **租户 Schema**,由租户管理员通过界面维护
> - `apps/setting/` 是 `tenant_apps`**非** `shared_apps`
### 1.2 依赖关系
```
apps/setting/
├── 依赖 → core.cacheRedis统一租户前缀
├── 依赖 → org.Staffcreated_by / updated_by FK
└── 被依赖 ← apps/property读取字段规则、枚举选项
← apps/client读取查重范围、枚举选项
```
---
## 二、可配置枚举表(类型 B
### 2.1 `lookup_groups`(枚举分组)
每个分组代表一类可配置枚举(如「客源来源」「跟进目的」),由研发预置,租户管理员**不可新增或删除分组**,仅可管理分组内的选项。
```sql
-- ============================================================
-- 可配置枚举分组Tenant Schema
-- 研发预置,租户不可修改分组本身
-- ============================================================
CREATE TABLE lookup_groups (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
module VARCHAR(50) NOT NULL, -- 'client' | 'property'
key VARCHAR(100) NOT NULL, -- 'source' | 'follow_purpose'
label_zh VARCHAR(50) NOT NULL, -- 界面显示名称,如「客源来源」
description TEXT, -- 说明文案(前端 tooltip 使用)
sort_order SMALLINT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (module, key)
);
```
**MVP 预置分组(种子数据)**
| module | key | label_zh | description |
|--------|-----|----------|-------------|
| `client` | `source` | 客源来源 | 客源从何处获取,用于来源渠道分析 |
| `client` | `follow_purpose` | 跟进目的 | 客源跟进时选择的目的分类 |
| `property` | `source` | 房源来源 | 房源从何处获取 |
---
### 2.2 `lookup_items`(枚举选项)
```sql
-- ============================================================
-- 可配置枚举选项Tenant Schema
-- 租户管理员可增删排序is_system=True 的预制项不可物理删除
-- ============================================================
CREATE TABLE lookup_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
group_id UUID NOT NULL REFERENCES lookup_groups(id) ON DELETE CASCADE,
value VARCHAR(100) NOT NULL, -- 存储值,英文 snake_case如 'door_to_door'
label_zh VARCHAR(50) NOT NULL, -- 显示文本(如「上门」)
is_system BOOLEAN NOT NULL DEFAULT FALSE, -- True=系统预制,不可删除,仅可停用
is_active BOOLEAN NOT NULL DEFAULT TRUE,
sort_order SMALLINT NOT NULL DEFAULT 0,
created_by UUID REFERENCES staff(id) ON DELETE SET NULL, -- 系统预制时为 NULL
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (group_id, value)
);
CREATE INDEX idx_lookup_items_group_active
ON lookup_items(group_id, is_active, sort_order);
```
**关键约束**
- `is_system = TRUE` 的记录不允许物理删除Service 层强制拦截)
- `is_active = FALSE` 后:前端录入下拉不展示;历史已选该值的记录保留原值,展示时追加「(已停用)」后缀
- `value` 一旦写入不允许修改(历史数据依赖);如需改名,停用旧项、新增新项
---
### 2.3 MVP 预置种子数据(`is_system = TRUE`
以下选项在租户初始化时自动写入:
#### 客源来源(`client.source`
| value | label_zh | sort_order |
|-------|----------|------------|
| `store_reception` | 门店接待 | 1 |
| `old_client_referral` | 老客户转介绍 | 2 |
| `stationed_dispatch` | 驻守派单 | 3 |
| `walk_in` | 上门 | 4 |
| `online_58` | 网络-58同城 | 5 |
| `online_anjuke` | 网络-安居客 | 6 |
| `wechat` | 微信 | 7 |
| `friend_referral` | 朋友介绍 | 8 |
#### 跟进目的(`client.follow_purpose`
| value | label_zh | sort_order |
|-------|----------|------------|
| `callback` | 回拨 | 1 |
| `push_property` | 推房 | 2 |
| `showing` | 带看 | 3 |
| `maintain` | 维护 | 4 |
| `other` | 其他 | 5 |
#### 房源来源(`property.source`
| value | label_zh | sort_order |
|-------|----------|------------|
| `proactive_development` | 主动开发 | 1 |
| `owner_walk_in` | 业主上门 | 2 |
| `old_client_referral` | 老客户转介绍 | 3 |
| `online_inquiry` | 网络来电 | 4 |
---
## 三、行为规则与开关(类型 C
### 3.1 `tenant_settings`(标量配置键值表)
存储开关bool、阈值int、单选枚举string等标量类型配置项。
```sql
-- ============================================================
-- 租户标量配置键值对Tenant Schema
-- ============================================================
CREATE TABLE tenant_settings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
category VARCHAR(50) NOT NULL, -- 配置分类:'client' | 'property' | 'showroom'
key VARCHAR(100) NOT NULL, -- 配置 key如 'duplicate_check_scope'
value JSONB NOT NULL, -- 存储任意类型bool/int/str如 {"v": "self"}
value_type VARCHAR(20) NOT NULL -- 'bool' | 'int' | 'string' | 'enum'(用于前端渲染控件)
CHECK (value_type IN ('bool', 'int', 'string', 'enum')),
updated_by UUID REFERENCES staff(id) ON DELETE SET NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (category, key)
);
CREATE INDEX idx_tenant_settings_category ON tenant_settings(category);
```
**存储格式约定**
- `bool``{"v": true}``{"v": false}`
- `int``{"v": 30}`
- `string``{"v": "some_value"}`
- `enum``{"v": "self", "choices": ["self", "dept", "company"]}``choices` 由代码硬编码,不存 DB
**MVP 阶段预置 key**
| category | key | value_type | 默认值 | 说明 |
|----------|-----|-----------|--------|------|
| `client` | `duplicate_check_scope` | `enum` | `{"v": "self"}` | 新增私客查重范围:`self`(本人)/ `dept`(本部门)/ `company`(全公司) |
> 未来 P1 阶段可按需追加 key无需修改表结构。
---
### 3.2 `field_requirement_rules`(字段必填规则表)
按「模块 × 房源用途 × 交易状态 × 字段」四元组确定一条规则,控制录入界面的字段显示状态。
```sql
-- ============================================================
-- 字段必填/隐藏规则Tenant Schema
-- MVP 仅支持 module='property'
-- ============================================================
CREATE TABLE field_requirement_rules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
module VARCHAR(20) NOT NULL, -- 'property' | 'client'MVP 只用 'property'
entity_type VARCHAR(50) NOT NULL, -- 与 property.property_type CHECK 约束值对齐
-- 'residential'|'villa'|'commercial_residential'|'shop'|'office'|'other'
trade_status VARCHAR(20) NOT NULL, -- 交易大类:'sale'|'rent'|'sale_rent'|'*'(全部)
CHECK (trade_status IN ('sale', 'rent', 'sale_rent', '*')),
field_key VARCHAR(50) NOT NULL, -- 字段 key如 'orientation'|'decoration'|'floor'
requirement VARCHAR(10) NOT NULL -- 规则值
CHECK (requirement IN ('required', 'optional', 'hidden')),
updated_by UUID REFERENCES staff(id) ON DELETE SET NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (module, entity_type, trade_status, field_key)
);
CREATE INDEX idx_field_req_lookup
ON field_requirement_rules(module, entity_type, trade_status);
```
**与 `property.property_type` 对齐说明**
`entity_type` 的值域与 `property.property_type` 的 CHECK 约束完全一致:
| entity_type | property_type label_zh |
|-------------|----------------------|
| `residential` | 住宅 |
| `villa` | 别墅 |
| `commercial_residential` | 商住 |
| `shop` | 商铺 |
| `office` | 写字楼 |
| `other` | 其他 |
**`trade_status``property.status` 的映射关系**
| trade_status | 对应 property.status 值 |
|--------------|------------------------|
| `sale` | `for_sale` |
| `rent` | `for_rent` |
| `sale_rent` | `for_sale_rent` |
| `*` | 所有状态通用规则fallback |
> **重要**`trade_status` 是录入场景的交易意图分类,不是 `property.status` 的完整枚举。规则匹配逻辑:先查精确匹配(`entity_type + trade_status`),不存在则查 `*` 通配规则。
**MVP 初始规则(研发预置,管理员可覆盖)**
| module | entity_type | trade_status | field_key | requirement |
|--------|-------------|--------------|-----------|-------------|
| `property` | `residential` | `sale` | `orientation` | `optional` |
| `property` | `residential` | `sale` | `decoration` | `optional` |
| `property` | `residential` | `sale` | `floor` | `optional` |
| `property` | `residential` | `sale` | `building_area` | `required` |
| `property` | `residential` | `sale` | `inner_area` | `optional` |
| `property` | `residential` | `sale` | `room_layout` | `required` |
| `property` | `residential` | `rent` | `decoration` | `optional` |
| `property` | `residential` | `rent` | `floor` | `optional` |
| `property` | `residential` | `rent` | `building_area` | `required` |
| `property` | `residential` | `rent` | `room_layout` | `required` |
**MVP 可配置字段范围(对应 PRD AC-4**
| field_key | 说明 | 字段类型 |
|-----------|------|---------|
| `orientation` | 朝向(`property.orientation` 枚举) | 枚举 |
| `decoration` | 装修情况(`property.decoration` 枚举) | 枚举 |
| `floor` | 所在楼层/总楼层 | 数值 |
| `building_area` | 建筑面积(㎡) | 数值 |
| `inner_area` | 套内面积(㎡) | 数值 |
| `room_layout` | 房型(室/厅/卫) | 数值组 |
| `ownership_years` | 产权年限(年) | 数值 |
| `parking_count` | 车位数 | 数值 |
---
## 四、服务层设计
所有业务模块**禁止直接查询配置表**,必须通过统一服务层读取:
```python
# apps/setting/services/tenant_settings_service.py
class TenantSettingsService:
"""
系统配置统一读取服务。
所有配置均经 Redis 缓存TTL 5min写入时主动 invalidate。
Redis Key 规范:{tenant_schema}:setting:{type}:{key}
"""
def get(self, category: str, key: str, default=None):
"""
读取标量配置tenant_settings 表)
Cache Key: {tenant_schema}:setting:kv:{category}.{key}
返回 JSONB value 字段中 'v' 的值(已解包)
"""
def set(self, category: str, key: str, value, updated_by_id) -> None:
"""
写入标量配置 + 主动 invalidate 缓存
"""
def get_lookup_items(self, module: str, key: str) -> list[dict]:
"""
获取可配置枚举选项lookup_items 表)
仅返回 is_active=True 的项,按 sort_order 排序
Cache Key: {tenant_schema}:setting:lookup:{module}.{key}
返回格式:[{"value": "walk_in", "label_zh": "上门", "is_system": True}, ...]
"""
def get_field_requirements(
self, module: str, entity_type: str, trade_status: str
) -> dict[str, str]:
"""
获取字段必填规则
匹配顺序:精确匹配(entity_type + trade_status) > 通配规则(trade_status='*')
Cache Key: {tenant_schema}:setting:field_req:{module}.{entity_type}.{trade_status}
返回格式:{"orientation": "optional", "decoration": "required", ...}
"""
```
---
## 五、Redis 缓存键规范
| 用途 | Cache Key | TTL | 失效触发 |
|------|-----------|-----|---------|
| 标量配置 | `{schema}:setting:kv:{category}.{key}` | 300s | `TenantSettingsService.set()` |
| 可配置枚举 | `{schema}:setting:lookup:{module}.{key}` | 300s | 管理员保存 lookup_items |
| 字段规则 | `{schema}:setting:field_req:{module}.{entity_type}.{trade_status}` | 300s | 管理员保存 field_requirement_rules |
| 客源规则(整体) | `{schema}:setting:client_rules` | 300s | 任意客源规则变更 |
> TTL 300s5 分钟)对应 PRD 成功指标「配置变更生效时延 ≤ 5 分钟」。
---
## 六、目录结构
```
apps/setting/
├── models/
│ ├── lookup.py # LookupGroup, LookupItem
│ └── setting.py # TenantSetting, FieldRequirementRule
├── services/
│ └── tenant_settings_service.py # 统一配置读取服务(禁止直接查表)
├── views/
│ ├── lookup_views.py # 参数配置页面US-SETTING-001-A
│ ├── field_rule_views.py # 房源字段规则US-SETTING-001-B
│ └── client_rule_views.py # 客源规则US-SETTING-001-C
├── templates/setting/
├── fixtures/
│ ├── lookup_groups.json # 分组种子数据3 组)
│ ├── lookup_items.json # 选项种子数据is_system=True
│ ├── tenant_settings.json # 默认配置种子数据
│ └── field_requirement_rules.json # 默认字段规则
├── migrations/
│ ├── 0001_lookup_groups.py
│ ├── 0002_lookup_items.py
│ ├── 0003_tenant_settings.py
│ └── 0004_field_requirement_rules.py
└── urls.py
```
---
## 七、迁移执行顺序
```
0001_lookup_groups # 先建分组表(无外键依赖)
0002_lookup_items # 再建选项表(依赖 lookup_groups + staff
0003_tenant_settings # 独立,无外键依赖
0004_field_requirement_rules # 独立,仅依赖 staff
```
迁移执行后,通过 `call_command('loaddata', 'lookup_groups')` 等方式加载 fixtures 种子数据。
---
## 八、关键约束与禁止项
| 约束 | 规则 |
|------|------|
| 不可删除系统预制项 | `lookup_items.is_system = True` 的记录Service 层硬拦截物理删除请求 |
| 不可修改已有 value | `lookup_items.value` 写入后只读;修改请停用旧项 + 新增新项 |
| 不可直接查询配置表 | 业务模块property/client**必须**通过 `TenantSettingsService` 读取,禁止 ORM 直查 |
| entity_type 值域 | 必须与 `property.property_type` CHECK 约束完全一致(见第三章) |
| Redis Key 前缀 | 必须携带租户 schema 前缀,格式:`{tenant_schema}:setting:{type}:{key}` |
---
## 九、设计决策ADR
| 决策 | 选择 | 理由 |
|------|------|------|
| 枚举两级架构 | `enum_labels`Public/固定)+ `lookup_items`Tenant/可配置)分离 | 保障系统一致性的同时给租户灵活度 |
| `lookup_groups` 由研发预置 | 是 | 防止租户随意创建不规范分组,控制可配置范围边界 |
| `tenant_settings` 使用 JSONB | 是 | 支持 bool/int/string 等多类型,无需为每类型单独建列 |
| `field_requirement_rules` 不新增字段 | 是 | 规则层只控制「必填/选填/隐藏」,字段存在性由 DATA_MODEL_PROPERTY 决定 |
| 服务层统一读取 | `TenantSettingsService` | 统一缓存管理,业务层与配置存储解耦 |
| trade_status 不复用 property.status | 独立 `sale/rent/sale_rent/*` | 录入场景的交易意图与房源全生命周期状态不同,避免耦合 |

View File

@@ -0,0 +1,762 @@
> **For AI assistants**: Read this entire file before writing any code. All decisions here are final. Do not suggest alternatives unless asked.
# Fonrey — 统一枚举字典ENUMS
> **定位**:本文件是 Fonrey 全局枚举标准Public + Tenant的统一实现基线。
> **版本**v2.1
> **日期**2026-04-27
> **适用范围**`DATA_MODEL_PUBLIC.md`、`DATA_MODEL_LOGIN.md`、`DATA_MODEL_ORG.md`、`DATA_MODEL_COMPLEX.md`、`DATA_MODEL_PROPERTY.md`、`DATA_MODEL_CLIENT.md`、`DATA_MODEL_PERMISSION.md`、`DATA_MODEL_SETTING.md`
---
## 一、枚举分层标准(必须遵守)
Fonrey 采用两层枚举体系:
1. **固定枚举Fixed Enum**
- 值域固定,受 `CHECK` 或强业务约束保护
- 作为系统契约,不能随意改值
- 可落地到 `public.enum_labels`(用于统一标签)
2. **可配置枚举Configurable Enum**
- 值域由租户自行维护
- 存储在 Tenant Schema`lookup_groups` + `lookup_items`
- **禁止**对业务表字段加固定 `CHECK IN (...)`
---
## 二、全局固定枚举Public / 平台级)
### 2.1 tenant 生命周期
**domain**: `public.tenant.plan`
- `basic`:基础版
- `professional`:专业版
- `enterprise`:企业版
**domain**: `public.tenant.status`
- `creating`:创建中
- `active`:正常
- `suspended`:已挂起
- `pending_delete`:待删除
- `deleted`:已删除
- `failed`:创建/初始化失败
**domain**: `public.tenant.suspended_reason`
- `overdue`:欠费
- `violation`:违规
- `requested`:客户申请
- `other`:其他
### 2.2 平台管理员
**domain**: `public.platform_admin.role`
- `super_admin`:超级管理员
- `ops_operator`:运营管理员
- `read_only_auditor`:只读审计员
### 2.3 平台审计与备份导出
**domain**: `public.platform_audit.result`
- `SUCCESS`:成功
- `FAILED`:失败
**domain**: `public.backup_schedule.frequency`
- `hourly`:每小时
- `daily`:每日
- `weekly`:每周
**domain**: `public.backup_schedule.storage_target`
- `local`:本地存储
- `s3`Amazon S3
- `r2`Cloudflare R2
- `gcs`Google Cloud Storage
**domain**: `public.backup_record.trigger_type`
- `auto`:自动触发
- `manual`:手动触发
- `pre_upgrade`:升级前触发
- `pre_restore`:恢复前触发
**domain**: `public.backup_record.status`
- `pending`:待执行
- `in_progress`:执行中
- `success`:成功
- `failed`:失败
**domain**: `public.export_task.format`
- `csv`CSV
- `json`JSON
- `sql_dump`SQL 导出
**domain**: `public.export_task.status`
- `pending`:待执行
- `in_progress`:执行中
- `done`:已完成
- `failed`:失败
### 2.4 升级与发布Public
**domain**: `public.upgrade_event.event_type`
- `upgrade`:升级
- `rollback`:回滚
**domain**: `public.upgrade_event.upgrade_type`
- `A_app`A类应用升级
- `B_schema`B类数据库结构升级
- `C_feature`C类功能开关升级
**domain**: `public.upgrade_event.strategy`
- `full`:全量发布
- `canary`:灰度发布
**domain**: `public.upgrade_event.status`
- `draft`:草稿
- `pre_check`:预检查
- `pre_backup`:预备份
- `batch_running`:批次执行中
- `batch_done`:批次完成
- `halted`:已暂停
- `succeeded`:已成功
- `failed`:失败
- `rollback_running`:回滚中
- `rolled_back`:已回滚
**domain**: `public.upgrade_event.failure_policy`
- `halt_batch`:失败即停止批次
- `continue`:失败继续
**domain**: `public.client_release.platform`
- `win32`Windows 客户端
**domain**: `public.client_release.arch`
- `x64`x64 架构
- `arm64`ARM64 架构
**domain**: `public.client_release.release_type`
- `normal`:普通更新
- `force`:强制更新
**domain**: `public.client_release.status`
- `draft`:草稿
- `published`:已发布
- `archived`:已归档
---
## 三、Tenant 固定枚举(模块级,值域统一)
> 说明:以下字段在 Tenant Schema 中存储,但值域为系统统一标准,属于“全局实现标准”。
## 3.1 登录认证account/login
**domain**: `login.user_account.status`
- `active`:启用
- `disabled`:停用
- `locked`:锁定
**domain**: `login.login_attempt.failure_reason`
- `wrong_password`:用户名或密码错误
- `wrong_captcha`:验证码错误
- `account_locked`:账号锁定
- `account_disabled`:账号停用
- `tenant_not_found`:租户不存在
---
## 3.2 组织人事org
**domain**: `org.org_unit.type`
- `company`:公司
- `division`:事业部
- `region`:大区
- `area`:区域
- `district`:片区
- `store`:门店
- `group`:店组
- `functional`:职能部门
**domain**: `org.org_unit.attribute`
- `direct`:直营
- `franchise`:加盟
**domain**: `org.staff.role`
- `agent`:经纪人
- `store_manager`:店长
- `area_manager`:区域经理
- `admin`:系统管理员
- `operator`:运营/行政
- `system`:系统账号
**domain**: `org.staff.status`
- `active`:在职
- `probation`:试用
- `resigned`:离职
- `frozen`:冻结
**domain**: `org.staff_personal_info.gender`
- `male`:男
- `female`:女
- `unknown`:未知
**domain**: `org.staff_personal_info.id_type`
- `id_card`:身份证
- `passport`:护照
- `other`:其他
**domain**: `org.staff_transfer.transfer_type`
- `onboard`:入职
- `transfer`:调动
- `resign`:离职
- `rejoin`:复职
- `supervisor_change`:上级变更
- `role_change`:角色变更
- `freeze`:冻结账号
- `unfreeze`:恢复账号
**domain**: `org.staff_account.platform`
- `fonrey`:房睿主账号
- `58anjuke`58安居客
- `cnreic`:中国网络经纪人
- `wechat_mp`:微信公众号
---
## 3.3 权限系统permission
**domain**: `permission.module`
- `home`:首页
- `property`:房源
- `new_house`:新房
- `client`:客源
- `transaction`:交易
- `data`:数据
- `marketing`:营销
- `hr`人事OA
- `contract`:合同
- `trinet`:三网
- `system`:系统
- `mobile`:移动端
- `smart_store`:智能门店
- `recharge`:在线充值
**domain**: `permission.value_type`
- `BOOLEAN`:开关型
- `SCOPE`:范围型
- `INTEGER`:数值型
**domain**: `permission.role_category`
- `agent`:置业顾问
- `store_manager`:店管
- `director`:总经
- `operator`:运营/行政
- `custom`:自定义
**domain**: `permission.scope_level`
- `none`:无
- `self`:本人
- `group`:本组
- `store`:本门店
- `area`:本区域
- `region`:本大区
- `company`:全公司
**domain**: `permission.override_mode`
- `REPLACE`:覆盖
- `RESTRICT`:限制
- `GRANT`:授予
**domain**: `permission.data_scope_type`
- `self`:本人
- `group`:本组
- `store`:本门店
- `area`:本区域
- `region`:本大区
- `company`:全公司
- `custom_unit`:自定义组织单元
**domain**: `permission.change_log.target_type`
- `role`:角色
- `role_permission`:角色权限
- `staff_role`:员工角色
- `staff_override`:员工权限覆盖
- `staff_scope`:员工数据范围
**domain**: `permission.change_log.action`
- `create`:创建
- `update`:更新
- `delete`:删除
- `assign`:分配
- `revoke`:撤销
---
## 3.4 楼盘区域complex
**domain**: `complex.school.type`
- `primary`:小学
- `middle`:初中
- `high`:高中
- `k9`:九年一贯制
- `k12`:十二年一贯制
**domain**: `complex.school.nature`
- `public`:公立
- `private`:私立
- `international`:国际
**domain**: `complex.school.level`
- `normal`:普通
- `key`:重点
- `top`:名校
**domain**: `complex.building_type`
- `slab`:板楼
- `tower`:塔楼
- `slab_tower`:板塔结合
**domain**: `complex.water_type`
- `civil`:民水
- `commercial`:商水
**domain**: `complex.electricity_type`
- `civil`:民电
- `commercial`:商电
**domain**: `complex.school_zone_type`
- `guaranteed`:对口
- `reference`:参考
- `lottery`:摇号
**domain**: `complex.photo.category`
- `complex`:楼盘图
- `layout`:户型图
- `vr`VR图
- `other`:其他
---
## 3.5 房源property
**domain**: `property.property_type`
- `residential`:住宅
- `villa`:别墅
- `commercial_residential`:商住
- `shop`:商铺
- `office`:写字楼
- `other`:其他
**domain**: `property.status`
- `for_sale`:出售
- `for_rent`:出租
- `for_sale_rent`:租售
- `suspended`:暂缓
- `sold_elsewhere`:他售
- `rented_elsewhere`:他租
- `sold`:成交
- `unlisted`:未挂牌
**domain**: `property.attribute`
- `public`:公盘
- `private`:私盘
- `special`:特盘
- `sealed`:封盘
**domain**: `property.orientation`
- `east`:东
- `south`:南
- `west`:西
- `north`:北
- `southeast`:东南
- `northeast`:东北
- `east_west`:东西
- `south_north`:南北
- `northwest`:西北
- `southwest`:西南
**domain**: `property.decoration`
- `rough`:毛坯
- `plain`:清水
- `simple`:简装
- `medium`:中装
- `fine`:精装
- `luxury`:豪装
**domain**: `property.house_status`
- `owner_occupied`:业主自住
- `vacant`:空置
- `tenant_occupied`:租客在住
- `unknown`:未知
**domain**: `property.viewing_time`
- `anytime`:随时看房
- `by_appointment`:预约看房
- `inconvenient`:不便看房
**domain**: `property.grade`
- `A_urgent`A急迫
- `A`A
- `B`B较强
- `C`C一般
- `D`D较弱
**domain**: `property.contact.gender`
- `male`:先生
- `female`:女士
**domain**: `property.contact.identity`
- `owner`:业主
- `contact`:联系人
- `subletter`:转租人
- `tenant`:租客
- `agent`:代理人
- `corporate`:企业法人
**domain**: `property.listing_history.listing_type`
- `for_sale`:出售挂牌
- `for_rent`:出租挂牌
**domain**: `property.listing_history.status`
- `active`:生效中
- `ended`:已结束
**domain**: `property.follow_log.log_type`
- `written`:手写跟进
- `modified`:修改跟进
- `sensitive_op`:敏感操作
- `sensitive_view`:敏感查看
- `other`:其他
- `system`:系统
**domain**: `property.follow_log.ai_tag`
- `ai_for_sale`AI判断可售
- `ai_not_for_sale`AI判断不可售
**domain**: `property.follow_attachment.file_type`
- `bmp`BMP
- `jpg`JPG
- `png`PNG
- `svg`SVG
- `gif`GIF
**domain**: `property.key.key_type`
- `mechanical`:机械钥匙
- `password`:密码钥匙
**domain**: `property.commission.owner_type`
- `owner`:产权人本人
- `authorized_third`:授权第三方
**domain**: `property.commission.status`
- `active`:有效
- `expired`:过期
- `cancelled`:取消
**domain**: `property.commission_attachment.category`
- `id_card`:身份证件
- `property_cert`:产权证明
- `commission_letter`:委托书
- `other`:其他
**domain**: `property.field_survey.status`
- `draft`:草稿
- `submitted`:已提交
**domain**: `property.survey_photo.category`
- `layout`:户型图
- `living_room`:客厅
- `dining_room`:餐厅
- `bedroom`:卧室
- `bathroom`:卫生间
- `kitchen`:厨房
- `entrance`:入户
- `balcony`:阳台
- `study`:书房
- `indoor_other`:室内其他
- `outdoor`:室外
**domain**: `property.photo.category`
- `cover`:封面
- `entrance`:入户
- `living_room`:客厅
- `dining_room`:餐厅
- `bedroom`:卧室
- `bathroom`:卫生间
- `kitchen`:厨房
- `balcony`:阳台
- `study`:书房
- `indoor_other`:室内其他
- `outdoor`:室外
- `panorama`:全景
**domain**: `property.attachment.category`
- `id_card`:身份证件
- `property_cert`:产权证明
- `commission_letter`:委托书
- `other`:其他
**domain**: `property.number_holder_approval.status`
- `pending`:待审批
- `approved`:已通过
- `rejected`:已驳回
---
## 3.6 客源client
**domain**: `client.client_type`
- `private`:私客
- `public`:公客
- `transacted`:成交客
**domain**: `client.status`
- `buying`:求购
- `renting`:求租
- `buy_or_rent`:租购
- `suspended`:暂缓
- `bought`:已购
- `rented_done`:已租
- `public`:公客
- `invalid`:无效
**domain**: `client.grade`
- `A`A急迫
- `B`B较强
- `C`C一般
- `D`D较弱
- `E`E暂不关注
**domain**: `client.property_usage`
- `residential`:住宅
- `villa`:别墅
- `commercial_residential`:商住
- `shop`:商铺
- `office`:写字楼
- `other`:其他
**domain**: `client.buying_purpose`
- `rigid`:刚需
- `investment`:投资
- `school_district`:学区
- `upgrade`:改善
- `commercial`:商用
- `other`:其他
**domain**: `client.payment_method`
- `full`:全额
- `mortgage`:商业贷款
- `mortgage_fund`:商贷+公积金
- `fund`:公积金
**domain**: `client.properties_owned`
- `none`:无
- `local_none`:本地无/外地有
- `local_has`:本地有
**domain**: `client.id_type`
- `id_card`:身份证
- `passport`:护照
- `hk_macao`:港澳通行证
- `other`:其他
**domain**: `client.transfer_to_public_type`
- `manual`:手动转公
- `auto`:自动转公
- `marketing_jump`:营销客跳公
- `resource_public`:资料客素公
**domain**: `client.invalid_reason`
- `invalid_phone`:号码无效
- `peer_agent`:同行
- `ad`:广告推销
- `no_intent`:无意向
- `other`:其他
**domain**: `client.transacted_type`
- `bought`:我购
- `rented`:我租
**domain**: `client.transacted_property_type`
- `second_hand`:二手
- `new_house`:新房
**domain**: `client.activity_level`
- `new_matched`:新配对
- `active_7d`7日活跃
- `active_30d`30日活跃
- `active_90d`90日活跃
- `expiring`:即将过期
- `frozen`:暂缓中
- `invalid`:无效
**domain**: `client.contact.gender`
- `male`:先生
- `female`:女士
**domain**: `client.requirement_type`
- `second_hand`:二手
- `new_house`:新房
- `rental`:租房
**domain**: `client.floor_preference`
- `no_first`:不要一楼
- `low`:低楼层
- `mid`:中楼层
- `high`:高楼层
- `no_top`:不要顶楼
**domain**: `client.orientation`
- `east`:东
- `south`:南
- `west`:西
- `north`:北
**domain**: `client.decoration`
- `rough`:毛坯
- `plain`:清水
- `simple`:简装
- `medium`:中装
- `fine`:精装
- `luxury`:豪装
**domain**: `client.building_age_range`
- `within_5y`5年内
- `5_10y`5-10年
- `10_15y`10-15年
- `15_20y`15-20年
- `over_20y`20年以上
**domain**: `client.follow_log.log_type`
- `written`:写入跟进
- `modified`:修改跟进
- `sensitive_view`:敏感查看
- `other`:其他
- `system`:系统
**domain**: `client.viewing.viewing_type`
- `appointment`:预约
- `viewing`:带看
- `revisit`:复看
- `empty`:空看
**domain**: `client.viewing.client_intent`
- `interested`:感兴趣
- `not_interested`:不感兴趣
- `negotiating`:谈判中
- `cancelled`:取消
**domain**: `client.property_match.match_source`
- `recorded`:录客配房
- `system`:系统配房
**domain**: `client.property_match.match_group`
- `quality_layout`:优质户型
- `price_reduced`:降价
- `hot`:热门
- `newly_listed`:新上
**domain**: `client.property_match.status`
- `suggested`:待推送
- `shared`:已分享
- `rejected`:已反馈不合适
- `viewed`:客户已查看
**domain**: `client.status_log.change_type`
- `status_change`:改状态
- `grade_change`:改等级
- `to_public`:转公客
- `to_transacted`:转成交
- `to_invalid`:转无效
- `owner_change`:改归属人
- `source_change`:改来源
- `merge`:合并客源
---
## 3.7 系统配置setting
**domain**: `setting.tenant_setting.value_type`
- `bool`:布尔
- `int`:整数
- `string`:字符串
- `enum`:枚举
**domain**: `setting.field_rule.module`
- `property`:房源
- `client`:客源(预留)
**domain**: `setting.field_rule.entity_type`(与 `property.property_type` 对齐)
- `residential`:住宅
- `villa`:别墅
- `commercial_residential`:商住
- `shop`:商铺
- `office`:写字楼
- `other`:其他
**domain**: `setting.field_rule.trade_status`
- `sale`:出售
- `rent`:出租
- `sale_rent`:租售
- `*`:全部
**domain**: `setting.field_rule.requirement`
- `required`:必填
- `optional`:选填
- `hidden`:隐藏
---
## 四、Tenant 可配置枚举字段清单lookup_items 权威)
> 以下字段值域由 `lookup_items` 维护,属于租户级配置,不在业务表中写死 `CHECK`。
| domain统一命名 | 对应字段 | 当前状态 | 说明 |
|---|---|---|---|
| `client.source` | `clients.source` | ✅ 已落地 | 客源来源 |
| `client.follow_purpose` | `client_follow_logs.purpose` | ✅ 已落地 | 客源跟进目的 |
| `property.source` | `properties.source` | ✅ 已落地 | 房源来源 |
| `property.follow_purpose` | `follow_logs.purpose` | 🔄 建议统一 | 房源跟进目的(建议与 `client.follow_purpose` 共享或独立分组) |
| `property.commission_type` | `commissions.commission_type` | 🔄 待入组 | 委托类型(独家/非独家等) |
| `client.match_feedback` | `client_property_matches.feedback` | 🔄 待入组 | 配房反馈原因 |
| `org.reward_punish_category` | `staff_reward_punish.category` | 🔄 待入组 | 人事奖惩类别 |
### 4.1 lookup_groups 规范Tenant Schema
- `module`: 业务模块标识(如 `client` / `property` / `org`
- `key`: 领域键(如 `source` / `follow_purpose`
- 同一组内 `value` 不可重复(`UNIQUE(group_id, value)`
- `is_system = TRUE` 的项禁止物理删除(仅可停用)
---
## 五、统一实现约束
1. **固定枚举值不可改名**:只能新增或停用,禁止修改既有 value。
2. **中文展示从字典取值**:前端/UI 不得硬编码中文。
3. **可配置枚举不得加固定 CHECK**:防止租户自定义被数据库约束阻断。
4. **跨模块同名枚举必须复用语义**:如 `status` 在不同领域必须使用 domain 区分,不允许混用。
5. **缓存规范**
- 固定枚举:`public:enum_labels:{domain}`(建议 TTL 24h
- 可配置枚举:`{schema}:setting:lookup:{module}.{key}`TTL 300s
---
## 六、变更流程(必须同步)
新增或修改任一枚举时,必须同时更新:
1. 本文档 `ENUMS.md`
2. 对应 `DATA_MODEL_*.md` 字段定义CHECK / 注释 / 业务规则)
3. `enum_labels` 种子数据(若为固定枚举)或 `lookup_groups/items` fixture若为可配置枚举
4. 服务层缓存失效逻辑Redis key
---
## 七、与 ADR 的一致性说明
本文件已对齐以下冻结决策:
- 固定枚举与可配置枚举双轨并存
- 状态机和值域以文档为权威来源
- Tenant 级可配置枚举统一由 `setting` 模块托管
- Agent 编码前先读枚举标准,禁止各模块自行定义“影子枚举”

View File

@@ -1,280 +1,280 @@
# Fonrey 房睿 — MVP 范围书
**Status**: Draft
**Author**: Product Team
**Last Updated**: 2026-04-24
**Version**: 1.0
> **For AI assistants**: 本文件定义 Phase 1MVP的边界。在任何功能实现前先对照本文确认是否在范围内。范围外的功能禁止在 MVP 阶段实现。
---
## 1. 产品背景与目标
**Fonrey房睿** 是一套面向中小型房产经纪公司的 B2B SaaS 管理平台,解决以下核心痛点:
- 房源/客源信息散乱,全靠人工记录
- 跟进记录缺失,数据流失严重
- 重复录入浪费大量经纪人时间
- 无法支撑 89,000+ 数据量级下的高效房客匹配
**MVP 目标**:在一家种子客户(单租户)环境下,完整跑通"录入房源 → 录入客源 → 匹配带看 → 成交"的核心业务链路。
---
## 2. MVP 核心功能清单Phase 1 必须实现)
### 2.1 优先级定义
| 优先级 | 含义 |
|--------|------|
| **P0** | MVP 上线前必须完成,阻断核心业务链路 |
| **P1** | MVP 上线后第一个迭代周期内完成 |
| **P2** | 已规划,列入路线图但不阻断上线 |
---
### 2.2 模块优先级矩阵
#### 🏠 房源管理
| 功能 | 优先级 | 说明 |
|------|--------|------|
| 录入住宅(二手出售/出租) | **P0** | 核心业务入口 |
| 房源列表(二手&租赁) | **P0** | 含筛选、排序、分页 |
| 房源详情页 | **P0** | 含基本信息、产证、交易信息展示 |
| 跟进记录(全部/写入/修改/其他) | **P0** | 含钥匙、委托、实勘 |
| 图片管理(相册上传/分类/排序) | **P0** | 核心房源内容 |
| 业主联系人管理 | **P0** | 含新增/编辑/查看同业主房源 |
| 价格调整(调价/调价记录) | **P0** | 核心运营操作 |
| 房源状态变更(在售/暂缓/成交/下架) | **P0** | 状态机核心 |
| 房源维护完成度(诊断面板) | **P1** | 提升数据质量 |
| 敏感信息跟进(查看权限控制) | **P1** | 需配合权限模块 |
| 附件管理 | **P1** | 非阻断性 |
| 市场报盘 | **P1** | 运营辅助功能 |
| 价格解读 | **P1** | 分析辅助 |
| 录入别墅/商铺/商住/写字楼/其他 | **P2** | 住宅优先,商业类低频 |
| 全部商铺列表 / 全部写字楼列表 | **P2** | 配合 P2 录入功能 |
| 房源广场 | **P2** | 跨租户/公共池功能 |
#### 🏙️ 楼盘管理
| 功能 | 优先级 | 说明 |
|------|--------|------|
| 楼盘列表 + 楼盘详情(楼盘信息/楼栋/结构) | **P0** | 房源数据底座,必须先行 |
| 区域管理(城区/商圈) | **P0** | 房源关联必须 |
| 楼盘照片管理 | **P1** | 数据完善 |
| 楼盘价格走势 | **P1** | 分析辅助 |
| 周边配套(学校管理) | **P1** | 补充信息 |
| 应用数据标准 | **P2** | 明确不做 |
#### 👥 客源管理
| 功能 | 优先级 | 说明 |
|------|--------|------|
| 录入私客(求购/求租) | **P0** | 核心业务 |
| 私客列表(全部/求购/求租) | **P0** | 含筛选、排序 |
| 私客详情(基本信息/需求信息) | **P0** | |
| 跟进记录(全部/写入/修改/其他) | **P0** | |
| 带看管理(预约带看/新增带看) | **P0** | 房客匹配核心 |
| 联系人管理 | **P0** | |
| 客源状态变更(改等级/改状态) | **P0** | |
| 转公客 / 转成交 / 转无效 | **P0** | 生命周期核心 |
| 二手配房(智能匹配) | **P1** | 核心价值,但可后续迭代 |
| 客源解读 | **P1** | AI 辅助分析 |
| 客源信息概览 | **P1** | 汇总视图 |
| 客源收藏夹 | **P1** | 辅助功能 |
| 公客管理 | **P2** | 私客优先 |
| 成交客管理 | **P2** | |
| 暂缓私客 | **P2** | |
#### 🏢 组织人事
| 功能 | 优先级 | 说明 |
|------|--------|------|
| 公司组织结构(部门/门店树) | **P0** | 权限系统基础 |
| 员工列表/员工详情 | **P0** | |
| 员工入职/账号创建 | **P0** | |
| 员工离职 / 调动 | **P1** | |
| 员工通讯录 | **P1** | |
| 异动记录 | **P1** | |
| 奖惩记录 | **P2** | |
| 职务管理 | **P1** | |
| 门店分布地图 | **P2** | |
#### 🔐 权限管理
| 功能 | 优先级 | 说明 |
|------|--------|------|
| 角色管理(预设角色 + 自定义角色) | **P0** | 权限基础 |
| 人员权限列表 | **P0** | |
| 角色批量分配 | **P0** | |
| 功能权限(菜单级) | **P0** | |
| 数据权限(部门/个人/全司) | **P0** | |
| 字段级权限(敏感字段可见性) | **P1** | 配合房源/客源敏感信息 |
| 个人特定权限覆盖 | **P1** | |
#### 🔑 用户登录
| 功能 | 优先级 | 说明 |
|------|--------|------|
| 账号密码登录 | **P0** | |
| 多租户识别(子域名/域名) | **P0** | |
| Token 管理 / 会话超时 | **P0** | |
| 短信验证码登录 | **P1** | |
| 密码重置 | **P1** | |
| 记住登录状态 | **P1** | |
#### ⚙️ 系统配置
| 功能 | 优先级 | 说明 |
|------|--------|------|
| 首页设置 | **P1** | |
| 房源设置(字段必填/自定义字段/标签) | **P0** | 影响录入表单 |
| 相关方设置 | **P1** | |
| 客源设置(基本配置/参数配置) | **P1** | |
| 人事OA设置 | **P2** | |
| 交易设置 | **P2** | |
| 财务设置 | **P2** | |
| 合同设置 | **P2** | |
#### 🖥️ 系统管理(运营后台)
| 功能 | 优先级 | 说明 |
|------|--------|------|
| 租户管理(开通/暂停/配置) | **P1** | 单租户种子阶段可手动 |
| 系统健康监控 | **P1** | |
| 操作审计日志 | **P2** | |
| 灰度发布 / 滚动升级 | **P2** | |
#### 💻 客户端发布
| 功能 | 优先级 | 说明 |
|------|--------|------|
| Windows 桌面客户端(内置浏览器) | **P1** | 种子客户使用 Web 端可先行 |
| 自动更新机制 | **P1** | 配合客户端 |
---
## 3. 非目标Out of Scope — MVP 阶段绝对不做)
以下功能在 MVP 阶段**明确不实现**AI 生成代码时不得为这些功能预留接口或引入相关依赖:
| 功能 | 原因 |
|------|------|
| 移动端适配 | v2 规划 |
| 新房模块(新房管理/新房设置) | 独立模块,后续版本 |
| 合同管理模块 | 独立模块,后续版本 |
| 财务管理/提成结算 | 独立模块,后续版本 |
| 三网发布(安居客/链家/贝壳对接) | 独立模块,后续版本 |
| 数据报表/行程量化 | 独立模块,后续版本 |
| 在线充值/增值服务 | 独立模块,后续版本 |
| 任务管理OA任务/入职祝福) | 低优先 |
| 考勤管理 | 独立 HR 模块 |
| 审批流程 | 独立 OA 模块 |
| 智慧大屏 / VR换装 | 增值产品 |
| 房源广场(跨租户公共池) | 多租户复杂场景 |
---
## 4. 用户故事MVP 核心路径)
### Story 1 — 经纪人录入房源
> As a **一线经纪人**,
> I want to **快速录入一套二手住宅并上传图片和业主联系方式**,
> So that **这套房源的信息能被团队所有成员找到和跟进**.
**验收标准**
- 可在 3 分钟内完成住宅基本信息录入
- 上传图片后自动按分类展示
- 录入后即刻出现在房源列表
---
### Story 2 — 经纪人跟进房源
> As a **一线经纪人**,
> I want to **对我负责的房源记录每次跟进(面访/电话/钥匙/实勘)**,
> So that **我的跟进历史有据可查,团队不会重复联系同一业主**.
**验收标准**
- 跟进记录按时间线倒序展示
- 支持写入跟进、修改跟进、其他跟进(钥匙/委托/实勘)
- 敏感信息跟进只对有权限的人员可见
---
### Story 3 — 经纪人录入客源
> As a **一线经纪人**,
> I want to **录入意向购房/租房客户并跟进其需求变化**,
> So that **我能在合适时机将客户与合适房源匹配**.
**验收标准**
- 区分求购/求租两种意向
- 支持跟进记录
- 可安排带看并记录带看结果
---
### Story 4 — 转成交
> As a **一线经纪人**,
> I want to **将已达成交易的客源标记为"成交"并关联成交房源**,
> So that **成交数据进入系统留存,房源状态自动更新**.
**验收标准**
- 转成交时必须选择关联房源
- 成交后客源状态自动变为"成交客"
- 关联房源状态建议变更为"成交"(可手动确认)
---
### Story 5 — 店长查看团队数据
> As a **门店店长**,
> I want to **查看本门店所有员工的房源和客源列表**,
> So that **我能掌握团队整体情况并合理分配资源**.
**验收标准**
- 数据权限按部门隔离,店长可见本门店数据
- 可筛选查看特定员工的房源/客源
- 无法看到其他门店的数据
---
## 5. MVP 技术边界
| 约束 | 决策 |
|------|------|
| 租户数 | **单租户**种子阶段,多租户架构已就位但不激活多租户切换 UI |
| 数据量 | 目标支撑 **89,000 条**房源,测试阶段以 10,000 条压测 |
| 浏览器支持 | Chrome 最新版 / Edge 最新版,不支持 IE |
| 语言 | 简体中文,不做国际化 |
| 移动端 | **不做**Web 端 Desktop-first |
| 导出 | Excel/CSV 导出通过 Celery 异步,不超时 |
---
## 6. MVP 交付检查清单
在 MVP 正式上线前,以下项目必须全部勾选:
- [ ] 房源录入(住宅)完整流程可用
- [ ] 房源列表可筛选/排序/分页
- [ ] 客源录入(求购/求租)完整流程可用
- [ ] 带看创建与记录可用
- [ ] 转成交流程可用
- [ ] 楼盘数据可录入(为房源提供底座)
- [ ] 员工账号可创建/分配角色
- [ ] 权限隔离:经纪人只能看自己数据,店长能看本店数据
- [ ] 89,000 条数据量下列表查询 < 2 秒(含索引优化)
- [ ] 图片上传到 Cloudflare R2 可用
- [ ] 多租户 Schema 隔离验证通过
---
## 7. 版本路线图
| 版本 | 目标 | 核心功能 |
|------|------|---------|
| **v0.1 MVP** | 单租户种子验证 | P0 功能全部上线 |
| **v0.2** | 功能完善 | P1 功能上线,开始多租户测试 |
| **v0.3** | 商业化就绪 | Windows 客户端、多租户正式开放、系统配置完善 |
| **v1.0** | 正式发布 | 新房模块、合同/财务模块路线图确认 |
# Fonrey 房睿 — MVP 范围书
**Status**: Draft
**Author**: Product Team
**Last Updated**: 2026-04-24
**Version**: 1.0
> **For AI assistants**: 本文件定义 Phase 1MVP的边界。在任何功能实现前先对照本文确认是否在范围内。范围外的功能禁止在 MVP 阶段实现。
---
## 1. 产品背景与目标
**Fonrey房睿** 是一套面向中小型房产经纪公司的 B2B SaaS 管理平台,解决以下核心痛点:
- 房源/客源信息散乱,全靠人工记录
- 跟进记录缺失,数据流失严重
- 重复录入浪费大量经纪人时间
- 无法支撑 89,000+ 数据量级下的高效房客匹配
**MVP 目标**:在一家种子客户(单租户)环境下,完整跑通"录入房源 → 录入客源 → 匹配带看 → 成交"的核心业务链路。
---
## 2. MVP 核心功能清单Phase 1 必须实现)
### 2.1 优先级定义
| 优先级 | 含义 |
|--------|------|
| **P0** | MVP 上线前必须完成,阻断核心业务链路 |
| **P1** | MVP 上线后第一个迭代周期内完成 |
| **P2** | 已规划,列入路线图但不阻断上线 |
---
### 2.2 模块优先级矩阵
#### 🏠 房源管理
| 功能 | 优先级 | 说明 |
|------|--------|------|
| 录入住宅(二手出售/出租) | **P0** | 核心业务入口 |
| 房源列表(二手&租赁) | **P0** | 含筛选、排序、分页 |
| 房源详情页 | **P0** | 含基本信息、产证、交易信息展示 |
| 跟进记录(全部/写入/修改/其他) | **P0** | 含钥匙、委托、实勘 |
| 图片管理(相册上传/分类/排序) | **P0** | 核心房源内容 |
| 业主联系人管理 | **P0** | 含新增/编辑/查看同业主房源 |
| 价格调整(调价/调价记录) | **P0** | 核心运营操作 |
| 房源状态变更(在售/暂缓/成交/下架) | **P0** | 状态机核心 |
| 房源维护完成度(诊断面板) | **P1** | 提升数据质量 |
| 敏感信息跟进(查看权限控制) | **P1** | 需配合权限模块 |
| 附件管理 | **P1** | 非阻断性 |
| 市场报盘 | **P1** | 运营辅助功能 |
| 价格解读 | **P1** | 分析辅助 |
| 录入别墅/商铺/商住/写字楼/其他 | **P2** | 住宅优先,商业类低频 |
| 全部商铺列表 / 全部写字楼列表 | **P2** | 配合 P2 录入功能 |
| 房源广场 | **P2** | 跨租户/公共池功能 |
#### 🏙️ 楼盘管理
| 功能 | 优先级 | 说明 |
|------|--------|------|
| 楼盘列表 + 楼盘详情(楼盘信息/楼栋/结构) | **P0** | 房源数据底座,必须先行 |
| 区域管理(城区/商圈) | **P0** | 房源关联必须 |
| 楼盘照片管理 | **P1** | 数据完善 |
| 楼盘价格走势 | **P1** | 分析辅助 |
| 周边配套(学校管理) | **P1** | 补充信息 |
| 应用数据标准 | **P2** | 明确不做 |
#### 👥 客源管理
| 功能 | 优先级 | 说明 |
|------|--------|------|
| 录入私客(求购/求租) | **P0** | 核心业务 |
| 私客列表(全部/求购/求租) | **P0** | 含筛选、排序 |
| 私客详情(基本信息/需求信息) | **P0** | |
| 跟进记录(全部/写入/修改/其他) | **P0** | |
| 带看管理(预约带看/新增带看) | **P0** | 房客匹配核心 |
| 联系人管理 | **P0** | |
| 客源状态变更(改等级/改状态) | **P0** | |
| 转公客 / 转成交 / 转无效 | **P0** | 生命周期核心 |
| 二手配房(智能匹配) | **P1** | 核心价值,但可后续迭代 |
| 客源解读 | **P1** | AI 辅助分析 |
| 客源信息概览 | **P1** | 汇总视图 |
| 客源收藏夹 | **P1** | 辅助功能 |
| 公客管理 | **P2** | 私客优先 |
| 成交客管理 | **P2** | |
| 暂缓私客 | **P2** | |
#### 🏢 组织人事
| 功能 | 优先级 | 说明 |
|------|--------|------|
| 公司组织结构(部门/门店树) | **P0** | 权限系统基础 |
| 员工列表/员工详情 | **P0** | |
| 员工入职/账号创建 | **P0** | |
| 员工离职 / 调动 | **P1** | |
| 员工通讯录 | **P1** | |
| 异动记录 | **P1** | |
| 奖惩记录 | **P2** | |
| 职务管理 | **P1** | |
| 门店分布地图 | **P2** | |
#### 🔐 权限管理
| 功能 | 优先级 | 说明 |
|------|--------|------|
| 角色管理(预设角色 + 自定义角色) | **P0** | 权限基础 |
| 人员权限列表 | **P0** | |
| 角色批量分配 | **P0** | |
| 功能权限(菜单级) | **P0** | |
| 数据权限(部门/个人/全司) | **P0** | |
| 字段级权限(敏感字段可见性) | **P1** | 配合房源/客源敏感信息 |
| 个人特定权限覆盖 | **P1** | |
#### 🔑 用户登录
| 功能 | 优先级 | 说明 |
|------|--------|------|
| 账号密码登录 | **P0** | |
| 多租户识别(子域名/域名) | **P0** | |
| Token 管理 / 会话超时 | **P0** | |
| 短信验证码登录 | **P1** | |
| 密码重置 | **P1** | |
| 记住登录状态 | **P1** | |
#### ⚙️ 系统配置
| 功能 | 优先级 | 说明 |
|------|--------|------|
| 首页设置 | **P1** | |
| 房源设置(字段必填/自定义字段/标签) | **P0** | 影响录入表单 |
| 相关方设置 | **P1** | |
| 客源设置(基本配置/参数配置) | **P1** | |
| 人事OA设置 | **P2** | |
| 交易设置 | **P2** | |
| 财务设置 | **P2** | |
| 合同设置 | **P2** | |
#### 🖥️ 系统管理(运营后台)
| 功能 | 优先级 | 说明 |
|------|--------|------|
| 租户管理(开通/暂停/配置) | **P1** | 单租户种子阶段可手动 |
| 系统健康监控 | **P1** | |
| 操作审计日志 | **P2** | |
| 灰度发布 / 滚动升级 | **P2** | |
#### 💻 客户端发布
| 功能 | 优先级 | 说明 |
|------|--------|------|
| Windows 桌面客户端(内置浏览器) | **P1** | 种子客户使用 Web 端可先行 |
| 自动更新机制 | **P1** | 配合客户端 |
---
## 3. 非目标Out of Scope — MVP 阶段绝对不做)
以下功能在 MVP 阶段**明确不实现**AI 生成代码时不得为这些功能预留接口或引入相关依赖:
| 功能 | 原因 |
|------|------|
| 移动端适配 | v2 规划 |
| 新房模块(新房管理/新房设置) | 独立模块,后续版本 |
| 合同管理模块 | 独立模块,后续版本 |
| 财务管理/提成结算 | 独立模块,后续版本 |
| 三网发布(安居客/链家/贝壳对接) | 独立模块,后续版本 |
| 数据报表/行程量化 | 独立模块,后续版本 |
| 在线充值/增值服务 | 独立模块,后续版本 |
| 任务管理OA任务/入职祝福) | 低优先 |
| 考勤管理 | 独立 HR 模块 |
| 审批流程 | 独立 OA 模块 |
| 智慧大屏 / VR换装 | 增值产品 |
| 房源广场(跨租户公共池) | 多租户复杂场景 |
---
## 4. 用户故事MVP 核心路径)
### Story 1 — 经纪人录入房源
> As a **一线经纪人**,
> I want to **快速录入一套二手住宅并上传图片和业主联系方式**,
> So that **这套房源的信息能被团队所有成员找到和跟进**.
**验收标准**
- 可在 3 分钟内完成住宅基本信息录入
- 上传图片后自动按分类展示
- 录入后即刻出现在房源列表
---
### Story 2 — 经纪人跟进房源
> As a **一线经纪人**,
> I want to **对我负责的房源记录每次跟进(面访/电话/钥匙/实勘)**,
> So that **我的跟进历史有据可查,团队不会重复联系同一业主**.
**验收标准**
- 跟进记录按时间线倒序展示
- 支持写入跟进、修改跟进、其他跟进(钥匙/委托/实勘)
- 敏感信息跟进只对有权限的人员可见
---
### Story 3 — 经纪人录入客源
> As a **一线经纪人**,
> I want to **录入意向购房/租房客户并跟进其需求变化**,
> So that **我能在合适时机将客户与合适房源匹配**.
**验收标准**
- 区分求购/求租两种意向
- 支持跟进记录
- 可安排带看并记录带看结果
---
### Story 4 — 转成交
> As a **一线经纪人**,
> I want to **将已达成交易的客源标记为"成交"并关联成交房源**,
> So that **成交数据进入系统留存,房源状态自动更新**.
**验收标准**
- 转成交时必须选择关联房源
- 成交后客源状态自动变为"成交客"
- 关联房源状态建议变更为"成交"(可手动确认)
---
### Story 5 — 店长查看团队数据
> As a **门店店长**,
> I want to **查看本门店所有员工的房源和客源列表**,
> So that **我能掌握团队整体情况并合理分配资源**.
**验收标准**
- 数据权限按部门隔离,店长可见本门店数据
- 可筛选查看特定员工的房源/客源
- 无法看到其他门店的数据
---
## 5. MVP 技术边界
| 约束 | 决策 |
|------|------|
| 租户数 | **单租户**种子阶段,多租户架构已就位但不激活多租户切换 UI |
| 数据量 | 目标支撑 **89,000 条**房源,测试阶段以 10,000 条压测 |
| 浏览器支持 | Chrome 最新版 / Edge 最新版,不支持 IE |
| 语言 | 简体中文,不做国际化 |
| 移动端 | **不做**Web 端 Desktop-first |
| 导出 | Excel/CSV 导出通过 Celery 异步,不超时 |
---
## 6. MVP 交付检查清单
在 MVP 正式上线前,以下项目必须全部勾选:
- [ ] 房源录入(住宅)完整流程可用
- [ ] 房源列表可筛选/排序/分页
- [ ] 客源录入(求购/求租)完整流程可用
- [ ] 带看创建与记录可用
- [ ] 转成交流程可用
- [ ] 楼盘数据可录入(为房源提供底座)
- [ ] 员工账号可创建/分配角色
- [ ] 权限隔离:经纪人只能看自己数据,店长能看本店数据
- [ ] 89,000 条数据量下列表查询 < 2 秒(含索引优化)
- [ ] 图片上传到 Cloudflare R2 可用
- [ ] 多租户 Schema 隔离验证通过
---
## 7. 版本路线图
| 版本 | 目标 | 核心功能 |
|------|------|---------|
| **v0.1 MVP** | 单租户种子验证 | P0 功能全部上线 |
| **v0.2** | 功能完善 | P1 功能上线,开始多租户测试 |
| **v0.3** | 商业化就绪 | Windows 客户端、多租户正式开放、系统配置完善 |
| **v1.0** | 正式发布 | 新房模块、合同/财务模块路线图确认 |

View File

@@ -56,7 +56,9 @@
| [US-PERMISSION-003](#US-PERMISSION-003-管理员批量为员工分配角色) | 权限管理 | 管理员批量为员工分配角色 | [ ] |
| [US-PERMISSION-004](#US-PERMISSION-004-系统执行功能权限控制菜单级) | 权限管理 | 系统执行功能权限控制(菜单级) | [ ] |
| [US-PERMISSION-005](#US-PERMISSION-005-系统执行数据权限控制部门个人全司) | 权限管理 | 系统执行数据权限控制(部门/个人/全司) | [ ] |
| [US-SETTING-001](#US-SETTING-001-管理员配置房源相关设置字段必填自定义字段标签) | 系统配置 | 管理员配置房源相关设置(字段必填/自定义字段/标签 | [ ] |
| [US-SETTING-001-A](#US-SETTING-001-A-管理员配置可选枚举值-Lookup-Items) | 系统配置 | 管理员配置可选枚举值Lookup Items | [ ] |
| [US-SETTING-001-B](#US-SETTING-001-B-管理员配置房源字段必填规则) | 系统配置 | 管理员配置房源字段必填规则 | [ ] |
| [US-SETTING-001-C](#US-SETTING-001-C-管理员配置客源录入规则) | 系统配置 | 管理员配置客源录入规则(查重范围/必填字段) | [ ] |
#### Phase 2 — 增强功能P1
@@ -437,12 +439,40 @@
### 系统配置
##### US-SETTING-001 管理员配置房源相关设置字段必填自定义字段标签
##### US-SETTING-001-A 管理员配置可选枚举值 Lookup Items
- 参考PRD文档`Project/fonrey/PRD/系统配置/系统配置.md` - 房源设置(字段必填/自定义字段/标签)
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL_PUBLIC.md`
- 参考PRD文档`Project/fonrey/PRD/系统配置/系统配置模块PRD.md` - US-SETTING-001-A
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL.md`(待 Atlas 补充 `lookup_items` DDL
- 状态:[ ]
- 验收标准:可配置房源录入表单中哪些字段为必填;可新增自定义字段并在房源表单中展示;标签配置后可在房源筛选中使用;配置变更后房源录入表单实时生效
- 验收标准:
- 管理员进入「系统设置 → 参数配置」,页面按模块分组展示所有可配置参数项(客源来源、跟进目的、房源来源)
- 可新增自定义选项,新选项追加至列表末尾并立即对经纪人录入下拉生效(刷新后)
- 系统预制选项(`is_system=True`)不可删除,仅可停用;停用后前端下拉不再展示,历史数据保留并标注「已停用」
- 支持调整选项排序(拖拽或修改排序值),排序变更在保存后前端生效
- 配置保存时主动失效 Redis 缓存 key `{tenant_schema}:setting:lookup:{module}.{key}`,最长 5 分钟延迟
##### US-SETTING-001-B 管理员配置房源字段必填规则
- 参考PRD文档`Project/fonrey/PRD/系统配置/系统配置模块PRD.md` - US-SETTING-001-B
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL.md`(待 Atlas 补充 `field_requirement_rules` DDL`Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md`
- 状态:[ ]
- 验收标准:
- 管理员进入「系统设置 → 房源字段规则」,以「用途 × 交易状态」矩阵展示配置(住宅×出售、住宅×出租)
- 每个字段显示当前规则(必填 / 选填 / 隐藏),以三态 Toggle 或 Radio 形式编辑,保存后生效
- 规则应用于录入界面:必填字段显示「*」标记,提交时为空则拦截;隐藏字段不渲染
- 规则变更仅影响新录入,不影响存量房源数据
- MVP 可配置字段:朝向、装修情况、楼层、建筑面积、套内面积、房型、产权年限、车位数
##### US-SETTING-001-C 管理员配置客源录入规则
- 参考PRD文档`Project/fonrey/PRD/系统配置/系统配置模块PRD.md` - US-SETTING-001-C
- 参考DATA_MODEL文档`Project/fonrey/DATA_MODEL/DATA_MODEL.md`(待 Atlas 补充 `tenant_settings` DDL`Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md`
- 状态:[ ]
- 验收标准:
- 管理员进入「系统设置 → 客源规则」,可配置新增私客时的查重范围:本人(默认)/ 本部门 / 全公司
- 查重规则实时生效:经纪人录入手机号失焦后,系统按当前查重范围加密比对,若重复则提示归属人和录入时间,经纪人可选「仍然录入」或「放弃」
- 可配置客源必填字段开关(等级、来源默认必填;总价区间、居室需求、购房目的默认选填)
- 配置保存时主动失效 Redis 缓存 key `{tenant_schema}:setting:client_rules`,经纪人下次打开录入界面即应用最新规则
---

File diff suppressed because it is too large Load Diff

View File

@@ -1,128 +0,0 @@
## 首页设置
## 房源设置
### 新增/编辑/查看
### 字段/标签设置
### 相关方设置
### 相关方保护规则设置
### 跟进/面访/回访
### 实勘视频/VR/实地核验
### 预约拍摄设置
### 钥匙/委托/政府核验
### 作业盘设置
### 维护人员设置
### 列表/房源/分级
### 营销设置
### 楼盘设置
### 资料房/业主委托/预录入
### 隐私保护及防骚扰
### 房源检查及纠错
## 新房设置
### 新房基本设置
### 新房参数设置
## 客源设置
### 客源基本配置
### 客源参数配置
### 客源相关方配置
### 客源行政跨部权限
## 交易设置
### 交易流程
### 二手售后流程
### 新房售后流程
### 参数&备件条件
## 财务设置
### 业绩管理
### 资金管理
### 结算设置
### 提成设置
## 人事OA设置
### 组织人事基本设置
### 员工自动升降级设置
### 审批流程设置
## 任务设置
### 参数设置
### 入职周年祝福设置
## 合同设置
### 合同基本设置
## 通用及移动端设置
### 指标设置
### 安全设置
### 其他设置
### 电话智能监控设置
### 黑名单设置
## 安装与登录设置

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,275 @@
# 系统配置模块 — 数据模型设计说明
**致**Atlas架构师
**来自**NovaPM
**日期**2026-04-27
**关联 PRD**`PRD/系统配置/系统配置模块PRD.md`
**关联文档**`DATA_MODEL/ENUMS.md``DATA_MODEL/DATA_MODEL.md`
---
## 一、背景与问题
在设计系统配置模块的数据模型时,我发现当前 `DATA_MODEL/ENUMS.md` 已经明确了「固定枚举」与「可配置枚举」的分层设计,但两者的边界在文档中未完全显式化。本说明文档的目的是:
1. 厘清三类配置数据各自应存在哪张表
2. 指出需要新增的两张表及其建议 DDL
3. 说明与 ENUMS.md 现有设计的关系,以及需要 Atlas 补充/修改的内容
请 Atlas 在完成 `DATA_MODEL/DATA_MODEL.md``DATA_MODEL/ENUMS.md` 的修订后,同步通知 Nova 确认。
---
## 二、三类配置数据的划分
系统配置涉及三类性质不同的数据,**分属不同的表和 Schema**,请严格区分:
### 类型 A固定系统枚举存 Public Schema / `enum_labels`
**特征**
- 值域固定,所有租户共享同一套
- 与数据库 `CHECK CONSTRAINT` 绑定(如 `decoration IN ('rough','plain','simple','medium','fine','luxury')`
- 只能由平台研发通过 migration 修改
- 租户管理员**无权**增删
**代表字段**`property.decoration`(装修)、`property.orientation`(朝向)、`property.status`(交易状态)、`client.status`(客源状态)、`client.grade`(客源等级)、`common.gender``common.id_type`
**现状**ENUMS.md 已完整定义,`enum_labels` 表 DDL 已存在。**无需改动。**
---
### 类型 B租户可配置枚举存 Tenant Schema / `lookup_items`**需新增**
**特征**
- 各租户选项不同如来源渠道A 公司有「抖音」B 公司没有)
- 租户管理员可通过界面增删排序
- 系统预制初始值(`is_system = True`),预制值不可删除但可停用
- **无** `CHECK CONSTRAINT`(值域动态)
-`enum_labels` 完全独立,不存在于 Public Schema
**代表字段**:客源来源(`client.source`)、跟进目的(`client_follow_logs.follow_purpose`)、房源来源(`property.source`
**ENUMS.md 现状**
- `§2.14 跟进目的` 已明确标注「此枚举为可配置项,存储方式:`lookup_items` 表」,但 `lookup_items` 的 DDL 尚未在任何 DATA_MODEL 文档中定义
- `client.source`(来源)在 ENUMS.md 中未定义(因为它是可配置的),但竞品系统有 50+ 预制来源选项
**需要 Atlas 做的事**
1.`DATA_MODEL/DATA_MODEL.md` 中新增 `lookup_groups``lookup_items` 表的 DDL
2. 在 ENUMS.md 中补充一节「可配置枚举说明」,列出哪些 domain 属于 `lookup_items` 而非 `enum_labels`
3. 确认 `apps/setting/` 下新增 `lookup.py` models 文件
**建议 DDL供参考Atlas 可调整)**
```sql
-- ============================================================
-- 可配置枚举分组(租户 Schema
-- ============================================================
CREATE TABLE lookup_groups (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
module VARCHAR(50) NOT NULL, -- 'client' | 'property'
key VARCHAR(100) NOT NULL, -- 'source' | 'follow_purpose'
label_zh VARCHAR(50) NOT NULL, -- 界面显示名称,如「客源来源」
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (module, key)
);
-- ============================================================
-- 可配置枚举选项(租户 Schema
-- ============================================================
CREATE TABLE lookup_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
group_id UUID NOT NULL REFERENCES lookup_groups(id) ON DELETE CASCADE,
value VARCHAR(100) NOT NULL, -- 存储值(英文 key建议 snake_case
label_zh VARCHAR(50) NOT NULL, -- 显示文本
is_system BOOLEAN NOT NULL DEFAULT FALSE, -- True=系统预制,不可删除
is_active BOOLEAN NOT NULL DEFAULT TRUE,
sort_order SMALLINT NOT NULL DEFAULT 0,
created_by UUID REFERENCES staff(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (group_id, value)
);
CREATE INDEX idx_lookup_items_group_active ON lookup_items(group_id, is_active, sort_order);
```
**预制种子数据参考**(需写入 migration fixtures`is_system = TRUE`
| module | key | 预制选项(部分) |
|--------|-----|----------------|
| `client` | `source` | 门店接待、老客户转介绍、驻守派单、上门、网络-58同城、网络-安居客、微信、朋友介绍 |
| `client` | `follow_purpose` | 回拨、推房、带看、维护、其他 |
| `property` | `source` | 主动开发、业主上门、老客户转介绍、网络来电 |
---
### 类型 C行为规则与开关存 Tenant Schema**需新增两张表**
#### C-1键值配置表 `tenant_settings`
**特征**
- 存储开关bool、阈值int、枚举选择string等标量类型配置
- 每个 key 全局唯一,有默认值
- 租户管理员通过界面修改
**建议 DDL**
```sql
-- ============================================================
-- 租户标量配置表(键值对)
-- ============================================================
CREATE TABLE tenant_settings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
category VARCHAR(50) NOT NULL, -- 配置分类:'client' | 'property' | 'showroom'
key VARCHAR(100) NOT NULL, -- 配置 key如 'duplicate_check_scope'
value JSONB NOT NULL, -- 存储任意类型bool/int/str/list
value_type VARCHAR(20) NOT NULL, -- 'bool' | 'int' | 'string' | 'enum'(用于前端渲染)
updated_by UUID REFERENCES staff(id),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (category, key)
);
```
**MVP 阶段需要预置的 key**
| category | key | value_type | 默认值 | 说明 |
|----------|-----|-----------|--------|------|
| `client` | `duplicate_check_scope` | `enum` | `"self"` | 新增私客查重范围:`self`/`dept`/`company` |
#### C-2字段必填规则表 `field_requirement_rules`
**特征**
- 按「模块 × 实体用途 × 交易状态 × 字段」四元组确定一条规则
- 规则值为三态:`required` / `optional` / `hidden`
- MVP 仅需支持 `property` 模块
**建议 DDL**
```sql
-- ============================================================
-- 字段必填/隐藏规则表(租户 Schema
-- ============================================================
CREATE TABLE field_requirement_rules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
module VARCHAR(20) NOT NULL, -- 'property' | 'client'
entity_type VARCHAR(50) NOT NULL, -- property_type 值,如 'residential' | 'shop'
trade_status VARCHAR(50) NOT NULL, -- 'sale' | 'rent' | 'sale_rent'or '*' 表示所有)
field_key VARCHAR(50) NOT NULL, -- 字段 key如 'orientation' | 'decoration'
requirement VARCHAR(10) NOT NULL -- 'required' | 'optional' | 'hidden'
CHECK (requirement IN ('required', 'optional', 'hidden')),
updated_by UUID REFERENCES staff(id),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (module, entity_type, trade_status, field_key)
);
CREATE INDEX idx_field_req_lookup ON field_requirement_rules(module, entity_type, trade_status);
```
**MVP 初始规则**(建议由研发预置,管理员可覆盖):
| module | entity_type | trade_status | field_key | requirement |
|--------|-------------|--------------|-----------|-------------|
| `property` | `residential` | `sale` | `orientation` | `optional` |
| `property` | `residential` | `sale` | `decoration` | `optional` |
| `property` | `residential` | `sale` | `floor` | `optional` |
| `property` | `residential` | `rent` | `decoration` | `optional` |
| `property` | `residential` | `rent` | `floor` | `optional` |
---
## 三、与 ENUMS.md 的冲突与修改建议
### 冲突点 1`lookup_items` DDL 缺失
**现状**ENUMS.md §2.14 和 §六.4 已提到「可配置枚举通过 `lookup_items` 表管理」,但 `lookup_items` 的 DDL 既不在 ENUMS.md 中,也不在 DATA_MODEL.md 中。
**建议修改**:在 `DATA_MODEL/ENUMS.md` 末尾(或新增 §七)补充 `lookup_groups` + `lookup_items` 的 DDL 和说明,明确与 `enum_labels` 的区别:
```markdown
## 七、可配置枚举lookup_items
`enum_labels`Public Schema固定不同`lookup_items` 存储在 **Tenant Schema**
由租户管理员自主维护。适用于各租户选项不同的枚举字段。
[此处补充 DDL 和说明]
**属于 lookup_items 的 domain**
- `client.source`(客源来源)
- `client.follow_purpose`(跟进目的)— 已在 §2.14 说明
- `property.source`(房源来源)
```
### 冲突点 2`client.source`(来源)未在 ENUMS.md 定义
**现状**:来源是可配置枚举,不应在 ENUMS.md 中定义固定值。但目前 ENUMS.md 中没有任何地方说明「哪些 domain 是可配置的、对应哪张表」,导致读者不清楚来源应该去哪里查。
**建议修改**:在 ENUMS.md §六 维护约定中新增一条:
```markdown
5. **可配置枚举对照表**:以下 domain 属于 `lookup_items`,不在本文件定义,
不建立 CHECK 约束,请查阅 `PRD/系统配置/系统配置模块PRD.md`
- `client.source`
- `client.follow_purpose`(已标注)
- `property.source`
```
### 冲突点 3`tenant_settings` 和 `field_requirement_rules` 完全缺失
**现状**:现有 DATA_MODEL 文档未涵盖这两张表。
**建议修改**:在 `DATA_MODEL/DATA_MODEL.md` 中新增「系统配置模块数据模型」章节,包含这两张表的 DDL。本文件的建议 DDL 见第二章 C 节)
---
## 四、服务层约定(供研发参考)
所有业务模块通过统一服务层读取配置,**禁止直接查询配置表**
```python
# apps/setting/services/tenant_settings_service.py
class TenantSettingsService:
def get(self, key: str, default=None):
"""
读取标量配置tenant_settings 表)
缓存 key{tenant_schema}:setting:kv:{key}TTL 5min
"""
def get_lookup_items(self, module: str, key: str) -> list[dict]:
"""
获取可配置枚举选项lookup_items 表)
仅返回 is_active=True 的项,按 sort_order 排序
缓存 key{tenant_schema}:setting:lookup:{module}.{key}TTL 5min
写入时主动 invalidate
"""
def get_field_requirements(
self, module: str, entity_type: str, trade_status: str
) -> dict[str, str]:
"""
获取字段必填规则,返回 {field_key: 'required'|'optional'|'hidden'}
缓存 key{tenant_schema}:setting:field_req:{module}.{entity_type}.{trade_status}TTL 5min
"""
```
---
## 五、需要 Atlas 完成的具体动作
| 编号 | 动作 | 修改文件 | 优先级 |
|------|------|---------|--------|
| A-1 | 新增 `lookup_groups` + `lookup_items` DDL | `DATA_MODEL/DATA_MODEL.md` 或单独 `DATA_MODEL_SETTING.md` | P0开发依赖 |
| A-2 | 新增 `tenant_settings` DDL | 同上 | P0 |
| A-3 | 新增 `field_requirement_rules` DDL | 同上 | P0 |
| A-4 | ENUMS.md §七 补充可配置枚举说明和对照表 | `DATA_MODEL/ENUMS.md` | P0 |
| A-5 | ENUMS.md §六 维护约定新增第 5 条(可配置枚举对照) | `DATA_MODEL/ENUMS.md` | P0 |
| A-6 | 确认 `entity_type` 字段的值域与 `property.property_type` 的 CHECK 约束完全一致 | `DATA_MODEL_PROPERTY.md` 对齐 | P0 |
| A-7 | 确认 `trade_status` 字段的值域(`sale`/`rent`/`sale_rent`/`*`)是否与 `property.status` 兼容 | `DATA_MODEL_PROPERTY.md` 对齐 | P1 |
完成以上动作后,请更新 `DATA_MODEL/DATA_MODEL.md` 的版本号,并通知 Nova 做最终 PRD 对齐确认。
---
*本文档由 Nova 起草,数据模型最终决策权归 Atlas。如有架构层面的调整请反馈给 Nova 同步更新 PRD 中的技术考量章节。*

View File

@@ -0,0 +1,273 @@
# PRD系统配置模块MVP
**状态**Draft
**作者**NovaPM
**最后更新**2026-04-27
**版本**v0.1
**关联 Task**US-SETTING-001
**相关文档**`DATA_MODEL/ENUMS.md``PRD/系统配置/系统配置参数数据.md``DATA_MODEL/DATA_MODEL.md`
---
## 1. 问题陈述
**背景**Fonrey 各业务模块(房源录入、客源录入、跟进记录、带看)中存在大量下拉选项、业务规则开关、字段必填控制。如果这些值硬编码在代码中,将导致:
- 不同租户无法根据自身业务习惯调整选项(如来源渠道、跟进目的)
- 管理员无法灵活控制录入数据质量(如哪些字段必填)
- 业务规则变更(如查重范围)需要研发介入,无法自服务
**核心问题**:租户管理员无法通过界面自主维护业务枚举选项和录入规则,导致高频配置变更依赖研发,降低了系统的可用性与租户自服务能力。
**MVP 范围决策**:竞品系统配置涵盖 10+ 大类、100+ 设置项MVP 阶段仅实现影响核心业务链路的 3 类配置,其余推迟至 P1/P2。
---
## 2. 目标与成功指标
| 目标 | 度量指标 | 当前基线 | 目标值 | 测量窗口 |
| ------------- | -------------- | ------------- | -------------------- | -------- |
| 管理员可自主维护枚举选项 | 系统配置依赖研发变更的工单数 | 100%(所有变更需研发) | 0枚举类变更全部自服务 | 上线后 30 天 |
| 房源/客源录入数据质量提升 | 必填字段为空的录入比例 | 未知(当前无必填控制) | < 5% | 上线后 60 天 |
| 配置变更生效时效 | 配置保存到前端生效的时间 | N/A | ≤ 5 分钟Redis 缓存 TTL | — |
---
## 3. 非目标MVP 明确不做)
以下内容在竞品中存在,但 MVP 阶段不实现:
- ❌ 房源标签配置(颜色/排序/启用)— 标签功能整体推迟至 P1
- ❌ 私盘数量上限配置 — 推迟至 P1当前无私盘流转业务逻辑
- ❌ 带看规则配置(补录时间、附件必填等)— 带看功能推迟至 P1
- ❌ 跟进置顶条数限制 — 推迟至 P1
- ❌ 公司信息Logo、名称、联系方式— 归入系统管理模块(平台运营层),非租户配置
- ❌ 区域/商圈配置 — 归入楼盘管理模块US-COMPLEX-003
- ❌ 委托/交易/财务/合同相关配置 — 超出 MVP 范围
- ❌ 通知消息配置 — P2
- ❌ 发布平台配置 — P2
---
## 4. 目标用户
**主要角色**:系统管理员(租户侧,每租户 13 人)
> 典型画像:门店运营负责人或行政主管,熟悉业务流程,无技术背景,通过系统后台进行日常运营配置。使用频率:初始开通时高频(完成初始化配置),此后低频(按需调整)。
**间接受益角色**
- 一线经纪人 — 看到的下拉选项和必填规则由管理员配置决定
- 店长/经理 — 配置直接影响客源来源分析报表的数据质量
---
## 5. User Stories 与验收标准
---
### US-SETTING-001-A管理员配置可选枚举值Lookup Items
> **As** 系统管理员,
> **I want** 在「系统设置 → 参数配置」页面维护各业务模块的下拉选项(如客源来源、跟进目的),
> **So that** 经纪人录入时看到的选项符合公司实际业务,不再依赖研发修改代码。
#### 验收标准
**AC-1参数分组展示**
- Given 管理员进入「系统设置 → 参数配置」
- When 页面加载完成
- Then 页面按模块分组展示所有可配置参数项,至少包含:
- 「客源」分组:客源来源、跟进目的
- 「房源」分组:房源来源
- 每个分组展示当前已有选项列表(名称 + 排序 + 状态)
**AC-2新增自定义选项**
- Given 管理员点击某参数项的「编辑」按钮
- When 弹出编辑抽屉,填写「选项名称」并提交
- Then 新选项追加至列表末尾,排序值自动计算
- And 经纪人在录入界面的对应下拉中立即可见该新选项(刷新后生效)
**AC-3停用选项不可删除系统预制项**
- Given 管理员在编辑态对某选项执行「停用」操作
- When 确认提交
- Then 该选项的 `is_active` 置为 False
- And 前端录入下拉中不再展示该选项
- And 历史已选该值的记录保留原值不变,仅展示时标注「已停用」
- And 系统预制选项(`is_system = True`)的「删除」按钮禁用,仅可停用
**AC-4调整选项排序**
- Given 管理员在编辑态拖拽选项或修改排序值
- When 保存后
- Then 经纪人录入下拉中按新排序展示
**AC-5缓存一致性**
- Given 管理员保存配置变更
- When 变更写入数据库成功
- Then 对应 Redis 缓存 key 主动失效(`{tenant_schema}:setting:lookup:{module}.{key}`
- And 所有经纪人在下次请求时获取到最新选项(最长 5 分钟延迟)
**AC-6MVP 必须覆盖的参数项**
| 模块 | 参数 key | 中文名 | 系统预制值(部分示例) |
|------|---------|--------|------------------|
| `client` | `source` | 客源来源 | 门店接待、老客户转介绍、网络58/安居客)、驻守、上门、朋友介绍 |
| `client` | `follow_purpose` | 跟进目的 | 回拨、推房、带看、维护、其他 |
| `property` | `source` | 房源来源 | 主动开发、业主上门、老客户转介绍、网络来电 |
---
### US-SETTING-001-B管理员配置房源字段必填规则
> **As** 系统管理员,
> **I want** 按「房源用途 × 交易状态」的组合,控制哪些字段在录入时为必填/选填/隐藏,
> **So that** 系统能在录入时强制采集公司要求的关键信息,提升房源数据完整度。
#### 验收标准
**AC-1规则配置页面**
- Given 管理员进入「系统设置 → 房源字段规则」
- When 页面加载
- Then 以「用途 × 交易状态」为维度展示配置矩阵MVP 仅需支持:
- 住宅 × 出售
- 住宅 × 出租
- 商铺 × 出售(可选)
- 商铺 × 出租(可选)
- 每个组合展示可配置字段列表,字段来源见 AC-4
**AC-2规则编辑**
- Given 管理员点击某组合的「编辑」按钮
- When 进入编辑态
- Then 每个字段显示当前规则(必填 / 选填 / 隐藏),以三态 Toggle 或 Radio 形式展示
- And 管理员修改后点击「保存」提交
**AC-3规则应用于录入界面**
- Given 管理员保存了「住宅 × 出售」的规则:朝向=必填,车位数=隐藏
- When 经纪人在录入界面新增「住宅 × 出售」房源
- Then 朝向字段显示必填标记(*),提交时若为空则拦截并提示
- And 车位数字段不显示(隐藏)
- And 规则变更在配置保存后立即对新录入生效(不影响存量房源)
**AC-4MVP 可配置字段范围**
| 字段 | 字段 key | 说明 |
|------|---------|------|
| 朝向 | `orientation` | 对应 `property.orientation` 枚举 |
| 装修情况 | `decoration` | 对应 `property.decoration` 枚举 |
| 楼层 | `floor` | 所在楼层 / 总楼层 |
| 建筑面积 | `building_area` | 数值字段 |
| 套内面积 | `inner_area` | 数值字段 |
| 房型(室/厅/卫) | `room_layout` | 数值字段组 |
| 产权年限 | `ownership_years` | 数值字段 |
| 车位数 | `parking_count` | 数值字段 |
> **注**:字段是否存在于数据模型由 `DATA_MODEL_PROPERTY.md` 决定,本配置只控制「是否必填/展示」,不新增字段。
---
### US-SETTING-001-C管理员配置客源录入规则
> **As** 系统管理员,
> **I want** 配置新增私客时的查重范围,以及必填字段控制,
> **So that** 减少客源重复录入风险,并确保客源数据质量满足公司管理要求。
#### 验收标准
**AC-1查重范围配置**
- Given 管理员进入「系统设置 → 客源规则」
- When 查看「新增私客查重范围」设置项
- Then 可选值为:
- `本人`(默认)— 同一经纪人不可重复录入同一手机号
- `本部门` — 同部门内不可重复
- `全公司` — 全租户范围不可重复
- And 每个选项有说明文案,明确告知管理员改动影响范围
**AC-2查重规则应用**
- Given 管理员将查重范围设置为「本部门」
- When 经纪人录入私客手机号时
- Then 系统在失焦后实时检测同部门内是否存在相同手机号(加密比对)
- And 若存在重复页面提示「该号码在本部门已有记录归属人XXX录入时间XX」
- And 经纪人可选择「仍然录入」(走审批流)或「放弃」
**AC-3客源必填字段配置**
- Given 管理员在「客源规则」页面勾选必填字段
- When 保存
- Then 经纪人录入私客时,已勾选的字段显示必填标记(*),提交时校验
- And MVP 可配置必填的字段范围:
| 字段 | 字段 key | 默认值 |
|------|---------|--------|
| 等级 | `grade` | 必填 |
| 来源 | `source` | 必填 |
| 求购/求租总价区间 | `budget_range` | 选填 |
| 居室需求 | `room_requirement` | 选填 |
| 购房目的 | `buying_purpose` | 选填 |
**AC-4配置变更即时生效**
- Given 管理员修改任一客源规则并保存
- When 经纪人下次打开录入界面
- Then 应用最新规则(无需重新登录)
- And Redis 缓存 key `{tenant_schema}:setting:client_rules` 在保存时主动失效
---
## 6. 解决方案概述
**页面结构**
```
系统设置(侧边栏)
├── 参数配置 ← US-SETTING-001-ALookup Items 管理)
├── 房源字段规则 ← US-SETTING-001-BFieldRequirementRule
└── 客源规则 ← US-SETTING-001-CTenantSetting + 必填规则)
```
**核心设计决策**
1. **Lookup Items 与 enum_labels 分离**:固定系统枚举(装修/朝向/状态/等级)存放在 Public Schema 的 `enum_labels` 表,由平台管理员通过 migration 维护,租户无权修改。可配置枚举(来源/跟进目的)存放在 Tenant Schema 的 `lookup_items` 表,由租户管理员自主维护。详见数据模型说明文档。
2. **字段规则不新增字段**`field_requirement_rules` 只控制「必填/选填/隐藏」状态,字段本身的存在性由数据模型决定。避免配置层与数据模型层职责混淆。
3. **所有配置读取走服务层**:业务模块(房源/客源录入)通过 `TenantSettingsService` 统一读取配置,不直接查表,便于统一缓存管理。
---
## 7. 技术考量
**依赖**
- `apps/setting/` — 配置模块宿主 App已在 AGENTS.md 目录结构中定义)
- `core/cache.py` — Redis 工具(租户前缀管理)
- `DATA_MODEL/ENUMS.md``enum_labels` 设计权威来源,`lookup_items` 需与之对齐
**已知风险**
| 风险 | 可能性 | 影响 | 缓解措施 |
|------|--------|------|---------|
| 缓存失效不及时导致配置延迟生效 | 中 | 低 | 保存时主动 invalidate最长 5 min TTL 兜底 |
| 字段必填规则与前端渲染逻辑耦合 | 中 | 中 | 后端在每次 form 请求时返回规则快照,前端不缓存规则 |
| 历史数据与「已停用枚举值」展示冲突 | 低 | 低 | 已停用值在展示时追加「(已停用)」后缀,数据库值不变 |
**待解决的 Open Questions**(启动开发前必须确认):
- [ ] `lookup_items` 表的最终 DDL 由 Atlas 确认后同步至 `DATA_MODEL/DATA_MODEL.md`**Owner: Atlas / Deadline: 开发启动前**
- [ ] 字段必填规则是否需要支持「按角色」粒度(如经纪人必填、店长选填)— **当前决策MVP 不做角色粒度,全员统一规则。如需变更请在 Review 中提出。**
- [ ] `FieldRequirementRule` 中「房源用途」的枚举值与 `property.property_type` 是否完全一致 — **Owner: Atlas 对齐 DATA_MODEL_PROPERTY.md**
---
## 8. 上线计划
| 阶段 | 时间 | 受众 | 成功门槛 |
|------|------|------|---------|
| 内部联调 | Sprint N | 开发团队 + 测试 | 3 个 US 核心流程无 P0 Bug |
| Alpha 验证 | Sprint N+1 | 1 家种子客户管理员 | 管理员可独立完成初始化配置,无需研发介入 |
| MVP 上线 | Sprint N+2 | 全部租户 | 配置变更工单量为 0全自服务 |
**回滚条件**:配置保存后前端报错率 > 5%,或经纪人录入报错率相比上线前上升 > 2%,立即回滚并排查。
---
## 9. 附录
- [竞品系统配置参数数据](./系统配置参数数据.md)
- [数据模型设计说明(写给 Atlas](./系统配置数据模型设计说明_for_Atlas.md)
- ENUMS.md`DATA_MODEL/ENUMS.md`
- 总体数据模型:`DATA_MODEL/DATA_MODEL.md`

View File

@@ -132,7 +132,7 @@ apps/property/
| ----- | ---------------------------------- | -------------------------- | ------------------------------------- |
| 登录认证 | [`登录管理技术方案.md`](./登录管理技术方案.md) | `PRD/登录管理/` | `DATA_MODEL/DATA_MODEL_LOGIN.md` |
| 权限管理 | [`权限管理系统技术方案.md`](./权限管理系统技术方案.md) | `PRD/权限管理/` | `DATA_MODEL/DATA_MODEL_PERMISSION.md` |
| 房源管理 | _待补充_ | `PRD/房源管理/` | `DATA_MODEL/DATA_MODEL_PROPERTY.md` |
| 房源管理 | [`房源管理技术方案.md`](./房源管理技术方案.md) | `PRD/房源管理/` | `DATA_MODEL/DATA_MODEL_PROPERTY.md` |
| 客源管理 | _待补充_ | `PRD/客源管理/` | `DATA_MODEL/DATA_MODEL_CLIENT.md` |
| 楼盘管理 | _待补充_ | `PRD/房源管理/`(含楼盘) | `DATA_MODEL/DATA_MODEL_COMPLEX.md` |
| 组织人事 | _待补充_ | `PRD/组织人事管理/` | `DATA_MODEL/DATA_MODEL_ORG.md` |

View File

@@ -0,0 +1,444 @@
> **For AI assistants**: Read this entire file before writing any code. All decisions here are final. Do not suggest alternatives unless asked.
# Fonrey 房源管理技术方案
**版本**: 1.0
**项目**: Fonrey 房产经纪管理系统
**技术栈**: Django 4.x + HTMX + Alpine.js + PostgreSQL 16 + Redis + Celery + Cloudflare R2
**关联 PRD**: `PRD/房源管理/房源管理模块PRD.md`v2.1
**关联数据模型**: `DATA_MODEL/DATA_MODEL_PROPERTY.md`(本方案不重复 DDL
**关联枚举字典**: `DATA_MODEL/ENUMS.md`
**最后更新**: 2026-04-27
---
## 一、文档定位与边界
本文件只定义房源模块的:
1. 服务边界与模块协作
2. API 端点设计(重点)
3. HTMX 局刷协议
4. 权限接入、异步任务、缓存与性能约束
5. 测试与验收映射
> **不在本文件展开**表结构字段、索引、DDL、触发器。请以 `DATA_MODEL_PROPERTY.md` 为唯一权威来源。
---
## 二、P0 范围(与 Task Board 对齐)
本方案覆盖 `PRD/TASK.md` 中房源 P0 User Story
- US-PROPERTY-001录入二手住宅出售/出租)
- US-PROPERTY-002查看与筛选房源列表
- US-PROPERTY-003查看房源详情
- US-PROPERTY-004写入与查看跟进记录
- US-PROPERTY-005管理房源图片上传/分类/排序)
- US-PROPERTY-006管理业主联系人
- US-PROPERTY-007调整房源价格
- US-PROPERTY-008变更房源状态
---
## 三、模块架构边界
## 3.1 模块职责
`apps/property` 负责:
- 房源主流程:新增、列表检索、详情展示、状态与价格维护
- 房源协作数据:联系人、跟进、图片
- 房源审计轨迹:挂牌历史、调价记录、敏感查看记录(通过服务层写入)
## 3.2 外部依赖
| 依赖模块 | 依赖内容 | 依赖方式 |
|---|---|---|
| `apps/complex` | 小区/楼盘基础信息、联想搜索 | Service 调用 + 外键引用 |
| `apps/org` | 员工组织信息(相关方、操作人) | 外键 + Service 查询 |
| `apps/permission` | RBAC/Scope 权限判定 | PermissionChecker + ScopeQueryBuilder |
| `apps/setting` | 可配置枚举(来源、跟进目的等) | lookup_items 读取缓存 |
| `core/encryption.py` | 手机号加密/脱敏/哈希 | 统一工具调用,禁止明文 |
| `core/cache.py` | 筛选缓存、任务进度、短期详情缓存 | Redis Key 带租户前缀 |
| `Celery` | 导出、图片处理、完成度重算 | 异步任务 |
| `Cloudflare R2` | 房源图片与附件对象存储 | 预签名上传 + 回写元数据 |
## 3.3 分层约束(必须遵守)
- `views.py` 仅做:参数校验、权限门禁、调用 service、组织响应
- 业务规则全部下沉到 `services/`
- 所有写操作必须落审计(至少 follow_logs 或 change_logs
- 耗时 >500ms 的流程必须异步化
---
## 四、API 设计总原则P0
1. **页面端点与数据端点分离**
- 页面SSR + HTMX 容器):`/property/...`
- 数据 APIJSON`/api/property/...`
2. **HTMX 局刷优先**列表筛选、Tab 内容、弹窗提交走局部刷新。
3. **列表性能目标**89k 房源规模下,`p95 < 2s`(索引 + Keyset 分页 + 预加载)。
4. **统一错误协议**
- JSON`{"error":"...","code":"SNAKE_CASE"}`
- HTMX返回片段 + `HX-Trigger` Toast
5. **权限前置**:所有 API 在 service 执行前完成权限与 scope 过滤。
6. **敏感信息最小暴露**:默认打码;明文查看必须具备权限并记录审计。
---
## 五、端点清单(核心)
## 5.1 页面路由SSR + HTMX 容器)
| 路径 | 方法 | 鉴权 | 说明 |
|---|---|---|---|
| `/property/list/` | GET | 是 | 房源列表主页面(包含筛选栏与列表容器) |
| `/property/create/` | GET | 是 | 新增房源页面P0: 住宅) |
| `/property/{property_id}/` | GET | 是 | 房源详情主页面(多 Tab 容器) |
| `/property/{property_id}/edit/` | GET | 是 | 编辑房源页面(非弹窗) |
## 5.2 HTMX 片段端点
| 路径 | 方法 | 用途 | 返回 |
|---|---|---|---|
| `/property/fragments/list-table/` | GET | 列表筛选/排序/翻页局刷 | HTML 片段 |
| `/property/fragments/filter-panel/` | GET | 筛选项联动刷新(区域→商圈等) | HTML 片段 |
| `/property/{id}/fragments/tab/{tab_name}/` | GET | 详情页 Tab 懒加载(跟进/相册/附件等) | HTML 片段 |
| `/property/{id}/fragments/contact-panel/` | GET | 联系人侧栏局刷 | HTML 片段 |
| `/property/{id}/fragments/follow-timeline/` | GET | 跟进时间线局刷(筛选后) | HTML 片段 |
| `/property/{id}/fragments/photo-grid/` | GET | 相册宫格局刷 | HTML 片段 |
> 所有 fragment 端点必须校验 `HX-Request=true`,非 HTMX 请求返回 400。
## 5.3 JSON APIP0
| 端点 | 方法 | 鉴权 | 权限码(建议) | 说明 |
|---|---|---|---|---|
| `/api/property/` | POST | 是 | `property.create.allow` | 新增房源US-001 |
| `/api/property/list/query/` | POST | 是 | `property.list.view.scope` | 列表查询US-002 |
| `/api/property/{id}/detail/` | GET | 是 | `property.list.view.scope` | 详情聚合数据US-003 |
| `/api/property/{id}/status/change/` | POST | 是 | `property.status.change.allow` | 改状态US-008 |
| `/api/property/{id}/price/change/` | POST | 是 | `property.price.change.allow` | 调价US-007 |
| `/api/property/{id}/follow-logs/` | POST | 是 | `property.follow.create.allow` | 新增跟进US-004 |
| `/api/property/{id}/follow-logs/query/` | POST | 是 | `property.follow.view.scope` | 跟进查询US-004 |
| `/api/property/{id}/contacts/` | POST | 是 | `property.contact.create.allow` | 新增联系人US-006 |
| `/api/property/{id}/contacts/{contact_id}/` | PATCH | 是 | `property.contact.edit.allow` | 编辑联系人US-006 |
| `/api/property/{id}/contacts/same-owner/` | GET | 是 | `property.list.view.scope` | 同业主其他房源US-006 |
| `/api/property/{id}/owner-phone/view/` | POST | 是 | `property.owner_phone.view.daily_limit` | 查看号码(审计+额度) |
| `/api/property/{id}/photos/upload-token/` | POST | 是 | `property.photo.upload.allow` | 获取 R2 预签名US-005 |
| `/api/property/{id}/photos/commit/` | POST | 是 | `property.photo.upload.allow` | 上传回执写库US-005 |
| `/api/property/{id}/photos/reorder/` | POST | 是 | `property.photo.edit.allow` | 图片排序US-005 |
| `/api/property/{id}/photos/category/batch/` | POST | 是 | `property.photo.edit.allow` | 批量改分类US-005 |
| `/api/property/{id}/photos/{photo_id}/set-cover/` | POST | 是 | `property.photo.edit.allow` | 设封面US-005 |
| `/api/property/export/jobs/` | POST | 是 | `property.list.export.scope` | 创建导出任务US-002 |
| `/api/property/export/jobs/{job_id}/` | GET | 是 | `property.list.export.scope` | 查询导出任务状态 |
| `/api/property/export/jobs/{job_id}/download/` | GET | 是 | `property.list.export.scope` | 下载导出结果 |
---
## 六、关键 API 规范(请求/响应)
## 6.1 新增房源
`POST /api/property/`
```json
{
"property_type": "residential",
"trade_type": "sale_rent",
"complex_id": "uuid",
"building_no": "5",
"unit_no": "2",
"room_no": "1102",
"floor_current": 11,
"floor_total": 33,
"area": "89.50",
"layout": {"bedroom": 3, "living": 2, "bathroom": 2, "kitchen": 1, "balcony": 1},
"sale_price": "368.00",
"rent_price": null,
"owner_contacts": [
{
"name": "张三",
"identity": "owner",
"phone": "13800000000",
"gender": "male"
}
],
"related_staff": {
"first_agent_id": "uuid",
"number_agent_id": "uuid",
"sale_agent_id": "uuid"
}
}
```
成功:`201`
```json
{
"id": "uuid",
"code": "FY202604270001",
"redirect_url": "/property/uuid/",
"message": "保存成功"
}
```
## 6.2 列表查询
`POST /api/property/list/query/`
```json
{
"keyword": "阳光花园",
"filters": {
"district_ids": ["uuid"],
"status": ["sale", "rent"],
"attribute": ["public"],
"price_sale": {"min": 200, "max": 500}
},
"sort": {"field": "updated_at", "order": "desc"},
"pagination": {"mode": "keyset", "cursor": "opaque_cursor", "limit": 20}
}
```
成功:`200`
```json
{
"items": [{"id": "uuid", "title": "阳光花园 5-2-1102", "status": "sale"}],
"next_cursor": "opaque_cursor_2",
"total_estimate": 89432
}
```
## 6.3 改状态
`POST /api/property/{id}/status/change/`
```json
{
"from_status": "sale",
"to_status": "paused",
"reason": "业主暂不出售"
}
```
校验规则:
- 必须符合状态机(例如 sale -> paused/other_sale/deal
- reason 必填,<= 50 字
## 6.4 调价
`POST /api/property/{id}/price/change/`
```json
{
"sale_price": "350.00",
"bottom_price": "338.00",
"reason": "业主急售"
}
```
成功后联动:
- 更新当前挂牌价
- 追加 `price_changes` 记录
- 触发详情页价格区域局刷
## 6.5 写跟进
`POST /api/property/{id}/follow-logs/`
```json
{
"follow_type": "write_follow",
"purpose": "owner_follow",
"content": "今日电话沟通业主接受350万附近报价。",
"visibility": "team",
"attachments": ["object_key_1", "object_key_2"]
}
```
校验规则:
- content: 6~500 字
- 附件图片最多 10 张,单张 <=20MB
## 6.6 导出任务
1) 创建:`POST /api/property/export/jobs/`
```json
{"query_snapshot": {...}, "columns": ["code", "title", "status", "sale_price"]}
```
2) 查询:`GET /api/property/export/jobs/{job_id}/`
```json
{"status":"running", "progress":65}
```
3) 下载:`GET /api/property/export/jobs/{job_id}/download/`
- 任务完成后返回一次性下载 URL
---
## 七、HTMX 交互约定(房源模块)
## 7.1 请求头与响应头
- 请求必须带:`HX-Request: true`
- 成功提示:`HX-Trigger: {"toast":{"level":"success","message":"保存成功"}}`
- 失败提示:`HX-Trigger: {"toast":{"level":"error","message":"保存失败"}}`
- 跳转:`HX-Redirect: /property/{id}/`
## 7.2 片段模板命名
| 场景 | 模板 |
|---|---|
| 列表表格 | `templates/property/fragments/list_table.html` |
| 跟进时间线 | `templates/property/fragments/follow_timeline.html` |
| 联系人面板 | `templates/property/fragments/contact_panel.html` |
| 相册宫格 | `templates/property/fragments/photo_grid.html` |
## 7.3 推荐前端触发方式
- 筛选提交:`hx-post="/property/fragments/list-table/"`
- 切 Tab`hx-get="/property/{id}/fragments/tab/follow/"`
- 弹窗提交:`hx-post` + `hx-target` 当前弹窗容器,成功后触发父容器刷新
---
## 八、权限与数据范围设计
## 8.1 P0 必需权限项(最小集合)
> 与 `DATA_MODEL_PERMISSION.md` 对齐;若 `permission_defs` 尚未落库,则按下列 code 补种子。
| 权限 code | 类型 | 说明 |
|---|---|---|
| `property.list.view.scope` | SCOPE | 查看房源范围 |
| `property.list.export.scope` | SCOPE | 导出房源范围 |
| `property.create.allow` | BOOLEAN | 新增房源 |
| `property.list.edit.allow` | BOOLEAN | 编辑房源基础信息 |
| `property.price.change.allow` | BOOLEAN | 调价 |
| `property.status.change.allow` | BOOLEAN | 改状态 |
| `property.follow.view.scope` | SCOPE | 查看跟进范围 |
| `property.follow.create.allow` | BOOLEAN | 新增跟进 |
| `property.contact.create.allow` | BOOLEAN | 新增联系人 |
| `property.contact.edit.allow` | BOOLEAN | 编辑联系人 |
| `property.photo.upload.allow` | BOOLEAN | 上传图片 |
| `property.photo.edit.allow` | BOOLEAN | 改分类/排序/封面 |
| `property.owner_phone.view.daily_limit` | INTEGER | 每日查看号码上限0=不限制) |
## 8.2 Scope 与业务属性叠加
最终查询范围 = `权限 scope 过滤``业务状态过滤``房源属性规则(公盘/私盘/特盘/封盘)`
- 权限系统不直接改写 `properties.attribute`
- 房源属性由业务规则决定可见性,权限只决定“理论上可看范围”
## 8.3 查看号码审计
调用 `/owner-phone/view/` 时必须:
1. 校验 `daily_limit`
2. 解密返回明文(仅本次)
3. 自动写入 `follow_logs``sensitive_view`
4. `sensitive_view` 记录不可删除
---
## 九、异步任务与缓存策略
## 9.1 Celery 任务
| 任务 | 触发时机 | 说明 |
|---|---|---|
| `property_export_task` | 创建导出任务后 | 按查询快照导出 Excel 到 R2 |
| `property_photo_postprocess_task` | 图片上传 commit 后 | 生成缩略图、提取元数据 |
| `property_completeness_recalc_task` | 调价/状态变更/跟进/图片更新后 | 异步重算维护完成度 |
> 所有任务参数必须包含 `tenant_schema_name`,任务开头显式切 schema。
## 9.2 Redis Key 规范
| Key | TTL | 说明 |
|---|---|---|
| `{schema}:property:list:query:{hash}` | 60s | 热门筛选结果短缓存 |
| `{schema}:property:detail:{id}` | 120s | 详情聚合缓存 |
| `{schema}:property:export:{job_id}` | 24h | 导出任务状态 |
| `{schema}:property:owner_phone:view:{staff_id}:{date}` | 24h | 每日查看号码计数 |
---
## 十、性能与可靠性约束
1. 列表查询强制 Keyset 分页(禁止 OFFSET
2. 所有筛选字段必须走已建索引(见 `DATA_MODEL_PROPERTY.md`)。
3. 大列表默认返回精简字段,详情页再按 Tab 懒加载。
4. 导出走异步任务,前端轮询任务状态,禁止同步导出。
5. 批量写操作使用事务,失败要回滚并返回结构化错误。
---
## 十一、安全与合规
- 手机号、微信等敏感信息:入库加密、展示脱敏
- API 全链路 HTTPS
- 操作审计必须包含:操作者、时间、旧值/新值、来源 IP
- 文件上传白名单:`bmp/jpg/jpeg/png/gif/svg`P0 与 PRD 对齐)
- 上传大小限制20MB/文件
- 防刷:列表查询、号码查看、导出任务创建均需限流
---
## 十二、错误码约定(房源模块)
| code | HTTP | 场景 |
|---|---|---|
| `PROPERTY_NOT_FOUND` | 404 | 房源不存在或无访问权限 |
| `PROPERTY_INVALID_STATE_TRANSITION` | 400 | 非法状态流转 |
| `PROPERTY_PRICE_INVALID` | 400 | 价格参数非法 |
| `PROPERTY_FOLLOW_CONTENT_TOO_SHORT` | 400 | 跟进内容不足 6 字 |
| `PROPERTY_PHONE_VIEW_LIMIT_EXCEEDED` | 429 | 查看号码超限 |
| `PROPERTY_EXPORT_JOB_NOT_READY` | 409 | 导出未完成即下载 |
| `PROPERTY_PERMISSION_DENIED` | 403 | 权限不足 |
---
## 十三、测试映射P0
| User Story | 最低测试覆盖 |
|---|---|
| US-PROPERTY-001 | 新增成功 / 必填校验失败 / 无权限403 |
| US-PROPERTY-002 | 关键词+组合筛选 / Keyset 分页 / 导出任务创建 |
| US-PROPERTY-003 | 详情加载 / 号码默认脱敏 / 查看号码审计 |
| US-PROPERTY-004 | 跟进写入成功 / 内容长度校验 / 时间线筛选 |
| US-PROPERTY-005 | 上传签名获取 / commit 落库 / 排序与封面设置 |
| US-PROPERTY-006 | 联系人新增编辑 / 同业主房源查询 |
| US-PROPERTY-007 | 调价成功并写历史 / 理由缺失失败 |
| US-PROPERTY-008 | 合法流转成功 / 非法流转失败 |
**测试强制要求**
- 集成测试使用 `TenantClient`
- HTMX 请求必须带 `HTTP_HX_REQUEST=true`
- 权限三态200 / 403 / 302
- Celery 在测试环境 eager 执行
---
## 十四、落地顺序建议(供开发阶段使用)
1. 先搭建列表查询 + Scope 权限过滤US-002
2. 再打通新增与详情主链路US-001,003
3. 上线状态变更与调价US-007,008
4. 补齐跟进、联系人、图片US-004,005,006
5. 最后接入导出异步与性能压测
---
## 十五、与其他文档的同步规则
- 枚举变更:同步 `DATA_MODEL/ENUMS.md`
- 权限 code 变更:同步 `DATA_MODEL/DATA_MODEL_PERMISSION.md`
- API 变更:同步本文件与对应 PRD 验收条目
- 超出 P0 的能力(如地图找房、商业地产)只能写“预留”,不得提前实现

View File

@@ -1,4 +1,4 @@
> **For AI assistants**: Read this entire file before writing any code. All decisions here are final. Do not suggest alternatives unless asked.
**For AI assistants**: Read this entire file before writing any code. All decisions here are final. Do not suggest alternatives unless asked.
# Fonrey 权限管理系统技术方案建议
**版本**: 1.0 | **项目**: Fonrey 房产经纪管理系统 | **技术栈**: Django 4.x + HTMX + Alpine.js + PostgreSQL + Redis

File diff suppressed because it is too large Load Diff

View File

@@ -51,7 +51,7 @@
| Token | Hex | Tailwind 类 | 使用场景 |
| ------------- | --------- | ---------------- | ------------------------- |
| `primary-50` | `#F0FDFA` | `bg-primary-50` | 页面强调区微底色、Tag 极淡底 |
| `primary-100` | `#CCFBF1 | `bg-primary-100` | 选中背景、标签底色 |
| `primary-100` | `#CCFBF1` | `bg-primary-100` | 选中背景、标签底色 |
| `primary-200` | `#99F6E4` | `bg-primary-200` | Hover 标签底色 |
| `primary-500` | `#14B8A6` | `bg-primary-500` | 辅助主色(图标、强调文字) |
| `primary-600` | `#0F766E` | `bg-primary-600` | **主按钮、激活态、Tab 下划线(基准主色)** |

View File

@@ -0,0 +1,101 @@
# Fonrey Web Coding 开工前缺失清单
> 记录时间2026-04-26
> 目的:在开始 Web Coding 落地前,把缺失但会直接卡住实施的文档一次补齐。
## 一、必须补齐的 6 份关键文档
### 1. 项目入口文档
- README.md / 项目总览
- 本地启动步骤
- 环境变量说明
- 数据库初始化与迁移说明
- 种子数据说明
### 2. ADR 架构决策记录
需要冻结以下决策:
- 登录态session / token / 混合方案
- django-tenants 本地开发模式
- 单租户 MVP 与多租户架构共存方式
- HTMX 页面局刷约定
- 目录结构最终落点
- 状态机与枚举字典权威来源
### 3. 枚举字典 / 状态字典
建议新增:
- ENUMS.md
- STATE_MACHINE.md
至少冻结:
- 客源状态
- 客源等级
- 房源状态
- 操作日志类型
- 租户状态机
要求统一:
- 中文值
- 英文值
- 数据库 CHECK 值
- UI 展示值
- 允许的状态迁移
### 4. 页面路由 + 组件映射
需要明确:
- 每个模块有哪些页面
- 每个页面对应什么 URL
- 每个页面复用哪些组件
- 哪些页面是列表 / 详情 / 弹窗 / 抽屉 / partial
- 每个页面的空态、加载态、错误态、权限态
### 5. API 契约规范
需要明确:
- 请求 / 响应格式
- 错误码规范
- 分页规范
- 搜索 / 筛选规范
- 上传规范
- 文件下载规范
- 权限拒绝返回规范
### 6. 本地开发与验证手册
需要明确:
- 本地环境启动方式
- PostgreSQL / Redis / Celery 启动方式
- django-tenants 初始化方式
- 测试租户创建方式
- 管理员账号 seed 方式
- 静态资源与对象存储本地替代方案
- dev / staging / production 配置差异
## 二、建议优先级
### P0先补不补就不能稳定开工
- 项目入口文档
- ADR
- 枚举字典 / 状态字典
- 页面路由 + 组件映射
### P1随后补直接影响实现质量
- API 契约规范
- 本地开发与验证手册
## 三、当前项目的直接风险
- 需求、数据模型、任务表已经较完整,但“可执行工程包”还不够
- Review 已指出枚举不一致、分页规范缺位、性能基准缺位等问题
- 没有启动手册和种子数据Web Coding 容易停留在文档层,无法稳定进入实现层
## 四、建议的落地顺序
1. 先补 README / 启动手册
2. 冻结 ADR
3. 冻结 ENUMS / STATE_MACHINE
4. 补页面路由与组件映射
5. 补 API 契约
6. 补本地开发与验证手册
7. 再开始正式 Web Coding
---
这份清单的目标不是增加文档数量,而是减少实现时的来回返工。

View File

@@ -0,0 +1,347 @@
# Fonrey PRD 需求文档生成提示词模板
> 本模板专注于**产品需求定义**:做什么、为谁做、怎么验收。
> 技术实现细节(数据模型 DDL、API 路由、架构设计)由配套的 **TECH_STACK & DATA_MODEL 模板**负责,两份文档互不重复。
---
## ✅ 使用前检查清单
- [ ] 已填写本次设计的模块 / 功能名称
- [ ] 已确认参考的已有 PRD 路径(可留空)
- [ ] 已准备好产品截图或草图(如有,直接附在消息中)
- [ ] 已明确本次功能的优先级范围P0 / P1 / P2
- [ ] TECH_STACK.md 已存在,可供 AI 读取技术约束
---
## 📋 完整提示词(复制后填写 `【...】` 再发送)
## 角色与背景
你是 **Nova**,一名拥有 10 年以上经验的资深产品经理,擅长 B2B SaaS 产品的全生命周期管理。
核心方法论:先问问题再给答案,先定义问题再讨论方案,先看证据再拍板决策。
你输出的每一份需求文档都必须包含清晰的用户故事、可量化的验收标准、明确的边界Non-Goals
你永远不写模糊需求,每条需求都可以被工程师直接实现、测试和验收。
**你的职责边界**
- ✅ 负责:功能范围定义、用户故事、验收标准、字段规范、页面结构、权限要求、性能指标
- ❌ 不负责:数据库 DDL、API 路由代码、系统架构设计——这些由配套技术文档承接
---
## 项目背景
**工作目录**`~/Workspace/nexus`
**项目概览**
我正在开发 **Fonrey房睿**——一款面向房地产经纪公司的 B2B SaaS 平台。
核心目标:解决房源 / 客源信息散乱、跟进缺失、重复录入等痛点,支撑 89,000+ 条数据量级下的高效匹配。
**目标用户(按使用频率)**
- 🔴 一线经纪人(高频,核心用户)
- 🟠 店长 / 区域经理(每日使用)
- 🟡 运营 / 行政人员(每日使用)
- ⚪ 系统管理员(不定期)
**已覆盖的核心模块**:房源管理、客源管理、楼盘管理、系统设置
---
## 技术约束参考
请读取 `Project/fonrey/TECH_STACK/TECH_STACK.md` 了解技术约束。
PRD 中涉及交互模式时须遵守以下原则,**不得建议替代方案**
| 约束项 | 要求 |
|--------|------|
| 前端交互 | HTMX 局部刷新 + Alpine.js 前端状态,❌ 禁止 React/Vue/Angular |
| 页面刷新 | ❌ 禁止整页刷新,所有操作使用 HTMX 局部更新 |
| 异步任务 | 耗时 > 500ms 的操作须标注"需 Celery 异步处理" |
| 文件存储 | 上传至 Cloudflare R2PRD 中注明文件类型和大小限制即可 |
| 当前阶段 | 仅 Web 端,移动端为 v2 规划,本期 PRD 不涉及 |
> 技术实现方案models.py、urls.py、视图函数由工程师参考配套 TECH 文档设计PRD 不输出代码。
---
## 方法论参考
请读取 `Project/fonrey/prompt/product-manager.md` 并运用其中的专业知识与 PRD 格式规范。
核心原则(生成文档时必须体现):
1. **先问题后方案**:每个功能点必须说明"解决了谁的什么痛点"
2. **Non-Goals 必填**:明确本次不做什么,防止需求蔓延
3. **验收标准可测试**:每条 AC 格式为 `Given / When / Then`,含正常流与异常流
4. **优先级标注**P0MVP 必须)/ P1重要但可延迟/ P2优化迭代
5. **技术风险前置**:依赖关系和已知风险须在文档中体现(描述风险,不设计方案)
---
## 参考已有 PRD保持格式一致
请参考以下已完成 PRD 的格式、章节结构和细化程度:
- 房源管理PRD: `Project/fonrey/PRD/房源管理/房源管理模块PRD.md`
- 楼盘管理PRD: `Project/fonrey/PRD/房源管理/楼盘管理模块PRD.md`
- 客源管理PRD: `Project/fonrey/PRD/客源管理/客源管理模块PRD.md`
- 权限管理PRD: `Project/fonrey/PRD/权限管理/权限管理模块PRD.md`
- 权限管理样本数据:`Project/fonrey/PRD/权限管理/首页.md`
- 权限管理样本数据:`Project/fonrey/PRD/权限管理/房源-二手租赁.md`
- 权限管理样本数据:`Project/fonrey/PRD/权限管理/客源.md`
- 权限管理样本数据:`Project/fonrey/PRD/权限管理/小区.md`
- 组织人事管理PRD: `Project/fonrey/PRD/组织人事管理/组织人事管理模块PRD.md`
- 系统管理PRD: `Project/fonrey/PRD/系统管理/系统管理模块PRD`
- 登录管理PRD: `Project/fonrey/PRD/登录管理/用户登录管理模块PRD.md`
- 发布管理PRD: `Project/fonrey/PRD/发布管理/客户端发布管理模块PRD.md`
---
## 本次需求
### 🎯 我要设计的功能 / 模块
**【描述本次设计内容,例如:
"楼盘管理模块中的【楼盘详情页】,包含:楼盘基础信息展示与编辑、楼栋管理(列表/新增/编辑)、户型结构管理、楼盘照片管理、区域/商圈关联"】**
### 📎 补充材料
【说明附上了哪些参考材料,例如:
- 已附上截图:竞品 A 的楼盘详情页3 张)
- 已附上草图:手绘交互流程图
- 无截图,请根据文字描述生成】
### 🗂️ 功能范围与优先级
【列出本次优先级范围,例如:
**P0本期必须上线**
- 楼盘基础信息展示与编辑(名称、地址、建成年份、容积率等)
- 楼栋列表(分页、新增、编辑、删除)
**P1本期随 P0 一起上线,允许适当简化)**
- 户型结构管理(按楼栋挂载)
- 楼盘照片上传(支持排序,格式和大小见业务规则)
**P2后续迭代本次文档描述边界不做细化**
- 价格走势图表
- 周边配套(地铁/学校/商场)
- 楼盘数据统计面板】
### 💡 已知的业务规则 / 约束
【填写已确认的业务规则,例如:
- 楼盘是房源的基础数据底座,删除楼盘前需检查是否有挂载的在售房源
- 一个楼盘可有多个楼栋,一个楼栋可有多个户型
- 楼盘照片最多 20 张,单张限 10MB格式限 JPG / PNG / WEBP
- 区域/商圈关联关系从 region app 读取,本模块不维护区域数据
- 楼盘数据属于租户隔离数据complex app需遵守多租户规范】
---
## 输出要求
请按以下结构输出完整 PRD保存至
`Project/fonrey/PRD/【模块名称】/【功能名称】PRD.md`
输出语言:**中文**(技术术语、字段名可保留英文)
---
# PRD【功能名称】
**状态**Draft
**作者**BillyPM
**最后更新**:【当前日期】
**版本**v0.1
**关联 Django App**:【如 complex / property / client】
**关联干系人**:工程负责人 / 设计负责人 / 运营负责人
---
## 1. 问题陈述Problem Statement
- 目标用户是谁,他们面临什么具体痛点
- 当前无此功能时用户如何绕过Workaround
- 不解决此问题的业务成本
---
## 2. 目标与成功指标Goals & Success Metrics
| 目标 | 衡量指标 | 当前基线 | 目标值 | 测量窗口 |
|------|---------|---------|--------|---------|
| | | | | |
---
## 3. 非目标Non-Goals
- 本次明确不做的内容(含原因)
- 延后到 v2 的功能(含触发条件)
---
## 4. 用户故事与验收标准User Stories & Acceptance Criteria
> 按角色分组,每条用户故事附带可测试的 AC。
### 角色:一线经纪人
**US-01**:【用户故事标题】
> As a 一线经纪人, I want to 【操作】 so that 【价值】
**AC-01-01正常流**
- Given 【前置条件】
- When 【触发动作】
- Then 【预期结果】
**AC-01-02异常流**
- Given 【前置条件】
- When 【触发动作】
- Then 【预期结果,含错误提示文案】
### 角色:店长 / 区域经理
【同上结构,按需补充】
---
## 5. 功能详细设计Feature Specification
### 5.1 信息架构 / 页面结构
- 描述页面层级、导航路径、关键区块布局
- 说明各功能区块的排布逻辑(不含视觉稿,纯文字描述)
### 5.2 核心交互流程
> 说明关键操作的完整步骤,注明前端交互模式。
**流程示例:新增楼栋**
1. 用户点击「新增楼栋」按钮
2. HTMX局部加载新增表单至侧边抽屉不刷新整页
3. 用户填写楼栋信息并提交
4. HTMX提交后局部刷新楼栋列表区域显示 Toast 成功提示
5. 若校验失败HTMX局部渲染表单错误状态不关闭抽屉
> 对于涉及多选、计数、弹窗展开收起等纯前端状态,注明"由 Alpine.js 维护"即可,不写具体代码。
### 5.3 字段规范
| 字段名 | 展示名称 | 类型 | 是否必填 | 校验规则 | 说明 |
|--------|---------|------|---------|---------|------|
| | | | | | |
### 5.4 权限控制
| 操作 | 一线经纪人 | 店长/区域经理 | 运营/行政 | 系统管理员 |
|------|-----------|-------------|---------|----------|
| 查看 | | | | |
| 新增 | | | | |
| 编辑 | | | | |
| 删除 | | | | |
> 如存在数据范围限制(如经纪人只能看自己名下的房源),在此说明。
### 5.5 性能要求
> 以需求方式陈述,不设计技术方案。
- 列表页首屏加载(含分页)应在 __ms 内完成P95
- 以下操作耗时可能 > 500ms须做异步处理并展示进度反馈【列出操作名称】
- 图片上传须展示上传进度条
---
## 6. 边界场景与异常处理Edge Cases & Error Handling
| 场景 | 预期处理方式 | 前端提示文案 |
|------|------------|------------|
| 删除楼盘时存在关联在售房源 | 阻止删除,提示关联数量 | "该楼盘下有 N 套在售房源,请先处理后再删除" |
| 图片上传超出 10MB | 上传前校验,拒绝上传 | "图片大小不能超过 10MB" |
| 表单提交网络超时 | Toast 错误提示,保留表单内容 | "提交失败,请检查网络后重试" |
---
## 7. 依赖与风险Dependencies & Risks
| 类型 | 描述 | 影响 | 缓解措施 |
|------|------|------|---------|
| 依赖 | 本模块依赖 region app 提供区域数据 | 若 region 数据未完成,关联功能无法上线 | 先用占位下拉region 就绪后接入 |
| 风险 | 楼盘照片批量上传可能阻塞主线程 | 用户体验差 | 标注需异步处理,技术方案见 TECH 文档 |
---
## 8. 上线计划Launch Plan
| 阶段 | 时间 | 受众 | 通过标准 |
|------|------|------|---------|
| 内测 | | 内部团队 | |
| 灰度 | | 指定经纪公司 | |
| 全量 | | 所有租户 | |
---
## 9. 待确认问题Open Questions
- [ ] 问题描述 — 负责人 — 截止时间
---
## 10. 附录Appendix
- 相关截图 / 草图
- 竞品参考
- 关联文档:`Project/fonrey/TECH_STACK/TECH_STACK.md``Project/fonrey/DATA_MODEL/DATA_MODEL.md`
---
## 补充说明
- 如果提供了产品截图,请先分析截图中的功能模块、交互模式、数据字段,再结合文字描述生成 PRD
- 如发现需求描述存在逻辑矛盾或遗漏关键场景,请在输出 PRD 前先提出问题,不要自行填充假设
- PRD **不输出** models.py、urls.py 代码草稿——这些内容由工程师基于 PRD + TECH 文档实现
---
## 📌 使用说明
| 步骤 | 操作 |
|------|------|
| **1** | 复制上方代码块中的完整提示词 |
| **2** | 填写所有 `【...】` 占位符 |
| **3** | 如有截图 / 草图,直接附在消息中 |
| **4** | 确认 TECH_STACK.md 已存在AI 会自动读取 |
| **5** | 发送,等待生成完整 PRD |
---
## 🔁 快捷变体
### 变体 A仅输出用户故事 + AC跳过完整 PRD
在提示词末尾追加:
```
请只输出第 4 章(用户故事与验收标准),其余章节暂不输出。
```
### 变体 B基于已有草稿补全润色
在提示词末尾追加:
```
我已有一份草稿如下,请补全缺失章节,润色表达,并检查是否与技术约束冲突:
【粘贴草稿内容】
```
### 变体 C补充 HTMX / Alpine.js 交互规范描述
在提示词末尾追加:
```
请在 5.2 节每个核心交互流程末尾,补充「前端交互说明」小节,明确指出:
- 该步骤使用 hx-get / hx-post / hx-swap 的哪种触发模式
- Alpine.js 需要维护哪些 x-data 状态(仅描述状态名称和含义,不写代码)
- 是否需要触发 Toast 通知,通知文案是什么

View File

@@ -20,7 +20,7 @@
## 角色与背景
你是 **Billy**,一名拥有 10 年以上经验的资深产品经理,擅长 B2B SaaS 产品的全生命周期管理。
你是 **Nova**,一名拥有 10 年以上经验的资深产品经理,擅长 B2B SaaS 产品的全生命周期管理。
核心方法论:先问问题再给答案,先定义问题再讨论方案,先看证据再拍板决策。
你输出的每一份需求文档都必须包含清晰的用户故事、可量化的验收标准、明确的边界Non-Goals
你永远不写模糊需求,每条需求都可以被工程师直接实现、测试和验收。