1532 lines
61 KiB
Markdown
1532 lines
61 KiB
Markdown
# Fonrey 房产经纪管理系统 — DATA MODEL 设计文档
|
||
|
||
> **作者**: Backend Architect
|
||
> **版本**: v1.0
|
||
> **日期**: 2026-04-24
|
||
> **技术栈**: 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 │ │
|
||
│ │ (shared) │ │ schema │ │ schema │ │
|
||
│ │ │ │ │ │ │ │
|
||
│ │ - tenants │ │ - properties │ │ - properties │ │
|
||
│ │ - domains │ │ - clients │ │ - clients │ │
|
||
│ │ │ │ - complexes │ │ - complexes │ │
|
||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
**选型理由**:
|
||
- `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)` 万元精度,避免浮点误差 |
|
||
|
||
---
|
||
|
||
## 二、公共 Schema(Shared / Public)
|
||
|
||
```sql
|
||
-- ============================================================
|
||
-- 文件: shared_schema.sql
|
||
-- 用途: django-tenants 公共 Schema,存放租户注册信息
|
||
-- ============================================================
|
||
|
||
-- 租户表(每家房产公司一条记录)
|
||
CREATE TABLE public.tenants (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
schema_name VARCHAR(63) UNIQUE NOT NULL, -- PG schema 名,最长 63 字符
|
||
name VARCHAR(255) NOT NULL, -- 公司名称
|
||
short_name VARCHAR(100), -- 简称/品牌名
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||
paid_until DATE, -- 订阅到期日
|
||
on_trial BOOLEAN NOT NULL DEFAULT TRUE,
|
||
extra JSONB NOT NULL DEFAULT '{}' -- 预留扩展字段
|
||
);
|
||
|
||
-- 域名映射表(支持多域名绑定一个租户)
|
||
CREATE TABLE public.domains (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
domain VARCHAR(253) UNIQUE NOT NULL, -- 含子域名的完整域名
|
||
tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,
|
||
is_primary BOOLEAN NOT NULL DEFAULT FALSE,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
|
||
CREATE INDEX idx_domains_tenant ON public.domains(tenant_id);
|
||
CREATE INDEX idx_domains_primary ON public.domains(tenant_id) WHERE is_primary = TRUE;
|
||
```
|
||
|
||
---
|
||
|
||
## 三、租户 Schema(Tenant Schema)
|
||
|
||
以下所有表均在每个租户的独立 Schema 内创建。
|
||
|
||
---
|
||
|
||
### 3.1 组织人事模块(Organization & HR)
|
||
|
||
```sql
|
||
-- ============================================================
|
||
-- 组织架构:公司 → 区域 → 门店 → 组
|
||
-- ============================================================
|
||
|
||
-- 组织节点表(树形结构,支持无限层级)
|
||
CREATE TABLE org_units (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
name VARCHAR(100) NOT NULL,
|
||
type VARCHAR(20) NOT NULL
|
||
CHECK (type IN ('company','region','store','group')),
|
||
parent_id UUID REFERENCES org_units(id) ON DELETE RESTRICT,
|
||
path TEXT NOT NULL, -- 物化路径:/root_id/parent_id/self_id/
|
||
depth SMALLINT NOT NULL DEFAULT 0,
|
||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
deleted_at TIMESTAMPTZ
|
||
);
|
||
|
||
CREATE INDEX idx_org_units_parent ON org_units(parent_id) WHERE deleted_at IS NULL;
|
||
CREATE INDEX idx_org_units_path ON org_units USING gist(path gist_trgm_ops);
|
||
-- 注:gist_trgm_ops 需要 pg_trgm 扩展,用于路径前缀查询
|
||
|
||
-- 员工表
|
||
CREATE TABLE staff (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
org_unit_id UUID NOT NULL REFERENCES org_units(id) ON DELETE RESTRICT,
|
||
name VARCHAR(50) NOT NULL,
|
||
phone_hash VARCHAR(64), -- SHA-256 哈希,用于唯一性校验
|
||
phone_enc BYTEA, -- AES-256-GCM 加密后的手机号
|
||
email VARCHAR(255),
|
||
role VARCHAR(30) NOT NULL
|
||
CHECK (role IN ('agent','store_manager','admin','operator','system')),
|
||
job_title VARCHAR(100), -- 职务描述
|
||
avatar_key TEXT, -- R2/S3 存储路径
|
||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||
joined_at DATE,
|
||
left_at DATE,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
deleted_at TIMESTAMPTZ,
|
||
|
||
-- 关联 Django auth user(用于登录认证)
|
||
user_id INTEGER UNIQUE -- FK to django auth_user
|
||
);
|
||
|
||
CREATE INDEX idx_staff_org ON staff(org_unit_id) WHERE deleted_at IS NULL;
|
||
CREATE INDEX idx_staff_role ON staff(role) WHERE deleted_at IS NULL;
|
||
CREATE UNIQUE INDEX idx_staff_phone_hash ON staff(phone_hash) WHERE deleted_at IS NULL;
|
||
```
|
||
|
||
---
|
||
|
||
### 3.2 区域与楼盘模块(Region & Complex Management)
|
||
|
||
```sql
|
||
-- ============================================================
|
||
-- 行政区 → 商圈 → 楼盘/小区 → 楼栋
|
||
-- 注:楼盘数据是房源录入的基础底座,数据质量直接影响房源录入效率
|
||
-- ============================================================
|
||
|
||
-- 城市/行政区
|
||
CREATE TABLE districts (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
name VARCHAR(50) NOT NULL,
|
||
city VARCHAR(50) NOT NULL DEFAULT '',
|
||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||
is_active BOOLEAN NOT NULL DEFAULT TRUE
|
||
);
|
||
|
||
-- 商圈/板块
|
||
CREATE TABLE business_areas (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
district_id UUID NOT NULL REFERENCES districts(id) ON DELETE RESTRICT,
|
||
name VARCHAR(100) NOT NULL,
|
||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||
is_active BOOLEAN NOT NULL DEFAULT TRUE
|
||
);
|
||
|
||
CREATE INDEX idx_business_areas_district ON business_areas(district_id);
|
||
|
||
-- 地铁线路
|
||
CREATE TABLE metro_lines (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
name VARCHAR(50) NOT NULL,
|
||
color VARCHAR(7) -- 线路颜色 HEX
|
||
);
|
||
|
||
-- 地铁站
|
||
CREATE TABLE metro_stations (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
metro_line_id UUID NOT NULL REFERENCES metro_lines(id) ON DELETE CASCADE,
|
||
name VARCHAR(50) NOT NULL,
|
||
sort_order INTEGER NOT NULL DEFAULT 0
|
||
);
|
||
|
||
-- 学校
|
||
CREATE TABLE schools (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
district_id UUID REFERENCES districts(id) ON DELETE SET NULL,
|
||
name VARCHAR(100) NOT NULL,
|
||
type VARCHAR(20) -- 小学/初中/高中/九年一贯制 等
|
||
);
|
||
|
||
CREATE INDEX idx_schools_district ON schools(district_id);
|
||
|
||
-- 楼盘/小区(核心基础表)
|
||
CREATE TABLE complexes (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
name VARCHAR(200) NOT NULL,
|
||
alias VARCHAR(200), -- 别名/曾用名
|
||
district_id UUID REFERENCES districts(id) ON DELETE SET NULL,
|
||
business_area_id UUID REFERENCES business_areas(id) ON DELETE SET NULL,
|
||
address VARCHAR(500),
|
||
latitude NUMERIC(10,7),
|
||
longitude NUMERIC(10,7),
|
||
|
||
-- 楼盘物理属性
|
||
developer VARCHAR(200), -- 开发商
|
||
property_company VARCHAR(200), -- 物业公司
|
||
property_fee NUMERIC(8,2), -- 物业费 元/㎡/月
|
||
green_rate NUMERIC(5,2), -- 绿化率 %
|
||
plot_ratio NUMERIC(5,2), -- 容积率
|
||
built_year SMALLINT, -- 竣工年份
|
||
ownership_years VARCHAR(20), -- 产权年限枚举
|
||
|
||
-- 配套信息
|
||
has_elevator BOOLEAN,
|
||
parking_info TEXT, -- 车位情况描述
|
||
|
||
-- 全文检索向量(定期更新)
|
||
search_vector TSVECTOR,
|
||
|
||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
deleted_at TIMESTAMPTZ,
|
||
created_by UUID REFERENCES staff(id) ON DELETE SET NULL
|
||
);
|
||
|
||
CREATE INDEX idx_complexes_district ON complexes(district_id) WHERE deleted_at IS NULL;
|
||
CREATE INDEX idx_complexes_name_trgm ON complexes USING gin(name gin_trgm_ops);
|
||
CREATE INDEX idx_complexes_search ON complexes USING gin(search_vector);
|
||
CREATE INDEX idx_complexes_geo ON complexes(latitude, longitude) WHERE deleted_at IS NULL;
|
||
|
||
-- 楼盘与商圈多对多(一个楼盘可跨多个商圈)
|
||
CREATE TABLE complex_business_areas (
|
||
complex_id UUID NOT NULL REFERENCES complexes(id) ON DELETE CASCADE,
|
||
business_area_id UUID NOT NULL REFERENCES business_areas(id) ON DELETE CASCADE,
|
||
PRIMARY KEY (complex_id, business_area_id)
|
||
);
|
||
|
||
-- 楼盘与学校关联
|
||
CREATE TABLE complex_schools (
|
||
complex_id UUID NOT NULL REFERENCES complexes(id) ON DELETE CASCADE,
|
||
school_id UUID NOT NULL REFERENCES schools(id) ON DELETE CASCADE,
|
||
school_zone VARCHAR(50), -- 学区情况:对口/参考等
|
||
PRIMARY KEY (complex_id, school_id)
|
||
);
|
||
|
||
-- 楼盘与地铁站关联
|
||
CREATE TABLE complex_metro_stations (
|
||
complex_id UUID NOT NULL REFERENCES complexes(id) ON DELETE CASCADE,
|
||
station_id UUID NOT NULL REFERENCES metro_stations(id) ON DELETE CASCADE,
|
||
distance_meters INTEGER, -- 步行距离(米)
|
||
PRIMARY KEY (complex_id, station_id)
|
||
);
|
||
|
||
-- 楼栋
|
||
CREATE TABLE buildings (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
complex_id UUID NOT NULL REFERENCES complexes(id) ON DELETE CASCADE,
|
||
name VARCHAR(50) NOT NULL, -- 楼栋名,如"1号楼"
|
||
total_floors SMALLINT NOT NULL,
|
||
has_elevator BOOLEAN,
|
||
building_type VARCHAR(30), -- 楼型:板楼/塔楼/板塔结合
|
||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
|
||
CREATE INDEX idx_buildings_complex ON buildings(complex_id);
|
||
```
|
||
|
||
---
|
||
|
||
### 3.3 房源核心模块(Property Core)
|
||
|
||
```sql
|
||
-- ============================================================
|
||
-- 房源主表:系统最核心的表,全部筛选/排序/搜索围绕此表展开
|
||
-- 设计重点:89,000+ 数据量,复合索引策略,分区预留
|
||
-- ============================================================
|
||
|
||
CREATE TABLE properties (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
||
-- ── 基础分类 ──
|
||
property_type VARCHAR(20) NOT NULL
|
||
CHECK (property_type IN ('residential','villa','commercial_residential',
|
||
'shop','office','other')),
|
||
-- residential=住宅, villa=别墅, commercial_residential=商住,
|
||
-- shop=商铺, office=写字楼, other=其他
|
||
|
||
-- ── 交易状态 ──
|
||
status VARCHAR(20) NOT NULL DEFAULT 'for_sale'
|
||
CHECK (status IN ('for_sale','for_rent','for_sale_rent',
|
||
'suspended','sold_elsewhere','rented_elsewhere',
|
||
'sold','unlisted')),
|
||
-- for_sale=出售, for_rent=出租, for_sale_rent=租售,
|
||
-- suspended=暂缓, sold_elsewhere=他售, rented_elsewhere=他租,
|
||
-- sold=成交, unlisted=未挂牌
|
||
|
||
-- ── 流通属性 ──
|
||
attribute VARCHAR(20) NOT NULL DEFAULT 'public'
|
||
CHECK (attribute IN ('public','private','special','sealed')),
|
||
-- public=公盘, private=私盘, special=特盘, sealed=封盘
|
||
private_reason TEXT, -- 私盘/封盘必填说明
|
||
|
||
-- ── 位置信息 ──
|
||
complex_id UUID NOT NULL REFERENCES complexes(id) ON DELETE RESTRICT,
|
||
building_id UUID REFERENCES buildings(id) ON DELETE SET NULL,
|
||
block_no VARCHAR(30), -- 栋/幢/弄号
|
||
unit_no VARCHAR(30), -- 单元号
|
||
room_no VARCHAR(30), -- 房号/门牌号
|
||
floor SMALLINT NOT NULL, -- 所在楼层
|
||
total_floors SMALLINT NOT NULL, -- 总楼层
|
||
CONSTRAINT chk_floor CHECK (floor > 0 AND floor <= total_floors),
|
||
|
||
-- ── 户型 ──
|
||
bedroom_count SMALLINT NOT NULL DEFAULT 0, -- 室
|
||
living_room_count SMALLINT NOT NULL DEFAULT 0, -- 厅
|
||
bathroom_count SMALLINT NOT NULL DEFAULT 0, -- 卫
|
||
kitchen_count SMALLINT NOT NULL DEFAULT 0, -- 厨
|
||
balcony_count SMALLINT NOT NULL DEFAULT 0, -- 阳台数
|
||
|
||
-- ── 面积 ──
|
||
area NUMERIC(8,2) NOT NULL, -- 建筑面积 m²
|
||
inner_area NUMERIC(8,2), -- 套内面积 m²(编辑时填写)
|
||
|
||
-- ── 价格 ──
|
||
sale_price NUMERIC(12,2), -- 挂牌售价(万元)
|
||
sale_bottom_price NUMERIC(12,2), -- 售底价(万元,内部可见)
|
||
sale_record_price NUMERIC(12,2), -- 备案/核验价(万元)
|
||
rent_price NUMERIC(10,2), -- 挂牌租价(元/月)
|
||
|
||
-- ── 基础物理属性 ──
|
||
orientation VARCHAR(10)
|
||
CHECK (orientation IN ('east','south','west','north',
|
||
'southeast','northeast','east_west',
|
||
'south_north','northwest','southwest')),
|
||
decoration VARCHAR(10)
|
||
CHECK (decoration IN ('rough','plain','simple','medium',
|
||
'fine','luxury')),
|
||
-- rough=毛坯, plain=清水, simple=简装, medium=中装, fine=精装, luxury=豪装
|
||
|
||
has_elevator BOOLEAN,
|
||
built_year SMALLINT,
|
||
|
||
-- ── 用途 ──
|
||
usage_type VARCHAR(30), -- 住宅/商住/商业/普通住宅/花园洋房 等
|
||
usage_subtype VARCHAR(30), -- 细分用途
|
||
|
||
-- ── 商铺专属 ──
|
||
shop_frontage NUMERIC(6,2), -- 开间(米)
|
||
shop_depth NUMERIC(6,2), -- 进深(米)
|
||
shop_height NUMERIC(6,2), -- 层高(米)
|
||
shop_location VARCHAR(20)
|
||
CHECK (shop_location IS NULL OR
|
||
shop_location IN ('street','mall','residential',
|
||
'ground_floor','complex')),
|
||
|
||
-- ── 房屋状态 ──
|
||
house_status VARCHAR(20)
|
||
CHECK (house_status IN ('owner_occupied','vacant',
|
||
'tenant_occupied','unknown')),
|
||
viewing_time VARCHAR(20)
|
||
CHECK (viewing_time IN ('anytime','by_appointment','inconvenient')),
|
||
|
||
-- ── 等级与标签 ──
|
||
grade VARCHAR(5)
|
||
CHECK (grade IN ('A_urgent','A','B','C','D')),
|
||
-- A_urgent=A(急迫), A=A, B=B(较强), C=C(一般), D=D
|
||
|
||
-- ── 交易属性 ──
|
||
ownership_years VARCHAR(30), -- 房本年限:不满2年/满2年/满5年 等
|
||
ownership_years_detail VARCHAR(20), -- 满五/不满五
|
||
ownership_nature VARCHAR(20)
|
||
CHECK (ownership_nature IS NULL OR
|
||
ownership_nature IN ('commercial','reform_housing',
|
||
'collective','economic')),
|
||
-- commercial=商品房, reform_housing=房改房, collective=集资房, economic=经济适用房
|
||
is_only_house BOOLEAN, -- 唯一住房
|
||
payment_method VARCHAR(30)
|
||
CHECK (payment_method IS NULL OR
|
||
payment_method IN ('full','mortgage','installment','advance')),
|
||
tax_included VARCHAR(10)
|
||
CHECK (tax_included IS NULL OR
|
||
tax_included IN ('each_party','net','inclusive')),
|
||
has_mortgage BOOLEAN,
|
||
has_loan BOOLEAN,
|
||
has_seal BOOLEAN,
|
||
has_restriction BOOLEAN,
|
||
original_price NUMERIC(12,2), -- 原购价(万元)
|
||
sale_reason TEXT, -- 售房原因(最多200字)
|
||
|
||
-- ── 营销备注 ──
|
||
remarks TEXT, -- 房源备注(最多500字)
|
||
|
||
-- ── 相关方(冗余存储 UUID,完整信息查 staff 表)──
|
||
first_recorder_id UUID REFERENCES staff(id) ON DELETE SET NULL, -- 首录方
|
||
number_holder_id UUID REFERENCES staff(id) ON DELETE SET NULL, -- 号码方
|
||
seller_agent_id UUID REFERENCES staff(id) ON DELETE SET NULL, -- 出售方
|
||
buyer_agent_id UUID REFERENCES staff(id) ON DELETE SET NULL, -- 实买方
|
||
|
||
-- ── 来源 ──
|
||
source VARCHAR(50), -- 房源来源渠道(由运营维护枚举)
|
||
|
||
-- ── 维护完成度(冗余缓存,定期重算)──
|
||
completeness_score SMALLINT NOT NULL DEFAULT 0, -- 0-100 分
|
||
|
||
-- ── 时间轨迹 ──
|
||
listed_at TIMESTAMPTZ, -- 最近一次挂牌时间
|
||
last_followed_at TIMESTAMPTZ, -- 最后跟进时间(冗余,加速排序)
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
deleted_at TIMESTAMPTZ,
|
||
created_by UUID REFERENCES staff(id) ON DELETE SET NULL,
|
||
updated_by UUID REFERENCES staff(id) ON DELETE SET NULL,
|
||
|
||
-- ── 全文检索向量 ──
|
||
search_vector TSVECTOR
|
||
);
|
||
|
||
-- ── 索引策略(针对高频查询路径设计)──
|
||
|
||
-- 1. 最核心的列表页:按状态 + 属性 + 类型过滤
|
||
CREATE INDEX idx_properties_status_attr ON properties(status, attribute, property_type)
|
||
WHERE deleted_at IS NULL;
|
||
|
||
-- 2. 区域筛选(通过 complex 表 JOIN 优化)
|
||
CREATE INDEX idx_properties_complex ON properties(complex_id)
|
||
WHERE deleted_at IS NULL;
|
||
|
||
-- 3. 价格排序(出售最常用)
|
||
CREATE INDEX idx_properties_sale_price ON properties(sale_price DESC NULLS LAST)
|
||
WHERE deleted_at IS NULL AND status IN ('for_sale','for_sale_rent');
|
||
|
||
-- 4. 面积区间筛选
|
||
CREATE INDEX idx_properties_area ON properties(area)
|
||
WHERE deleted_at IS NULL;
|
||
|
||
-- 5. 挂牌日期倒序(最新挂牌)
|
||
CREATE INDEX idx_properties_listed_at ON properties(listed_at DESC NULLS LAST)
|
||
WHERE deleted_at IS NULL;
|
||
|
||
-- 6. 最后跟进日期(超时未跟进功能)
|
||
CREATE INDEX idx_properties_last_followed ON properties(last_followed_at DESC NULLS LAST)
|
||
WHERE deleted_at IS NULL;
|
||
|
||
-- 7. 户型筛选
|
||
CREATE INDEX idx_properties_bedroom ON properties(bedroom_count)
|
||
WHERE deleted_at IS NULL;
|
||
|
||
-- 8. 等级筛选
|
||
CREATE INDEX idx_properties_grade ON properties(grade)
|
||
WHERE deleted_at IS NULL;
|
||
|
||
-- 9. 完成度排序(引导补全信息)
|
||
CREATE INDEX idx_properties_completeness ON properties(completeness_score)
|
||
WHERE deleted_at IS NULL;
|
||
|
||
-- 10. 全文搜索
|
||
CREATE INDEX idx_properties_search ON properties USING gin(search_vector);
|
||
|
||
-- 11. 与我相关(相关方快速定位)
|
||
CREATE INDEX idx_properties_seller_agent ON properties(seller_agent_id)
|
||
WHERE deleted_at IS NULL;
|
||
CREATE INDEX idx_properties_number_holder ON properties(number_holder_id)
|
||
WHERE deleted_at IS NULL;
|
||
|
||
-- 12. 复合索引:列表默认排序(状态 + 挂牌时间)
|
||
CREATE INDEX idx_properties_list_default ON properties(status, listed_at DESC NULLS LAST)
|
||
WHERE deleted_at IS NULL;
|
||
```
|
||
|
||
---
|
||
|
||
### 3.4 房源联系人(Property Contacts)
|
||
|
||
```sql
|
||
-- ============================================================
|
||
-- 业主/联系人:手机号加密存储,哈希值支持重复检测
|
||
-- 安全要点:任何查看明文号码的行为均触发审计日志
|
||
-- ============================================================
|
||
|
||
CREATE TABLE property_contacts (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
property_id UUID NOT NULL REFERENCES properties(id) ON DELETE CASCADE,
|
||
|
||
name VARCHAR(50) NOT NULL,
|
||
gender VARCHAR(10) NOT NULL DEFAULT 'male'
|
||
CHECK (gender IN ('male','female')),
|
||
identity VARCHAR(20) NOT NULL DEFAULT 'contact'
|
||
CHECK (identity IN ('owner','contact','subletter',
|
||
'tenant','agent','corporate')),
|
||
-- owner=业主, contact=联系人, subletter=二房东, tenant=租客,
|
||
-- agent=代理人, corporate=企业法人
|
||
|
||
-- 手机号:加密存储 + 哈希索引(重复检测用)
|
||
phone_enc BYTEA NOT NULL, -- AES-256-GCM 加密
|
||
phone_hash VARCHAR(64) NOT NULL, -- SHA-256(phone) 用于去重查询
|
||
phone2_enc BYTEA,
|
||
phone2_hash VARCHAR(64),
|
||
|
||
wechat VARCHAR(100), -- 微信号(相对不敏感,可明文)
|
||
qq VARCHAR(20),
|
||
remarks TEXT,
|
||
|
||
-- 是否为号码方(关联审批流)
|
||
is_number_holder BOOLEAN NOT NULL DEFAULT FALSE,
|
||
number_holder_approved_at TIMESTAMPTZ, -- 审批通过时间
|
||
|
||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
deleted_at TIMESTAMPTZ,
|
||
created_by UUID REFERENCES staff(id) ON DELETE SET NULL,
|
||
updated_by UUID REFERENCES staff(id) ON DELETE SET NULL
|
||
);
|
||
|
||
CREATE INDEX idx_contacts_property ON property_contacts(property_id)
|
||
WHERE deleted_at IS NULL;
|
||
|
||
-- 关键:手机号哈希全局索引(用于重复房源检测)
|
||
CREATE INDEX idx_contacts_phone_hash ON property_contacts(phone_hash)
|
||
WHERE deleted_at IS NULL;
|
||
CREATE INDEX idx_contacts_phone2_hash ON property_contacts(phone2_hash)
|
||
WHERE phone2_hash IS NOT NULL AND deleted_at IS NULL;
|
||
```
|
||
|
||
---
|
||
|
||
### 3.5 挂牌历史(Listing History)
|
||
|
||
```sql
|
||
-- ============================================================
|
||
-- 挂牌历史:记录房源每次上架的完整快照
|
||
-- 设计重点:不可删除(合规),仅追加
|
||
-- ============================================================
|
||
|
||
CREATE TABLE listing_histories (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
property_id UUID NOT NULL REFERENCES properties(id) ON DELETE RESTRICT,
|
||
|
||
listing_type VARCHAR(20) NOT NULL
|
||
CHECK (listing_type IN ('for_sale','for_rent')),
|
||
status VARCHAR(20) NOT NULL DEFAULT 'active'
|
||
CHECK (status IN ('active','ended')),
|
||
|
||
-- 价格快照
|
||
sale_price NUMERIC(12,2),
|
||
rent_price NUMERIC(10,2),
|
||
sale_unit_price NUMERIC(10,2), -- 元/m²,计算字段
|
||
|
||
-- 交易信息快照
|
||
ownership_years VARCHAR(30),
|
||
is_only_house BOOLEAN,
|
||
tax_included VARCHAR(10),
|
||
sale_reason TEXT,
|
||
|
||
-- 经纪人快照
|
||
seller_agent_id UUID REFERENCES staff(id) ON DELETE SET NULL,
|
||
seller_agent_snapshot JSONB, -- 存储经纪人姓名+门店(防止变更后丢失)
|
||
|
||
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
ended_at TIMESTAMPTZ,
|
||
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
-- 注:无 deleted_at,此表记录不可删除
|
||
);
|
||
|
||
CREATE INDEX idx_listing_histories_property ON listing_histories(property_id);
|
||
CREATE INDEX idx_listing_histories_active ON listing_histories(property_id)
|
||
WHERE status = 'active';
|
||
```
|
||
|
||
---
|
||
|
||
### 3.6 调价记录(Price Change Log)
|
||
|
||
```sql
|
||
-- ============================================================
|
||
-- 调价记录:支持折线图展示,不可删除
|
||
-- ============================================================
|
||
|
||
CREATE TABLE price_changes (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
property_id UUID NOT NULL REFERENCES properties(id) ON DELETE RESTRICT,
|
||
|
||
old_sale_price NUMERIC(12,2),
|
||
new_sale_price NUMERIC(12,2),
|
||
old_bottom_price NUMERIC(12,2),
|
||
new_bottom_price NUMERIC(12,2),
|
||
old_record_price NUMERIC(12,2),
|
||
new_record_price NUMERIC(12,2),
|
||
old_rent_price NUMERIC(10,2),
|
||
new_rent_price NUMERIC(10,2),
|
||
|
||
change_reason TEXT NOT NULL, -- 最多200字
|
||
changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
changed_by UUID NOT NULL REFERENCES staff(id) ON DELETE RESTRICT
|
||
);
|
||
|
||
CREATE INDEX idx_price_changes_property ON price_changes(property_id);
|
||
CREATE INDEX idx_price_changes_time ON price_changes(property_id, changed_at DESC);
|
||
```
|
||
|
||
---
|
||
|
||
### 3.7 跟进日志(Follow-up Logs)
|
||
|
||
```sql
|
||
-- ============================================================
|
||
-- 跟进日志:系统最高写入频率的表,按 property_id 分区预留
|
||
-- 6 种类型:写入跟进/修改跟进/敏感信息跟进/敏感信息查看/其他跟进/系统日志
|
||
-- ============================================================
|
||
|
||
CREATE TABLE follow_logs (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
property_id UUID NOT NULL REFERENCES properties(id) ON DELETE CASCADE,
|
||
|
||
log_type VARCHAR(30) NOT NULL
|
||
CHECK (log_type IN ('written','modified','sensitive_op',
|
||
'sensitive_view','other','system')),
|
||
-- written=写入跟进(经纪人主动写)
|
||
-- modified=修改跟进(字段变更自动生成)
|
||
-- sensitive_op=敏感信息跟进(相关方保护变更)
|
||
-- sensitive_view=敏感信息查看(查看号码等)
|
||
-- other=其他跟进(钥匙/新增联系人等)
|
||
-- system=系统日志
|
||
|
||
-- 写入跟进专用字段
|
||
purpose VARCHAR(50), -- 跟进目的(由运营维护枚举值)
|
||
content TEXT, -- 跟进内容,最少6字最多500字
|
||
ai_tag VARCHAR(20)
|
||
CHECK (ai_tag IS NULL OR ai_tag IN ('ai_for_sale','ai_not_for_sale')),
|
||
|
||
-- 修改跟进专用字段
|
||
change_detail JSONB,
|
||
-- 格式:{"field": "sale_price", "old": 850, "new": 800, "label": "售价"}
|
||
-- 支持多字段同时变更
|
||
|
||
-- 系统标签(显示在日志时间线上的 tag)
|
||
log_tag VARCHAR(50),
|
||
-- 如:查看号码/图片下载/改状态/改价格/改等级/修改相关方 等
|
||
|
||
-- 可见性控制
|
||
is_public BOOLEAN NOT NULL DEFAULT TRUE,
|
||
-- FALSE = 仅本人及上级可见
|
||
|
||
-- 操作人
|
||
operator_id UUID REFERENCES staff(id) ON DELETE SET NULL,
|
||
operator_snapshot JSONB, -- {name, role, org_unit_name, store_group}
|
||
|
||
-- 是否可删除(敏感信息查看类型 = FALSE)
|
||
is_deletable BOOLEAN NOT NULL DEFAULT TRUE,
|
||
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
deleted_at TIMESTAMPTZ -- 仅 is_deletable=TRUE 时才能软删
|
||
);
|
||
|
||
-- 核心索引:时间线展示
|
||
CREATE INDEX idx_follow_logs_property_time ON follow_logs(property_id, created_at DESC)
|
||
WHERE deleted_at IS NULL;
|
||
|
||
-- 按类型过滤(6个 Tab 查询)
|
||
CREATE INDEX idx_follow_logs_type ON follow_logs(property_id, log_type, created_at DESC)
|
||
WHERE deleted_at IS NULL;
|
||
|
||
-- 操作员过滤(跟进日志搜索功能)
|
||
CREATE INDEX idx_follow_logs_operator ON follow_logs(operator_id, created_at DESC)
|
||
WHERE deleted_at IS NULL;
|
||
|
||
-- 不可删除类型专用索引(合规审计)
|
||
CREATE INDEX idx_follow_logs_sensitive ON follow_logs(property_id, created_at DESC)
|
||
WHERE log_type IN ('sensitive_view','sensitive_op');
|
||
|
||
-- 跟进日志附件(一条跟进可附多张图)
|
||
CREATE TABLE follow_log_attachments (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
follow_log_id UUID NOT NULL REFERENCES follow_logs(id) ON DELETE CASCADE,
|
||
file_key TEXT NOT NULL, -- R2/S3 存储路径
|
||
file_name VARCHAR(255) NOT NULL,
|
||
file_size INTEGER NOT NULL, -- bytes
|
||
file_type VARCHAR(10)
|
||
CHECK (file_type IN ('bmp','jpg','png','svg','gif')),
|
||
sort_order SMALLINT NOT NULL DEFAULT 0,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
|
||
CREATE INDEX idx_follow_attachments_log ON follow_log_attachments(follow_log_id);
|
||
|
||
-- 跟进录音(独立存储,支持音频文件)
|
||
CREATE TABLE follow_log_recordings (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
follow_log_id UUID NOT NULL REFERENCES follow_logs(id) ON DELETE CASCADE,
|
||
file_key TEXT NOT NULL,
|
||
duration_seconds INTEGER,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
### 3.8 钥匙管理(Key Management)
|
||
|
||
```sql
|
||
CREATE TABLE property_keys (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
property_id UUID NOT NULL REFERENCES properties(id) ON DELETE CASCADE,
|
||
|
||
key_type VARCHAR(20) NOT NULL
|
||
CHECK (key_type IN ('mechanical','password')),
|
||
|
||
-- 钥匙持有方
|
||
holder_id UUID REFERENCES staff(id) ON DELETE SET NULL,
|
||
holder_snapshot JSONB, -- {name, store_group}(防人员变动丢失)
|
||
storage_unit_id UUID REFERENCES org_units(id) ON DELETE SET NULL, -- 保管部门
|
||
|
||
-- 他司钥匙标记
|
||
is_other_agency BOOLEAN NOT NULL DEFAULT FALSE,
|
||
other_agency_info VARCHAR(30), -- 他司信息,最多30字
|
||
|
||
remarks TEXT, -- 备注,最多200字
|
||
|
||
is_active BOOLEAN NOT NULL DEFAULT TRUE, -- FALSE = 钥匙已归还/失效
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
created_by UUID REFERENCES staff(id) ON DELETE SET NULL
|
||
);
|
||
|
||
CREATE INDEX idx_property_keys_property ON property_keys(property_id)
|
||
WHERE is_active = TRUE;
|
||
|
||
-- 钥匙附件
|
||
CREATE TABLE key_attachments (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
key_id UUID NOT NULL REFERENCES property_keys(id) ON DELETE CASCADE,
|
||
file_key TEXT NOT NULL,
|
||
file_name VARCHAR(255) NOT NULL,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
### 3.9 委托管理(Commission Management)
|
||
|
||
```sql
|
||
CREATE TABLE commissions (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
property_id UUID NOT NULL REFERENCES properties(id) ON DELETE CASCADE,
|
||
|
||
commission_type VARCHAR(50) NOT NULL, -- 独家委托/非独家委托(运营维护枚举)
|
||
period_start DATE NOT NULL,
|
||
period_end DATE,
|
||
is_open_ended BOOLEAN NOT NULL DEFAULT FALSE, -- 无固定结束日期
|
||
|
||
-- 委托方(负责经纪人)
|
||
agent_id UUID REFERENCES staff(id) ON DELETE SET NULL,
|
||
agent_snapshot JSONB,
|
||
|
||
-- 签约方式(选择后动态展示委托书模板)
|
||
signing_method VARCHAR(50),
|
||
|
||
-- 委托人(产权人)信息
|
||
owner_type VARCHAR(20) NOT NULL DEFAULT 'owner'
|
||
CHECK (owner_type IN ('owner','authorized_third')),
|
||
-- 从 property_contacts 中选择
|
||
property_owner_contact_id UUID REFERENCES property_contacts(id) ON DELETE SET NULL,
|
||
owner_name VARCHAR(50), -- 产权人姓名
|
||
owner_id_type VARCHAR(20), -- 证件类型:身份证/护照 等
|
||
owner_id_number VARCHAR(50), -- 证件号码(加密存储)
|
||
owner_id_number_enc BYTEA,
|
||
|
||
remarks TEXT, -- 备注,最多200字
|
||
|
||
-- 状态
|
||
status VARCHAR(20) NOT NULL DEFAULT 'active'
|
||
CHECK (status IN ('active','expired','cancelled')),
|
||
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
created_by UUID REFERENCES staff(id) ON DELETE SET NULL
|
||
);
|
||
|
||
CREATE INDEX idx_commissions_property ON commissions(property_id);
|
||
CREATE INDEX idx_commissions_active ON commissions(property_id)
|
||
WHERE status = 'active';
|
||
|
||
-- 委托附件(身份证/房产证/委托书 等)
|
||
CREATE TABLE commission_attachments (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
commission_id UUID NOT NULL REFERENCES commissions(id) ON DELETE CASCADE,
|
||
category VARCHAR(20) NOT NULL
|
||
CHECK (category IN ('id_card','property_cert',
|
||
'commission_letter','other')),
|
||
file_key TEXT NOT NULL,
|
||
file_name VARCHAR(255) NOT NULL,
|
||
file_size INTEGER,
|
||
sort_order SMALLINT NOT NULL DEFAULT 0,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
|
||
CREATE INDEX idx_commission_attachments_commission ON commission_attachments(commission_id);
|
||
```
|
||
|
||
---
|
||
|
||
### 3.10 实勘管理(Field Survey)
|
||
|
||
```sql
|
||
CREATE TABLE field_surveys (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
property_id UUID NOT NULL REFERENCES properties(id) ON DELETE CASCADE,
|
||
|
||
status VARCHAR(10) NOT NULL DEFAULT 'draft'
|
||
CHECK (status IN ('draft','submitted')),
|
||
|
||
-- GPS 定位
|
||
gps_latitude NUMERIC(10,7),
|
||
gps_longitude NUMERIC(10,7),
|
||
gps_accuracy NUMERIC(6,2), -- 精度(米)
|
||
|
||
description TEXT, -- 实勘说明,最多200字
|
||
|
||
submitted_at TIMESTAMPTZ,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
created_by UUID NOT NULL REFERENCES staff(id) ON DELETE RESTRICT
|
||
);
|
||
|
||
CREATE INDEX idx_field_surveys_property ON field_surveys(property_id);
|
||
CREATE INDEX idx_field_surveys_submitted ON field_surveys(property_id)
|
||
WHERE status = 'submitted';
|
||
|
||
-- 实勘照片(按空间分类)
|
||
CREATE TABLE survey_photos (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
survey_id UUID NOT NULL REFERENCES field_surveys(id) ON DELETE CASCADE,
|
||
category VARCHAR(20) NOT NULL
|
||
CHECK (category IN ('layout','living_room','dining_room',
|
||
'bedroom','bathroom','kitchen',
|
||
'entrance','balcony','study',
|
||
'indoor_other','outdoor')),
|
||
file_key TEXT NOT NULL, -- R2/S3 路径
|
||
thumbnail_key TEXT, -- 缩略图路径
|
||
file_size INTEGER,
|
||
width INTEGER,
|
||
height INTEGER,
|
||
sort_order SMALLINT NOT NULL DEFAULT 0,
|
||
is_vr_screenshot BOOLEAN NOT NULL DEFAULT FALSE,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
|
||
CREATE INDEX idx_survey_photos_survey ON survey_photos(survey_id);
|
||
CREATE INDEX idx_survey_photos_category ON survey_photos(survey_id, category);
|
||
```
|
||
|
||
---
|
||
|
||
### 3.11 房源图片管理(Property Photos)
|
||
|
||
```sql
|
||
-- ============================================================
|
||
-- 房源图片:与实勘照片分离存储,经纪人自主上传和管理
|
||
-- 封面限1张,全景类型单独处理
|
||
-- ============================================================
|
||
|
||
CREATE TABLE property_photos (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
property_id UUID NOT NULL REFERENCES properties(id) ON DELETE CASCADE,
|
||
|
||
category VARCHAR(20) NOT NULL
|
||
CHECK (category IN ('cover','entrance','living_room',
|
||
'dining_room','bedroom','bathroom',
|
||
'kitchen','balcony','study',
|
||
'indoor_other','outdoor','panorama')),
|
||
|
||
file_key TEXT NOT NULL, -- R2/S3 原图路径
|
||
thumbnail_key TEXT, -- 缩略图路径(Cloudflare Images 生成)
|
||
file_name VARCHAR(255),
|
||
file_size INTEGER,
|
||
width INTEGER,
|
||
height INTEGER,
|
||
|
||
is_cover BOOLEAN NOT NULL DEFAULT FALSE,
|
||
sort_order SMALLINT NOT NULL DEFAULT 0,
|
||
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
created_by UUID REFERENCES staff(id) ON DELETE SET NULL
|
||
);
|
||
|
||
CREATE INDEX idx_property_photos_property ON property_photos(property_id);
|
||
CREATE INDEX idx_property_photos_cover ON property_photos(property_id)
|
||
WHERE is_cover = TRUE;
|
||
CREATE INDEX idx_property_photos_category ON property_photos(property_id, category);
|
||
|
||
-- 唯一约束:每个房源只能有一张封面
|
||
CREATE UNIQUE INDEX idx_property_photos_unique_cover
|
||
ON property_photos(property_id)
|
||
WHERE is_cover = TRUE;
|
||
```
|
||
|
||
---
|
||
|
||
### 3.12 房源附件(Property Attachments)
|
||
|
||
```sql
|
||
CREATE TABLE property_attachments (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
property_id UUID NOT NULL REFERENCES properties(id) ON DELETE CASCADE,
|
||
|
||
category VARCHAR(20) NOT NULL DEFAULT 'other'
|
||
CHECK (category IN ('id_card','property_cert',
|
||
'commission_letter','other')),
|
||
|
||
file_key TEXT NOT NULL,
|
||
file_name VARCHAR(255) NOT NULL,
|
||
file_size INTEGER NOT NULL,
|
||
file_type VARCHAR(50), -- MIME type
|
||
|
||
sort_order SMALLINT NOT NULL DEFAULT 0,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
created_by UUID REFERENCES staff(id) ON DELETE SET NULL
|
||
);
|
||
|
||
CREATE INDEX idx_property_attachments_property ON property_attachments(property_id);
|
||
CREATE INDEX idx_property_attachments_category ON property_attachments(property_id, category);
|
||
```
|
||
|
||
---
|
||
|
||
### 3.13 房源营销信息(Property Marketing)
|
||
|
||
```sql
|
||
CREATE TABLE property_marketing (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
property_id UUID NOT NULL UNIQUE REFERENCES properties(id) ON DELETE CASCADE,
|
||
|
||
marketing_title VARCHAR(30), -- 营销标题 0-30字
|
||
core_selling_points TEXT, -- 核心卖点,最多200字
|
||
owner_attitude TEXT, -- 业主心态,最多200字
|
||
layout_description TEXT, -- 户型介绍,最多200字
|
||
complex_description TEXT, -- 小区介绍,最多200字
|
||
|
||
-- AI 生成标记
|
||
ai_generated_points BOOLEAN NOT NULL DEFAULT FALSE,
|
||
ai_generated_attitude BOOLEAN NOT NULL DEFAULT FALSE,
|
||
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_by UUID REFERENCES staff(id) ON DELETE SET NULL
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
### 3.14 产证信息(Property Certificate)
|
||
|
||
```sql
|
||
CREATE TABLE property_certificates (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
property_id UUID NOT NULL UNIQUE REFERENCES properties(id) ON DELETE CASCADE,
|
||
|
||
owner_name VARCHAR(100),
|
||
owner_id_number VARCHAR(50), -- 身份证号/统一社会信用代码
|
||
owner_cert_type VARCHAR(20), -- 身份证/护照/营业执照
|
||
property_location VARCHAR(500), -- 房屋坐落(产权证书上的地址),最多50字
|
||
|
||
-- 产证状态
|
||
cert_status VARCHAR(30),
|
||
cert_no VARCHAR(100), -- 产证号
|
||
first_registered_at DATE, -- 首次登记时间
|
||
ownership_nature VARCHAR(30),
|
||
land_nature VARCHAR(30), -- 土地性质
|
||
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_by UUID REFERENCES staff(id) ON DELETE SET NULL
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
### 3.15 楼盘基本信息扩展(Complex Property Info)
|
||
|
||
```sql
|
||
-- 补充:楼盘与房源通过 complex_id 关联,楼盘信息首次填写后修改需走楼盘管理系统
|
||
-- 楼盘价格走势(用于楼盘详情页展示)
|
||
CREATE TABLE complex_price_trends (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
complex_id UUID NOT NULL REFERENCES complexes(id) ON DELETE CASCADE,
|
||
record_month DATE NOT NULL, -- 月份(取该月1日存储)
|
||
avg_sale_price NUMERIC(10,2), -- 月均售价(万元/套)
|
||
avg_unit_price NUMERIC(10,2), -- 月均单价(元/m²)
|
||
transaction_count INTEGER, -- 成交套数
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
|
||
CREATE UNIQUE INDEX idx_complex_price_trend_month
|
||
ON complex_price_trends(complex_id, record_month);
|
||
```
|
||
|
||
---
|
||
|
||
### 3.16 维护完成度评分(Completeness Scoring)
|
||
|
||
```sql
|
||
-- ============================================================
|
||
-- 维护完成度:不直接存完整计算明细(减少宽表),
|
||
-- 以触发器/Celery 任务异步更新 properties.completeness_score
|
||
-- 此表存储各维度的得分快照,供详情页展示
|
||
-- ============================================================
|
||
|
||
CREATE TABLE property_completeness (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
property_id UUID NOT NULL UNIQUE REFERENCES properties(id) ON DELETE CASCADE,
|
||
|
||
-- 各维度得分(满分见 PRD 8.2)
|
||
score_core_info SMALLINT NOT NULL DEFAULT 0, -- 重点信息 满分8
|
||
score_attachment SMALLINT NOT NULL DEFAULT 0, -- 附件 满分8
|
||
score_survey SMALLINT NOT NULL DEFAULT 0, -- 实勘 满分16
|
||
score_vr SMALLINT NOT NULL DEFAULT 0, -- VR 满分8
|
||
score_key SMALLINT NOT NULL DEFAULT 0, -- 钥匙 满分10
|
||
score_commission SMALLINT NOT NULL DEFAULT 0, -- 委托 满分10
|
||
score_verification SMALLINT NOT NULL DEFAULT 0, -- 验证 满分7
|
||
score_follow_up SMALLINT NOT NULL DEFAULT 0, -- 跟进 满分8
|
||
score_viewing SMALLINT NOT NULL DEFAULT 0, -- 带看 满分8
|
||
score_other SMALLINT NOT NULL DEFAULT 0, -- 其他 满分7
|
||
total_score SMALLINT NOT NULL DEFAULT 0, -- 总分 0-100
|
||
|
||
calculated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
### 3.17 客源管理(Client Management)
|
||
|
||
```sql
|
||
-- ============================================================
|
||
-- 客源:私客为核心,公客/成交客为后续版本
|
||
-- ============================================================
|
||
|
||
CREATE TABLE clients (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
|
||
client_type VARCHAR(20) NOT NULL DEFAULT 'private'
|
||
CHECK (client_type IN ('private','public','transacted')),
|
||
status VARCHAR(20) NOT NULL DEFAULT 'active'
|
||
CHECK (status IN ('active','converted_public',
|
||
'transacted','invalid')),
|
||
|
||
name VARCHAR(50) NOT NULL,
|
||
gender VARCHAR(10)
|
||
CHECK (gender IN ('male','female','unknown')),
|
||
|
||
-- 手机号加密存储
|
||
phone_enc BYTEA NOT NULL,
|
||
phone_hash VARCHAR(64) NOT NULL,
|
||
phone2_enc BYTEA,
|
||
phone2_hash VARCHAR(64),
|
||
|
||
-- 购房需求
|
||
purpose VARCHAR(10) NOT NULL
|
||
CHECK (purpose IN ('buy','rent')),
|
||
budget_min NUMERIC(12,2),
|
||
budget_max NUMERIC(12,2),
|
||
area_min NUMERIC(8,2),
|
||
area_max NUMERIC(8,2),
|
||
bedroom_needs SMALLINT[], -- 可接受的卧室数量数组
|
||
|
||
-- 意向区域(存 district/business_area ID 数组)
|
||
district_ids UUID[],
|
||
business_area_ids UUID[],
|
||
|
||
-- 活跃度分层(由系统计算)
|
||
activity_level VARCHAR(10)
|
||
CHECK (activity_level IN ('hot','warm','cold','frozen')),
|
||
last_active_at TIMESTAMPTZ,
|
||
|
||
-- 负责经纪人
|
||
agent_id UUID REFERENCES staff(id) ON DELETE SET NULL,
|
||
org_unit_id UUID REFERENCES org_units(id) ON DELETE SET NULL,
|
||
|
||
source VARCHAR(50),
|
||
remarks TEXT,
|
||
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
deleted_at TIMESTAMPTZ,
|
||
created_by UUID REFERENCES staff(id) ON DELETE SET NULL
|
||
);
|
||
|
||
CREATE INDEX idx_clients_agent ON clients(agent_id) WHERE deleted_at IS NULL;
|
||
CREATE INDEX idx_clients_phone_hash ON clients(phone_hash) WHERE deleted_at IS NULL;
|
||
CREATE INDEX idx_clients_status ON clients(status, client_type) WHERE deleted_at IS NULL;
|
||
CREATE INDEX idx_clients_activity ON clients(activity_level, last_active_at DESC)
|
||
WHERE deleted_at IS NULL;
|
||
|
||
-- 客源跟进日志(复用结构,单独表避免与房源日志混合)
|
||
CREATE TABLE client_follow_logs (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
|
||
purpose VARCHAR(50),
|
||
content TEXT,
|
||
log_tag VARCHAR(50),
|
||
is_public BOOLEAN NOT NULL DEFAULT TRUE,
|
||
operator_id UUID REFERENCES staff(id) ON DELETE SET NULL,
|
||
operator_snapshot JSONB,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
deleted_at TIMESTAMPTZ
|
||
);
|
||
|
||
CREATE INDEX idx_client_logs_client ON client_follow_logs(client_id, created_at DESC)
|
||
WHERE deleted_at IS NULL;
|
||
|
||
-- 智能配房记录(客源 ↔ 房源 匹配)
|
||
CREATE TABLE client_property_matches (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
|
||
property_id UUID NOT NULL REFERENCES properties(id) ON DELETE CASCADE,
|
||
match_score NUMERIC(5,2), -- 匹配度评分
|
||
match_reason JSONB, -- 匹配原因详情
|
||
status VARCHAR(20) NOT NULL DEFAULT 'suggested'
|
||
CHECK (status IN ('suggested','shared','viewing','rejected')),
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
created_by UUID REFERENCES staff(id) ON DELETE SET NULL
|
||
);
|
||
|
||
CREATE UNIQUE INDEX idx_client_property_match
|
||
ON client_property_matches(client_id, property_id);
|
||
|
||
-- 带看记录
|
||
CREATE TABLE viewings (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
property_id UUID NOT NULL REFERENCES properties(id) ON DELETE RESTRICT,
|
||
client_id UUID NOT NULL REFERENCES clients(id) ON DELETE RESTRICT,
|
||
agent_id UUID REFERENCES staff(id) ON DELETE SET NULL,
|
||
|
||
viewing_type VARCHAR(20) NOT NULL DEFAULT 'first'
|
||
CHECK (viewing_type IN ('first','revisit','empty','interview')),
|
||
-- first=带看, revisit=复看, empty=空看, interview=面访
|
||
|
||
scheduled_at TIMESTAMPTZ,
|
||
completed_at TIMESTAMPTZ,
|
||
result VARCHAR(20)
|
||
CHECK (result IN ('interested','not_interested',
|
||
'negotiating','cancelled')),
|
||
remarks TEXT,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
|
||
CREATE INDEX idx_viewings_property ON viewings(property_id);
|
||
CREATE INDEX idx_viewings_client ON viewings(client_id);
|
||
```
|
||
|
||
---
|
||
|
||
### 3.18 系统设置(System Settings)
|
||
|
||
```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 property_tags (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
name VARCHAR(50) NOT NULL,
|
||
color VARCHAR(7), -- HEX 颜色
|
||
is_system BOOLEAN NOT NULL DEFAULT FALSE, -- 系统预置标签不可删除
|
||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||
is_active BOOLEAN NOT NULL DEFAULT TRUE
|
||
);
|
||
|
||
-- 房源 ↔ 标签 多对多
|
||
CREATE TABLE property_tag_relations (
|
||
property_id UUID NOT NULL REFERENCES properties(id) ON DELETE CASCADE,
|
||
tag_id UUID NOT NULL REFERENCES property_tags(id) ON DELETE CASCADE,
|
||
PRIMARY KEY (property_id, tag_id)
|
||
);
|
||
|
||
CREATE INDEX idx_property_tags_property ON property_tag_relations(property_id);
|
||
CREATE INDEX idx_property_tags_tag ON property_tag_relations(tag_id);
|
||
|
||
-- 收藏(经纪人收藏房源)
|
||
CREATE TABLE property_favorites (
|
||
staff_id UUID NOT NULL REFERENCES staff(id) ON DELETE CASCADE,
|
||
property_id UUID NOT NULL REFERENCES properties(id) ON DELETE CASCADE,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
PRIMARY KEY (staff_id, property_id)
|
||
);
|
||
|
||
CREATE INDEX idx_property_favorites_staff ON property_favorites(staff_id);
|
||
|
||
-- 保护房设置
|
||
CREATE TABLE property_protections (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
property_id UUID NOT NULL UNIQUE REFERENCES properties(id) ON DELETE CASCADE,
|
||
is_protected BOOLEAN NOT NULL DEFAULT FALSE,
|
||
reason TEXT,
|
||
start_at TIMESTAMPTZ,
|
||
end_at TIMESTAMPTZ,
|
||
set_by UUID REFERENCES staff(id) ON DELETE SET NULL,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
|
||
-- 筛选方案(保存的搜索条件)
|
||
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);
|
||
|
||
-- 号码方修改审批
|
||
CREATE TABLE number_holder_approvals (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
property_id UUID NOT NULL REFERENCES properties(id) ON DELETE CASCADE,
|
||
contact_id UUID NOT NULL REFERENCES property_contacts(id) ON DELETE CASCADE,
|
||
applicant_id UUID NOT NULL REFERENCES staff(id) ON DELETE RESTRICT,
|
||
approver_id UUID REFERENCES staff(id) ON DELETE SET NULL,
|
||
status VARCHAR(20) NOT NULL DEFAULT 'pending'
|
||
CHECK (status IN ('pending','approved','rejected')),
|
||
remarks TEXT,
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||
decided_at TIMESTAMPTZ
|
||
);
|
||
|
||
CREATE INDEX idx_number_holder_approvals_status ON number_holder_approvals(status)
|
||
WHERE status = 'pending';
|
||
```
|
||
|
||
---
|
||
|
||
## 四、关键索引汇总与查询优化策略
|
||
|
||
### 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)*
|