Sync: update data model docs

This commit is contained in:
2026-04-24 15:16:42 +08:00
parent f7e0d2b400
commit 81d97ce6c1
9 changed files with 3281 additions and 1532 deletions

View File

@@ -87,7 +87,7 @@
| **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`§3.3 | 系统核心表,每套二手房源的完整档案,支持出售/出租/出售兼出租三态 |
| **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) | 系统/人工推荐的客源↔房源配对 |
@@ -117,7 +117,7 @@ OrgUnit (组织架构)
| [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 等) | ✅ 完成 |
| 本文档 §3.3§3.16 | 房源核心properties 及配套 12 张表)、系统设置 | ✅ 完成 |
| [DATA_MODEL_PROPERTY.md](./DATA_MODEL_PROPERTY.md) | 房源管理properties 及配套 22 张表,含跟进/钥匙/委托/实勘/营销/产证/完成度等 | ✅ 完成 |
---
@@ -220,765 +220,46 @@ CREATE INDEX idx_domains_primary ON public.domains(tenant_id) WHERE is_primary =
---
### 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.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` 唯一约束:每套房源仅一张封面
---

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,860 @@
<mxfile host="app.diagrams.net" modified="2026-04-24" agent="OpenCode" version="21.0.0">
<diagram name="Fonrey ER Diagram" id="fonrey-er-v1">
<mxGraphModel dx="1422" dy="762" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="3300" pageHeight="2340" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<!-- ═══════════════════════════════════════════════════ -->
<!-- SWIM LANE BACKGROUNDS -->
<!-- ═══════════════════════════════════════════════════ -->
<!-- ORG / HR region -->
<mxCell id="region-org" value="ORG / HR" style="swimlane;startSize=30;fillColor=#0d3349;strokeColor=#22d3ee;fontColor=#22d3ee;fontSize=12;fontStyle=1;swimlaneLine=1;rounded=1;arcSize=3;" vertex="1" parent="1">
<mxGeometry x="40" y="60" width="340" height="760" as="geometry"/>
</mxCell>
<!-- REGION & COMPLEX region -->
<mxCell id="region-complex" value="REGION &amp; COMPLEX" style="swimlane;startSize=30;fillColor=#063b2f;strokeColor=#34d399;fontColor=#34d399;fontSize=12;fontStyle=1;swimlaneLine=1;rounded=1;arcSize=3;" vertex="1" parent="1">
<mxGeometry x="420" y="60" width="820" height="1380" as="geometry"/>
</mxCell>
<!-- PROPERTY region -->
<mxCell id="region-property" value="PROPERTY" style="swimlane;startSize=30;fillColor=#2d1a5e;strokeColor=#a78bfa;fontColor=#a78bfa;fontSize=12;fontStyle=1;swimlaneLine=1;rounded=1;arcSize=3;" vertex="1" parent="1">
<mxGeometry x="1280" y="60" width="900" height="1700" as="geometry"/>
</mxCell>
<!-- CLIENT region -->
<mxCell id="region-client" value="CLIENT" style="swimlane;startSize=30;fillColor=#3d1f06;strokeColor=#fbbf24;fontColor=#fbbf24;fontSize=12;fontStyle=1;swimlaneLine=1;rounded=1;arcSize=3;" vertex="1" parent="1">
<mxGeometry x="2220" y="60" width="860" height="1380" as="geometry"/>
</mxCell>
<!-- ═══════════════════════════════════════════════════ -->
<!-- ORG MODULE -->
<!-- ═══════════════════════════════════════════════════ -->
<!-- org_units -->
<mxCell id="org-units" value="&lt;b&gt;org_units&lt;/b&gt;
&lt;hr/&gt;
🔑 PK id: uuid
parent_id: uuid (FK → self)
type: varchar(20)
name: varchar(100)
path: varchar(500) [物化路径]
depth: smallint
sort_order: int
is_active: bool
created_at: timestamptz
deleted_at: timestamptz" style="text;html=1;strokeColor=#22d3ee;fillColor=#0d3349;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-org">
<mxGeometry x="30" y="60" width="280" height="185" as="geometry"/>
</mxCell>
<!-- staff -->
<mxCell id="staff" value="&lt;b&gt;staff&lt;/b&gt;
&lt;hr/&gt;
🔑 PK id: uuid
FK org_unit_id → org_units
name: varchar(50)
phone_enc: text [AES-256-GCM]
phone_hash: varchar(64) [SHA-256]
id_no_enc: text [AES]
user_id: uuid [FK → auth_user]
entry_date: date
status: active/resigned/...
is_active: bool
created_at: timestamptz
deleted_at: timestamptz" style="text;html=1;strokeColor=#22d3ee;fillColor=#0d3349;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-org">
<mxGeometry x="30" y="310" width="280" height="215" as="geometry"/>
</mxCell>
<!-- ═══════════════════════════════════════════════════ -->
<!-- REGION & COMPLEX MODULE -->
<!-- ═══════════════════════════════════════════════════ -->
<!-- districts -->
<mxCell id="districts" value="&lt;b&gt;districts&lt;/b&gt;
&lt;hr/&gt;
🔑 PK id: uuid
city: varchar(50)
name: varchar(50)
short_name: varchar(20)
sort_order: int
is_active: bool
created_at: timestamptz" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
<mxGeometry x="30" y="60" width="280" height="150" as="geometry"/>
</mxCell>
<!-- business_areas -->
<mxCell id="business-areas" value="&lt;b&gt;business_areas&lt;/b&gt;
&lt;hr/&gt;
🔑 PK id: uuid
🔗 FK district_id → districts
name: varchar(100)
latitude: numeric(10,7)
longitude: numeric(10,7)
sort_order: int
is_active: bool" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
<mxGeometry x="30" y="310" width="280" height="155" as="geometry"/>
</mxCell>
<!-- schools -->
<mxCell id="schools" value="&lt;b&gt;schools&lt;/b&gt;
&lt;hr/&gt;
🔑 PK id: uuid
🔗 FK district_id → districts
name: varchar(100)
type: primary/middle/high/k9/k12
nature: public/private/international
level: normal/key/top
is_active: bool" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
<mxGeometry x="490" y="60" width="290" height="155" as="geometry"/>
</mxCell>
<!-- complexes -->
<mxCell id="complexes" value="&lt;b&gt;complexes&lt;/b&gt;
&lt;hr/&gt;
🔑 PK id: uuid
🔗 FK district_id → districts
🔗 FK created_by → staff
name: varchar(200) [⚠ 不可直接修改]
address: varchar(500) [只读]
address_summary: varchar(100)
latitude: numeric(10,7)
longitude: numeric(10,7)
property_usage_types: varchar[]
building_structure: varchar(30)
building_type: slab/tower/slab_tower
land_use_years: varchar(30)
built_years: smallint[]
total_units: int
total_households: int
total_floor_area: numeric(12,2)
plot_area: numeric(12,2)
plot_ratio: numeric(5,2)
green_rate: numeric(5,2)
developer: varchar(200)
property_company: varchar(200)
property_fee: numeric(8,2)
property_phone: varchar(30)
parking_total: int
parking_underground: int
parking_ratio: varchar(20)
water_type: civil/commercial
electricity_type: civil/commercial
has_central_heating: bool
has_gas: bool
lock_building: bool
lock_room: bool
lock_info: bool
lock_standard_room: bool
search_vector: tsvector
remarks: text
is_active: bool
created_at: timestamptz
updated_at: timestamptz
deleted_at: timestamptz" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
<mxGeometry x="30" y="570" width="340" height="570" as="geometry"/>
</mxCell>
<!-- complex_aliases -->
<mxCell id="complex-aliases" value="&lt;b&gt;complex_aliases&lt;/b&gt;
&lt;hr/&gt;
🔑 PK id: uuid
🔗 FK complex_id → complexes
alias: varchar(200)
is_system: bool [系统别名只读]
created_at: timestamptz
🔗 FK created_by → staff" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
<mxGeometry x="490" y="570" width="290" height="130" as="geometry"/>
</mxCell>
<!-- complex_business_areas (join) -->
<mxCell id="complex-biz-areas" value="&lt;b&gt;complex_business_areas&lt;/b&gt; [N:M join]
&lt;hr/&gt;
🔗 FK complex_id → complexes
🔗 FK business_area_id → business_areas
is_primary: bool [UNIQUE where TRUE]" style="text;html=1;strokeColor=#34d399;fillColor=#0a2e22;strokeWidth=1;dashed=1;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#6ee7b7;whiteSpace=pre;" vertex="1" parent="region-complex">
<mxGeometry x="30" y="490" width="370" height="70" as="geometry"/>
</mxCell>
<!-- complex_schools (join) -->
<mxCell id="complex-schools" value="&lt;b&gt;complex_schools&lt;/b&gt; [N:M join]
&lt;hr/&gt;
🔗 FK complex_id → complexes
🔗 FK school_id → schools
zone_type: guaranteed/reference/lottery" style="text;html=1;strokeColor=#34d399;fillColor=#0a2e22;strokeWidth=1;dashed=1;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#6ee7b7;whiteSpace=pre;" vertex="1" parent="region-complex">
<mxGeometry x="490" y="250" width="300" height="75" as="geometry"/>
</mxCell>
<!-- buildings -->
<mxCell id="buildings" value="&lt;b&gt;buildings&lt;/b&gt;
&lt;hr/&gt;
🔑 PK id: uuid
🔗 FK complex_id → complexes
🔗 FK school_id → schools [楼栋级学区]
name: varchar(50)
is_standard: bool
property_usage_type: varchar(20)
built_year: smallint
total_floors: smallint
land_use_years: varchar(30)
has_elevator: bool
is_active: bool
created_at: timestamptz
🔗 FK created_by → staff" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
<mxGeometry x="30" y="1000" width="310" height="225" as="geometry"/>
</mxCell>
<!-- room_units -->
<mxCell id="room-units" value="&lt;b&gt;room_units&lt;/b&gt;
&lt;hr/&gt;
🔑 PK id: uuid
🔗 FK building_id → buildings
floor: smallint
floor_name: varchar(20)
room_no: varchar(30)
display_no: varchar(50)
is_standard: bool
is_active: bool
created_at: timestamptz
updated_at: timestamptz
UNIQUE(building_id, floor, room_no)" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
<mxGeometry x="30" y="1260" width="310" height="200" as="geometry"/>
</mxCell>
<!-- complex_price_trends -->
<mxCell id="complex-price-trends" value="&lt;b&gt;complex_price_trends&lt;/b&gt;
&lt;hr/&gt;
🔑 PK id: uuid
🔗 FK complex_id → complexes
record_month: date [存月份1日]
avg_sale_price: numeric(12,2)
avg_unit_price: numeric(10,2)
transaction_count: int
listing_count: int
created_at: timestamptz
UNIQUE(complex_id, record_month)" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
<mxGeometry x="400" y="1000" width="380" height="185" as="geometry"/>
</mxCell>
<!-- metro_lines -->
<mxCell id="metro-lines" value="&lt;b&gt;metro_lines&lt;/b&gt;
&lt;hr/&gt;
🔑 PK id: uuid
city: varchar(50)
name: varchar(50)
color: varchar(7) [HEX]
sort_order: int
is_active: bool" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
<mxGeometry x="30" y="1520" width="260" height="130" as="geometry"/>
</mxCell>
<!-- metro_stations -->
<mxCell id="metro-stations" value="&lt;b&gt;metro_stations&lt;/b&gt;
&lt;hr/&gt;
🔑 PK id: uuid
🔗 FK metro_line_id → metro_lines
name: varchar(50)
latitude: numeric(10,7)
longitude: numeric(10,7)
sort_order: int
is_active: bool" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
<mxGeometry x="320" y="1520" width="280" height="150" as="geometry"/>
</mxCell>
<!-- complex_metro_stations (join) -->
<mxCell id="complex-metro-stations" value="&lt;b&gt;complex_metro_stations&lt;/b&gt; [N:M join]
&lt;hr/&gt;
🔗 FK complex_id → complexes
🔗 FK station_id → metro_stations
distance_meters: int [步行距离]" style="text;html=1;strokeColor=#34d399;fillColor=#0a2e22;strokeWidth=1;dashed=1;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#6ee7b7;whiteSpace=pre;" vertex="1" parent="region-complex">
<mxGeometry x="320" y="1700" width="320" height="70" as="geometry"/>
</mxCell>
<!-- complex_photos -->
<mxCell id="complex-photos" value="&lt;b&gt;complex_photos&lt;/b&gt;
&lt;hr/&gt;
🔑 PK id: uuid
🔗 FK complex_id → complexes
category: complex/layout/vr/other
file_key: text [R2/S3]
thumbnail_key: text
file_name: varchar(255)
file_size: int
width, height: int
is_cover: bool [UNIQUE where TRUE]
sort_order: smallint
created_at: timestamptz
🔗 FK created_by → staff" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
<mxGeometry x="490" y="770" width="300" height="205" as="geometry"/>
</mxCell>
<!-- ═══════════════════════════════════════════════════ -->
<!-- PROPERTY MODULE -->
<!-- ═══════════════════════════════════════════════════ -->
<!-- properties -->
<mxCell id="properties" value="&lt;b&gt;properties&lt;/b&gt;
&lt;hr/&gt;
🔑 PK id: uuid
🔗 FK complex_id → complexes
🔗 FK building_id → buildings
🔗 FK room_unit_id → room_units
🔗 FK agent_id → staff
listing_type: sale/rent/both
status: varchar(20)
sale_price: numeric(12,2) [万元]
rent_price: numeric(10,2) [元/月]
area: numeric(8,2) [m²]
floor: smallint
total_floors: smallint
bedroom: smallint
living_room: smallint
bathroom: smallint
orientation: varchar(30)
decoration: varchar(20)
has_elevator: bool
built_year: smallint
ownership_years: varchar(20)
is_exclusive: bool [独家委托]
completeness_score: int
search_vector: tsvector
source: varchar(30)
remarks: text
created_at: timestamptz
updated_at: timestamptz
deleted_at: timestamptz
🔗 FK created_by → staff
🔗 FK updated_by → staff
&lt;i&gt;[89,000+ rows · 复合索引 · 分区预留]&lt;/i&gt;" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
<mxGeometry x="30" y="60" width="380" height="560" as="geometry"/>
</mxCell>
<!-- property_contacts -->
<mxCell id="property-contacts" value="&lt;b&gt;property_contacts&lt;/b&gt;
&lt;hr/&gt;
🔑 PK id: uuid
🔗 FK property_id → properties
name: varchar(50)
phone_enc: text [AES-256-GCM]
phone_hash: varchar(64) [SHA-256]
role: owner/agent/tenant
is_primary: bool
created_at: timestamptz
deleted_at: timestamptz" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
<mxGeometry x="30" y="670" width="310" height="170" as="geometry"/>
</mxCell>
<!-- property_follow_logs -->
<mxCell id="property-follow-logs" value="&lt;b&gt;property_follow_logs&lt;/b&gt;
&lt;hr/&gt;
🔑 PK id: uuid
🔗 FK property_id → properties
🔗 FK staff_id → staff
log_type: call/visit/price_change/note/...
content: text
phone_no_viewed: bool [敏感操作]
created_at: timestamptz
🔗 FK created_by → staff
⚠ NO DELETE — append-only audit log" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
<mxGeometry x="470" y="60" width="380" height="185" as="geometry"/>
</mxCell>
<!-- listing_histories -->
<mxCell id="listing-histories" value="&lt;b&gt;listing_histories&lt;/b&gt;
&lt;hr/&gt;
🔑 PK id: uuid
🔗 FK property_id → properties
listed_at: timestamptz
delisted_at: timestamptz
list_price: numeric(12,2)
reason: varchar(50)
created_at: timestamptz" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
<mxGeometry x="470" y="300" width="310" height="155" as="geometry"/>
</mxCell>
<!-- property_photos -->
<mxCell id="property-photos" value="&lt;b&gt;property_photos&lt;/b&gt;
&lt;hr/&gt;
🔑 PK id: uuid
🔗 FK property_id → properties
category: listing/vr/layout/other
file_key: text [R2/S3]
thumbnail_key: text
is_cover: bool
sort_order: smallint
width, height: int
file_size: int
created_at: timestamptz
🔗 FK created_by → staff" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
<mxGeometry x="30" y="900" width="310" height="205" as="geometry"/>
</mxCell>
<!-- property_keys -->
<mxCell id="property-keys" value="&lt;b&gt;property_keys&lt;/b&gt;
&lt;hr/&gt;
🔑 PK id: uuid
🔗 FK property_id → properties
🔗 FK holder_id → staff
key_no: varchar(50)
status: held/returned
taken_at: timestamptz
returned_at: timestamptz
notes: text" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
<mxGeometry x="470" y="510" width="310" height="165" as="geometry"/>
</mxCell>
<!-- property_commissions -->
<mxCell id="property-commissions" value="&lt;b&gt;property_commissions&lt;/b&gt;
&lt;hr/&gt;
🔑 PK id: uuid
🔗 FK property_id → properties
commission_type: exclusive/open
rate: numeric(5,4)
amount: numeric(12,2)
start_date: date
end_date: date
signed_at: timestamptz
document_key: text
created_at: timestamptz" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
<mxGeometry x="470" y="740" width="330" height="185" as="geometry"/>
</mxCell>
<!-- property_inspections -->
<mxCell id="property-inspections" value="&lt;b&gt;property_inspections&lt;/b&gt;
&lt;hr/&gt;
🔑 PK id: uuid
🔗 FK property_id → properties
🔗 FK staff_id → staff
inspected_at: timestamptz
status: pending/done/cancelled
notes: text
attachments: jsonb
created_at: timestamptz" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
<mxGeometry x="30" y="1160" width="320" height="165" as="geometry"/>
</mxCell>
<!-- property_marketing -->
<mxCell id="property-marketing" value="&lt;b&gt;property_marketing&lt;/b&gt;
&lt;hr/&gt;
🔑 PK id: uuid
🔗 FK property_id → properties [UNIQUE 1:1]
title: varchar(200)
highlights: text[]
description: text
tags: varchar[]
platforms: jsonb
published_at: timestamptz
updated_at: timestamptz" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
<mxGeometry x="470" y="990" width="340" height="175" as="geometry"/>
</mxCell>
<!-- property_certificates -->
<mxCell id="property-certificates" value="&lt;b&gt;property_certificates&lt;/b&gt;
&lt;hr/&gt;
🔑 PK id: uuid
🔗 FK property_id → properties [UNIQUE 1:1]
cert_no: varchar(50)
owner_name: varchar(100)
ownership_type: varchar(30)
area_registered: numeric(8,2)
issue_date: date
document_key: text" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
<mxGeometry x="30" y="1390" width="330" height="165" as="geometry"/>
</mxCell>
<!-- completeness_scores -->
<mxCell id="completeness-scores" value="&lt;b&gt;completeness_scores&lt;/b&gt;
&lt;hr/&gt;
🔑 PK id: uuid
🔗 FK property_id → properties [UNIQUE 1:1]
score: int [0-100]
missing_fields: text[]
calculated_at: timestamptz
version: int" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
<mxGeometry x="470" y="1230" width="310" height="135" as="geometry"/>
</mxCell>
<!-- ═══════════════════════════════════════════════════ -->
<!-- CLIENT MODULE -->
<!-- ═══════════════════════════════════════════════════ -->
<!-- clients -->
<mxCell id="clients" value="&lt;b&gt;clients&lt;/b&gt;
&lt;hr/&gt;
🔑 PK id: uuid
🔗 FK agent_id → staff
client_type: private/public/closed
status: active/inactive/converted
name: varchar(50)
phone_enc: text [AES-256-GCM]
phone_hash: varchar(64) [SHA-256]
budget_min/max: numeric
activity_level: 1-5 [Celery每日计算]
is_protected: bool [防自动转公客]
transfer_to_public_type: auto/manual
last_follow_at: timestamptz
source: varchar(30)
remarks: text
created_at: timestamptz
deleted_at: timestamptz
🔗 FK created_by → staff
&lt;i&gt;[私客/公客/成交客 三态状态机]&lt;/i&gt;" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-client">
<mxGeometry x="30" y="60" width="370" height="360" as="geometry"/>
</mxCell>
<!-- client_requirements -->
<mxCell id="client-requirements" value="&lt;b&gt;client_requirements&lt;/b&gt;
&lt;hr/&gt;
🔑 PK id: uuid
🔗 FK client_id → clients
req_type: second_hand/new/rent
district_ids: uuid[]
business_area_ids: uuid[]
price_min: numeric
price_max: numeric
area_min: numeric
area_max: numeric
bedrooms: int[]
school_ids: uuid[]
has_elevator: bool
is_active: bool
created_at: timestamptz" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-client">
<mxGeometry x="30" y="480" width="350" height="260" as="geometry"/>
</mxCell>
<!-- client_follow_logs -->
<mxCell id="client-follow-logs" value="&lt;b&gt;client_follow_logs&lt;/b&gt;
&lt;hr/&gt;
🔑 PK id: uuid
🔗 FK client_id → clients
🔗 FK staff_id → staff
log_type: call/visit/match/note/status_change
content: text
next_follow_date: date
created_at: timestamptz
🔗 FK created_by → staff
⚠ NO DELETE — append-only audit log" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-client">
<mxGeometry x="430" y="60" width="380" height="200" as="geometry"/>
</mxCell>
<!-- client_viewings -->
<mxCell id="client-viewings" value="&lt;b&gt;client_viewings&lt;/b&gt;
&lt;hr/&gt;
🔑 PK id: uuid
🔗 FK client_id → clients
🔗 FK property_id → properties
🔗 FK agent_id → staff
viewed_at: timestamptz
feedback: text
rating: smallint [1-5]
status: planned/done/cancelled
created_at: timestamptz" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-client">
<mxGeometry x="430" y="310" width="360" height="195" as="geometry"/>
</mxCell>
<!-- client_property_matches -->
<mxCell id="client-matches" value="&lt;b&gt;client_property_matches&lt;/b&gt;
&lt;hr/&gt;
🔑 PK id: uuid
🔗 FK client_id → clients
🔗 FK property_id → properties
🔗 FK agent_id → staff
match_type: system/manual
score: numeric(5,2)
status: pending/sent/viewed/dismissed
sent_at: timestamptz
viewed_at: timestamptz
created_at: timestamptz" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-client">
<mxGeometry x="30" y="800" width="380" height="205" as="geometry"/>
</mxCell>
<!-- client_status_logs -->
<mxCell id="client-status-logs" value="&lt;b&gt;client_status_logs&lt;/b&gt;
&lt;hr/&gt;
🔑 PK id: uuid
🔗 FK client_id → clients
from_status: varchar(20)
to_status: varchar(20)
transfer_type: auto/manual
reason: text
created_at: timestamptz
🔗 FK created_by → staff
⚠ NO DELETE — append-only audit log" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-client">
<mxGeometry x="430" y="560" width="370" height="195" as="geometry"/>
</mxCell>
<!-- client_favorite_folders -->
<mxCell id="client-fav-folders" value="&lt;b&gt;client_favorite_folders&lt;/b&gt;
&lt;hr/&gt;
🔑 PK id: uuid
🔗 FK client_id → clients
name: varchar(100)
sort_order: int
created_at: timestamptz" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-client">
<mxGeometry x="30" y="1070" width="300" height="130" as="geometry"/>
</mxCell>
<!-- client_folder_items -->
<mxCell id="client-folder-items" value="&lt;b&gt;client_folder_items&lt;/b&gt;
&lt;hr/&gt;
🔑 PK id: uuid
🔗 FK folder_id → client_favorite_folders
🔗 FK property_id → properties
sort_order: int
created_at: timestamptz" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-client">
<mxGeometry x="370" y="1070" width="320" height="130" as="geometry"/>
</mxCell>
<!-- ═══════════════════════════════════════════════════ -->
<!-- EDGES / RELATIONSHIPS -->
<!-- ═══════════════════════════════════════════════════ -->
<!-- OrgUnit self-ref -->
<mxCell id="e-org-self" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.3;entryDx=0;entryDy=0;strokeColor=#22d3ee;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="org-units" target="org-units" parent="region-org">
<mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="340" y="153"/><mxPoint x="340" y="108"/></Array></mxGeometry>
</mxCell>
<mxCell id="e-org-self-lbl" value="自引用 parent_id" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#22d3ee;" vertex="1" connectable="0" parent="e-org-self"><mxGeometry x="0.1" relative="1" as="geometry"/></mxCell>
<!-- OrgUnit → Staff -->
<mxCell id="e-org-staff" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#22d3ee;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="org-units" target="staff" parent="region-org">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-org-staff-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#22d3ee;" vertex="1" connectable="0" parent="e-org-staff"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- District → BusinessArea -->
<mxCell id="e-dist-biz" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="districts" target="business-areas" parent="region-complex">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-dist-biz-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" vertex="1" connectable="0" parent="e-dist-biz"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- District → Schools -->
<mxCell id="e-dist-school" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="districts" target="schools" parent="region-complex">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-dist-school-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" vertex="1" connectable="0" parent="e-dist-school"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- District → Complexes -->
<mxCell id="e-dist-complex" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="districts" target="complexes" parent="region-complex">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-dist-complex-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" vertex="1" connectable="0" parent="e-dist-complex"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- BusinessArea ↔ Complexes via join -->
<mxCell id="e-biz-join" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;dashed=1;endArrow=open;startArrow=open;fontSize=9;" edge="1" source="business-areas" target="complex-biz-areas" parent="region-complex">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-join-complex" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;dashed=1;endArrow=open;startArrow=open;fontSize=9;" edge="1" source="complex-biz-areas" target="complexes" parent="region-complex">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<!-- Schools ↔ Complexes via join -->
<mxCell id="e-school-join" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;dashed=1;endArrow=open;startArrow=open;fontSize=9;" edge="1" source="schools" target="complex-schools" parent="region-complex">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-school-join2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;dashed=1;endArrow=open;startArrow=open;fontSize=9;" edge="1" source="complex-schools" target="complexes" parent="region-complex">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<!-- Complexes → complex_aliases -->
<mxCell id="e-complex-alias" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="complexes" target="complex-aliases" parent="region-complex">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-complex-alias-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" vertex="1" connectable="0" parent="e-complex-alias"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Complexes → complex_photos -->
<mxCell id="e-complex-photos" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="complexes" target="complex-photos" parent="region-complex">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-complex-photos-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" vertex="1" connectable="0" parent="e-complex-photos"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Complexes → complex_price_trends -->
<mxCell id="e-complex-trend" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="complexes" target="complex-price-trends" parent="region-complex">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-complex-trend-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" vertex="1" connectable="0" parent="e-complex-trend"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Complexes → Buildings -->
<mxCell id="e-complex-bldg" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="complexes" target="buildings" parent="region-complex">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-complex-bldg-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" vertex="1" connectable="0" parent="e-complex-bldg"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Buildings → RoomUnits -->
<mxCell id="e-bldg-room" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="buildings" target="room-units" parent="region-complex">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-bldg-room-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" vertex="1" connectable="0" parent="e-bldg-room"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- MetroLine → MetroStation -->
<mxCell id="e-metro-line-station" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="metro-lines" target="metro-stations" parent="region-complex">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-metro-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" vertex="1" connectable="0" parent="e-metro-line-station"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- MetroStation ↔ Complexes via join -->
<mxCell id="e-metro-join1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;dashed=1;endArrow=open;startArrow=open;fontSize=9;" edge="1" source="metro-stations" target="complex-metro-stations" parent="region-complex">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-metro-join2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;dashed=1;endArrow=open;startArrow=open;fontSize=9;" edge="1" source="complex-metro-stations" target="complexes" parent="region-complex">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<!-- Properties → PropertyContacts -->
<mxCell id="e-prop-contact" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="property-contacts" parent="region-property">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-prop-contact-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-contact"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Properties → FollowLogs -->
<mxCell id="e-prop-follow" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="property-follow-logs" parent="region-property">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-prop-follow-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-follow"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Properties → ListingHistories -->
<mxCell id="e-prop-listing" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="listing-histories" parent="region-property">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-prop-listing-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-listing"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Properties → Photos -->
<mxCell id="e-prop-photos" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="property-photos" parent="region-property">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-prop-photos-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-photos"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Properties → Keys -->
<mxCell id="e-prop-keys" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="property-keys" parent="region-property">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-prop-keys-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-keys"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Properties → Commissions -->
<mxCell id="e-prop-comm" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="property-commissions" parent="region-property">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-prop-comm-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-comm"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Properties → Inspections -->
<mxCell id="e-prop-insp" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="property-inspections" parent="region-property">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-prop-insp-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-insp"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Properties → Marketing (1:1) -->
<mxCell id="e-prop-marketing" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERone;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="property-marketing" parent="region-property">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-prop-marketing-lbl" value="1:1" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-marketing"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Properties → Certificates (1:1) -->
<mxCell id="e-prop-cert" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERone;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="property-certificates" parent="region-property">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-prop-cert-lbl" value="1:1" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-cert"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Properties → Completeness (1:1) -->
<mxCell id="e-prop-score" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERone;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="completeness-scores" parent="region-property">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-prop-score-lbl" value="1:1" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-score"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Clients → ClientRequirements -->
<mxCell id="e-client-req" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#fbbf24;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="clients" target="client-requirements" parent="region-client">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-client-req-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fbbf24;" vertex="1" connectable="0" parent="e-client-req"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Clients → FollowLogs -->
<mxCell id="e-client-follow" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#fbbf24;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="clients" target="client-follow-logs" parent="region-client">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-client-follow-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fbbf24;" vertex="1" connectable="0" parent="e-client-follow"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Clients → Viewings -->
<mxCell id="e-client-viewing" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#fbbf24;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="clients" target="client-viewings" parent="region-client">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-client-viewing-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fbbf24;" vertex="1" connectable="0" parent="e-client-viewing"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Clients → Matches -->
<mxCell id="e-client-match" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#fbbf24;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="clients" target="client-matches" parent="region-client">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-client-match-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fbbf24;" vertex="1" connectable="0" parent="e-client-match"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Clients → StatusLogs -->
<mxCell id="e-client-statuslog" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#fbbf24;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="clients" target="client-status-logs" parent="region-client">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-client-statuslog-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fbbf24;" vertex="1" connectable="0" parent="e-client-statuslog"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Clients → FavFolders -->
<mxCell id="e-client-fav" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#fbbf24;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="clients" target="client-fav-folders" parent="region-client">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-client-fav-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fbbf24;" vertex="1" connectable="0" parent="e-client-fav"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- FavFolders → FolderItems -->
<mxCell id="e-fav-items" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#fbbf24;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="client-fav-folders" target="client-folder-items" parent="region-client">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-fav-items-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fbbf24;" vertex="1" connectable="0" parent="e-fav-items"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- ═══════════════════════════════════════════════════ -->
<!-- CROSS-REGION EDGES (parent=1) -->
<!-- ═══════════════════════════════════════════════════ -->
<!-- Complexes → Properties -->
<mxCell id="e-complex-prop" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#a78bfa;dashed=0;endArrow=ERmany;startArrow=ERone;fontSize=9;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.25;entryDx=0;entryDy=0;" edge="1" source="complexes" target="properties" parent="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-complex-prop-lbl" value="1:N complex_id" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-complex-prop"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Buildings → Properties -->
<mxCell id="e-bldg-prop" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#a78bfa;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="buildings" target="properties" parent="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-bldg-prop-lbl" value="1:N building_id" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-bldg-prop"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- RoomUnits → Properties -->
<mxCell id="e-room-prop" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#a78bfa;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="room-units" target="properties" parent="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-room-prop-lbl" value="1:N room_unit_id" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-room-prop"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Staff → Properties (agent_id) -->
<mxCell id="e-staff-prop" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#22d3ee;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="staff" target="properties" parent="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-staff-prop-lbl" value="agent_id" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#22d3ee;" vertex="1" connectable="0" parent="e-staff-prop"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Staff → Clients (agent_id) -->
<mxCell id="e-staff-client" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#22d3ee;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="staff" target="clients" parent="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-staff-client-lbl" value="agent_id" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#22d3ee;" vertex="1" connectable="0" parent="e-staff-client"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Properties → Viewings (cross-region) -->
<mxCell id="e-prop-viewing" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#fb923c;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="client-viewings" parent="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-prop-viewing-lbl" value="property_id" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fb923c;" vertex="1" connectable="0" parent="e-prop-viewing"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Properties → Matches (cross-region) -->
<mxCell id="e-prop-match" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#fb923c;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="client-matches" parent="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-prop-match-lbl" value="property_id" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fb923c;" vertex="1" connectable="0" parent="e-prop-match"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Properties → FolderItems (cross-region) -->
<mxCell id="e-prop-folder" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#fb923c;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="client-folder-items" parent="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-prop-folder-lbl" value="property_id" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fb923c;" vertex="1" connectable="0" parent="e-prop-folder"><mxGeometry relative="1" as="geometry"/></mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,774 @@
<mxfile host="app.diagrams.net" modified="2026-04-24" agent="OpenCode" version="21.0.0">
<diagram name="Fonrey ER Diagram" id="fonrey-er-v1">
<mxGraphModel dx="1422" dy="762" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="3300" pageHeight="2340" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<!-- ═══════════════════════════════════════════════════ -->
<!-- SWIM LANE BACKGROUNDS -->
<!-- ═══════════════════════════════════════════════════ -->
<!-- ORG / HR region -->
<mxCell id="region-org" value="ORG / HR" style="swimlane;startSize=30;fillColor=#0d3349;strokeColor=#22d3ee;fontColor=#22d3ee;fontSize=12;fontStyle=1;swimlaneLine=1;rounded=1;arcSize=3;" vertex="1" parent="1">
<mxGeometry x="40" y="60" width="340" height="760" as="geometry"/>
</mxCell>
<!-- REGION &amp; COMPLEX region -->
<mxCell id="region-complex" value="REGION &amp; COMPLEX" style="swimlane;startSize=30;fillColor=#063b2f;strokeColor=#34d399;fontColor=#34d399;fontSize=12;fontStyle=1;swimlaneLine=1;rounded=1;arcSize=3;" vertex="1" parent="1">
<mxGeometry x="420" y="60" width="820" height="1380" as="geometry"/>
</mxCell>
<!-- PROPERTY region -->
<mxCell id="region-property" value="PROPERTY" style="swimlane;startSize=30;fillColor=#2d1a5e;strokeColor=#a78bfa;fontColor=#a78bfa;fontSize=12;fontStyle=1;swimlaneLine=1;rounded=1;arcSize=3;" vertex="1" parent="1">
<mxGeometry x="1280" y="60" width="900" height="1700" as="geometry"/>
</mxCell>
<!-- CLIENT region -->
<mxCell id="region-client" value="CLIENT" style="swimlane;startSize=30;fillColor=#3d1f06;strokeColor=#fbbf24;fontColor=#fbbf24;fontSize=12;fontStyle=1;swimlaneLine=1;rounded=1;arcSize=3;" vertex="1" parent="1">
<mxGeometry x="2220" y="60" width="860" height="1380" as="geometry"/>
</mxCell>
<!-- ═══════════════════════════════════════════════════ -->
<!-- ORG MODULE -->
<!-- ═══════════════════════════════════════════════════ -->
<!-- org_units -->
<mxCell id="org-units" value="<b>org_units</b>
<hr/>
🔑 PK id: uuid
parent_id: uuid (FK → self)
type: varchar(20)
name: varchar(100)
path: varchar(500) [物化路径]
depth: smallint
sort_order: int
is_active: bool
created_at: timestamptz
deleted_at: timestamptz" style="text;html=1;strokeColor=#22d3ee;fillColor=#0d3349;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-org">
<mxGeometry x="30" y="60" width="280" height="185" as="geometry"/>
</mxCell>
<!-- staff -->
<mxCell id="staff" value="<b>staff</b>
<hr/>
🔑 PK id: uuid
FK org_unit_id → org_units
name: varchar(50)
phone_enc: text [AES-256-GCM]
phone_hash: varchar(64) [SHA-256]
id_no_enc: text [AES]
user_id: uuid [FK → auth_user]
entry_date: date
status: active/resigned/...
is_active: bool
created_at: timestamptz
deleted_at: timestamptz" style="text;html=1;strokeColor=#22d3ee;fillColor=#0d3349;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-org">
<mxGeometry x="30" y="310" width="280" height="215" as="geometry"/>
</mxCell>
<!-- ═══════════════════════════════════════════════════ -->
<!-- REGION &amp; COMPLEX MODULE -->
<!-- ═══════════════════════════════════════════════════ -->
<!-- districts -->
<mxCell id="districts" value="<b>districts</b>
<hr/>
🔑 PK id: uuid
city: varchar(50)
name: varchar(50)
short_name: varchar(20)
sort_order: int
is_active: bool
created_at: timestamptz" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
<mxGeometry x="30" y="60" width="280" height="150" as="geometry"/>
</mxCell>
<!-- business_areas -->
<mxCell id="business-areas" value="<b>business_areas</b>
<hr/>
🔑 PK id: uuid
🔗 FK district_id → districts
name: varchar(100)
latitude: numeric(10,7)
longitude: numeric(10,7)
sort_order: int
is_active: bool" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
<mxGeometry x="30" y="310" width="280" height="155" as="geometry"/>
</mxCell>
<!-- schools -->
<mxCell id="schools" value="<b>schools</b>
<hr/>
🔑 PK id: uuid
🔗 FK district_id → districts
name: varchar(100)
type: primary/middle/high/k9/k12
nature: public/private/international
level: normal/key/top
is_active: bool" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
<mxGeometry x="490" y="60" width="290" height="155" as="geometry"/>
</mxCell>
<!-- complexes -->
<mxCell id="complexes" value="<b>complexes</b>
<hr/>
🔑 PK id: uuid
🔗 FK district_id → districts
🔗 FK created_by → staff
name: varchar(200) [⚠ 不可直接修改]
address: varchar(500) [只读]
address_summary: varchar(100)
latitude: numeric(10,7)
longitude: numeric(10,7)
property_usage_types: varchar[]
building_structure: varchar(30)
building_type: slab/tower/slab_tower
land_use_years: varchar(30)
built_years: smallint[]
total_units: int
total_households: int
total_floor_area: numeric(12,2)
plot_area: numeric(12,2)
plot_ratio: numeric(5,2)
green_rate: numeric(5,2)
developer: varchar(200)
property_company: varchar(200)
property_fee: numeric(8,2)
property_phone: varchar(30)
parking_total: int
parking_underground: int
parking_ratio: varchar(20)
water_type: civil/commercial
electricity_type: civil/commercial
has_central_heating: bool
has_gas: bool
lock_building: bool
lock_room: bool
lock_info: bool
lock_standard_room: bool
search_vector: tsvector
remarks: text
is_active: bool
created_at: timestamptz
updated_at: timestamptz
deleted_at: timestamptz" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
<mxGeometry x="30" y="570" width="340" height="570" as="geometry"/>
</mxCell>
<!-- complex_aliases -->
<mxCell id="complex-aliases" value="<b>complex_aliases</b>
<hr/>
🔑 PK id: uuid
🔗 FK complex_id → complexes
alias: varchar(200)
is_system: bool [系统别名只读]
created_at: timestamptz
🔗 FK created_by → staff" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
<mxGeometry x="490" y="570" width="290" height="130" as="geometry"/>
</mxCell>
<!-- complex_business_areas (join) -->
<mxCell id="complex-biz-areas" value="<b>complex_business_areas</b> [N:M join]
<hr/>
🔗 FK complex_id → complexes
🔗 FK business_area_id → business_areas
is_primary: bool [UNIQUE where TRUE]" style="text;html=1;strokeColor=#34d399;fillColor=#0a2e22;strokeWidth=1;dashed=1;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#6ee7b7;whiteSpace=pre;" vertex="1" parent="region-complex">
<mxGeometry x="30" y="490" width="370" height="70" as="geometry"/>
</mxCell>
<!-- complex_schools (join) -->
<mxCell id="complex-schools" value="<b>complex_schools</b> [N:M join]
<hr/>
🔗 FK complex_id → complexes
🔗 FK school_id → schools
zone_type: guaranteed/reference/lottery" style="text;html=1;strokeColor=#34d399;fillColor=#0a2e22;strokeWidth=1;dashed=1;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#6ee7b7;whiteSpace=pre;" vertex="1" parent="region-complex">
<mxGeometry x="490" y="250" width="300" height="75" as="geometry"/>
</mxCell>
<!-- buildings -->
<mxCell id="buildings" value="<b>buildings</b>
<hr/>
🔑 PK id: uuid
🔗 FK complex_id → complexes
🔗 FK school_id → schools [楼栋级学区]
name: varchar(50)
is_standard: bool
property_usage_type: varchar(20)
built_year: smallint
total_floors: smallint
land_use_years: varchar(30)
has_elevator: bool
is_active: bool
created_at: timestamptz
🔗 FK created_by → staff" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
<mxGeometry x="30" y="1000" width="310" height="225" as="geometry"/>
</mxCell>
<!-- room_units -->
<mxCell id="room-units" value="<b>room_units</b>
<hr/>
🔑 PK id: uuid
🔗 FK building_id → buildings
floor: smallint
floor_name: varchar(20)
room_no: varchar(30)
display_no: varchar(50)
is_standard: bool
is_active: bool
created_at: timestamptz
updated_at: timestamptz
UNIQUE(building_id, floor, room_no)" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
<mxGeometry x="30" y="1260" width="310" height="200" as="geometry"/>
</mxCell>
<!-- complex_price_trends -->
<mxCell id="complex-price-trends" value="<b>complex_price_trends</b>
<hr/>
🔑 PK id: uuid
🔗 FK complex_id → complexes
record_month: date [存月份1日]
avg_sale_price: numeric(12,2)
avg_unit_price: numeric(10,2)
transaction_count: int
listing_count: int
created_at: timestamptz
UNIQUE(complex_id, record_month)" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
<mxGeometry x="400" y="1000" width="380" height="185" as="geometry"/>
</mxCell>
<!-- metro_lines -->
<mxCell id="metro-lines" value="<b>metro_lines</b>
<hr/>
🔑 PK id: uuid
city: varchar(50)
name: varchar(50)
color: varchar(7) [HEX]
sort_order: int
is_active: bool" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
<mxGeometry x="30" y="1520" width="260" height="130" as="geometry"/>
</mxCell>
<!-- metro_stations -->
<mxCell id="metro-stations" value="<b>metro_stations</b>
<hr/>
🔑 PK id: uuid
🔗 FK metro_line_id → metro_lines
name: varchar(50)
latitude: numeric(10,7)
longitude: numeric(10,7)
sort_order: int
is_active: bool" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
<mxGeometry x="320" y="1520" width="280" height="150" as="geometry"/>
</mxCell>
<!-- complex_metro_stations (join) -->
<mxCell id="complex-metro-stations" value="<b>complex_metro_stations</b> [N:M join]
<hr/>
🔗 FK complex_id → complexes
🔗 FK station_id → metro_stations
distance_meters: int [步行距离]" style="text;html=1;strokeColor=#34d399;fillColor=#0a2e22;strokeWidth=1;dashed=1;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#6ee7b7;whiteSpace=pre;" vertex="1" parent="region-complex">
<mxGeometry x="320" y="1700" width="320" height="70" as="geometry"/>
</mxCell>
<!-- complex_photos -->
<mxCell id="complex-photos" value="<b>complex_photos</b>
<hr/>
🔑 PK id: uuid
🔗 FK complex_id → complexes
category: complex/layout/vr/other
file_key: text [R2/S3]
thumbnail_key: text
file_name: varchar(255)
file_size: int
width, height: int
is_cover: bool [UNIQUE where TRUE]
sort_order: smallint
created_at: timestamptz
🔗 FK created_by → staff" style="text;html=1;strokeColor=#34d399;fillColor=#063b2f;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-complex">
<mxGeometry x="490" y="770" width="300" height="205" as="geometry"/>
</mxCell>
<!-- ═══════════════════════════════════════════════════ -->
<!-- PROPERTY MODULE -->
<!-- ═══════════════════════════════════════════════════ -->
<!-- properties -->
<mxCell id="properties" value="<b>properties</b>
<hr/>
🔑 PK id: uuid
🔗 FK complex_id → complexes
🔗 FK building_id → buildings
🔗 FK room_unit_id → room_units
🔗 FK agent_id → staff
listing_type: sale/rent/both
status: varchar(20)
sale_price: numeric(12,2) [万元]
rent_price: numeric(10,2) [元/月]
area: numeric(8,2) [m²]
floor: smallint
total_floors: smallint
bedroom: smallint
living_room: smallint
bathroom: smallint
orientation: varchar(30)
decoration: varchar(20)
has_elevator: bool
built_year: smallint
ownership_years: varchar(20)
is_exclusive: bool [独家委托]
completeness_score: int
search_vector: tsvector
source: varchar(30)
remarks: text
created_at: timestamptz
updated_at: timestamptz
deleted_at: timestamptz
🔗 FK created_by → staff
🔗 FK updated_by → staff
<i>[89,000+ rows · 复合索引 · 分区预留]</i>" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
<mxGeometry x="30" y="60" width="380" height="560" as="geometry"/>
</mxCell>
<!-- property_contacts -->
<mxCell id="property-contacts" value="<b>property_contacts</b>
<hr/>
🔑 PK id: uuid
🔗 FK property_id → properties
name: varchar(50)
phone_enc: text [AES-256-GCM]
phone_hash: varchar(64) [SHA-256]
role: owner/agent/tenant
is_primary: bool
created_at: timestamptz
deleted_at: timestamptz" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
<mxGeometry x="30" y="670" width="310" height="170" as="geometry"/>
</mxCell>
<!-- property_follow_logs -->
<mxCell id="property-follow-logs" value="<b>property_follow_logs</b>
<hr/>
🔑 PK id: uuid
🔗 FK property_id → properties
🔗 FK staff_id → staff
log_type: call/visit/price_change/note/...
content: text
phone_no_viewed: bool [敏感操作]
created_at: timestamptz
🔗 FK created_by → staff
⚠ NO DELETE — append-only audit log" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
<mxGeometry x="470" y="60" width="380" height="185" as="geometry"/>
</mxCell>
<!-- listing_histories -->
<mxCell id="listing-histories" value="<b>listing_histories</b>
<hr/>
🔑 PK id: uuid
🔗 FK property_id → properties
listed_at: timestamptz
delisted_at: timestamptz
list_price: numeric(12,2)
reason: varchar(50)
created_at: timestamptz" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
<mxGeometry x="470" y="300" width="310" height="155" as="geometry"/>
</mxCell>
<!-- property_photos -->
<mxCell id="property-photos" value="<b>property_photos</b>
<hr/>
🔑 PK id: uuid
🔗 FK property_id → properties
category: listing/vr/layout/other
file_key: text [R2/S3]
thumbnail_key: text
is_cover: bool
sort_order: smallint
width, height: int
file_size: int
created_at: timestamptz
🔗 FK created_by → staff" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
<mxGeometry x="30" y="900" width="310" height="205" as="geometry"/>
</mxCell>
<!-- property_keys -->
<mxCell id="property-keys" value="<b>property_keys</b>
<hr/>
🔑 PK id: uuid
🔗 FK property_id → properties
🔗 FK holder_id → staff
key_no: varchar(50)
status: held/returned
taken_at: timestamptz
returned_at: timestamptz
notes: text" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
<mxGeometry x="470" y="510" width="310" height="165" as="geometry"/>
</mxCell>
<!-- property_commissions -->
<mxCell id="property-commissions" value="<b>property_commissions</b>
<hr/>
🔑 PK id: uuid
🔗 FK property_id → properties
commission_type: exclusive/open
rate: numeric(5,4)
amount: numeric(12,2)
start_date: date
end_date: date
signed_at: timestamptz
document_key: text
created_at: timestamptz" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
<mxGeometry x="470" y="740" width="330" height="185" as="geometry"/>
</mxCell>
<!-- property_inspections -->
<mxCell id="property-inspections" value="<b>property_inspections</b>
<hr/>
🔑 PK id: uuid
🔗 FK property_id → properties
🔗 FK staff_id → staff
inspected_at: timestamptz
status: pending/done/cancelled
notes: text
attachments: jsonb
created_at: timestamptz" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
<mxGeometry x="30" y="1160" width="320" height="165" as="geometry"/>
</mxCell>
<!-- property_marketing -->
<mxCell id="property-marketing" value="<b>property_marketing</b>
<hr/>
🔑 PK id: uuid
🔗 FK property_id → properties [UNIQUE 1:1]
title: varchar(200)
highlights: text[]
description: text
tags: varchar[]
platforms: jsonb
published_at: timestamptz
updated_at: timestamptz" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
<mxGeometry x="470" y="990" width="340" height="175" as="geometry"/>
</mxCell>
<!-- property_certificates -->
<mxCell id="property-certificates" value="<b>property_certificates</b>
<hr/>
🔑 PK id: uuid
🔗 FK property_id → properties [UNIQUE 1:1]
cert_no: varchar(50)
owner_name: varchar(100)
ownership_type: varchar(30)
area_registered: numeric(8,2)
issue_date: date
document_key: text" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
<mxGeometry x="30" y="1390" width="330" height="165" as="geometry"/>
</mxCell>
<!-- completeness_scores -->
<mxCell id="completeness-scores" value="<b>completeness_scores</b>
<hr/>
🔑 PK id: uuid
🔗 FK property_id → properties [UNIQUE 1:1]
score: int [0-100]
missing_fields: text[]
calculated_at: timestamptz
version: int" style="text;html=1;strokeColor=#a78bfa;fillColor=#2d1a5e;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-property">
<mxGeometry x="470" y="1230" width="310" height="135" as="geometry"/>
</mxCell>
<!-- ═══════════════════════════════════════════════════ -->
<!-- CLIENT MODULE -->
<!-- ═══════════════════════════════════════════════════ -->
<!-- clients -->
<mxCell id="clients" value="<b>clients</b>
<hr/>
🔑 PK id: uuid
🔗 FK agent_id → staff
client_type: private/public/closed
status: active/inactive/converted
name: varchar(50)
phone_enc: text [AES-256-GCM]
phone_hash: varchar(64) [SHA-256]
budget_min/max: numeric
activity_level: 1-5 [Celery每日计算]
is_protected: bool [防自动转公客]
transfer_to_public_type: auto/manual
last_follow_at: timestamptz
source: varchar(30)
remarks: text
created_at: timestamptz
deleted_at: timestamptz
🔗 FK created_by → staff
<i>[私客/公客/成交客 三态状态机]</i>" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-client">
<mxGeometry x="30" y="60" width="370" height="360" as="geometry"/>
</mxCell>
<!-- client_requirements -->
<mxCell id="client-requirements" value="<b>client_requirements</b>
<hr/>
🔑 PK id: uuid
🔗 FK client_id → clients
req_type: second_hand/new/rent
district_ids: uuid[]
business_area_ids: uuid[]
price_min: numeric
price_max: numeric
area_min: numeric
area_max: numeric
bedrooms: int[]
school_ids: uuid[]
has_elevator: bool
is_active: bool
created_at: timestamptz" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-client">
<mxGeometry x="30" y="480" width="350" height="260" as="geometry"/>
</mxCell>
<!-- client_follow_logs -->
<mxCell id="client-follow-logs" value="<b>client_follow_logs</b>
<hr/>
🔑 PK id: uuid
🔗 FK client_id → clients
🔗 FK staff_id → staff
log_type: call/visit/match/note/status_change
content: text
next_follow_date: date
created_at: timestamptz
🔗 FK created_by → staff
⚠ NO DELETE — append-only audit log" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-client">
<mxGeometry x="430" y="60" width="380" height="200" as="geometry"/>
</mxCell>
<!-- client_viewings -->
<mxCell id="client-viewings" value="<b>client_viewings</b>
<hr/>
🔑 PK id: uuid
🔗 FK client_id → clients
🔗 FK property_id → properties
🔗 FK agent_id → staff
viewed_at: timestamptz
feedback: text
rating: smallint [1-5]
status: planned/done/cancelled
created_at: timestamptz" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-client">
<mxGeometry x="430" y="310" width="360" height="195" as="geometry"/>
</mxCell>
<!-- client_property_matches -->
<mxCell id="client-matches" value="<b>client_property_matches</b>
<hr/>
🔑 PK id: uuid
🔗 FK client_id → clients
🔗 FK property_id → properties
🔗 FK agent_id → staff
match_type: system/manual
score: numeric(5,2)
status: pending/sent/viewed/dismissed
sent_at: timestamptz
viewed_at: timestamptz
created_at: timestamptz" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-client">
<mxGeometry x="30" y="800" width="380" height="205" as="geometry"/>
</mxCell>
<!-- client_status_logs -->
<mxCell id="client-status-logs" value="<b>client_status_logs</b>
<hr/>
🔑 PK id: uuid
🔗 FK client_id → clients
from_status: varchar(20)
to_status: varchar(20)
transfer_type: auto/manual
reason: text
created_at: timestamptz
🔗 FK created_by → staff
⚠ NO DELETE — append-only audit log" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-client">
<mxGeometry x="430" y="560" width="370" height="195" as="geometry"/>
</mxCell>
<!-- client_favorite_folders -->
<mxCell id="client-fav-folders" value="<b>client_favorite_folders</b>
<hr/>
🔑 PK id: uuid
🔗 FK client_id → clients
name: varchar(100)
sort_order: int
created_at: timestamptz" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-client">
<mxGeometry x="30" y="1070" width="300" height="130" as="geometry"/>
</mxCell>
<!-- client_folder_items -->
<mxCell id="client-folder-items" value="<b>client_folder_items</b>
<hr/>
🔑 PK id: uuid
🔗 FK folder_id → client_favorite_folders
🔗 FK property_id → properties
sort_order: int
created_at: timestamptz" style="text;html=1;strokeColor=#fbbf24;fillColor=#3d1f06;align=left;verticalAlign=top;spacingLeft=8;spacingTop=4;overflow=hidden;rotatable=0;fontSize=11;fontFamily=monospace;fontColor=#e2e8f0;whiteSpace=pre;" vertex="1" parent="region-client">
<mxGeometry x="370" y="1070" width="320" height="130" as="geometry"/>
</mxCell>
<!-- ═══════════════════════════════════════════════════ -->
<!-- EDGES / RELATIONSHIPS -->
<!-- ═══════════════════════════════════════════════════ -->
<!-- OrgUnit self-ref -->
<mxCell id="e-org-self" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.3;entryDx=0;entryDy=0;strokeColor=#22d3ee;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="org-units" target="org-units" parent="region-org">
<mxGeometry relative="1" as="geometry"><Array as="points"><mxPoint x="340" y="153"/><mxPoint x="340" y="108"/></Array></mxGeometry>
</mxCell>
<mxCell id="e-org-self-lbl" value="自引用 parent_id" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#22d3ee;" vertex="1" connectable="0" parent="e-org-self"><mxGeometry x="0.1" relative="1" as="geometry"/></mxCell>
<!-- OrgUnit → Staff -->
<mxCell id="e-org-staff" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#22d3ee;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="org-units" target="staff" parent="region-org">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-org-staff-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#22d3ee;" vertex="1" connectable="0" parent="e-org-staff"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- District → BusinessArea -->
<mxCell id="e-dist-biz" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="districts" target="business-areas" parent="region-complex">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-dist-biz-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" vertex="1" connectable="0" parent="e-dist-biz"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- District → Schools -->
<mxCell id="e-dist-school" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="districts" target="schools" parent="region-complex">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-dist-school-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" vertex="1" connectable="0" parent="e-dist-school"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- District → Complexes -->
<mxCell id="e-dist-complex" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="districts" target="complexes" parent="region-complex">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-dist-complex-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" vertex="1" connectable="0" parent="e-dist-complex"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- BusinessArea ↔ Complexes via join -->
<mxCell id="e-biz-join" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;dashed=1;endArrow=open;startArrow=open;fontSize=9;" edge="1" source="business-areas" target="complex-biz-areas" parent="region-complex">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-join-complex" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;dashed=1;endArrow=open;startArrow=open;fontSize=9;" edge="1" source="complex-biz-areas" target="complexes" parent="region-complex">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<!-- Schools ↔ Complexes via join -->
<mxCell id="e-school-join" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;dashed=1;endArrow=open;startArrow=open;fontSize=9;" edge="1" source="schools" target="complex-schools" parent="region-complex">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-school-join2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;dashed=1;endArrow=open;startArrow=open;fontSize=9;" edge="1" source="complex-schools" target="complexes" parent="region-complex">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<!-- Complexes → complex_aliases -->
<mxCell id="e-complex-alias" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="complexes" target="complex-aliases" parent="region-complex">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-complex-alias-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" vertex="1" connectable="0" parent="e-complex-alias"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Complexes → complex_photos -->
<mxCell id="e-complex-photos" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="complexes" target="complex-photos" parent="region-complex">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-complex-photos-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" vertex="1" connectable="0" parent="e-complex-photos"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Complexes → complex_price_trends -->
<mxCell id="e-complex-trend" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="complexes" target="complex-price-trends" parent="region-complex">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-complex-trend-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" vertex="1" connectable="0" parent="e-complex-trend"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Complexes → Buildings -->
<mxCell id="e-complex-bldg" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="complexes" target="buildings" parent="region-complex">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-complex-bldg-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" vertex="1" connectable="0" parent="e-complex-bldg"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Buildings → RoomUnits -->
<mxCell id="e-bldg-room" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="buildings" target="room-units" parent="region-complex">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-bldg-room-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" vertex="1" connectable="0" parent="e-bldg-room"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- MetroLine → MetroStation -->
<mxCell id="e-metro-line-station" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="metro-lines" target="metro-stations" parent="region-complex">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-metro-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#34d399;" vertex="1" connectable="0" parent="e-metro-line-station"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- MetroStation ↔ Complexes via join -->
<mxCell id="e-metro-join1" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;dashed=1;endArrow=open;startArrow=open;fontSize=9;" edge="1" source="metro-stations" target="complex-metro-stations" parent="region-complex">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-metro-join2" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#34d399;dashed=1;endArrow=open;startArrow=open;fontSize=9;" edge="1" source="complex-metro-stations" target="complexes" parent="region-complex">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<!-- Properties → PropertyContacts -->
<mxCell id="e-prop-contact" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="property-contacts" parent="region-property">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-prop-contact-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-contact"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Properties → FollowLogs -->
<mxCell id="e-prop-follow" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="property-follow-logs" parent="region-property">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-prop-follow-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-follow"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Properties → ListingHistories -->
<mxCell id="e-prop-listing" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="listing-histories" parent="region-property">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-prop-listing-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-listing"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Properties → Photos -->
<mxCell id="e-prop-photos" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="property-photos" parent="region-property">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-prop-photos-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-photos"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Properties → Keys -->
<mxCell id="e-prop-keys" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="property-keys" parent="region-property">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-prop-keys-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-keys"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Properties → Commissions -->
<mxCell id="e-prop-comm" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="property-commissions" parent="region-property">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-prop-comm-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-comm"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Properties → Inspections -->
<mxCell id="e-prop-insp" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="property-inspections" parent="region-property">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-prop-insp-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-insp"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Properties → Marketing (1:1) -->
<mxCell id="e-prop-marketing" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERone;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="property-marketing" parent="region-property">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-prop-marketing-lbl" value="1:1" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-marketing"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Properties → Certificates (1:1) -->
<mxCell id="e-prop-cert" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERone;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="property-certificates" parent="region-property">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-prop-cert-lbl" value="1:1" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-cert"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Properties → Completeness (1:1) -->
<mxCell id="e-prop-score" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#a78bfa;endArrow=ERone;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="completeness-scores" parent="region-property">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-prop-score-lbl" value="1:1" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-prop-score"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Clients → ClientRequirements -->
<mxCell id="e-client-req" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#fbbf24;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="clients" target="client-requirements" parent="region-client">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-client-req-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fbbf24;" vertex="1" connectable="0" parent="e-client-req"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Clients → FollowLogs -->
<mxCell id="e-client-follow" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#fbbf24;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="clients" target="client-follow-logs" parent="region-client">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-client-follow-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fbbf24;" vertex="1" connectable="0" parent="e-client-follow"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Clients → Viewings -->
<mxCell id="e-client-viewing" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#fbbf24;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="clients" target="client-viewings" parent="region-client">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-client-viewing-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fbbf24;" vertex="1" connectable="0" parent="e-client-viewing"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Clients → Matches -->
<mxCell id="e-client-match" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#fbbf24;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="clients" target="client-matches" parent="region-client">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-client-match-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fbbf24;" vertex="1" connectable="0" parent="e-client-match"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Clients → StatusLogs -->
<mxCell id="e-client-statuslog" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#fbbf24;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="clients" target="client-status-logs" parent="region-client">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-client-statuslog-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fbbf24;" vertex="1" connectable="0" parent="e-client-statuslog"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Clients → FavFolders -->
<mxCell id="e-client-fav" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#fbbf24;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="clients" target="client-fav-folders" parent="region-client">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-client-fav-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fbbf24;" vertex="1" connectable="0" parent="e-client-fav"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- FavFolders → FolderItems -->
<mxCell id="e-fav-items" style="edgeStyle=orthogonalEdgeStyle;rounded=0;strokeColor=#fbbf24;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="client-fav-folders" target="client-folder-items" parent="region-client">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-fav-items-lbl" value="1:N" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fbbf24;" vertex="1" connectable="0" parent="e-fav-items"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- ═══════════════════════════════════════════════════ -->
<!-- CROSS-REGION EDGES (parent=1) -->
<!-- ═══════════════════════════════════════════════════ -->
<!-- Complexes → Properties -->
<mxCell id="e-complex-prop" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#a78bfa;dashed=0;endArrow=ERmany;startArrow=ERone;fontSize=9;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.25;entryDx=0;entryDy=0;" edge="1" source="complexes" target="properties" parent="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-complex-prop-lbl" value="1:N complex_id" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-complex-prop"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Buildings → Properties -->
<mxCell id="e-bldg-prop" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#a78bfa;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="buildings" target="properties" parent="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-bldg-prop-lbl" value="1:N building_id" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-bldg-prop"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- RoomUnits → Properties -->
<mxCell id="e-room-prop" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#a78bfa;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="room-units" target="properties" parent="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-room-prop-lbl" value="1:N room_unit_id" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#a78bfa;" vertex="1" connectable="0" parent="e-room-prop"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Staff → Properties (agent_id) -->
<mxCell id="e-staff-prop" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#22d3ee;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="staff" target="properties" parent="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-staff-prop-lbl" value="agent_id" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#22d3ee;" vertex="1" connectable="0" parent="e-staff-prop"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Staff → Clients (agent_id) -->
<mxCell id="e-staff-client" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#22d3ee;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="staff" target="clients" parent="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-staff-client-lbl" value="agent_id" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#22d3ee;" vertex="1" connectable="0" parent="e-staff-client"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Properties → Viewings (cross-region) -->
<mxCell id="e-prop-viewing" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#fb923c;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="client-viewings" parent="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-prop-viewing-lbl" value="property_id" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fb923c;" vertex="1" connectable="0" parent="e-prop-viewing"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Properties → Matches (cross-region) -->
<mxCell id="e-prop-match" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#fb923c;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="client-matches" parent="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-prop-match-lbl" value="property_id" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fb923c;" vertex="1" connectable="0" parent="e-prop-match"><mxGeometry relative="1" as="geometry"/></mxCell>
<!-- Properties → FolderItems (cross-region) -->
<mxCell id="e-prop-folder" style="edgeStyle=orthogonalEdgeStyle;rounded=1;orthogonalLoop=1;strokeColor=#fb923c;dashed=1;endArrow=ERmany;startArrow=ERone;fontSize=9;" edge="1" source="properties" target="client-folder-items" parent="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="e-prop-folder-lbl" value="property_id" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;fontSize=9;fontColor=#fb923c;" vertex="1" connectable="0" parent="e-prop-folder"><mxGeometry relative="1" as="geometry"/></mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>