1169 lines
43 KiB
Markdown
1169 lines
43 KiB
Markdown
# Fonrey — 房源模块数据模型(DATA_MODEL_PROPERTY)
|
||
|
||
> **权威定义**:本文件是房源模块所有表结构的唯一权威来源。
|
||
> **主文档引用**:`DATA_MODEL.md` §3.3–§3.16 为本文件的概览摘要,开发以本文件为准。
|
||
> **版本**:v1.0 | **日期**:2026-04-24
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
1. [模块说明](#1-模块说明)
|
||
2. [表清单](#2-表清单)
|
||
3. [枚举值总览](#3-枚举值总览)
|
||
4. [DDL 定义](#4-ddl-定义)
|
||
- 4.1 [properties(房源主表)](#41-properties房源主表)
|
||
- 4.2 [property_contacts(联系人)](#42-property_contacts联系人)
|
||
- 4.3 [listing_histories(挂牌历史)](#43-listing_histories挂牌历史)
|
||
- 4.4 [price_changes(调价记录)](#44-price_changes调价记录)
|
||
- 4.5 [follow_logs(跟进日志)](#45-follow_logs跟进日志)
|
||
- 4.6 [follow_log_attachments(跟进附件)](#46-follow_log_attachments跟进附件)
|
||
- 4.7 [follow_log_recordings(跟进录音)](#47-follow_log_recordings跟进录音)
|
||
- 4.8 [property_keys(钥匙管理)](#48-property_keys钥匙管理)
|
||
- 4.9 [key_attachments(钥匙附件)](#49-key_attachments钥匙附件)
|
||
- 4.10 [commissions(委托管理)](#410-commissions委托管理)
|
||
- 4.11 [commission_attachments(委托附件)](#411-commission_attachments委托附件)
|
||
- 4.12 [field_surveys(实勘管理)](#412-field_surveys实勘管理)
|
||
- 4.13 [survey_photos(实勘照片)](#413-survey_photos实勘照片)
|
||
- 4.14 [property_photos(房源图片)](#414-property_photos房源图片)
|
||
- 4.15 [property_attachments(房源附件)](#415-property_attachments房源附件)
|
||
- 4.16 [property_marketing(营销信息)](#416-property_marketing营销信息)
|
||
- 4.17 [property_certificates(产证信息)](#417-property_certificates产证信息)
|
||
- 4.18 [property_completeness(维护完成度)](#418-property_completeness维护完成度)
|
||
- 4.19 [property_tags / property_tag_relations(标签)](#419-property_tags--property_tag_relations标签)
|
||
- 4.20 [property_favorites(收藏)](#420-property_favorites收藏)
|
||
- 4.21 [property_protections(保护房)](#421-property_protections保护房)
|
||
- 4.22 [number_holder_approvals(号码方审批)](#422-number_holder_approvals号码方审批)
|
||
5. [触发器](#5-触发器)
|
||
6. [查询模式参考](#6-查询模式参考)
|
||
7. [禁止操作](#7-禁止操作)
|
||
|
||
---
|
||
|
||
## 1 模块说明
|
||
|
||
**房源(Property)** 是 Fonrey 系统的核心领域对象,代表一套二手房源的完整档案。
|
||
|
||
核心业务规则:
|
||
|
||
| 规则 | 说明 |
|
||
|------|------|
|
||
| 多态交易状态 | 一套房源可同时处于出售、出租或租售三态(`status`) |
|
||
| 流通属性 | 公盘/私盘/特盘/封盘(`attribute`),控制可见范围 |
|
||
| 相关方体系 | 首录方/号码方/出售方/实买方,通过 `staff_id` 关联 |
|
||
| 联系人加密 | 业主/联系人手机号 AES-256-GCM 加密,SHA-256 哈希索引 |
|
||
| 跟进日志不可删 | `sensitive_view` 类型跟进 `is_deletable=FALSE`,合规强制 |
|
||
| 挂牌历史不可删 | `listing_histories` 无 `deleted_at`,append-only |
|
||
| 调价记录不可删 | `price_changes` 无 `deleted_at`,append-only |
|
||
| 完成度异步计算 | `completeness_score` 由 Celery 任务更新,不实时 |
|
||
|
||
---
|
||
|
||
## 2 表清单
|
||
|
||
| # | 表名 | 说明 | 是否可删除 |
|
||
|---|------|------|----------|
|
||
| 1 | `properties` | 房源主表 | 软删除(`deleted_at`) |
|
||
| 2 | `property_contacts` | 业主/联系人(手机号加密) | 软删除 |
|
||
| 3 | `listing_histories` | 挂牌历史快照 | **不可删除** |
|
||
| 4 | `price_changes` | 调价记录 | **不可删除** |
|
||
| 5 | `follow_logs` | 跟进日志(6种类型) | 部分不可删(`sensitive_view`) |
|
||
| 6 | `follow_log_attachments` | 跟进附件(图片) | 随日志联级 |
|
||
| 7 | `follow_log_recordings` | 跟进录音 | 随日志联级 |
|
||
| 8 | `property_keys` | 钥匙管理 | 软删除(`is_active=FALSE`) |
|
||
| 9 | `key_attachments` | 钥匙附件 | 随钥匙联级 |
|
||
| 10 | `commissions` | 委托管理 | 状态驱动(`status='cancelled'`) |
|
||
| 11 | `commission_attachments` | 委托附件 | 随委托联级 |
|
||
| 12 | `field_surveys` | 实勘管理 | 软删除 |
|
||
| 13 | `survey_photos` | 实勘照片(按空间分类) | 随实勘联级 |
|
||
| 14 | `property_photos` | 房源图片(经纪人管理) | 软删除 |
|
||
| 15 | `property_attachments` | 房源附件 | 直接删除 |
|
||
| 16 | `property_marketing` | 营销信息(1:1) | 随房源联级 |
|
||
| 17 | `property_certificates` | 产证信息(1:1) | 随房源联级 |
|
||
| 18 | `property_completeness` | 维护完成度快照(1:1) | 随房源联级 |
|
||
| 19 | `property_tags` | 标签字典 | 系统标签不可删 |
|
||
| 20 | `property_tag_relations` | 房源↔标签多对多 | 随房源/标签联级 |
|
||
| 21 | `property_favorites` | 经纪人收藏房源 | 直接删除 |
|
||
| 22 | `property_protections` | 保护房设置(1:1) | 随房源联级 |
|
||
| 23 | `number_holder_approvals` | 号码方变更审批 | 随房源联级 |
|
||
|
||
---
|
||
|
||
## 3 枚举值总览
|
||
|
||
### property_type(房源类型)
|
||
|
||
| 值 | 说明 |
|
||
|----|------|
|
||
| `residential` | 住宅 |
|
||
| `villa` | 别墅 |
|
||
| `commercial_residential` | 商住 |
|
||
| `shop` | 商铺 |
|
||
| `office` | 写字楼 |
|
||
| `other` | 其他 |
|
||
|
||
### status(交易状态)
|
||
|
||
| 值 | 说明 |
|
||
|----|------|
|
||
| `for_sale` | 出售 |
|
||
| `for_rent` | 出租 |
|
||
| `for_sale_rent` | 租售 |
|
||
| `suspended` | 暂缓 |
|
||
| `sold_elsewhere` | 他售 |
|
||
| `rented_elsewhere` | 他租 |
|
||
| `sold` | 成交 |
|
||
| `unlisted` | 未挂牌 |
|
||
|
||
### attribute(流通属性)
|
||
|
||
| 值 | 说明 |
|
||
|----|------|
|
||
| `public` | 公盘 |
|
||
| `private` | 私盘 |
|
||
| `special` | 特盘 |
|
||
| `sealed` | 封盘 |
|
||
|
||
### orientation(朝向)
|
||
|
||
| 值 | 说明 |
|
||
|----|------|
|
||
| `east` | 东 |
|
||
| `south` | 南 |
|
||
| `west` | 西 |
|
||
| `north` | 北 |
|
||
| `southeast` | 东南 |
|
||
| `northeast` | 东北 |
|
||
| `east_west` | 东西 |
|
||
| `south_north` | 南北 |
|
||
| `northwest` | 西北 |
|
||
| `southwest` | 西南 |
|
||
|
||
### decoration(装修情况)
|
||
|
||
| 值 | 说明 |
|
||
|----|------|
|
||
| `rough` | 毛坯 |
|
||
| `plain` | 清水 |
|
||
| `simple` | 简装 |
|
||
| `medium` | 中装 |
|
||
| `fine` | 精装 |
|
||
| `luxury` | 豪装 |
|
||
|
||
### grade(等级)
|
||
|
||
| 值 | 说明 |
|
||
|----|------|
|
||
| `A_urgent` | A(急迫) |
|
||
| `A` | A |
|
||
| `B` | B(较强) |
|
||
| `C` | C(一般) |
|
||
| `D` | D |
|
||
|
||
### follow_log.log_type(跟进日志类型)
|
||
|
||
| 值 | 说明 | 可删除 |
|
||
|----|------|--------|
|
||
| `written` | 经纪人主动写入跟进 | 是 |
|
||
| `modified` | 字段变更自动生成 | 是 |
|
||
| `sensitive_op` | 敏感信息操作跟进 | 否 |
|
||
| `sensitive_view` | 敏感信息查看(查看号码等) | **否** |
|
||
| `other` | 其他(钥匙/新增联系人等) | 是 |
|
||
| `system` | 系统日志 | 是 |
|
||
|
||
---
|
||
|
||
## 4 DDL 定义
|
||
|
||
### 4.1 properties(房源主表)
|
||
|
||
```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')),
|
||
|
||
-- ── 交易状态 ──
|
||
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')),
|
||
|
||
-- ── 流通属性 ──
|
||
attribute VARCHAR(20) NOT NULL DEFAULT 'public'
|
||
CHECK (attribute IN ('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')),
|
||
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(10)
|
||
CHECK (grade IN ('A_urgent','A','B','C','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')),
|
||
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字)
|
||
|
||
-- ── 相关方 ──
|
||
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), -- 房源来源渠道(lookup_items 维护)
|
||
|
||
-- ── 维护完成度(冗余缓存,Celery 定期重算)──
|
||
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
|
||
);
|
||
|
||
-- ── 索引策略 ──
|
||
|
||
-- 核心列表过滤:status + attribute + type
|
||
CREATE INDEX idx_properties_status_attr ON properties(status, attribute, property_type)
|
||
WHERE deleted_at IS NULL;
|
||
|
||
-- 区域筛选
|
||
CREATE INDEX idx_properties_complex ON properties(complex_id)
|
||
WHERE deleted_at IS NULL;
|
||
|
||
-- 售价排序
|
||
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');
|
||
|
||
-- 面积区间
|
||
CREATE INDEX idx_properties_area ON properties(area)
|
||
WHERE deleted_at IS NULL;
|
||
|
||
-- 挂牌时间倒序
|
||
CREATE INDEX idx_properties_listed_at ON properties(listed_at DESC NULLS LAST)
|
||
WHERE deleted_at IS NULL;
|
||
|
||
-- 超时跟进检测
|
||
CREATE INDEX idx_properties_last_followed ON properties(last_followed_at DESC NULLS LAST)
|
||
WHERE deleted_at IS NULL;
|
||
|
||
-- 户型筛选
|
||
CREATE INDEX idx_properties_bedroom ON properties(bedroom_count)
|
||
WHERE deleted_at IS NULL;
|
||
|
||
-- 等级筛选
|
||
CREATE INDEX idx_properties_grade ON properties(grade)
|
||
WHERE deleted_at IS NULL;
|
||
|
||
-- 完成度排序
|
||
CREATE INDEX idx_properties_completeness ON properties(completeness_score)
|
||
WHERE deleted_at IS NULL;
|
||
|
||
-- 全文搜索
|
||
CREATE INDEX idx_properties_search ON properties USING gin(search_vector);
|
||
|
||
-- 相关方快速定位
|
||
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;
|
||
|
||
-- 列表默认排序(status + listed_at)
|
||
CREATE INDEX idx_properties_list_default ON properties(status, listed_at DESC NULLS LAST)
|
||
WHERE deleted_at IS NULL;
|
||
|
||
-- 高频复合索引(status + attribute + complex_id + sale_price)
|
||
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 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;
|
||
```
|
||
|
||
---
|
||
|
||
### 4.3 listing_histories(挂牌历史)
|
||
|
||
```sql
|
||
-- ============================================================
|
||
-- 挂牌历史:记录房源每次上架的完整快照
|
||
-- 注意:无 deleted_at,不可删除(append-only,合规要求)
|
||
-- ============================================================
|
||
|
||
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, -- {name, store_group, org_unit_name}
|
||
|
||
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';
|
||
```
|
||
|
||
---
|
||
|
||
### 4.4 price_changes(调价记录)
|
||
|
||
```sql
|
||
-- ============================================================
|
||
-- 调价记录:支持折线图展示,不可删除(append-only)
|
||
-- ============================================================
|
||
|
||
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
|
||
-- 无 deleted_at:不可删除
|
||
);
|
||
|
||
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);
|
||
```
|
||
|
||
---
|
||
|
||
### 4.5 follow_logs(跟进日志)
|
||
|
||
```sql
|
||
-- ============================================================
|
||
-- 跟进日志:系统最高写入频率的表
|
||
-- 6 种类型:written / modified / sensitive_op / sensitive_view / other / system
|
||
-- sensitive_view 类型:is_deletable=FALSE,合规不可删
|
||
-- ============================================================
|
||
|
||
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')),
|
||
|
||
-- 写入跟进专用字段
|
||
purpose VARCHAR(50), -- 跟进目的(lookup_items 维护)
|
||
content TEXT, -- 最少6字,最多500字
|
||
|
||
-- AI 辅助判断标签
|
||
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": "售价"}
|
||
|
||
-- 系统显示标签
|
||
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}
|
||
|
||
-- 是否可删除(sensitive_view = 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');
|
||
```
|
||
|
||
---
|
||
|
||
### 4.6 follow_log_attachments(跟进附件)
|
||
|
||
```sql
|
||
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, -- Cloudflare R2 存储路径
|
||
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);
|
||
```
|
||
|
||
---
|
||
|
||
### 4.7 follow_log_recordings(跟进录音)
|
||
|
||
```sql
|
||
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, -- Cloudflare R2 存储路径
|
||
duration_seconds INTEGER, -- 录音时长(秒)
|
||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||
);
|
||
|
||
CREATE INDEX idx_follow_recordings_log ON follow_log_recordings(follow_log_id);
|
||
```
|
||
|
||
---
|
||
|
||
### 4.8 property_keys(钥匙管理)
|
||
|
||
```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;
|
||
```
|
||
|
||
---
|
||
|
||
### 4.9 key_attachments(钥匙附件)
|
||
|
||
```sql
|
||
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()
|
||
);
|
||
|
||
CREATE INDEX idx_key_attachments_key ON key_attachments(key_id);
|
||
```
|
||
|
||
---
|
||
|
||
### 4.10 commissions(委托管理)
|
||
|
||
```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, -- 独家委托/非独家委托(lookup_items 维护)
|
||
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, -- {name, store_group}
|
||
|
||
signing_method VARCHAR(50), -- 签约方式(选择后动态展示委托书模板)
|
||
|
||
-- 委托人(产权人)信息
|
||
owner_type VARCHAR(20) NOT NULL DEFAULT 'owner'
|
||
CHECK (owner_type IN ('owner','authorized_third')),
|
||
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, -- 证件号 AES-256-GCM 加密
|
||
|
||
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';
|
||
```
|
||
|
||
---
|
||
|
||
### 4.11 commission_attachments(委托附件)
|
||
|
||
```sql
|
||
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);
|
||
```
|
||
|
||
---
|
||
|
||
### 4.12 field_surveys(实勘管理)
|
||
|
||
```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';
|
||
```
|
||
|
||
---
|
||
|
||
### 4.13 survey_photos(实勘照片)
|
||
|
||
```sql
|
||
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, -- Cloudflare R2 路径
|
||
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);
|
||
```
|
||
|
||
---
|
||
|
||
### 4.14 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;
|
||
```
|
||
|
||
---
|
||
|
||
### 4.15 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);
|
||
```
|
||
|
||
---
|
||
|
||
### 4.16 property_marketing(营销信息)
|
||
|
||
```sql
|
||
-- 1:1 扩展表,营销标题/核心卖点/业主心态/户型介绍/小区介绍
|
||
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
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
### 4.17 property_certificates(产证信息)
|
||
|
||
```sql
|
||
-- 1:1 扩展表,存储产权证相关信息
|
||
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
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
### 4.18 property_completeness(维护完成度)
|
||
|
||
```sql
|
||
-- ============================================================
|
||
-- 维护完成度快照(1:1)
|
||
-- 各维度得分由 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,
|
||
|
||
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()
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
### 4.19 property_tags / property_tag_relations(标签)
|
||
|
||
```sql
|
||
-- 标签字典(系统预置 + 运营自定义)
|
||
CREATE TABLE property_tags (
|
||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
name VARCHAR(50) NOT NULL,
|
||
color VARCHAR(7), -- HEX 颜色,如 #FF5733
|
||
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);
|
||
```
|
||
|
||
---
|
||
|
||
### 4.20 property_favorites(收藏)
|
||
|
||
```sql
|
||
-- 经纪人收藏房源(快速访问)
|
||
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);
|
||
```
|
||
|
||
---
|
||
|
||
### 4.21 property_protections(保护房)
|
||
|
||
```sql
|
||
-- 1:1,标记某套房源是否受保护(防止被他人抢单/公盘化)
|
||
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()
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
### 4.22 number_holder_approvals(号码方审批)
|
||
|
||
```sql
|
||
-- 号码方变更审批流:经纪人申请,上级审批
|
||
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';
|
||
CREATE INDEX idx_number_holder_approvals_property ON number_holder_approvals(property_id);
|
||
```
|
||
|
||
---
|
||
|
||
## 5 触发器
|
||
|
||
### 5.1 房源全文搜索向量自动维护
|
||
|
||
```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();
|
||
```
|
||
|
||
### 5.2 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();
|
||
```
|
||
|
||
---
|
||
|
||
## 6 查询模式参考
|
||
|
||
### 6.1 房源列表页(高频)
|
||
|
||
```sql
|
||
-- 出售公盘,按挂牌时间倒序
|
||
SELECT p.id, p.status, p.attribute, p.sale_price, p.area,
|
||
p.bedroom_count, p.floor, p.total_floors, p.completeness_score,
|
||
p.listed_at
|
||
FROM properties p
|
||
WHERE p.status = 'for_sale'
|
||
AND p.attribute = 'public'
|
||
AND p.deleted_at IS NULL
|
||
ORDER BY p.listed_at DESC NULLS LAST
|
||
LIMIT 20 OFFSET 0;
|
||
-- 命中索引:idx_properties_list_default
|
||
```
|
||
|
||
### 6.2 区域筛选(楼盘 + 商圈下钻)
|
||
|
||
```sql
|
||
-- 某商圈下所有出售房源
|
||
SELECT p.*
|
||
FROM properties p
|
||
JOIN complexes c ON c.id = p.complex_id
|
||
JOIN complex_business_areas cba ON cba.complex_id = c.id
|
||
WHERE cba.business_area_id = :business_area_id
|
||
AND p.status IN ('for_sale','for_sale_rent')
|
||
AND p.deleted_at IS NULL
|
||
ORDER BY p.listed_at DESC NULLS LAST;
|
||
-- 命中索引:idx_properties_complex + complex_business_areas 索引
|
||
```
|
||
|
||
### 6.3 与我相关(经纪人仪表板)
|
||
|
||
```sql
|
||
SELECT * FROM properties
|
||
WHERE seller_agent_id = :my_staff_id
|
||
AND status NOT IN ('sold','unlisted')
|
||
AND deleted_at IS NULL
|
||
ORDER BY listed_at DESC NULLS LAST;
|
||
-- 命中索引:idx_properties_my_properties
|
||
```
|
||
|
||
### 6.4 重复房源检测(录入前必查)
|
||
|
||
```sql
|
||
-- 通过联系人手机号哈希检测
|
||
SELECT pc.property_id, p.status, p.deleted_at
|
||
FROM property_contacts pc
|
||
JOIN properties p ON p.id = pc.property_id
|
||
WHERE pc.phone_hash = SHA256(:input_phone)
|
||
AND pc.deleted_at IS NULL;
|
||
-- 命中索引:idx_contacts_phone_hash
|
||
```
|
||
|
||
### 6.5 跟进日志时间线(详情页)
|
||
|
||
```sql
|
||
-- 房源详情页跟进 Tab(全部类型,时间倒序)
|
||
SELECT fl.*, fla.file_key, fla.file_name
|
||
FROM follow_logs fl
|
||
LEFT JOIN follow_log_attachments fla ON fla.follow_log_id = fl.id
|
||
WHERE fl.property_id = :property_id
|
||
AND fl.deleted_at IS NULL
|
||
ORDER BY fl.created_at DESC;
|
||
-- 命中索引:idx_follow_logs_property_time
|
||
```
|
||
|
||
---
|
||
|
||
## 7 禁止操作
|
||
|
||
| 操作 | 影响表 | 原因 |
|
||
|------|--------|------|
|
||
| `DELETE FROM listing_histories` | `listing_histories` | 挂牌历史不可删除,合规审计要求 |
|
||
| `DELETE FROM price_changes` | `price_changes` | 调价记录不可删除 |
|
||
| `UPDATE follow_logs SET deleted_at = NOW() WHERE is_deletable = FALSE` | `follow_logs` | 敏感信息查看类型日志不可删除 |
|
||
| 直接修改 `properties.completeness_score` | `properties` | 完成度分数只由 Celery 任务计算更新,禁止 Application 层直接写入 |
|
||
| 直接修改 `properties.last_followed_at` | `properties` | 由触发器 `trg_update_last_followed` 自动维护 |
|
||
| 删除 `property_tags WHERE is_system = TRUE` | `property_tags` | 系统预置标签不可删除 |
|
||
| 明文存储手机号 | `property_contacts` | 手机号必须 AES-256-GCM 加密后存 `phone_enc`,同时更新 `phone_hash` |
|
||
|
||
---
|
||
|
||
*DATA_MODEL_PROPERTY.md — Fonrey 房产经纪管理系统房源模块数据模型 v1.0*
|
||
*下一步:API 接口规范(房源模块 URL 设计 + Request/Response Schema)*
|