Files
nexus/Project/fonrey/DATA_MODEL/DATA_MODEL.md
2026-04-29 15:43:49 +08:00

840 lines
47 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
> **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 设计文档
> **作者**: Backend Architect
> **版本**: 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 查询、合规审计
---
## 一、架构决策总览 (Architecture Decision Records)
### 1.1 多租户策略Schema-per-Tenant
```
┌──────────────────────────────────────────────────────────────────────┐
│ PostgreSQL Instance │
│ │
│ ┌─────────────────────────┐ ┌──────────────────┐ ┌────────────┐ │
│ │ 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 │ │
│ └─────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
```
**选型理由**
- `django-tenants` 的 Schema 隔离提供最强的数据安全边界
- 房产经纪公司之间数据绝对不能互通(合规要求)
- 每个 Schema 独立索引,避免全局锁竞争
- 支持按租户独立备份/恢复
### 1.2 核心领域模型关系图
```
[区域/商圈]──────────────────────────────┐
│ │
[学校管理] │
│ ▼
[楼盘/小区] ──── [楼栋] ─────────► [房源] ◄──── [挂牌历史]
│ │
│ ┌────────┼────────┐
│ │ │ │
│ [联系人] [跟进日志] [维护完成度]
│ │ │
│ ┌─────┘ ┌────┴──────┐
│ │ │ │
│ [电话查看] [钥匙] [委托] [实勘]
[客源] ──── [配对记录] ──── [带看记录]
[员工/组织] ──── [权限]
```
### 1.3 关键设计原则
| 原则 | 决策 |
| ----- | -------------------------------------- |
| 主键类型 | `UUID v4`(跨环境安全,避免枚举攻击) |
| 软删除 | 所有核心表含 `deleted_at`(历史可追溯) |
| 时间戳 | 全部使用 `TIMESTAMPTZ`(含时区) |
| 手机号存储 | AES-256-GCM 加密存储,建立 SHA-256 哈希索引 |
| 审计字段 | `created_by`, `updated_by` 全表覆盖 |
| 枚举值 | 业务枚举用 `VARCHAR` + CHECK系统枚举用 lookup 表 |
| 大文本 | `TEXT` 类型不设长度PG 内部优化) |
| 金额 | `NUMERIC(12,2)` 万元精度,避免浮点误差 |
---
## 二、领域概览Domain Overview
本节用业务语言描述系统的核心领域对象及其关系,作为各子模块数据模型的导读。
### 核心领域对象
#### Public Schema平台运营层
| 领域对象 | 表 | 业务说明 |
|----------|-----|----------|
| **Tenant租户** | `public.tenants` | 每家房产经纪公司一条记录含状态机creating/active/suspended/pending_delete/deleted、套餐、联系人 |
| **Domain域名** | `public.domains` | 子域名↔租户映射,支持多域名绑定,子域名创建后不可修改 |
| **TenantStatusLog** | `public.tenant_status_logs` | 租户状态变更不可变审计append-only |
| **PlatformAdmin** | `public.platform_admins` | 平台管理员账号3 种角色:超级管理员/运营人员/只读审计员 |
| **AdminMfaDevice** | `public.admin_mfa_devices` | 管理员 TOTP 设备(强制启用) |
| **AdminSession** | `public.admin_sessions` | 登录会话30 分钟超时,支持强制登出) |
| **IpWhitelist** | `public.ip_whitelist` | 管理控制台 CIDR 白名单 |
| **PlatformAuditLog** | `public.platform_audit_logs` | 所有写操作+高危操作审计append-only建议月度分区 |
| **BackupSchedule** | `public.backup_schedules` | 全局/租户级定时备份计划(频率/保留数/存储目标) |
| **BackupRecord** | `public.backup_records` | 备份任务执行记录(自动/手动/升级前/恢复前) |
| **ExportTask** | `public.export_tasks` | 数据导出异步任务CSV/JSON/SQL Dump24h 下载链接) |
| **SystemVersion** | `public.system_versions` | 平台版本历史,唯一 current 版本约束 |
| **UpgradeEvent** | `public.upgrade_events` | 升级/回滚事件,含灰度租户维度进度快照 |
| **EnumLabel** | `public.enum_labels` | 固定枚举字典(英文 Key → 中文标签),所有租户共享,供前端下拉渲染、导出报表中文标签、日志快照使用 |
#### Tenant Schema租户业务层
| 领域对象 | 表/子文档 | 业务说明 |
|----------|-----------|----------|
| **OrgUnit组织架构** | `org_units` → [DATA_MODEL_ORG.md](./DATA_MODEL_ORG.md) | 树形组织架构(总部/区域/城市/大区/分公司/门店/团队/虚拟团队),物化路径存储,支持权限继承 |
| **Staff员工** | `staff` → [DATA_MODEL_ORG.md](./DATA_MODEL_ORG.md) | 经纪人/店长/经理,绑定组织节点,手机号加密存储,与账号(登录)分离 |
| **District城区** | `districts` → [DATA_MODEL_COMPLEX.md](./DATA_MODEL_COMPLEX.md) | 行政区划,如「静安区」,是区域体系的顶层节点 |
| **BusinessArea商圈** | `business_areas` → [DATA_MODEL_COMPLEX.md](./DATA_MODEL_COMPLEX.md) | 商圈/板块,从属于城区,一个楼盘可归属多个商圈 |
| **School学校** | `schools` → [DATA_MODEL_COMPLEX.md](./DATA_MODEL_COMPLEX.md) | 对口学校数据库,是买家购房决策的核心参考,与楼盘多对多关联 |
| **Complex楼盘/小区)** | `complexes` → [DATA_MODEL_COMPLEX.md](./DATA_MODEL_COMPLEX.md) | 房源录入的基础底座,维护楼盘标准名称/坐标/锁定状态/别名等 |
| **Building楼栋/单元)** | `buildings` → [DATA_MODEL_COMPLEX.md](./DATA_MODEL_COMPLEX.md) | 楼盘下的物理楼栋,区分标准结构与非标结构 |
| **RoomUnit房号** | `room_units` → [DATA_MODEL_COMPLEX.md](./DATA_MODEL_COMPLEX.md) | 楼层+房间号,房源定位的最细粒度 |
| **Property房源** | `properties` → [DATA_MODEL_PROPERTY.md](./DATA_MODEL_PROPERTY.md) | 系统核心表,每套二手房源的完整档案,支持出售/出租/出售兼出租三态 |
| **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 大类别(置业顾问/店管/总经/运营/自定义) |
### 领域关系快速导航
```
District (城区)
└─ BusinessArea (商圈)
└─ Complex (楼盘) ─── School (对口学校)
├─ Building (楼栋)
│ └─ RoomUnit (房号)
└─ Property (房源)
├─ PropertyContact (联系人/委托方)
├─ FollowLog (跟进日志)
├─ Viewing (带看记录) ──── Client (客源)
└─ Match (配对记录) ──────┘
OrgUnit (组织架构)
└─ Staff (员工/经纪人) ─── Property / Client / Viewing / Match
```
### 子文档索引
| 子文档 | 覆盖模块 | 状态 |
|--------|----------|------|
| [DATA_MODEL_PUBLIC.md](./DATA_MODEL_PUBLIC.md) | Public schema 平台运营层tenants, domains, platform_admins, admin_sessions, audit_logs, backup, export, upgrade 共 13 张表) | ✅ 完成 |
| [DATA_MODEL_ORG.md](./DATA_MODEL_ORG.md) | 组织人事org_units, staff, 异动/奖惩/教育/家庭等) | ✅ 完成 |
| [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 | ✅ 完成 |
---
## 三、公共 SchemaShared / Public
> **权威源**:完整 DDL 已迁至 [`DATA_MODEL_PUBLIC.md`](./DATA_MODEL_PUBLIC.md),本节仅保留摘要索引。
> **覆盖范围**`public` schema 存储平台运营层数据——租户注册、管理员账号、审计日志、备份/导出任务、版本升级记录(共 13 张表)。
> **设计依据**:系统管理模块 PRD`PRD/系统管理/系统管理模块PRD.md`)。
### 表清单(开发以 DATA_MODEL_PUBLIC.md 为准)
| 表名 | 说明 | 节 |
|------|------|----|
| `public.tenants` | 租户主表django-tenants 核心,状态机 6 态) | §2.1 |
| `public.domains` | 域名↔租户映射(支持多域名,子域名不可修改) | §2.1 |
| `public.tenant_status_logs` | 租户状态变更不可变审计日志append-only | §2.1 |
| `public.platform_admins` | 平台管理员账号super_admin/ops_operator/read_only_auditor | §2.2 |
| `public.admin_mfa_devices` | 管理员 TOTP MFA 设备(强制启用) | §2.2 |
| `public.admin_sessions` | 管理员登录会话30 min 滚动超时,支持强制登出) | §2.2 |
| `public.ip_whitelist` | 管理控制台 CIDR 白名单 | §2.2 |
| `public.platform_audit_logs` | 所有写操作+高危操作审计append-only建议月度分区 | §2.3 |
| `public.backup_schedules` | 全局/租户级定时备份计划NULL tenant_id = 全局默认) | §2.4 |
| `public.backup_records` | 备份任务执行记录auto/manual/pre_upgrade/pre_restore | §2.4 |
| `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
- `public.tenants.schema_name` 创建后**不可修改**
- `public.tenants` 不再使用 `is_active` boolean改用 6 态 `status` 枚举
- `platform_admins` 与租户 `staff` **完全独立**,不共享 auth 系统
- `system_versions` 通过部分唯一索引确保全局只有一个 `status='current'`
---
<!-- §三 DDL 已迁至 DATA_MODEL_PUBLIC.md v1.02026-04-24 -->
## 四、租户 SchemaTenant Schema
以下所有表均在每个租户的独立 Schema 内创建。
---
### 3.1 组织人事模块Organization & HR
> **详细模型** → 见 [`DATA_MODEL_ORG.md`](./DATA_MODEL_ORG.md)
> 该文件为权威定义,包含完整字段、枚举、查询模式和禁止操作。
**核心表概览**(开发时以 DATA_MODEL_ORG.md 为准):
| 表名 | 说明 |
|------|------|
| `org_units` | 组织树节点(公司/事业部/大区/区域/片区/门店/店组/职能),物化路径树 |
| `staff` | 员工主表含加密手机号、角色、在职状态、Django auth 绑定 |
| `staff_personal_info` | 员工个人信息扩展证件、学历、婚育等1:1 |
| `staff_transfer_logs` | 人事异动不可变审计日志(入职/调动/离职/复职等) |
| `staff_reward_punish` | 奖惩记录 |
| `staff_work_experiences` | 工作经历 |
| `staff_educations` | 教育经历 |
| `staff_trainings` | 培训经历 |
| `staff_family_members` | 家庭成员 |
| `staff_accounts` | 第三方平台账号绑定58安居客/中国网络经纪人等) |
**关键约束提示**
- `staff.phone_enc` AES-256-GCM 加密,`staff.phone_hash` SHA-256 用于唯一索引
- `staff_transfer_logs` **无 deleted_at**,不可删除
- `org_units` 路径查询:`WHERE path LIKE '/root/{target_id}/%'`
- 员工离职:`status = 'resigned'` + `deleted_at` 软删除,记录永久保留
---
### 3.2 区域与楼盘模块Region & Complex Management
> **详细模型** → 见 [`DATA_MODEL_COMPLEX.md`](./DATA_MODEL_COMPLEX.md)
> 本节仅作概览,开发时以 DATA_MODEL_COMPLEX.md 为权威定义。
**核心表概览**(开发时以 DATA_MODEL_COMPLEX.md 为准):
| 表名 | 说明 | 关键字段 |
|------|------|----------|
| `districts` | 城区/行政区 | `city`, `name`, `short_name`, `sort_order` |
| `business_areas` | 商圈/板块(从属于城区) | `district_id`, `name`, `latitude`, `longitude` |
| `metro_lines` | 地铁线路 | `city`, `name`, `color` |
| `metro_stations` | 地铁站点 | `metro_line_id`, `name`, `latitude`, `longitude` |
| `schools` | 学校(对口学区) | `district_id`, `name`, `type`, `nature`, `level` |
| `complexes` | 楼盘/小区(房源底座) | `name`, `district_id`, `address`, `latitude/longitude`, `lock_*`, `search_vector` |
| `complex_aliases` | 楼盘别名(含系统别名/用户自定义别名) | `complex_id`, `alias`, `is_system` |
| `complex_business_areas` | 楼盘↔商圈多对多(含主商圈标识) | `complex_id`, `business_area_id`, `is_primary` |
| `complex_schools` | 楼盘↔学校关联(含学区类型) | `complex_id`, `school_id`, `zone_type` |
| `complex_metro_stations` | 楼盘↔地铁站关联(含步行距离) | `complex_id`, `station_id`, `distance_meters` |
| `buildings` | 楼栋/单元 | `complex_id`, `name`, `is_standard`, `total_floors` |
| `room_units` | 房号/结构单元(楼层+房间号) | `building_id`, `floor`, `room_no`, `is_standard` |
| `complex_photos` | 楼盘照片(楼盘图/户型图/VR | `complex_id`, `category`, `file_key`, `is_cover` |
| `complex_attachments` | 楼盘附件 | `complex_id`, `file_key`, `file_name` |
| `complex_price_trends` | 楼盘价格走势(月度) | `complex_id`, `record_month`, `avg_unit_price` |
---
### 3.3 房源模块Property Management
> **详细模型** → 见 [`DATA_MODEL_PROPERTY.md`](./DATA_MODEL_PROPERTY.md)
> 本节仅作概览,开发时以 DATA_MODEL_PROPERTY.md 为权威定义。
**核心表概览**(开发时以 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` |
**关键约束提示**
- `property_contacts.phone_hash` 是重复房源检测的主要依据,录入前必须查重
- `listing_histories` / `price_changes` **无 deleted_at**,不可删除
- `follow_logs``is_deletable=FALSE``sensitive_view` 类型)不可软删
- `completeness_score` 只由 Celery 任务写入Application 层禁止直接更新
- `last_followed_at` 由触发器 `trg_update_last_followed` 自动维护
- `property_photos.is_cover` 唯一约束:每套房源仅一张封面
---
### 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)
> 该文件为权威定义,包含完整字段、枚举、状态机、查询模式和禁止操作。
**核心表概览**(开发时以 DATA_MODEL_CLIENT.md 为准):
| 表名 | 说明 |
|------|------|
| `clients` | 客源主表(私客/公客/成交客),含加密手机号哈希、活跃度、归属人 |
| `client_contacts` | 联系人1:N手机号加密+哈希,支持多联系人 |
| `client_requirements` | 需求信息(可多类型:二手/新房/租房),含预算/面积/商圈/朝向等偏好 |
| `client_follow_logs` | 跟进日志高写入频率5种类型敏感查看类型不可删 |
| `client_follow_log_attachments` | 跟进附件(图片/录音最大20MB |
| `client_viewings` | 带看/预约记录1:N含陪看人/合作带看人) |
| `client_property_matches` | 智能配房结果(录客配房/系统配房,匹配度评分) |
| `client_status_logs` | 状态变更不可变审计日志(改状态/改等级/转公/转成交/转无效等) |
| `client_favorite_folders` | 私客收藏夹(经纪人自定义分组) |
| `client_folder_items` | 收藏夹与客源的多对多关联 |
| `client_school_preferences` | 意向学校(拆表,支持精确查询) |
**关键约束提示**
- `client_contacts.phone_hash` 是重复客源检测的唯一依据,录入前必须查重
- `client_status_logs` **无 deleted_at**,不可删除
- 私客超时(配置天数内无跟进)→ Celery 自动转公(`transfer_to_public_type = 'auto'`
- 活跃度 `activity_level` 由 Celery 每日凌晨批量计算,不实时更新
---
### 3.18 系统设置System Settings
> **归属说明**
> - `lookup_categories` / `lookup_items` / `saved_filters` 为**跨模块**系统表,权威定义在本节。
> - `property_tags` / `property_tag_relations` / `property_favorites` / `property_protections` / `number_holder_approvals` 属房源模块配套表,**权威定义已迁至** [`DATA_MODEL_PROPERTY.md §4.19-§4.22`](./DATA_MODEL_PROPERTY.md),本节不再重复 DDL修复 S1/S2
```sql
-- ============================================================
-- 枚举/选项管理:跟进目的、标签、来源渠道 等运营维护的枚举值
-- ============================================================
CREATE TABLE lookup_categories (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code VARCHAR(50) UNIQUE NOT NULL, -- 如follow_purpose, property_source
name VARCHAR(100) NOT NULL,
module VARCHAR(30) NOT NULL -- property/client/system
);
CREATE TABLE lookup_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
category_id UUID NOT NULL REFERENCES lookup_categories(id) ON DELETE CASCADE,
value VARCHAR(100) NOT NULL,
label VARCHAR(100) NOT NULL, -- 显示文本
sort_order INTEGER NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
metadata JSONB NOT NULL DEFAULT '{}', -- 扩展属性
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_lookup_items_category ON lookup_items(category_id)
WHERE is_active = TRUE;
CREATE UNIQUE INDEX idx_lookup_items_value ON lookup_items(category_id, value);
-- 筛选方案(保存的搜索条件,跨模块通用)
CREATE TABLE saved_filters (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
staff_id UUID NOT NULL REFERENCES staff(id) ON DELETE CASCADE,
name VARCHAR(100) NOT NULL,
module VARCHAR(20) NOT NULL DEFAULT 'property',
filter_params JSONB NOT NULL, -- 完整筛选参数 JSON
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_saved_filters_staff ON saved_filters(staff_id, module);
```
**已迁出本节的表**(权威源见子文档):
| 表名 | 权威定义位置 |
|------|-------------|
| `property_tags` | [`DATA_MODEL_PROPERTY.md §4.19`](./DATA_MODEL_PROPERTY.md) |
| `property_tag_relations` | [`DATA_MODEL_PROPERTY.md §4.19`](./DATA_MODEL_PROPERTY.md) |
| `property_favorites` | [`DATA_MODEL_PROPERTY.md §4.20`](./DATA_MODEL_PROPERTY.md) |
| `property_protections` | [`DATA_MODEL_PROPERTY.md §4.21`](./DATA_MODEL_PROPERTY.md) |
| `number_holder_approvals` | [`DATA_MODEL_PROPERTY.md §4.22`](./DATA_MODEL_PROPERTY.md) |
---
### 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` | 客源状态8 态) | `clients.status` |
| `client.grade` | 客源等级5 档 + E | `clients.grade` |
| `client.requirement_type` | 需求类型(旧:`client.purpose_type` | `client_requirements.requirement_type` |
| `client.property_usage` | 房源用途偏好(旧:`client.usage` | `client_requirements.property_usage` |
| `client.orientation` | 朝向偏好 | `client_requirements.orientation` |
| `client.payment_method` | 付款方式 | `clients.payment_method` |
| `property.status` | 房源状态 | `properties.status` |
| `property.attribute` | 房源属性(公/私/保护) | `properties.attribute` |
| `property.property_type` | 房源类型(旧:`property.usage` | `properties.property_type` |
| `property.grade` | 房源等级5 档) | `properties.grade` |
| `property.listing_history.listing_type` | 挂牌类型(旧:`property.listing_type` | `listing_histories.listing_type` |
| `property.decoration` | 装修程度 | `properties.decoration` |
| `property.orientation` | 朝向 | `properties.orientation` |
| `property.commission.status` | 委托状态(旧:`commission.type` | `commissions.status` |
| `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 房源列表页核心查询分析
```sql
-- 典型查询:出售状态 + 公盘 + 特定区域 + 价格区间 + 户型筛选 + 按挂牌日期排序
-- 优化方案:复合索引覆盖最高频维度组合
-- 高频组合索引status + attribute覆盖 90% 的列表查询)
CREATE INDEX idx_properties_list_composite ON properties
(status, attribute, complex_id, sale_price DESC NULLS LAST)
WHERE deleted_at IS NULL;
-- 与我相关查询(经纪人个人仪表板)
CREATE INDEX idx_properties_my_properties ON properties
(seller_agent_id, status, listed_at DESC NULLS LAST)
WHERE deleted_at IS NULL;
```
### 4.2 全文搜索触发器(自动维护 search_vector
```sql
-- 房源全文检索向量更新触发器
CREATE OR REPLACE FUNCTION update_property_search_vector()
RETURNS TRIGGER AS $$
BEGIN
NEW.search_vector :=
setweight(to_tsvector('simple', COALESCE(NEW.block_no, '') ||
' ' || COALESCE(NEW.unit_no, '') ||
' ' || COALESCE(NEW.room_no, '')), 'A') ||
setweight(to_tsvector('simple', COALESCE(NEW.remarks, '')), 'C');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_property_search_vector
BEFORE INSERT OR UPDATE OF block_no, unit_no, room_no, remarks
ON properties
FOR EACH ROW EXECUTE FUNCTION update_property_search_vector();
-- 楼盘全文检索向量(含别名,提升模糊搜索精度)
CREATE OR REPLACE FUNCTION update_complex_search_vector()
RETURNS TRIGGER AS $$
BEGIN
NEW.search_vector :=
setweight(to_tsvector('simple', COALESCE(NEW.name, '')), 'A') ||
setweight(to_tsvector('simple', COALESCE(NEW.alias, '')), 'B') ||
setweight(to_tsvector('simple', COALESCE(NEW.address, '')), 'C');
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_complex_search_vector
BEFORE INSERT OR UPDATE OF name, alias, address
ON complexes
FOR EACH ROW EXECUTE FUNCTION update_complex_search_vector();
```
### 4.3 last_followed_at 自动维护触发器
```sql
-- 每次写入跟进日志时,自动更新 properties.last_followed_at
CREATE OR REPLACE FUNCTION update_property_last_followed()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.log_type = 'written' THEN
UPDATE properties
SET last_followed_at = NEW.created_at,
updated_at = NOW()
WHERE id = NEW.property_id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_update_last_followed
AFTER INSERT ON follow_logs
FOR EACH ROW EXECUTE FUNCTION update_property_last_followed();
```
---
## 六、Redis 缓存策略
### 5.1 缓存 Key 规范
```
# 格式:{tenant_schema}:{module}:{entity}:{id}:{field}
# TTL 单位:秒
# 房源详情(高频读取)
{schema}:prop:detail:{property_id} TTL: 300 (5分钟)
# 房源联系人含解密号码敏感TTL 短)
{schema}:prop:contacts:{property_id} TTL: 60 (1分钟)
# 楼盘基础信息(低变更频率)
{schema}:complex:base:{complex_id} TTL: 3600 (1小时)
# 楼盘名称自动补全候选列表(联想搜索)
{schema}:complex:autocomplete:{prefix} TTL: 600 (10分钟)
# 员工信息(用于日志快照)
{schema}:staff:base:{staff_id} TTL: 1800 (30分钟)
# 枚举值/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
# 维护完成度Celery 计算后写入,详情页直接读 Redis
{schema}:prop:completeness:{property_id} TTL: 600
# 房源列表计数(筛选后总条数,避免 COUNT(*) 全扫)
{schema}:prop:count:{filter_hash} TTL: 30 (短TTL保证准确性)
```
### 5.2 缓存失效策略
```python
# Django Signal 驱动的缓存失效(在 models.py 中注册)
# 房源更新 → 失效详情缓存 + 完成度缓存
# 跟进日志新增 → 失效 last_followed_at 缓存
# 联系人更新 → 失效联系人缓存(立即)
# 楼盘更新 → 失效楼盘缓存 + 相关房源缓存(批量)
# 枚举更新 → 失效对应 lookup 缓存
```
---
## 七、Django Model 层设计要点
### 6.1 抽象基类
```python
# models/base.py
import uuid
from django.db import models
class UUIDPrimaryKeyModel(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
class Meta:
abstract = True
class TimeStampedModel(UUIDPrimaryKeyModel):
created_at = models.DateTimeField(auto_now_add=True, db_index=False)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
class SoftDeleteModel(TimeStampedModel):
deleted_at = models.DateTimeField(null=True, blank=True, db_index=False)
class Meta:
abstract = True
def soft_delete(self, deleted_by=None):
from django.utils import timezone
self.deleted_at = timezone.now()
self.save(update_fields=['deleted_at', 'updated_at'])
class AuditedModel(SoftDeleteModel):
created_by = models.ForeignKey(
'staff.Staff', null=True, on_delete=models.SET_NULL,
related_name='+', db_column='created_by'
)
updated_by = models.ForeignKey(
'staff.Staff', null=True, on_delete=models.SET_NULL,
related_name='+', db_column='updated_by'
)
class Meta:
abstract = True
```
### 6.2 加密字段 Mixin
```python
# utils/encryption.py
# 手机号加密AES-256-GCM + SHA-256 哈希索引
class EncryptedPhoneField:
"""
存储时phone → AES加密 → phone_enc (BYTEA)
phone → SHA256 → phone_hash (VARCHAR 64)
查询时phone_hash 走索引phone_enc 解密展示
打码展示前3位明文 + ******* + 后3位
"""
pass
```
### 6.3 Manager 过滤软删除
```python
class ActiveManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(deleted_at__isnull=True)
class PropertyManager(ActiveManager):
def public(self):
return self.get_queryset().filter(attribute='public')
def mine(self, staff_id):
return self.get_queryset().filter(seller_agent_id=staff_id)
```
---
## 八、数据量与性能预测
| 表名 | 预估行数 | 增长速度 | 分区策略 |
|------|---------|---------|---------|
| `properties` | 89,000+ | 中速 | 暂不分区,建议 500k 后按 `created_at` RANGE 分区 |
| `follow_logs` | 200万+ | 高速(最高频写入) | ✅ `PARTITION BY RANGE (created_at)` 月度分区 |
| `property_photos` | 500万+ | 高速 | ✅ `PARTITION BY RANGE (created_at)` 月度分区 |
| `permission_change_logs` | 100万+ | 中高速 | ✅ `PARTITION BY RANGE (operated_at)` 月度分区 |
| `login_attempts` | 500万+ | 高速(每次登录一条) | ✅ `PARTITION BY RANGE (attempted_at)` 月度分区 |
| `platform_audit_logs` | 10万+ | 低中速 | ✅ `PARTITION BY RANGE (created_at)` 月度分区 |
| `price_changes` | 50万 | 中速 | 无需分区 |
| `listing_histories` | 20万 | 低速 | 无需分区 |
| `clients` | 10万+ | 中速 | 暂不分区 |
| `viewings` | 100万 | 中速 | 无需分区 |
### 8.1 分区维护策略partition_maintenance_task
所有月度分区表统一由 **Celery Beat 定时任务** `partition_maintenance_task` 维护,每月 1 日凌晨 01:00UTC+8自动执行
```python
# apps/property/tasks.py及 permission/login/shared 各 App 对应任务)
@app.task(name="partition_maintenance_task")
def partition_maintenance_task():
"""
为下一个月预建所有分区表的分区。
- 检查是否已存在目标分区,幂等执行
- 失败时发送 Sentry 告警
"""
tables = [
("follow_logs", "created_at"),
("property_photos", "created_at"),
("permission_change_logs", "operated_at"),
("login_attempts", "attempted_at"),
("public.platform_audit_logs", "created_at"),
]
next_month = date.today().replace(day=1) + relativedelta(months=1)
month_start = next_month
month_end = next_month + relativedelta(months=1)
for table, _key in tables:
suffix = month_start.strftime("%Y_%m")
part_name = f"{table.replace('.', '_')}_{suffix}"
sql = f"""
CREATE TABLE IF NOT EXISTS {part_name}
PARTITION OF {table}
FOR VALUES FROM ('{month_start}') TO ('{month_end}');
"""
with connection.cursor() as cursor:
cursor.execute(sql)
```
**Celery Beat 配置**`celery.py`
```python
app.conf.beat_schedule["partition_maintenance_task"] = {
"task": "partition_maintenance_task",
"schedule": crontab(day_of_month=1, hour=1, minute=0), # 每月1日 01:00 UTC+8
}
```
> ⚠️ **注意**:每张分区表均保留一个 `_default` 默认分区作为兜底,防止任务失败时写入报错。`_default` 分区数据应在运维 SOP 中周期性检查(有数据则说明提前建分区失败)。
---
## 九、必须在开发启动前明确的数据架构决策
| 决策项 | 推荐方案 | 风险 |
|-------|---------|------|
| 小区数据来源 | 预导入基础数据(安居客/链家 API+ 支持手动新增兜底 | 高:影响录入体验 |
| 私盘可见范围 | 录入人所在门店可见(综合业务需求) | 需与权限模块约定 |
| 号码查看权限 | 角色级控制:经纪人限查自己相关房源,店长无限制 | 需合规确认 |
| 重复房源主键 | 主键:手机号 hash辅助小区+楼栋+单元+房号)组合 | 需双重校验 |
| 跟进目的枚举 | 存 lookup_items 表,运营可维护 | 初始化数据需提前收集 |
| 手机号加密算法 | AES-256-GCM密钥存 Django settings生产用 Vault | 密钥管理需单独规划 |
---
*本文档为 Fonrey 系统 DATA MODEL v1.0,随 PRD 迭代同步更新。*
*下一步建议API 接口规范URL 设计 + Request/Response Schema*