Files
nexus/Project/fonrey/DATA_MODEL/DATA_MODEL.md

622 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Fonrey 房产经纪管理系统 — DATA MODEL 设计文档
> **作者**: Backend Architect
> **版本**: v1.3
> **日期**: 2026-04-24v1.1 修复 S1/S2/S4v1.2 扩展 public schemav1.3 §三 DDL 迁至 DATA_MODEL_PUBLIC.md本文改为索引
> **技术栈**: 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 │ │ - 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 │ │
│ └─────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
```
**选型理由**
- `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` | 升级/回滚事件,含灰度租户维度进度快照 |
#### 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) | 系统/人工推荐的客源↔房源配对 |
### 领域关系快速导航
```
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 张表,含跟进/钥匙/委托/实勘/营销/产证/完成度/标签/收藏/保护/号码方审批等) | ✅ 完成 |
---
## 三、公共 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 |
**关键约束提示**
- `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.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) |
---
## 五、关键索引汇总与查询优化策略
### 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小时)
# 标签列表
{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万+ | 高速(最高频写入) | 按 `created_at` 月度 RANGE 分区 |
| `property_photos` | 500万+ | 高速 | 按 `property_id` HASH 分区16分区 |
| `price_changes` | 50万 | 中速 | 无需分区 |
| `listing_histories` | 20万 | 低速 | 无需分区 |
| `clients` | 10万+ | 中速 | 暂不分区 |
| `viewings` | 100万 | 中速 | 无需分区 |
---
## 九、必须在开发启动前明确的数据架构决策
| 决策项 | 推荐方案 | 风险 |
|-------|---------|------|
| 小区数据来源 | 预导入基础数据(安居客/链家 API+ 支持手动新增兜底 | 高:影响录入体验 |
| 私盘可见范围 | 录入人所在门店可见(综合业务需求) | 需与权限模块约定 |
| 号码查看权限 | 角色级控制:经纪人限查自己相关房源,店长无限制 | 需合规确认 |
| 重复房源主键 | 主键:手机号 hash辅助小区+楼栋+单元+房号)组合 | 需双重校验 |
| 跟进目的枚举 | 存 lookup_items 表,运营可维护 | 初始化数据需提前收集 |
| 手机号加密算法 | AES-256-GCM密钥存 Django settings生产用 Vault | 密钥管理需单独规划 |
---
*本文档为 Fonrey 系统 DATA MODEL v1.0,随 PRD 迭代同步更新。*
*下一步建议API 接口规范URL 设计 + Request/Response Schema*