Sync: expand data model and gitops notes
This commit is contained in:
@@ -70,7 +70,59 @@
|
||||
|
||||
---
|
||||
|
||||
## 二、公共 Schema(Shared / Public)
|
||||
## 二、领域概览(Domain Overview)
|
||||
|
||||
本节用业务语言描述系统的核心领域对象及其关系,作为各子模块数据模型的导读。
|
||||
|
||||
### 核心领域对象
|
||||
|
||||
| 领域对象 | 表/子文档 | 业务说明 |
|
||||
|----------|-----------|----------|
|
||||
| **Tenant(租户)** | `public.tenants` | 每家房产经纪公司对应一个租户,数据完全隔离(Schema-per-Tenant) |
|
||||
| **OrgUnit(组织架构)** | `org_units` → [DATA_MODEL_ORG.md](./DATA_MODEL_ORG.md) | 树形组织架构(总部/区域/城市/大区/分公司/门店/团队/虚拟团队),物化路径存储,支持权限继承 |
|
||||
| **Staff(员工)** | `staff` → [DATA_MODEL_ORG.md](./DATA_MODEL_ORG.md) | 经纪人/店长/经理,绑定组织节点,手机号加密存储,与账号(登录)分离 |
|
||||
| **District(城区)** | `districts` → [DATA_MODEL_COMPLEX.md](./DATA_MODEL_COMPLEX.md) | 行政区划,如「静安区」,是区域体系的顶层节点 |
|
||||
| **BusinessArea(商圈)** | `business_areas` → [DATA_MODEL_COMPLEX.md](./DATA_MODEL_COMPLEX.md) | 商圈/板块,从属于城区,一个楼盘可归属多个商圈 |
|
||||
| **School(学校)** | `schools` → [DATA_MODEL_COMPLEX.md](./DATA_MODEL_COMPLEX.md) | 对口学校数据库,是买家购房决策的核心参考,与楼盘多对多关联 |
|
||||
| **Complex(楼盘/小区)** | `complexes` → [DATA_MODEL_COMPLEX.md](./DATA_MODEL_COMPLEX.md) | 房源录入的基础底座,维护楼盘标准名称/坐标/锁定状态/别名等 |
|
||||
| **Building(楼栋/单元)** | `buildings` → [DATA_MODEL_COMPLEX.md](./DATA_MODEL_COMPLEX.md) | 楼盘下的物理楼栋,区分标准结构与非标结构 |
|
||||
| **RoomUnit(房号)** | `room_units` → [DATA_MODEL_COMPLEX.md](./DATA_MODEL_COMPLEX.md) | 楼层+房间号,房源定位的最细粒度 |
|
||||
| **Property(房源)** | `properties` → §3.3 | 系统核心表,每套二手房源的完整档案,支持出售/出租/出售兼出租三态 |
|
||||
| **Client(客源)** | `clients` → [DATA_MODEL_CLIENT.md](./DATA_MODEL_CLIENT.md) | 买家/租客档案,分私客/公客/成交客,含活跃度评分与自动公客转换机制 |
|
||||
| **Viewing(带看)** | `client_viewings` → [DATA_MODEL_CLIENT.md](./DATA_MODEL_CLIENT.md) | 经纪人带客户看房的完整记录 |
|
||||
| **Match(配对)** | `client_property_matches` → [DATA_MODEL_CLIENT.md](./DATA_MODEL_CLIENT.md) | 系统/人工推荐的客源↔房源配对 |
|
||||
|
||||
### 领域关系快速导航
|
||||
|
||||
```
|
||||
District (城区)
|
||||
└─ BusinessArea (商圈)
|
||||
└─ Complex (楼盘) ─── School (对口学校)
|
||||
├─ Building (楼栋)
|
||||
│ └─ RoomUnit (房号)
|
||||
└─ Property (房源)
|
||||
├─ PropertyContact (联系人/委托方)
|
||||
├─ FollowLog (跟进日志)
|
||||
├─ Viewing (带看记录) ──── Client (客源)
|
||||
└─ Match (配对记录) ──────┘
|
||||
|
||||
OrgUnit (组织架构)
|
||||
└─ Staff (员工/经纪人) ─── Property / Client / Viewing / Match
|
||||
```
|
||||
|
||||
### 子文档索引
|
||||
|
||||
| 子文档 | 覆盖模块 | 状态 |
|
||||
|--------|----------|------|
|
||||
| [DATA_MODEL_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 张表)、系统设置 | ✅ 完成 |
|
||||
|
||||
---
|
||||
|
||||
## 三、公共 Schema(Shared / Public)
|
||||
|
||||
|
||||
```sql
|
||||
-- ============================================================
|
||||
@@ -107,7 +159,7 @@ CREATE INDEX idx_domains_primary ON public.domains(tenant_id) WHERE is_primary =
|
||||
|
||||
---
|
||||
|
||||
## 三、租户 Schema(Tenant Schema)
|
||||
## 四、租户 Schema(Tenant Schema)
|
||||
|
||||
以下所有表均在每个租户的独立 Schema 内创建。
|
||||
|
||||
@@ -115,190 +167,56 @@ CREATE INDEX idx_domains_primary ON public.domains(tenant_id) WHERE is_primary =
|
||||
|
||||
### 3.1 组织人事模块(Organization & HR)
|
||||
|
||||
```sql
|
||||
-- ============================================================
|
||||
-- 组织架构:公司 → 区域 → 门店 → 组
|
||||
-- ============================================================
|
||||
> **详细模型** → 见 [`DATA_MODEL_ORG.md`](./DATA_MODEL_ORG.md)
|
||||
> 该文件为权威定义,包含完整字段、枚举、查询模式和禁止操作。
|
||||
|
||||
-- 组织节点表(树形结构,支持无限层级)
|
||||
CREATE TABLE org_units (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(100) NOT NULL,
|
||||
type VARCHAR(20) NOT NULL
|
||||
CHECK (type IN ('company','region','store','group')),
|
||||
parent_id UUID REFERENCES org_units(id) ON DELETE RESTRICT,
|
||||
path TEXT NOT NULL, -- 物化路径:/root_id/parent_id/self_id/
|
||||
depth SMALLINT NOT NULL DEFAULT 0,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
**核心表概览**(开发时以 DATA_MODEL_ORG.md 为准):
|
||||
|
||||
CREATE INDEX idx_org_units_parent ON org_units(parent_id) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_org_units_path ON org_units USING gist(path gist_trgm_ops);
|
||||
-- 注:gist_trgm_ops 需要 pg_trgm 扩展,用于路径前缀查询
|
||||
| 表名 | 说明 |
|
||||
|------|------|
|
||||
| `org_units` | 组织树节点(公司/事业部/大区/区域/片区/门店/店组/职能),物化路径树 |
|
||||
| `staff` | 员工主表,含加密手机号、角色、在职状态、Django auth 绑定 |
|
||||
| `staff_personal_info` | 员工个人信息扩展(证件、学历、婚育等,1:1) |
|
||||
| `staff_transfer_logs` | 人事异动不可变审计日志(入职/调动/离职/复职等) |
|
||||
| `staff_reward_punish` | 奖惩记录 |
|
||||
| `staff_work_experiences` | 工作经历 |
|
||||
| `staff_educations` | 教育经历 |
|
||||
| `staff_trainings` | 培训经历 |
|
||||
| `staff_family_members` | 家庭成员 |
|
||||
| `staff_accounts` | 第三方平台账号绑定(58安居客/中国网络经纪人等) |
|
||||
|
||||
-- 员工表
|
||||
CREATE TABLE staff (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
org_unit_id UUID NOT NULL REFERENCES org_units(id) ON DELETE RESTRICT,
|
||||
name VARCHAR(50) NOT NULL,
|
||||
phone_hash VARCHAR(64), -- SHA-256 哈希,用于唯一性校验
|
||||
phone_enc BYTEA, -- AES-256-GCM 加密后的手机号
|
||||
email VARCHAR(255),
|
||||
role VARCHAR(30) NOT NULL
|
||||
CHECK (role IN ('agent','store_manager','admin','operator','system')),
|
||||
job_title VARCHAR(100), -- 职务描述
|
||||
avatar_key TEXT, -- R2/S3 存储路径
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
joined_at DATE,
|
||||
left_at DATE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
|
||||
-- 关联 Django auth user(用于登录认证)
|
||||
user_id INTEGER UNIQUE -- FK to django auth_user
|
||||
);
|
||||
|
||||
CREATE INDEX idx_staff_org ON staff(org_unit_id) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_staff_role ON staff(role) WHERE deleted_at IS NULL;
|
||||
CREATE UNIQUE INDEX idx_staff_phone_hash ON staff(phone_hash) WHERE deleted_at IS NULL;
|
||||
```
|
||||
**关键约束提示**:
|
||||
- `staff.phone_enc` AES-256-GCM 加密,`staff.phone_hash` SHA-256 用于唯一索引
|
||||
- `staff_transfer_logs` **无 deleted_at**,不可删除
|
||||
- `org_units` 路径查询:`WHERE path LIKE '/root/{target_id}/%'`
|
||||
- 员工离职:`status = 'resigned'` + `deleted_at` 软删除,记录永久保留
|
||||
|
||||
---
|
||||
|
||||
### 3.2 区域与楼盘模块(Region & Complex Management)
|
||||
|
||||
```sql
|
||||
-- ============================================================
|
||||
-- 行政区 → 商圈 → 楼盘/小区 → 楼栋
|
||||
-- 注:楼盘数据是房源录入的基础底座,数据质量直接影响房源录入效率
|
||||
-- ============================================================
|
||||
> **详细模型** → 见 [`DATA_MODEL_COMPLEX.md`](./DATA_MODEL_COMPLEX.md)
|
||||
> 本节仅作概览,开发时以 DATA_MODEL_COMPLEX.md 为权威定义。
|
||||
|
||||
-- 城市/行政区
|
||||
CREATE TABLE districts (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(50) NOT NULL,
|
||||
city VARCHAR(50) NOT NULL DEFAULT '',
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE
|
||||
);
|
||||
**核心表概览**(开发时以 DATA_MODEL_COMPLEX.md 为准):
|
||||
|
||||
-- 商圈/板块
|
||||
CREATE TABLE business_areas (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
district_id UUID NOT NULL REFERENCES districts(id) ON DELETE RESTRICT,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_business_areas_district ON business_areas(district_id);
|
||||
|
||||
-- 地铁线路
|
||||
CREATE TABLE metro_lines (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(50) NOT NULL,
|
||||
color VARCHAR(7) -- 线路颜色 HEX
|
||||
);
|
||||
|
||||
-- 地铁站
|
||||
CREATE TABLE metro_stations (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
metro_line_id UUID NOT NULL REFERENCES metro_lines(id) ON DELETE CASCADE,
|
||||
name VARCHAR(50) NOT NULL,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
-- 学校
|
||||
CREATE TABLE schools (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
district_id UUID REFERENCES districts(id) ON DELETE SET NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
type VARCHAR(20) -- 小学/初中/高中/九年一贯制 等
|
||||
);
|
||||
|
||||
CREATE INDEX idx_schools_district ON schools(district_id);
|
||||
|
||||
-- 楼盘/小区(核心基础表)
|
||||
CREATE TABLE complexes (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(200) NOT NULL,
|
||||
alias VARCHAR(200), -- 别名/曾用名
|
||||
district_id UUID REFERENCES districts(id) ON DELETE SET NULL,
|
||||
business_area_id UUID REFERENCES business_areas(id) ON DELETE SET NULL,
|
||||
address VARCHAR(500),
|
||||
latitude NUMERIC(10,7),
|
||||
longitude NUMERIC(10,7),
|
||||
|
||||
-- 楼盘物理属性
|
||||
developer VARCHAR(200), -- 开发商
|
||||
property_company VARCHAR(200), -- 物业公司
|
||||
property_fee NUMERIC(8,2), -- 物业费 元/㎡/月
|
||||
green_rate NUMERIC(5,2), -- 绿化率 %
|
||||
plot_ratio NUMERIC(5,2), -- 容积率
|
||||
built_year SMALLINT, -- 竣工年份
|
||||
ownership_years VARCHAR(20), -- 产权年限枚举
|
||||
|
||||
-- 配套信息
|
||||
has_elevator BOOLEAN,
|
||||
parking_info TEXT, -- 车位情况描述
|
||||
|
||||
-- 全文检索向量(定期更新)
|
||||
search_vector TSVECTOR,
|
||||
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by UUID REFERENCES staff(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_complexes_district ON complexes(district_id) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_complexes_name_trgm ON complexes USING gin(name gin_trgm_ops);
|
||||
CREATE INDEX idx_complexes_search ON complexes USING gin(search_vector);
|
||||
CREATE INDEX idx_complexes_geo ON complexes(latitude, longitude) WHERE deleted_at IS NULL;
|
||||
|
||||
-- 楼盘与商圈多对多(一个楼盘可跨多个商圈)
|
||||
CREATE TABLE complex_business_areas (
|
||||
complex_id UUID NOT NULL REFERENCES complexes(id) ON DELETE CASCADE,
|
||||
business_area_id UUID NOT NULL REFERENCES business_areas(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (complex_id, business_area_id)
|
||||
);
|
||||
|
||||
-- 楼盘与学校关联
|
||||
CREATE TABLE complex_schools (
|
||||
complex_id UUID NOT NULL REFERENCES complexes(id) ON DELETE CASCADE,
|
||||
school_id UUID NOT NULL REFERENCES schools(id) ON DELETE CASCADE,
|
||||
school_zone VARCHAR(50), -- 学区情况:对口/参考等
|
||||
PRIMARY KEY (complex_id, school_id)
|
||||
);
|
||||
|
||||
-- 楼盘与地铁站关联
|
||||
CREATE TABLE complex_metro_stations (
|
||||
complex_id UUID NOT NULL REFERENCES complexes(id) ON DELETE CASCADE,
|
||||
station_id UUID NOT NULL REFERENCES metro_stations(id) ON DELETE CASCADE,
|
||||
distance_meters INTEGER, -- 步行距离(米)
|
||||
PRIMARY KEY (complex_id, station_id)
|
||||
);
|
||||
|
||||
-- 楼栋
|
||||
CREATE TABLE buildings (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
complex_id UUID NOT NULL REFERENCES complexes(id) ON DELETE CASCADE,
|
||||
name VARCHAR(50) NOT NULL, -- 楼栋名,如"1号楼"
|
||||
total_floors SMALLINT NOT NULL,
|
||||
has_elevator BOOLEAN,
|
||||
building_type VARCHAR(30), -- 楼型:板楼/塔楼/板塔结合
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_buildings_complex ON buildings(complex_id);
|
||||
```
|
||||
| 表名 | 说明 | 关键字段 |
|
||||
|------|------|----------|
|
||||
| `districts` | 城区/行政区 | `city`, `name`, `short_name`, `sort_order` |
|
||||
| `business_areas` | 商圈/板块(从属于城区) | `district_id`, `name`, `latitude`, `longitude` |
|
||||
| `metro_lines` | 地铁线路 | `city`, `name`, `color` |
|
||||
| `metro_stations` | 地铁站点 | `metro_line_id`, `name`, `latitude`, `longitude` |
|
||||
| `schools` | 学校(对口学区) | `district_id`, `name`, `type`, `nature`, `level` |
|
||||
| `complexes` | 楼盘/小区(房源底座) | `name`, `district_id`, `address`, `latitude/longitude`, `lock_*`, `search_vector` |
|
||||
| `complex_aliases` | 楼盘别名(含系统别名/用户自定义别名) | `complex_id`, `alias`, `is_system` |
|
||||
| `complex_business_areas` | 楼盘↔商圈多对多(含主商圈标识) | `complex_id`, `business_area_id`, `is_primary` |
|
||||
| `complex_schools` | 楼盘↔学校关联(含学区类型) | `complex_id`, `school_id`, `zone_type` |
|
||||
| `complex_metro_stations` | 楼盘↔地铁站关联(含步行距离) | `complex_id`, `station_id`, `distance_meters` |
|
||||
| `buildings` | 楼栋/单元 | `complex_id`, `name`, `is_standard`, `total_floors` |
|
||||
| `room_units` | 房号/结构单元(楼层+房间号) | `building_id`, `floor`, `room_no`, `is_standard` |
|
||||
| `complex_photos` | 楼盘照片(楼盘图/户型图/VR) | `complex_id`, `category`, `file_key`, `is_cover` |
|
||||
| `complex_attachments` | 楼盘附件 | `complex_id`, `file_key`, `file_name` |
|
||||
| `complex_price_trends` | 楼盘价格走势(月度) | `complex_id`, `record_month`, `avg_unit_price` |
|
||||
|
||||
---
|
||||
|
||||
@@ -1066,123 +984,30 @@ CREATE TABLE property_completeness (
|
||||
|
||||
### 3.17 客源管理(Client Management)
|
||||
|
||||
```sql
|
||||
-- ============================================================
|
||||
-- 客源:私客为核心,公客/成交客为后续版本
|
||||
-- ============================================================
|
||||
> **详细模型** → 见 [`DATA_MODEL_CLIENT.md`](./DATA_MODEL_CLIENT.md)
|
||||
> 该文件为权威定义,包含完整字段、枚举、状态机、查询模式和禁止操作。
|
||||
|
||||
CREATE TABLE clients (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
**核心表概览**(开发时以 DATA_MODEL_CLIENT.md 为准):
|
||||
|
||||
client_type VARCHAR(20) NOT NULL DEFAULT 'private'
|
||||
CHECK (client_type IN ('private','public','transacted')),
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active'
|
||||
CHECK (status IN ('active','converted_public',
|
||||
'transacted','invalid')),
|
||||
| 表名 | 说明 |
|
||||
|------|------|
|
||||
| `clients` | 客源主表(私客/公客/成交客),含加密手机号哈希、活跃度、归属人 |
|
||||
| `client_contacts` | 联系人(1:N),手机号加密+哈希,支持多联系人 |
|
||||
| `client_requirements` | 需求信息(可多类型:二手/新房/租房),含预算/面积/商圈/朝向等偏好 |
|
||||
| `client_follow_logs` | 跟进日志(高写入频率,5种类型,敏感查看类型不可删) |
|
||||
| `client_follow_log_attachments` | 跟进附件(图片/录音,最大20MB) |
|
||||
| `client_viewings` | 带看/预约记录(1:N,含陪看人/合作带看人) |
|
||||
| `client_property_matches` | 智能配房结果(录客配房/系统配房,匹配度评分) |
|
||||
| `client_status_logs` | 状态变更不可变审计日志(改状态/改等级/转公/转成交/转无效等) |
|
||||
| `client_favorite_folders` | 私客收藏夹(经纪人自定义分组) |
|
||||
| `client_folder_items` | 收藏夹与客源的多对多关联 |
|
||||
| `client_school_preferences` | 意向学校(拆表,支持精确查询) |
|
||||
|
||||
name VARCHAR(50) NOT NULL,
|
||||
gender VARCHAR(10)
|
||||
CHECK (gender IN ('male','female','unknown')),
|
||||
|
||||
-- 手机号加密存储
|
||||
phone_enc BYTEA NOT NULL,
|
||||
phone_hash VARCHAR(64) NOT NULL,
|
||||
phone2_enc BYTEA,
|
||||
phone2_hash VARCHAR(64),
|
||||
|
||||
-- 购房需求
|
||||
purpose VARCHAR(10) NOT NULL
|
||||
CHECK (purpose IN ('buy','rent')),
|
||||
budget_min NUMERIC(12,2),
|
||||
budget_max NUMERIC(12,2),
|
||||
area_min NUMERIC(8,2),
|
||||
area_max NUMERIC(8,2),
|
||||
bedroom_needs SMALLINT[], -- 可接受的卧室数量数组
|
||||
|
||||
-- 意向区域(存 district/business_area ID 数组)
|
||||
district_ids UUID[],
|
||||
business_area_ids UUID[],
|
||||
|
||||
-- 活跃度分层(由系统计算)
|
||||
activity_level VARCHAR(10)
|
||||
CHECK (activity_level IN ('hot','warm','cold','frozen')),
|
||||
last_active_at TIMESTAMPTZ,
|
||||
|
||||
-- 负责经纪人
|
||||
agent_id UUID REFERENCES staff(id) ON DELETE SET NULL,
|
||||
org_unit_id UUID REFERENCES org_units(id) ON DELETE SET NULL,
|
||||
|
||||
source VARCHAR(50),
|
||||
remarks TEXT,
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
created_by UUID REFERENCES staff(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_clients_agent ON clients(agent_id) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_clients_phone_hash ON clients(phone_hash) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_clients_status ON clients(status, client_type) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_clients_activity ON clients(activity_level, last_active_at DESC)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
-- 客源跟进日志(复用结构,单独表避免与房源日志混合)
|
||||
CREATE TABLE client_follow_logs (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
|
||||
purpose VARCHAR(50),
|
||||
content TEXT,
|
||||
log_tag VARCHAR(50),
|
||||
is_public BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
operator_id UUID REFERENCES staff(id) ON DELETE SET NULL,
|
||||
operator_snapshot JSONB,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX idx_client_logs_client ON client_follow_logs(client_id, created_at DESC)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
-- 智能配房记录(客源 ↔ 房源 匹配)
|
||||
CREATE TABLE client_property_matches (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
|
||||
property_id UUID NOT NULL REFERENCES properties(id) ON DELETE CASCADE,
|
||||
match_score NUMERIC(5,2), -- 匹配度评分
|
||||
match_reason JSONB, -- 匹配原因详情
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'suggested'
|
||||
CHECK (status IN ('suggested','shared','viewing','rejected')),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
created_by UUID REFERENCES staff(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX idx_client_property_match
|
||||
ON client_property_matches(client_id, property_id);
|
||||
|
||||
-- 带看记录
|
||||
CREATE TABLE viewings (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
property_id UUID NOT NULL REFERENCES properties(id) ON DELETE RESTRICT,
|
||||
client_id UUID NOT NULL REFERENCES clients(id) ON DELETE RESTRICT,
|
||||
agent_id UUID REFERENCES staff(id) ON DELETE SET NULL,
|
||||
|
||||
viewing_type VARCHAR(20) NOT NULL DEFAULT 'first'
|
||||
CHECK (viewing_type IN ('first','revisit','empty','interview')),
|
||||
-- first=带看, revisit=复看, empty=空看, interview=面访
|
||||
|
||||
scheduled_at TIMESTAMPTZ,
|
||||
completed_at TIMESTAMPTZ,
|
||||
result VARCHAR(20)
|
||||
CHECK (result IN ('interested','not_interested',
|
||||
'negotiating','cancelled')),
|
||||
remarks TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_viewings_property ON viewings(property_id);
|
||||
CREATE INDEX idx_viewings_client ON viewings(client_id);
|
||||
```
|
||||
**关键约束提示**:
|
||||
- `client_contacts.phone_hash` 是重复客源检测的唯一依据,录入前必须查重
|
||||
- `client_status_logs` **无 deleted_at**,不可删除
|
||||
- 私客超时(配置天数内无跟进)→ Celery 自动转公(`transfer_to_public_type = 'auto'`)
|
||||
- 活跃度 `activity_level` 由 Celery 每日凌晨批量计算,不实时更新
|
||||
|
||||
---
|
||||
|
||||
@@ -1290,7 +1115,7 @@ CREATE INDEX idx_number_holder_approvals_status ON number_holder_approvals(statu
|
||||
|
||||
---
|
||||
|
||||
## 四、关键索引汇总与查询优化策略
|
||||
## 五、关键索引汇总与查询优化策略
|
||||
|
||||
### 4.1 房源列表页核心查询分析
|
||||
|
||||
@@ -1372,7 +1197,7 @@ CREATE TRIGGER trg_update_last_followed
|
||||
|
||||
---
|
||||
|
||||
## 五、Redis 缓存策略
|
||||
## 六、Redis 缓存策略
|
||||
|
||||
### 5.1 缓存 Key 规范
|
||||
|
||||
@@ -1422,7 +1247,7 @@ CREATE TRIGGER trg_update_last_followed
|
||||
|
||||
---
|
||||
|
||||
## 六、Django Model 层设计要点
|
||||
## 七、Django Model 层设计要点
|
||||
|
||||
### 6.1 抽象基类
|
||||
|
||||
@@ -1500,7 +1325,7 @@ class PropertyManager(ActiveManager):
|
||||
|
||||
---
|
||||
|
||||
## 七、数据量与性能预测
|
||||
## 八、数据量与性能预测
|
||||
|
||||
| 表名 | 预估行数 | 增长速度 | 分区策略 |
|
||||
|------|---------|---------|---------|
|
||||
@@ -1514,7 +1339,7 @@ class PropertyManager(ActiveManager):
|
||||
|
||||
---
|
||||
|
||||
## 八、必须在开发启动前明确的数据架构决策
|
||||
## 九、必须在开发启动前明确的数据架构决策
|
||||
|
||||
| 决策项 | 推荐方案 | 风险 |
|
||||
|-------|---------|------|
|
||||
|
||||
574
Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md
Normal file
574
Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md
Normal file
@@ -0,0 +1,574 @@
|
||||
# Fonrey — 客源管理数据模型(DATA_MODEL_CLIENT)
|
||||
|
||||
> **所属系统**: Fonrey 房产经纪管理系统
|
||||
> **版本**: v1.0
|
||||
> **日期**: 2026-04-24
|
||||
> **关联模块**: `apps/client/` — 私客、公客、成交客、跟进记录、带看、智能配房
|
||||
|
||||
---
|
||||
|
||||
## 一、领域概览(Domain Overview)
|
||||
|
||||
### 核心概念
|
||||
|
||||
- **Client(客源)**:有购房/租房意向或历史成交记录的客户。核心实体,与房源(Property)是系统业务闭环的两端。
|
||||
- **客源类型**:
|
||||
- **私客(private)**:经纪人独占跟进的意向客户,是本期核心。
|
||||
- **公客(public)**:私客超时未跟进或手动转公后,进入全公司共享客源池。
|
||||
- **成交客(transacted)**:已完成购房/租房成交的客户,用于复购/转介绍跟进。
|
||||
- **ClientContact(联系人)**:一个客源可有多个联系人,每个联系人有独立手机号。手机号加密存储,用于重复检测(「私客与成交客重复」)。
|
||||
- **ClientRequirement(需求信息)**:购房/租房的详细偏好。一个客源可同时有「二手」「新房」「租房」三种需求类型(分别对应独立的需求记录)。
|
||||
- **ClientFollowLog(跟进日志)**:经纪人与客户每次沟通的书面记录,是客源活跃度计算的数据来源。
|
||||
- **Viewing(带看记录)**:与 Property 模块共享此表,记录经纪人带客户看房的过程。见主 DATA_MODEL.md 3.17 节。
|
||||
- **ClientPropertyMatch(智能配房)**:系统按需求自动匹配的房源列表,分「录客配房」和「系统配房」两种来源。
|
||||
- **ClientFavoriteFolder(收藏夹)**:经纪人自定义的客源分组收藏夹。
|
||||
|
||||
### 关键业务规则
|
||||
|
||||
1. **私客手机号唯一性**:录入联系人手机号时,系统通过 `phone_hash` 检测是否与现有私客/成交客/公客重复,并在列表顶部提示重复数量。
|
||||
2. **活跃度计算**:系统根据「最后跟进日期」自动计算客源活跃度,分为:新配偶(新建)/ 7日活跃 / 30日活跃 / 90日活跃 / 即将过期 / 无效。具体阈值由运营配置。
|
||||
3. **私客自动转公规则**:超过配置天数(如 30 天)无跟进记录,系统自动将私客标记为公客(`transfer_to_public_type = 'auto'`)。
|
||||
4. **状态机**:客源状态有严格流转规则(见第四章),不可跳过转台。
|
||||
5. **跟进目的枚举**:由 `lookup_items` 表维护,运营可配置,当前已知 23 项(见 Story 8)。
|
||||
6. **号码查看审计**:查看联系人明文号码需记录 `client_follow_logs`(`log_type = 'sensitive_view'`),不可删除。
|
||||
7. **需求类型独立存储**:同一客源可同时有「二手购房」「租房」两类需求,分别存储在独立需求记录中,由 `client_requirements.requirement_type` 区分。
|
||||
|
||||
---
|
||||
|
||||
## 二、实体关系
|
||||
|
||||
```
|
||||
Client (客源主表)
|
||||
│
|
||||
├── 1:N ── ClientContact (联系人,多个号码)
|
||||
├── 1:N ── ClientRequirement (需求信息,可多类型)
|
||||
├── 1:N ── ClientFollowLog (跟进日志,高写入频率)
|
||||
├── 1:N ── ClientViewing (带看预约)
|
||||
├── 1:N ── ClientPropertyMatch (智能配房结果)
|
||||
├── 1:1 ── ClientActivityCache (活跃度缓存,异步计算)
|
||||
├── N:M ── ClientFavoriteFolder (通过 client_folder_items 关联)
|
||||
└── 1:N ── ClientStatusLog (状态变更日志,不可删)
|
||||
|
||||
ClientFavoriteFolder
|
||||
└── 1:N ── ClientFolderItem (收藏夹中的客源)
|
||||
|
||||
Staff (员工)
|
||||
├── first_recorder_id → Client (首录人)
|
||||
└── owner_id → Client (归属人)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、Schema 定义
|
||||
|
||||
### 3.1 clients — 客源主表
|
||||
|
||||
| 字段 | 类型 | 约束 | 业务说明 |
|
||||
|------|------|------|----------|
|
||||
| id | UUID | PK | |
|
||||
| client_no | VARCHAR(30) | UNIQUE, NOT NULL | 系统生成的客源编号,格式由运营配置(如 KY20260424001) |
|
||||
| client_type | VARCHAR(20) | NOT NULL DEFAULT 'private' | `private`=私客 / `public`=公客 / `transacted`=成交客 |
|
||||
| status | VARCHAR(20) | NOT NULL DEFAULT 'buying' | 见下方枚举 |
|
||||
| grade | VARCHAR(5) | NOT NULL DEFAULT 'C' | `A_urgent`=A急迫 / `A` / `B`=较强 / `C`=一般 / `D`=较弱 / `E`=暂不关注 |
|
||||
| property_usage | VARCHAR(30) | NOT NULL DEFAULT 'residential' | `residential`=住宅 / `villa`=别墅 / `commercial_residential`=商住 / `shop`=商铺 / `office`=写字楼 / `other`=其他 |
|
||||
| buying_purpose | VARCHAR(20)[] | | 购房目的多选:`rigid`=刚需 / `investment`=投资 / `school_district`=学区 / `upgrade`=改善 / `commercial`=商用 / `other`=其他 |
|
||||
| payment_method | VARCHAR(30) | | `full`=全额 / `mortgage`=商业贷款 / `mortgage_fund`=商贷+公积金 / `fund`=公积金 |
|
||||
| properties_owned | VARCHAR(20) | | `none`=无 / `local_none`=本地无外地有 / `local_has`=本地有 |
|
||||
| has_loan_record | BOOLEAN | | 有无贷款记录 |
|
||||
| id_type | VARCHAR(20) | | 证件类型:`id_card` / `passport` / `hk_macao` / `other` |
|
||||
| id_number_enc | BYTEA | | 证件号码(AES 加密) |
|
||||
| source | VARCHAR(50) | | 客户来源(lookup_items 维护) |
|
||||
| remarks | TEXT | | 备注,最多200字 |
|
||||
| is_starred | BOOLEAN | NOT NULL DEFAULT FALSE | 是否收藏(快速标记,详细收藏夹用 client_folder_items) |
|
||||
| is_pinned | BOOLEAN | NOT NULL DEFAULT FALSE | 是否置顶(列表顶部置顶) |
|
||||
| is_big_value | BOOLEAN | NOT NULL DEFAULT FALSE | 是否大价值客户(影响筛选展示) |
|
||||
| is_protected | BOOLEAN | NOT NULL DEFAULT FALSE | 是否保护客(影响转公逻辑) |
|
||||
| prefers_new_house | BOOLEAN | | 偏好新房(用于筛选) |
|
||||
| transfer_to_public_type | VARCHAR(20) | | 转公客方式:`manual`=手动转公 / `auto`=自动转公(超时) / `marketing_jump`=营销客跳公 / `resource_public`=资料客素公 |
|
||||
| transferred_public_at | TIMESTAMPTZ | | 进入公客池时间 |
|
||||
| invalid_reason | VARCHAR(30) | | 无效原因:`invalid_phone`=号码无效 / `peer_agent`=同行 / `ad`=广告推销 / `no_intent`=无意向 / `other` |
|
||||
| invalidated_at | TIMESTAMPTZ | | 标记无效时间 |
|
||||
| transacted_at | DATE | | 成交日期 |
|
||||
| transacted_property_id | UUID | FK→properties, SET NULL | 成交关联的房源 |
|
||||
| transacted_price | NUMERIC(12,2) | | 成交价格(万元) |
|
||||
| transacted_type | VARCHAR(20) | | 成交类型:`bought`=我购 / `rented`=我租 |
|
||||
| transacted_property_type | VARCHAR(20) | | 成交房源类型:`second_hand`=二手 / `new_house`=新房 |
|
||||
| first_recorder_id | UUID | FK→staff, SET NULL | 首录人 |
|
||||
| owner_id | UUID | FK→staff, SET NULL | 归属人(私客独占跟进人) |
|
||||
| org_unit_id | UUID | FK→org_units, SET NULL | 归属部门(冗余,加速筛选) |
|
||||
| activity_level | VARCHAR(20) | | `new_matched`=新配偶 / `active_7d` / `active_30d` / `active_90d` / `expiring` / `frozen` / `invalid`(异步计算)|
|
||||
| last_active_at | TIMESTAMPTZ | | 最后有效跟进时间(触发器维护) |
|
||||
| last_follow_at | TIMESTAMPTZ | | 最后跟进时间(冗余,列表排序用) |
|
||||
| commission_date | DATE | | 委托日期 |
|
||||
| entrust_count | SMALLINT | NOT NULL DEFAULT 1 | 委托次数(成交后再委托则累加) |
|
||||
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||
| deleted_at | TIMESTAMPTZ | | 软删除 |
|
||||
| created_by | UUID | FK→staff, SET NULL | |
|
||||
| updated_by | UUID | FK→staff, SET NULL | |
|
||||
|
||||
**关键索引**:
|
||||
```sql
|
||||
CREATE UNIQUE INDEX idx_clients_client_no ON clients(client_no) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_clients_type_status ON clients(client_type, status) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_clients_owner ON clients(owner_id) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_clients_org_unit ON clients(org_unit_id) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_clients_activity ON clients(activity_level, last_active_at DESC) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_clients_grade ON clients(grade) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_clients_transferred_at ON clients(transferred_public_at DESC) WHERE client_type = 'public';
|
||||
CREATE INDEX idx_clients_last_follow ON clients(last_follow_at DESC NULLS LAST) WHERE deleted_at IS NULL;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.2 client_contacts — 联系人表
|
||||
|
||||
| 字段 | 类型 | 约束 | 业务说明 |
|
||||
|------|------|------|----------|
|
||||
| id | UUID | PK | |
|
||||
| client_id | UUID | NOT NULL, FK→clients, CASCADE | |
|
||||
| sort_order | SMALLINT | NOT NULL DEFAULT 0 | 联系人1为主联系人(sort_order=0) |
|
||||
| name | VARCHAR(50) | NOT NULL | 联系人姓名 |
|
||||
| gender | VARCHAR(10) | NOT NULL DEFAULT 'male' | `male`=先生 / `female`=女士 |
|
||||
| phone_enc | BYTEA | NOT NULL | AES-256-GCM 加密手机号(电话1) |
|
||||
| phone_hash | VARCHAR(64) | NOT NULL | SHA-256 哈希(重复检测) |
|
||||
| phone_country_code | VARCHAR(10) | NOT NULL DEFAULT '+86' | 国际区号 |
|
||||
| phone_is_invalid | BOOLEAN | NOT NULL DEFAULT FALSE | 是否被标记为无效号码 |
|
||||
| phone2_enc | BYTEA | | 备用电话2 |
|
||||
| phone2_hash | VARCHAR(64) | | |
|
||||
| wechat | VARCHAR(100) | | 微信号 |
|
||||
| qq | VARCHAR(20) | | QQ号 |
|
||||
| remarks | VARCHAR(200) | | 联系人备注,最多200字 |
|
||||
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||
| deleted_at | TIMESTAMPTZ | | 软删除(不影响客源本身) |
|
||||
| created_by | UUID | FK→staff, SET NULL | |
|
||||
|
||||
**关键索引**:
|
||||
```sql
|
||||
-- 关键:手机号哈希全局唯一索引(用于重复客源检测)
|
||||
CREATE INDEX idx_client_contacts_phone_hash ON client_contacts(phone_hash) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_client_contacts_phone2_hash ON client_contacts(phone2_hash) WHERE phone2_hash IS NOT NULL AND deleted_at IS NULL;
|
||||
CREATE INDEX idx_client_contacts_client ON client_contacts(client_id) WHERE deleted_at IS NULL;
|
||||
```
|
||||
|
||||
**业务注意**:
|
||||
- `sort_order = 0` 的联系人为主联系人,姓名用于客源姓名显示
|
||||
- 手机号标记无效(`phone_is_invalid = TRUE`)时,不影响记录存在,但该号码不再参与重复检测
|
||||
- 联系人软删除后客源仍保留,但若所有联系人均被删则客源实际上无有效号码
|
||||
|
||||
---
|
||||
|
||||
### 3.3 client_requirements — 需求信息表
|
||||
|
||||
一个客源可同时有多类需求(二手购房、新房、租房),每类需求独立一条记录。
|
||||
|
||||
| 字段 | 类型 | 约束 | 业务说明 |
|
||||
|------|------|------|----------|
|
||||
| id | UUID | PK | |
|
||||
| client_id | UUID | NOT NULL, FK→clients, CASCADE | |
|
||||
| requirement_type | VARCHAR(20) | NOT NULL | `second_hand`=二手 / `new_house`=新房 / `rental`=租房 |
|
||||
| is_primary | BOOLEAN | NOT NULL DEFAULT TRUE | 是否为主需求(用于列表展示) |
|
||||
| budget_min | NUMERIC(12,2) | | 最低预算(万元/元,依据需求类型) |
|
||||
| budget_max | NUMERIC(12,2) | | 最高预算 |
|
||||
| area_min | NUMERIC(8,2) | | 最小面积(㎡) |
|
||||
| area_max | NUMERIC(8,2) | | 最大面积 |
|
||||
| bedroom_counts | SMALLINT[] | | 可接受卧室数:如 [2,3](多选) |
|
||||
| floor_preferences | VARCHAR(20)[] | | 楼层偏好多选:`no_first`=不要一层 / `low`=低楼层 / `mid`=中楼层 / `high`=高楼层 / `no_top`=不要顶层 |
|
||||
| orientations | VARCHAR(10)[] | | 朝向多选:`east`/`south`/`west`/`north` |
|
||||
| decorations | VARCHAR(10)[] | | 装修偏好多选(枚举同 properties.decoration) |
|
||||
| building_age_ranges | VARCHAR(20)[] | | 楼龄多选:`within_5y`/`5_10y`/`10_15y`/`15_20y`/`over_20y` |
|
||||
| intent_district_ids | UUID[] | | 意向行政区 ID 数组 |
|
||||
| intent_business_area_ids | UUID[] | | 意向商圈 ID 数组 |
|
||||
| intent_complex_names | TEXT | | 意向小区(文本,逗号分隔,最多500字) |
|
||||
| transportation | VARCHAR(50) | | 交通要求(最多50字) |
|
||||
| intent_school_names | TEXT | | 意向学校(文本,逗号分隔) |
|
||||
| school_enrollment_date | DATE | | 入学时间(月份精度,取该月1日存储) |
|
||||
| traffic_preference | TEXT | | 交通备注 |
|
||||
| requirement_notes | VARCHAR(200) | | 需求备注(最多200字) |
|
||||
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||
|
||||
**关键索引**:
|
||||
```sql
|
||||
CREATE INDEX idx_client_requirements_client ON client_requirements(client_id);
|
||||
CREATE INDEX idx_client_requirements_type ON client_requirements(requirement_type, client_id);
|
||||
-- 智能配房时按预算/面积范围查询
|
||||
CREATE INDEX idx_client_requirements_budget ON client_requirements(budget_min, budget_max);
|
||||
CREATE INDEX idx_client_requirements_area ON client_requirements(area_min, area_max);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.4 client_follow_logs — 客源跟进日志
|
||||
|
||||
> 与 `follow_logs`(房源跟进)结构类似,独立存储以避免跨模块混淆。
|
||||
|
||||
| 字段 | 类型 | 约束 | 业务说明 |
|
||||
|------|------|------|----------|
|
||||
| id | UUID | PK | |
|
||||
| client_id | UUID | NOT NULL, FK→clients, CASCADE | |
|
||||
| log_type | VARCHAR(30) | NOT NULL | 见下方枚举 |
|
||||
| purpose | VARCHAR(50) | | 跟进目的(lookup_items 维护,23项) |
|
||||
| content | TEXT | | 跟进内容(最少6字,最多500字) |
|
||||
| log_tag | VARCHAR(50) | | 跟进标签:`has_recording`=有录音 / `has_photo`=有图片 / `not_satisfied`=对房源不满意 / `still_considering`=还在考虑 / `ready_to_deposit`=可交定金 |
|
||||
| change_detail | JSONB | | 修改跟进专用,格式:`{"field": "grade", "old": "C", "new": "B", "label": "等级"}` |
|
||||
| is_public | BOOLEAN | NOT NULL DEFAULT TRUE | FALSE=仅本人及上级可见 |
|
||||
| is_deletable | BOOLEAN | NOT NULL DEFAULT TRUE | 敏感信息查看类型为 FALSE,不可删除 |
|
||||
| operator_id | UUID | FK→staff, SET NULL | 操作人 |
|
||||
| operator_snapshot | JSONB | | `{name, store_group, role}`(防止人员调动后显示异常) |
|
||||
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||
| deleted_at | TIMESTAMPTZ | | 仅 is_deletable=TRUE 时可软删 |
|
||||
|
||||
**log_type 枚举**:
|
||||
```
|
||||
written = 写入跟进(经纪人主动写)
|
||||
modified = 修改跟进(字段变更自动生成)
|
||||
sensitive_view= 敏感信息查看(查看号码等,不可删)
|
||||
other = 其他跟进(系统自动:新增私客/状态变更等)
|
||||
system = 系统日志
|
||||
```
|
||||
|
||||
**关键索引**:
|
||||
```sql
|
||||
CREATE INDEX idx_client_follow_logs_client_time ON client_follow_logs(client_id, created_at DESC) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_client_follow_logs_type ON client_follow_logs(client_id, log_type, created_at DESC) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_client_follow_logs_operator ON client_follow_logs(operator_id, created_at DESC) WHERE deleted_at IS NULL;
|
||||
-- 不可删记录(合规审计)
|
||||
CREATE INDEX idx_client_follow_sensitive ON client_follow_logs(client_id, created_at DESC) WHERE log_type = 'sensitive_view';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.5 client_follow_log_attachments — 跟进附件
|
||||
|
||||
| 字段 | 类型 | 约束 | 业务说明 |
|
||||
|------|------|------|----------|
|
||||
| id | UUID | PK | |
|
||||
| follow_log_id | UUID | NOT NULL, FK→client_follow_logs, CASCADE | |
|
||||
| file_key | TEXT | NOT NULL | R2/S3 存储路径 |
|
||||
| file_name | VARCHAR(255) | NOT NULL | |
|
||||
| file_size | INTEGER | NOT NULL | bytes,最大 20MB |
|
||||
| file_type | VARCHAR(10) | CHECK | `bmp`/`jpg`/`png`/`gif` |
|
||||
| has_location | BOOLEAN | NOT NULL DEFAULT FALSE | 是否含 GPS 位置信息 |
|
||||
| sort_order | SMALLINT | NOT NULL DEFAULT 0 | |
|
||||
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||
|
||||
---
|
||||
|
||||
### 3.6 client_viewings — 带看记录(客源侧视图)
|
||||
|
||||
| 字段 | 类型 | 约束 | 业务说明 |
|
||||
|------|------|------|----------|
|
||||
| id | UUID | PK | |
|
||||
| client_id | UUID | NOT NULL, FK→clients, RESTRICT | |
|
||||
| property_id | UUID | NOT NULL, FK→properties, RESTRICT | |
|
||||
| viewing_type | VARCHAR(20) | NOT NULL DEFAULT 'viewing' | `appointment`=预约 / `viewing`=带看 / `revisit`=复看 / `empty`=空看 |
|
||||
| agent_id | UUID | FK→staff, SET NULL | 主带看经纪人 |
|
||||
| companion_ids | UUID[] | | 陪看人员 ID 数组(最多5人) |
|
||||
| cooperator_ids | UUID[] | | 合作带看人 ID 数组(最多5人) |
|
||||
| scheduled_at | TIMESTAMPTZ | | 预约时间 |
|
||||
| viewing_start_at | TIMESTAMPTZ | | 实际带看开始时间 |
|
||||
| viewing_end_at | TIMESTAMPTZ | | 结束时间 |
|
||||
| situation | TEXT | | 带看情况(必填,≥6字) |
|
||||
| client_intent | VARCHAR(20) | | 客户意向:`interested`=感兴趣 / `not_interested`=不感兴趣 / `negotiating`=谈判中 / `cancelled`=取消 |
|
||||
| viewing_progress | SMALLINT | | 带看进度(1=一看,2=二看...,冗余字段,触发器维护) |
|
||||
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||
| deleted_at | TIMESTAMPTZ | | |
|
||||
| created_by | UUID | FK→staff, SET NULL | |
|
||||
|
||||
**关键索引**:
|
||||
```sql
|
||||
CREATE INDEX idx_client_viewings_client ON client_viewings(client_id, viewing_start_at DESC) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_client_viewings_property ON client_viewings(property_id) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_client_viewings_agent ON client_viewings(agent_id) WHERE deleted_at IS NULL;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.7 client_property_matches — 智能配房
|
||||
|
||||
| 字段 | 类型 | 约束 | 业务说明 |
|
||||
|------|------|------|----------|
|
||||
| id | UUID | PK | |
|
||||
| client_id | UUID | NOT NULL, FK→clients, CASCADE | |
|
||||
| property_id | UUID | NOT NULL, FK→properties, CASCADE | |
|
||||
| match_source | VARCHAR(20) | NOT NULL DEFAULT 'recorded' | `recorded`=录客配房(基于录入需求) / `system`=系统配房(算法推荐) |
|
||||
| match_group | VARCHAR(30) | | 分组:`quality_layout`=优质户型 / `price_reduced`=降价 / `hot`=热门 / `newly_listed`=新上 |
|
||||
| match_score | NUMERIC(5,2) | | 匹配度评分(0-100) |
|
||||
| match_reasons | JSONB | | 匹配原因详情,格式:`[{"key": "budget", "match": true}, ...]` |
|
||||
| status | VARCHAR(20) | NOT NULL DEFAULT 'suggested' | `suggested`=待推送 / `shared`=已分享 / `rejected`=已反馈不合适 / `viewed`=客户已查看 |
|
||||
| shared_at | TIMESTAMPTZ | | 分享时间 |
|
||||
| feedback | VARCHAR(50) | | 反馈原因(lookup_items 维护) |
|
||||
| calculated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 配房计算时间 |
|
||||
| created_by | UUID | FK→staff, SET NULL | |
|
||||
|
||||
**关键索引**:
|
||||
```sql
|
||||
CREATE UNIQUE INDEX idx_client_matches_pair ON client_property_matches(client_id, property_id);
|
||||
CREATE INDEX idx_client_matches_client ON client_property_matches(client_id, match_source, match_group);
|
||||
CREATE INDEX idx_client_matches_status ON client_property_matches(client_id, status) WHERE status != 'rejected';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.8 client_status_logs — 状态变更日志(不可删)
|
||||
|
||||
| 字段 | 类型 | 约束 | 业务说明 |
|
||||
|------|------|------|----------|
|
||||
| id | UUID | PK | |
|
||||
| client_id | UUID | NOT NULL, FK→clients, RESTRICT | |
|
||||
| change_type | VARCHAR(30) | NOT NULL | `status_change`=改状态 / `grade_change`=改等级 / `to_public`=转公客 / `to_transacted`=转成交 / `to_invalid`=转无效 / `owner_change`=改归属人 / `source_change`=改来源 |
|
||||
| old_value | JSONB | | 变更前快照,格式:`{"status": "buying", "label": "求购"}` |
|
||||
| new_value | JSONB | | 变更后快照 |
|
||||
| reason | TEXT | | 变更理由(改状态必填,最多200字) |
|
||||
| operator_id | UUID | NOT NULL, FK→staff, RESTRICT | |
|
||||
| operated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||
| ⚠️ 无 deleted_at | — | — | 此表记录**不可删除** |
|
||||
|
||||
**关键索引**:
|
||||
```sql
|
||||
CREATE INDEX idx_client_status_logs_client ON client_status_logs(client_id, operated_at DESC);
|
||||
CREATE INDEX idx_client_status_logs_type ON client_status_logs(change_type, operated_at DESC);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.9 client_favorite_folders — 私客收藏夹
|
||||
|
||||
| 字段 | 类型 | 约束 | 业务说明 |
|
||||
|------|------|------|----------|
|
||||
| id | UUID | PK | |
|
||||
| staff_id | UUID | NOT NULL, FK→staff, CASCADE | 收藏夹所属经纪人 |
|
||||
| name | VARCHAR(10) | NOT NULL | 收藏夹名称,最多10字 |
|
||||
| is_default | BOOLEAN | NOT NULL DEFAULT FALSE | 系统默认收藏夹 |
|
||||
| sort_order | INTEGER | NOT NULL DEFAULT 0 | |
|
||||
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||
| deleted_at | TIMESTAMPTZ | | |
|
||||
|
||||
```sql
|
||||
CREATE INDEX idx_favorite_folders_staff ON client_favorite_folders(staff_id) WHERE deleted_at IS NULL;
|
||||
-- 每个经纪人只能有一个默认收藏夹
|
||||
CREATE UNIQUE INDEX idx_favorite_folders_default ON client_favorite_folders(staff_id) WHERE is_default = TRUE AND deleted_at IS NULL;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.10 client_folder_items — 收藏夹中的客源
|
||||
|
||||
| 字段 | 类型 | 约束 | 业务说明 |
|
||||
|------|------|------|----------|
|
||||
| folder_id | UUID | NOT NULL, FK→client_favorite_folders, CASCADE | |
|
||||
| client_id | UUID | NOT NULL, FK→clients, CASCADE | |
|
||||
| added_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||
| PRIMARY KEY | (folder_id, client_id) | | |
|
||||
|
||||
```sql
|
||||
CREATE INDEX idx_folder_items_client ON client_folder_items(client_id);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.11 client_school_preferences — 意向学校(多对多)
|
||||
|
||||
> 单独拆表便于学校搜索,避免文本字段模糊查询。
|
||||
|
||||
| 字段 | 类型 | 约束 | 业务说明 |
|
||||
|------|------|------|----------|
|
||||
| id | UUID | PK | |
|
||||
| requirement_id | UUID | NOT NULL, FK→client_requirements, CASCADE | |
|
||||
| school_id | UUID | FK→schools, SET NULL | 从学校表选择,允许为 NULL(自由输入) |
|
||||
| school_name | VARCHAR(100) | NOT NULL | 学校名称(当 school_id 为 NULL 时为手动输入) |
|
||||
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||
|
||||
```sql
|
||||
CREATE INDEX idx_school_prefs_requirement ON client_school_preferences(requirement_id);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、枚举常量
|
||||
|
||||
### clients.status(客源状态)
|
||||
|
||||
```
|
||||
buying = 求购(私客活跃态)
|
||||
renting = 求租(私客活跃态)
|
||||
buy_or_rent = 租购(私客活跃态)
|
||||
suspended = 暂缓(暂时无需求,不计入活跃统计)
|
||||
bought = 已购(成交客:我购)
|
||||
rented_done = 已租(成交客:我租)
|
||||
public = 公客(已转入公客池)
|
||||
invalid = 无效(号码无效/无意向等)
|
||||
```
|
||||
|
||||
**状态流转规则**:
|
||||
```
|
||||
buying/renting/buy_or_rent
|
||||
→ suspended (改状态操作,可逆)
|
||||
→ public (手动转公 or 超时自动转公,不可逆)
|
||||
→ bought/rented_done (转成交,不可逆)
|
||||
→ invalid (转无效,需经理审批后可恢复)
|
||||
```
|
||||
|
||||
### clients.grade(等级)
|
||||
|
||||
```
|
||||
A_urgent = A(急迫)
|
||||
A = A
|
||||
B = B(较强)
|
||||
C = C(一般,默认值)
|
||||
D = D(较弱)
|
||||
E = E(暂不关注)
|
||||
```
|
||||
|
||||
### client_status_logs.change_type(变更类型)
|
||||
|
||||
```
|
||||
status_change = 改状态(含改等级时同时改状态的情况)
|
||||
grade_change = 改等级
|
||||
to_public = 转公客(manual=手动 or auto=自动)
|
||||
to_transacted = 转成交(记录成交信息)
|
||||
to_invalid = 转无效(含无效原因)
|
||||
owner_change = 改归属人
|
||||
source_change = 改来源
|
||||
merge = 合并客源(被合并的记录保留日志)
|
||||
```
|
||||
|
||||
### clients.activity_level(活跃度分层,系统计算)
|
||||
|
||||
| 值 | 含义 | 触发条件(示例,以运营配置为准) |
|
||||
|----|------|------|
|
||||
| `new_matched` | 新配偶 | 录入后 3 天内 |
|
||||
| `active_7d` | 7日活跃 | 最后跟进在 7 天内 |
|
||||
| `active_30d` | 30日活跃 | 最后跟进在 30 天内 |
|
||||
| `active_90d` | 90日活跃 | 最后跟进在 90 天内 |
|
||||
| `expiring` | 即将过期 | 距自动转公还有 N 天 |
|
||||
| `frozen` | 冻结(暂缓) | status = suspended |
|
||||
| `invalid` | 无效 | status = invalid |
|
||||
|
||||
---
|
||||
|
||||
## 五、查询模式
|
||||
|
||||
### 5.1 私客列表页(求购 Tab)核心查询
|
||||
|
||||
```sql
|
||||
-- 典型:当前经纪人名下 + 求购状态 + 等级筛选 + 按最后跟进排序
|
||||
SELECT c.id, c.status, c.grade, c.activity_level,
|
||||
c.last_follow_at, c.commission_date, c.buying_purpose,
|
||||
cc.name AS contact_name, -- JOIN 主联系人
|
||||
s.name AS owner_name, ou.name AS org_unit_name,
|
||||
COUNT(cpm.id) AS match_count -- 智能配房数量
|
||||
FROM clients c
|
||||
JOIN client_contacts cc ON cc.client_id = c.id AND cc.sort_order = 0 AND cc.deleted_at IS NULL
|
||||
JOIN staff s ON s.id = c.owner_id
|
||||
JOIN org_units ou ON ou.id = c.org_unit_id
|
||||
LEFT JOIN client_property_matches cpm ON cpm.client_id = c.id AND cpm.status != 'rejected'
|
||||
WHERE c.client_type = 'private'
|
||||
AND c.owner_id = :current_staff_id -- 与我相关
|
||||
AND c.status IN ('buying', 'buy_or_rent')
|
||||
AND c.deleted_at IS NULL
|
||||
GROUP BY c.id, cc.name, s.name, ou.name
|
||||
ORDER BY c.last_follow_at DESC NULLS LAST
|
||||
LIMIT 20 OFFSET :offset;
|
||||
```
|
||||
|
||||
### 5.2 重复客源检测(录入/编辑时触发)
|
||||
|
||||
```sql
|
||||
-- 手机号哈希碰撞检测(私客、成交客、公客三池同时检查)
|
||||
SELECT c.id, c.client_type, c.status, c.client_no,
|
||||
cc.name AS contact_name
|
||||
FROM client_contacts cc
|
||||
JOIN clients c ON cc.client_id = c.id
|
||||
WHERE cc.phone_hash = :new_phone_hash
|
||||
AND cc.deleted_at IS NULL
|
||||
AND c.deleted_at IS NULL
|
||||
AND c.status != 'invalid';
|
||||
```
|
||||
|
||||
### 5.3 活跃度批量更新(Celery 定时任务,每日凌晨执行)
|
||||
|
||||
```sql
|
||||
-- 更新活跃度(以7日活跃为例)
|
||||
UPDATE clients
|
||||
SET activity_level = 'active_7d',
|
||||
updated_at = NOW()
|
||||
WHERE client_type = 'private'
|
||||
AND status NOT IN ('invalid', 'public', 'bought', 'rented_done')
|
||||
AND last_follow_at >= NOW() - INTERVAL '7 days'
|
||||
AND deleted_at IS NULL;
|
||||
```
|
||||
|
||||
### 5.4 私客自动转公(超时无跟进,Celery 定时任务)
|
||||
|
||||
```sql
|
||||
-- 查询应自动转公的私客(阈值由运营配置,假设30天)
|
||||
SELECT id FROM clients
|
||||
WHERE client_type = 'private'
|
||||
AND status IN ('buying', 'renting', 'buy_or_rent')
|
||||
AND last_follow_at < NOW() - INTERVAL '30 days'
|
||||
AND is_protected = FALSE
|
||||
AND deleted_at IS NULL;
|
||||
-- 后续在 Application 层批量更新 client_type='public', transfer_to_public_type='auto'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、触发器
|
||||
|
||||
### 6.1 last_follow_at 自动维护
|
||||
|
||||
```sql
|
||||
-- 每次写入跟进日志时,自动更新 clients.last_follow_at
|
||||
CREATE OR REPLACE FUNCTION update_client_last_follow()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF NEW.log_type = 'written' THEN
|
||||
UPDATE clients
|
||||
SET last_follow_at = NEW.created_at,
|
||||
last_active_at = NEW.created_at,
|
||||
updated_at = NOW()
|
||||
WHERE id = NEW.client_id;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trg_client_last_follow
|
||||
AFTER INSERT ON client_follow_logs
|
||||
FOR EACH ROW EXECUTE FUNCTION update_client_last_follow();
|
||||
```
|
||||
|
||||
### 6.2 viewing_progress 自动维护
|
||||
|
||||
```sql
|
||||
-- 每次新增带看记录时,自动更新 clients 的带看进度冗余字段
|
||||
CREATE OR REPLACE FUNCTION update_client_viewing_progress()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
UPDATE clients
|
||||
SET updated_at = NOW()
|
||||
WHERE id = NEW.client_id;
|
||||
-- Application 层根据 COUNT(viewings) 计算具体进度
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trg_client_viewing_progress
|
||||
AFTER INSERT ON client_viewings
|
||||
FOR EACH ROW EXECUTE FUNCTION update_client_viewing_progress();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、禁止操作
|
||||
|
||||
- ❌ **严禁硬删除 clients 记录**:无效/转公客/成交客均通过 status 和 soft delete 处理,历史跟进/带看依赖外键
|
||||
- ❌ **严禁删除 client_status_logs**:状态变更为不可变审计日志
|
||||
- ❌ **严禁删除 log_type='sensitive_view' 的跟进记录**:必须通过 `is_deletable=FALSE` 约束在应用层拦截
|
||||
- ❌ **严禁明文存储联系人手机号**:必须走 `EncryptedPhoneField`,`phone_hash` 用于索引和重复检测
|
||||
- ❌ **严禁跳过状态机流转**:如私客不可直接跳过「求购」变为「无效」而不生成 status log
|
||||
- ❌ **严禁在没有 `client_type` 过滤的情况下查询客源列表**:私客/公客/成交客数据量均较大,必须按类型隔离查询
|
||||
- ❌ **严禁查询 clients 时不带 `deleted_at IS NULL`**:软删除过滤必须存在
|
||||
547
Project/fonrey/DATA_MODEL/DATA_MODEL_COMPLEX.md
Normal file
547
Project/fonrey/DATA_MODEL/DATA_MODEL_COMPLEX.md
Normal file
@@ -0,0 +1,547 @@
|
||||
# Fonrey — 楼盘与区域数据模型(DATA_MODEL_COMPLEX)
|
||||
|
||||
> **所属系统**: Fonrey 房产经纪管理系统
|
||||
> **版本**: v1.0
|
||||
> **日期**: 2026-04-24
|
||||
> **关联模块**: `apps/complex/` — 楼盘/小区、楼栋、结构(楼层+房号)、区域、学校
|
||||
|
||||
---
|
||||
|
||||
## 一、领域概览(Domain Overview)
|
||||
|
||||
### 核心概念
|
||||
|
||||
- **Complex(楼盘/小区)**:房源录入的基础底座。每套房源必须归属于某一楼盘。楼盘数据由运营/数据管理员集中维护,质量直接影响房源录入效率和搜索精度。
|
||||
- **Building(楼栋/单元)**:楼盘下的物理楼栋,是组织房源位置的第二级。一个楼盘可有多个楼栋(如「1号楼」「2栋2单元」)。
|
||||
- **RoomUnit(房号/结构单元)**:楼栋内特定楼层的某个房间标识,是房源定位的最细粒度。支持「标准结构」(经运营标准化)和「非标结构」(未归一化)两类。
|
||||
- **District(城区/行政区)**:行政区划,如静安区、闵行区。
|
||||
- **BusinessArea(商圈/板块)**:商圈是区域内的细分市场区域,如「南京西路商圈」,一个楼盘可跨多个商圈。
|
||||
- **School(学校)**:楼盘对口学校,是买家购房决策的核心关注点。一个楼盘可关联多所学校,一所学校可对口多个楼盘。
|
||||
- **MetroLine / MetroStation(地铁线路/站点)**:楼盘与最近地铁站的距离关系,用于通勤筛选。
|
||||
|
||||
### 关键业务规则
|
||||
|
||||
1. **楼盘名称不可在编辑页修改**:楼盘名称(`name`)变更须通过「合并楼盘」或「申请流程」处理,防止经纪人随意改名造成数据混乱。
|
||||
2. **数据锁定机制**:楼盘有 4 类锁(楼栋锁/房号锁/信息锁/标准房号锁),锁定后对应数据只有管理员可解锁修改。
|
||||
3. **非标结构处理**:未与标准结构关联的房号为「非标」,系统记录非标数量,引导运营逐步消除。
|
||||
4. **搜索依赖全文检索**:楼盘名称、别名、地址需维护 `search_vector`(`tsvector`)以支持模糊搜索和联想补全。
|
||||
5. **地理坐标优先级**:楼盘坐标是区域聚合展示(地图找房)的核心数据,完整度目标 ≥ 90%。
|
||||
6. **学校关联影响房源**:从楼盘详情删除对口学校,会级联删除该楼盘下所有房源的对应学区标注。
|
||||
|
||||
---
|
||||
|
||||
## 二、实体关系
|
||||
|
||||
```
|
||||
District (城区/行政区)
|
||||
└── 1:N ── BusinessArea (商圈/板块)
|
||||
└── N:M ── Complex (through complex_business_areas)
|
||||
|
||||
Complex (楼盘)
|
||||
├── N:M ── BusinessArea (through complex_business_areas)
|
||||
├── N:M ── School (through complex_schools)
|
||||
├── N:M ── MetroStation (through complex_metro_stations, 附带距离)
|
||||
├── 1:N ── Building (楼栋/单元)
|
||||
│ └── 1:N ── RoomUnit (楼层+房号)
|
||||
├── 1:N ── ComplexPhoto (楼盘照片:楼盘图/户型图/VR)
|
||||
├── 1:N ── ComplexAttachment(附件)
|
||||
├── 1:N ── ComplexPriceTrend(价格走势,月度)
|
||||
└── 1:N ── ComplexAlias (别名)
|
||||
|
||||
MetroLine (地铁线路)
|
||||
└── 1:N ── MetroStation (站点)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、Schema 定义
|
||||
|
||||
### 3.1 districts — 城区/行政区
|
||||
|
||||
| 字段 | 类型 | 约束 | 业务说明 |
|
||||
|------|------|------|----------|
|
||||
| id | UUID | PK | |
|
||||
| city | VARCHAR(50) | NOT NULL | 所属城市(支持多城市扩展,如「上海」「北京」) |
|
||||
| name | VARCHAR(50) | NOT NULL | 行政区名称,如「静安区」 |
|
||||
| short_name | VARCHAR(20) | | 简称,如「静安」 |
|
||||
| sort_order | INTEGER | NOT NULL DEFAULT 0 | 列表展示排序 |
|
||||
| is_active | BOOLEAN | NOT NULL DEFAULT TRUE | FALSE=已停用(不在筛选项中展示) |
|
||||
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||
|
||||
```sql
|
||||
CREATE UNIQUE INDEX idx_districts_city_name ON districts(city, name) WHERE is_active = TRUE;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.2 business_areas — 商圈/板块
|
||||
|
||||
| 字段 | 类型 | 约束 | 业务说明 |
|
||||
|------|------|------|----------|
|
||||
| id | UUID | PK | |
|
||||
| district_id | UUID | NOT NULL, FK→districts, RESTRICT | 所属城区 |
|
||||
| name | VARCHAR(100) | NOT NULL | 商圈名称 |
|
||||
| sort_order | INTEGER | NOT NULL DEFAULT 0 | |
|
||||
| latitude | NUMERIC(10,7) | | 商圈中心坐标(纬度) |
|
||||
| longitude | NUMERIC(10,7) | | 商圈中心坐标(经度) |
|
||||
| is_active | BOOLEAN | NOT NULL DEFAULT TRUE | |
|
||||
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||
|
||||
```sql
|
||||
CREATE INDEX idx_business_areas_district ON business_areas(district_id) WHERE is_active = TRUE;
|
||||
CREATE UNIQUE INDEX idx_business_areas_name ON business_areas(district_id, name);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.3 metro_lines — 地铁线路
|
||||
|
||||
| 字段 | 类型 | 约束 | 业务说明 |
|
||||
|------|------|------|----------|
|
||||
| id | UUID | PK | |
|
||||
| city | VARCHAR(50) | NOT NULL | 所属城市 |
|
||||
| name | VARCHAR(50) | NOT NULL | 线路名,如「1号线」 |
|
||||
| color | VARCHAR(7) | | 线路颜色 HEX(如 `#E3002B`) |
|
||||
| sort_order | INTEGER | NOT NULL DEFAULT 0 | |
|
||||
| is_active | BOOLEAN | NOT NULL DEFAULT TRUE | |
|
||||
|
||||
---
|
||||
|
||||
### 3.4 metro_stations — 地铁站点
|
||||
|
||||
| 字段 | 类型 | 约束 | 业务说明 |
|
||||
|------|------|------|----------|
|
||||
| id | UUID | PK | |
|
||||
| metro_line_id | UUID | NOT NULL, FK→metro_lines, CASCADE | 所属线路 |
|
||||
| name | VARCHAR(50) | NOT NULL | 站名 |
|
||||
| latitude | NUMERIC(10,7) | | 站点坐标 |
|
||||
| longitude | NUMERIC(10,7) | | |
|
||||
| sort_order | INTEGER | NOT NULL DEFAULT 0 | 沿线排序 |
|
||||
| is_active | BOOLEAN | NOT NULL DEFAULT TRUE | |
|
||||
|
||||
```sql
|
||||
CREATE INDEX idx_metro_stations_line ON metro_stations(metro_line_id) WHERE is_active = TRUE;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.5 schools — 学校
|
||||
|
||||
| 字段 | 类型 | 约束 | 业务说明 |
|
||||
|------|------|------|----------|
|
||||
| id | UUID | PK | |
|
||||
| district_id | UUID | FK→districts, SET NULL | 所属城区 |
|
||||
| name | VARCHAR(100) | NOT NULL | 学校名称 |
|
||||
| type | VARCHAR(20) | | 学校类型:`primary`=小学 / `middle`=初中 / `high`=高中 / `k9`=九年一贯制 / `k12`=十二年一贯制 |
|
||||
| nature | VARCHAR(20) | | 学校性质:`public`=公立 / `private`=私立 / `international`=国际学校 |
|
||||
| level | VARCHAR(20) | | 学校等级:`normal`=普通 / `key`=重点 / `top`=名校 |
|
||||
| is_active | BOOLEAN | NOT NULL DEFAULT TRUE | |
|
||||
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||
|
||||
```sql
|
||||
CREATE INDEX idx_schools_district ON schools(district_id) WHERE is_active = TRUE;
|
||||
CREATE INDEX idx_schools_name_trgm ON schools USING gin(name gin_trgm_ops);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.6 complexes — 楼盘/小区(核心基础表)
|
||||
|
||||
| 字段 | 类型 | 约束 | 业务说明 |
|
||||
|------|------|------|----------|
|
||||
| id | UUID | PK | |
|
||||
| name | VARCHAR(200) | NOT NULL | 标准楼盘名称,**不可在编辑页修改**(需走合并/申请流程) |
|
||||
| district_id | UUID | FK→districts, SET NULL | 所属城区 |
|
||||
| address | VARCHAR(500) | | 详细地址(不可在编辑页修改,需走纠错流程) |
|
||||
| address_summary | VARCHAR(100) | | 概要地址(如「海波路1000弄」,可编辑) |
|
||||
| latitude | NUMERIC(10,7) | | 楼盘坐标(纬度),完整度目标 ≥ 90% |
|
||||
| longitude | NUMERIC(10,7) | | |
|
||||
| **物业属性** | | | |
|
||||
| property_usage_types | VARCHAR(20)[] | | 物业类型多选:`residential`/`villa`/`commercial_residential`/`commercial`/`office`/`other` |
|
||||
| building_structure | VARCHAR(30) | | 楼栋结构枚举(运营维护):`unit_room`=单元-房号 / `other`=其他 |
|
||||
| building_type | VARCHAR(20) | | 建筑类型:`slab`=板楼 / `tower`=塔楼 / `slab_tower`=板塔结合 |
|
||||
| land_use_years | VARCHAR(30) | | 土地使用年限,如「70年」 |
|
||||
| built_year | SMALLINT | | 竣工年份(可多选,存最早竣工年) |
|
||||
| built_years | SMALLINT[] | | 竣工年份多值(楼盘分期竣工) |
|
||||
| ownership_category | VARCHAR(30)[] | | 权属类别多选(运营维护枚举) |
|
||||
| total_units | INTEGER | | 单元总数 |
|
||||
| total_households | INTEGER | | 总户数 |
|
||||
| **建设信息** | | | |
|
||||
| total_floor_area | NUMERIC(12,2) | | 小区总建筑面积(m²) |
|
||||
| plot_area | NUMERIC(12,2) | | 小区占地面积(m²) |
|
||||
| plot_ratio | NUMERIC(5,2) | | 容积率 |
|
||||
| green_rate | NUMERIC(5,2) | | 绿化率(%) |
|
||||
| developer | VARCHAR(200) | | 开发商 |
|
||||
| **物业信息** | | | |
|
||||
| property_company | VARCHAR(200) | | 物业公司 |
|
||||
| property_fee | NUMERIC(8,2) | | 物业费(元/m²/月) |
|
||||
| property_phone | VARCHAR(30) | | 物业电话 |
|
||||
| **停车** | | | |
|
||||
| parking_total | INTEGER | | 车位总数 |
|
||||
| parking_underground | INTEGER | | 地下车位数 |
|
||||
| parking_ratio | VARCHAR(20) | | 停车位配比,如「100:63」 |
|
||||
| **配套** | | | |
|
||||
| water_type | VARCHAR(10) | | `civil`=民水 / `commercial`=商水 |
|
||||
| electricity_type | VARCHAR(10) | | `civil`=民电 / `commercial`=商电 |
|
||||
| has_central_heating | BOOLEAN | | 是否统一供暖 |
|
||||
| has_gas | BOOLEAN | | 是否有燃气 |
|
||||
| remarks | TEXT | | 备注 |
|
||||
| **锁定状态** | | | |
|
||||
| lock_building | BOOLEAN | NOT NULL DEFAULT FALSE | 楼栋锁(锁定后不可增删楼栋) |
|
||||
| lock_room | BOOLEAN | NOT NULL DEFAULT FALSE | 房号锁 |
|
||||
| lock_info | BOOLEAN | NOT NULL DEFAULT FALSE | 信息锁(锁定后基本信息只读) |
|
||||
| lock_standard_room | BOOLEAN | NOT NULL DEFAULT FALSE | 标准房号锁 |
|
||||
| **全文检索** | | | |
|
||||
| search_vector | TSVECTOR | | 由触发器自动维护(name + alias + address) |
|
||||
| **状态** | | | |
|
||||
| is_active | BOOLEAN | NOT NULL DEFAULT TRUE | FALSE=已停用楼盘 |
|
||||
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||
| deleted_at | TIMESTAMPTZ | | 软删除 |
|
||||
| created_by | UUID | FK→staff, SET NULL | |
|
||||
| updated_by | UUID | FK→staff, SET NULL | |
|
||||
|
||||
**关键索引**:
|
||||
```sql
|
||||
CREATE INDEX idx_complexes_district ON complexes(district_id) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_complexes_name_trgm ON complexes USING gin(name gin_trgm_ops); -- 模糊搜索
|
||||
CREATE INDEX idx_complexes_search ON complexes USING gin(search_vector); -- 全文搜索
|
||||
CREATE INDEX idx_complexes_geo ON complexes(latitude, longitude) WHERE deleted_at IS NULL AND latitude IS NOT NULL;
|
||||
CREATE INDEX idx_complexes_active ON complexes(is_active) WHERE deleted_at IS NULL;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.7 complex_aliases — 楼盘别名
|
||||
|
||||
| 字段 | 类型 | 约束 | 业务说明 |
|
||||
|------|------|------|----------|
|
||||
| id | UUID | PK | |
|
||||
| complex_id | UUID | NOT NULL, FK→complexes, CASCADE | |
|
||||
| alias | VARCHAR(200) | NOT NULL | 别名(最多20字/条,多别名多行存储) |
|
||||
| is_system | BOOLEAN | NOT NULL DEFAULT FALSE | TRUE=系统/标准别名(只读),FALSE=用户自定义 |
|
||||
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||
| created_by | UUID | FK→staff, SET NULL | |
|
||||
|
||||
```sql
|
||||
CREATE INDEX idx_complex_aliases_complex ON complex_aliases(complex_id);
|
||||
CREATE INDEX idx_complex_aliases_alias_trgm ON complex_aliases USING gin(alias gin_trgm_ops);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.8 complex_business_areas — 楼盘与商圈多对多
|
||||
|
||||
| 字段 | 类型 | 约束 | 业务说明 |
|
||||
|------|------|------|----------|
|
||||
| complex_id | UUID | NOT NULL, FK→complexes, CASCADE | |
|
||||
| business_area_id | UUID | NOT NULL, FK→business_areas, CASCADE | |
|
||||
| is_primary | BOOLEAN | NOT NULL DEFAULT FALSE | 主商圈(唯一)用于列表显示 |
|
||||
| PRIMARY KEY | (complex_id, business_area_id) | | |
|
||||
|
||||
```sql
|
||||
-- 主商圈只能有一个
|
||||
CREATE UNIQUE INDEX idx_complex_biz_area_primary ON complex_business_areas(complex_id) WHERE is_primary = TRUE;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.9 complex_schools — 楼盘与学校关联
|
||||
|
||||
| 字段 | 类型 | 约束 | 业务说明 |
|
||||
|------|------|------|----------|
|
||||
| complex_id | UUID | NOT NULL, FK→complexes, CASCADE | |
|
||||
| school_id | UUID | NOT NULL, FK→schools, CASCADE | |
|
||||
| zone_type | VARCHAR(30) | | 学区类型:`guaranteed`=对口 / `reference`=参考 / `lottery`=摇号 |
|
||||
| PRIMARY KEY | (complex_id, school_id) | | |
|
||||
|
||||
```sql
|
||||
CREATE INDEX idx_complex_schools_school ON complex_schools(school_id);
|
||||
```
|
||||
|
||||
**业务注意**:删除此关联记录时,需同步清理对应房源的学区标注(Application 层事务处理)
|
||||
|
||||
---
|
||||
|
||||
### 3.10 complex_metro_stations — 楼盘与地铁站关联
|
||||
|
||||
| 字段 | 类型 | 约束 | 业务说明 |
|
||||
|------|------|------|----------|
|
||||
| complex_id | UUID | NOT NULL, FK→complexes, CASCADE | |
|
||||
| station_id | UUID | NOT NULL, FK→metro_stations, CASCADE | |
|
||||
| distance_meters | INTEGER | | 步行距离(米) |
|
||||
| PRIMARY KEY | (complex_id, station_id) | | |
|
||||
|
||||
```sql
|
||||
CREATE INDEX idx_complex_metro_complex ON complex_metro_stations(complex_id);
|
||||
CREATE INDEX idx_complex_metro_station ON complex_metro_stations(station_id);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.11 buildings — 楼栋/单元
|
||||
|
||||
| 字段 | 类型 | 约束 | 业务说明 |
|
||||
|------|------|------|----------|
|
||||
| id | UUID | PK | |
|
||||
| complex_id | UUID | NOT NULL, FK→complexes, CASCADE | |
|
||||
| name | VARCHAR(50) | NOT NULL | 楼栋名,如「1号楼」「A栋2单元」 |
|
||||
| is_standard | BOOLEAN | NOT NULL DEFAULT FALSE | TRUE=标准结构(经运营核准) |
|
||||
| property_usage_type | VARCHAR(20) | | 物业类型(可与楼盘不同,如商住楼盘内有纯商铺楼栋) |
|
||||
| built_year | SMALLINT | | 竣工年份 |
|
||||
| total_floors | SMALLINT | | 总层数 |
|
||||
| land_use_years | VARCHAR(30) | | 土地使用年限 |
|
||||
| has_elevator | BOOLEAN | | 是否有电梯 |
|
||||
| school_id | UUID | FK→schools, SET NULL | 关联对口学校(楼栋级别的学区差异) |
|
||||
| is_active | BOOLEAN | NOT NULL DEFAULT TRUE | |
|
||||
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||
| created_by | UUID | FK→staff, SET NULL | |
|
||||
|
||||
```sql
|
||||
CREATE INDEX idx_buildings_complex ON buildings(complex_id) WHERE is_active = TRUE;
|
||||
CREATE UNIQUE INDEX idx_buildings_name ON buildings(complex_id, name) WHERE is_active = TRUE;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.12 room_units — 房号/结构单元(楼层+房间号)
|
||||
|
||||
| 字段 | 类型 | 约束 | 业务说明 |
|
||||
|------|------|------|----------|
|
||||
| id | UUID | PK | |
|
||||
| building_id | UUID | NOT NULL, FK→buildings, CASCADE | |
|
||||
| floor | SMALLINT | NOT NULL | 楼层(实际层数,地下为负数) |
|
||||
| floor_name | VARCHAR(20) | | 楼层名称展示,如「1层」「B1层」 |
|
||||
| room_no | VARCHAR(30) | NOT NULL | 房号,如「01」「101」 |
|
||||
| display_no | VARCHAR(50) | | 展示用完整房号,如「3-1-101」 |
|
||||
| is_standard | BOOLEAN | NOT NULL DEFAULT FALSE | TRUE=已归一化为标准结构 |
|
||||
| is_active | BOOLEAN | NOT NULL DEFAULT TRUE | FALSE=已拆除/不存在 |
|
||||
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||
|
||||
```sql
|
||||
CREATE INDEX idx_room_units_building ON room_units(building_id) WHERE is_active = TRUE;
|
||||
CREATE UNIQUE INDEX idx_room_units_unique ON room_units(building_id, floor, room_no) WHERE is_active = TRUE;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.13 complex_photos — 楼盘照片
|
||||
|
||||
| 字段 | 类型 | 约束 | 业务说明 |
|
||||
|------|------|------|----------|
|
||||
| id | UUID | PK | |
|
||||
| complex_id | UUID | NOT NULL, FK→complexes, CASCADE | |
|
||||
| category | VARCHAR(20) | NOT NULL | `complex`=楼盘图 / `layout`=户型图 / `vr`=VR全景 / `other`=其他 |
|
||||
| file_key | TEXT | NOT NULL | R2/S3 路径 |
|
||||
| thumbnail_key | TEXT | | 缩略图路径 |
|
||||
| file_name | VARCHAR(255) | | |
|
||||
| file_size | INTEGER | | bytes |
|
||||
| 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() | |
|
||||
| created_by | UUID | FK→staff, SET NULL | |
|
||||
|
||||
```sql
|
||||
CREATE INDEX idx_complex_photos_complex ON complex_photos(complex_id);
|
||||
CREATE INDEX idx_complex_photos_category ON complex_photos(complex_id, category);
|
||||
CREATE UNIQUE INDEX idx_complex_photos_cover ON complex_photos(complex_id) WHERE is_cover = TRUE;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.14 complex_attachments — 楼盘附件
|
||||
|
||||
| 字段 | 类型 | 约束 | 业务说明 |
|
||||
|------|------|------|----------|
|
||||
| id | UUID | PK | |
|
||||
| complex_id | UUID | NOT NULL, FK→complexes, CASCADE | |
|
||||
| file_key | TEXT | NOT NULL | |
|
||||
| file_name | VARCHAR(255) | NOT NULL | |
|
||||
| file_size | INTEGER | | |
|
||||
| file_type | VARCHAR(50) | | MIME type |
|
||||
| sort_order | SMALLINT | NOT NULL DEFAULT 0 | |
|
||||
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||
| created_by | UUID | FK→staff, SET NULL | |
|
||||
|
||||
---
|
||||
|
||||
### 3.15 complex_price_trends — 楼盘价格走势(月度)
|
||||
|
||||
| 字段 | 类型 | 约束 | 业务说明 |
|
||||
|------|------|------|----------|
|
||||
| id | UUID | PK | |
|
||||
| complex_id | UUID | NOT NULL, FK→complexes, CASCADE | |
|
||||
| record_month | DATE | NOT NULL | 月份(统一存为该月1日,如 2026-04-01) |
|
||||
| avg_sale_price | NUMERIC(12,2) | | 月均售价(万元/套) |
|
||||
| avg_unit_price | NUMERIC(10,2) | | 月均单价(元/m²) |
|
||||
| transaction_count | INTEGER | | 成交套数 |
|
||||
| listing_count | INTEGER | | 当月挂牌套数 |
|
||||
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||
|
||||
```sql
|
||||
CREATE UNIQUE INDEX idx_complex_price_trend_month ON complex_price_trends(complex_id, record_month);
|
||||
CREATE INDEX idx_complex_price_trend_complex ON complex_price_trends(complex_id, record_month DESC);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、枚举常量
|
||||
|
||||
### complexes.building_type(建筑类型)
|
||||
|
||||
```
|
||||
slab = 板楼
|
||||
tower = 塔楼
|
||||
slab_tower = 板塔结合
|
||||
```
|
||||
|
||||
### complexes.water_type / electricity_type
|
||||
|
||||
```
|
||||
civil = 民水/民电(住宅水电费率)
|
||||
commercial = 商水/商电(商业水电费率,费用较高,影响买家决策)
|
||||
```
|
||||
|
||||
### complex_schools.zone_type(学区类型)
|
||||
|
||||
```
|
||||
guaranteed = 对口(直升)
|
||||
reference = 参考(可能入读)
|
||||
lottery = 摇号(通过摇号入学)
|
||||
```
|
||||
|
||||
### buildings.is_standard / room_units.is_standard
|
||||
|
||||
```
|
||||
TRUE = 已标准化(楼栋/房号已经运营核准,可用于精准房源定位)
|
||||
FALSE = 非标(用户自输入,未核准,存在歧义风险)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、查询模式
|
||||
|
||||
### 5.1 楼盘名称联想搜索(录入房源时的自动补全)
|
||||
|
||||
```sql
|
||||
-- 使用全文检索向量,支持中文分词近似匹配
|
||||
SELECT id, name, address_summary, district_id
|
||||
FROM complexes
|
||||
WHERE search_vector @@ plainto_tsquery('simple', :keyword)
|
||||
OR name ILIKE :keyword_prefix -- 前缀精确匹配优先
|
||||
AND deleted_at IS NULL
|
||||
AND is_active = TRUE
|
||||
ORDER BY
|
||||
ts_rank(search_vector, plainto_tsquery('simple', :keyword)) DESC,
|
||||
name
|
||||
LIMIT 20;
|
||||
```
|
||||
|
||||
### 5.2 楼盘列表(含房源数量统计)
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
c.id, c.name, c.address, c.latitude, c.longitude,
|
||||
d.name AS district_name,
|
||||
ba.name AS primary_business_area,
|
||||
COUNT(DISTINCT b.id) AS building_count,
|
||||
COUNT(DISTINCT p.id) FILTER (WHERE p.status IN ('for_sale','for_sale_rent')) AS sale_count,
|
||||
COUNT(DISTINCT p.id) FILTER (WHERE p.status IN ('for_rent','for_sale_rent')) AS rent_count
|
||||
FROM complexes c
|
||||
LEFT JOIN districts d ON d.id = c.district_id
|
||||
LEFT JOIN complex_business_areas cba ON cba.complex_id = c.id AND cba.is_primary = TRUE
|
||||
LEFT JOIN business_areas ba ON ba.id = cba.business_area_id
|
||||
LEFT JOIN buildings b ON b.complex_id = c.id AND b.is_active = TRUE
|
||||
LEFT JOIN properties p ON p.complex_id = c.id AND p.deleted_at IS NULL
|
||||
WHERE c.deleted_at IS NULL
|
||||
AND c.district_id = ANY(:district_ids) -- 区域筛选
|
||||
GROUP BY c.id, d.name, ba.name
|
||||
ORDER BY c.name
|
||||
LIMIT 20 OFFSET :offset;
|
||||
```
|
||||
|
||||
### 5.3 查询楼盘下的楼层-房号矩阵(结构管理)
|
||||
|
||||
```sql
|
||||
-- 选中单元后,加载楼层×房号矩阵
|
||||
SELECT
|
||||
ru.floor,
|
||||
ru.floor_name,
|
||||
ru.room_no,
|
||||
ru.display_no,
|
||||
ru.is_standard,
|
||||
p.id AS property_id, -- 如果该房号已有房源,关联显示
|
||||
p.status AS property_status
|
||||
FROM room_units ru
|
||||
LEFT JOIN properties p ON p.building_id = ru.building_id
|
||||
AND p.room_no = ru.room_no
|
||||
AND p.floor = ru.floor
|
||||
AND p.deleted_at IS NULL
|
||||
WHERE ru.building_id = :building_id
|
||||
AND ru.is_active = TRUE
|
||||
ORDER BY ru.floor DESC, ru.room_no;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、触发器
|
||||
|
||||
### 6.1 楼盘全文检索向量(含别名)
|
||||
|
||||
```sql
|
||||
CREATE OR REPLACE FUNCTION update_complex_search_vector()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.search_vector :=
|
||||
setweight(to_tsvector('simple', COALESCE(NEW.name, '')), 'A') ||
|
||||
setweight(to_tsvector('simple', COALESCE(NEW.address_summary, '')), 'B') ||
|
||||
setweight(to_tsvector('simple', COALESCE(NEW.address, '')), 'C');
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trg_complex_search_vector
|
||||
BEFORE INSERT OR UPDATE OF name, address_summary, address
|
||||
ON complexes
|
||||
FOR EACH ROW EXECUTE FUNCTION update_complex_search_vector();
|
||||
|
||||
-- 别名变更时同步更新楼盘 search_vector
|
||||
CREATE OR REPLACE FUNCTION update_complex_search_on_alias()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
UPDATE complexes
|
||||
SET search_vector = (
|
||||
setweight(to_tsvector('simple', COALESCE(name, '')), 'A') ||
|
||||
setweight(to_tsvector('simple',
|
||||
COALESCE((SELECT string_agg(alias, ' ') FROM complex_aliases WHERE complex_id = complexes.id), '')), 'B') ||
|
||||
setweight(to_tsvector('simple', COALESCE(address_summary, '')), 'C') ||
|
||||
setweight(to_tsvector('simple', COALESCE(address, '')), 'D')
|
||||
),
|
||||
updated_at = NOW()
|
||||
WHERE id = COALESCE(NEW.complex_id, OLD.complex_id);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trg_complex_alias_search
|
||||
AFTER INSERT OR UPDATE OR DELETE ON complex_aliases
|
||||
FOR EACH ROW EXECUTE FUNCTION update_complex_search_on_alias();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、禁止操作
|
||||
|
||||
- ❌ **严禁直接修改 complexes.name**:楼盘名称变更必须走「楼盘合并」流程或「管理员申请」,通过 Application 层拦截任何直接 UPDATE `name` 字段的操作
|
||||
- ❌ **严禁硬删除 complexes 记录**:有房源关联的楼盘不可删除(`RESTRICT` 外键),已有房源的楼盘软删除后房源仍可正常访问
|
||||
- ❌ **严禁删除 complex_schools 关联而不清理房源学区标注**:必须在同一事务中清理对应 `property.school_ids` 数据
|
||||
- ❌ **严禁在楼盘坐标为 NULL 时将其用于地图聚合**:坐标为空时不参与地图展示,过滤条件:`WHERE latitude IS NOT NULL`
|
||||
- ❌ **严禁在 lock_info=TRUE 时绕过 Application 层直接修改楼盘信息字段**:锁定状态必须在服务层检查,不依赖数据库约束
|
||||
- ❌ **严禁在没有 deleted_at IS NULL 过滤的情况下查询 complexes**:楼盘软删除过滤必须存在
|
||||
341
Project/fonrey/DATA_MODEL/DATA_MODEL_ORG.md
Normal file
341
Project/fonrey/DATA_MODEL/DATA_MODEL_ORG.md
Normal file
@@ -0,0 +1,341 @@
|
||||
# Fonrey — 组织人事数据模型(DATA_MODEL_ORG)
|
||||
|
||||
> **所属系统**: Fonrey 房产经纪管理系统
|
||||
> **版本**: v1.0
|
||||
> **日期**: 2026-04-24
|
||||
> **关联模块**: `apps/org/` — 组织架构、员工档案、人事异动、账号体系
|
||||
|
||||
---
|
||||
|
||||
## 一、领域概览(Domain Overview)
|
||||
|
||||
### 核心概念
|
||||
|
||||
- **OrgUnit(组织节点)**:公司组织树的节点,类型涵盖事业部 / 大区 / 区域 / 片区 / 门店 / 店组 / 职能。所有业务数据(房源、客源)最终归属到门店或店组级节点。
|
||||
- **Staff(员工)**:系统的核心操作人员,与 Django `auth_user` 绑定登录账号,与 `org_units` 绑定岗位归属。员工的组织归属直接影响数据可见范围。
|
||||
- **StaffTransferLog(人事异动记录)**:记录员工从入职到离职的全生命周期状态变化。每次异动(入职/调动/离职/复职)自动生成一条不可删除的日志。
|
||||
- **StaffAccount(账号信息)**:员工的多平台登录账号体系,包括 Fonrey 主账号 / 58安居客 / 中国网络经纪人等。
|
||||
|
||||
### 关键业务规则
|
||||
|
||||
1. **组织层级约束**:店组级部门 **必须** 挂在门店下;经纪人/店管的所属部门 **只能** 是门店或店组。
|
||||
2. **经纪人定义**:职务类别为「置业顾问」的员工即为经纪人,受业务规则特殊约束。
|
||||
3. **人员异动强制日志**:入职、调动、离职、复职等操作均自动生成 `staff_transfer_logs` 记录,不可删除。
|
||||
4. **账号与员工联动**:员工离职后,对应的 `auth_user.is_active` 设为 `False`,不可登录;复职后由管理员手动恢复。
|
||||
5. **手机号敏感字段**:员工手机号 AES-256-GCM 加密存储,SHA-256 哈希用于唯一性校验,通讯录展示脱敏格式。
|
||||
6. **数据归属继承**:员工调动时,名下房源/客源默认跟随员工到新部门;离职时可选择转移给指定账号。
|
||||
|
||||
---
|
||||
|
||||
## 二、实体关系
|
||||
|
||||
```
|
||||
OrgUnit (树形自引用,物化路径)
|
||||
│
|
||||
├── 1:N ── Staff (员工归属一个部门)
|
||||
│ │
|
||||
│ ├── 1:1 ── auth_user (Django 登录账号)
|
||||
│ ├── 1:N ── StaffTransferLog (人事异动记录)
|
||||
│ ├── 1:N ── StaffRewardPunish (奖惩记录)
|
||||
│ ├── 1:N ── StaffAccount (第三方账号绑定)
|
||||
│ └── 1:N ── StaffRemark (管理员备注)
|
||||
│
|
||||
└── 1:1 ── OrgUnit.parent_id (自引用)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、Schema 定义
|
||||
|
||||
### 3.1 org_units — 组织节点表
|
||||
|
||||
| 字段 | 类型 | 约束 | 业务说明 |
|
||||
|------|------|------|----------|
|
||||
| id | UUID | PK | |
|
||||
| name | VARCHAR(100) | NOT NULL | 部门/组织名称 |
|
||||
| type | VARCHAR(20) | NOT NULL, CHECK | 枚举:`company` / `division`(事业部) / `region`(大区) / `area`(区域) / `district`(片区) / `store`(门店) / `group`(店组) / `functional`(职能) |
|
||||
| parent_id | UUID | FK→self, RESTRICT | 父节点,根节点为 NULL |
|
||||
| path | TEXT | NOT NULL | 物化路径:`/root_id/.../self_id/`,用于子树查询 |
|
||||
| depth | SMALLINT | NOT NULL DEFAULT 0 | 节点深度(根=0),最大支持 8 层 |
|
||||
| sort_order | INTEGER | NOT NULL DEFAULT 0 | 同级排序 |
|
||||
| attribute | VARCHAR(10) | | 直营/加盟,枚举:`direct` / `franchise` |
|
||||
| address_city | VARCHAR(50) | | 部门所在城市 |
|
||||
| address_district | VARCHAR(50) | | 部门所在县区 |
|
||||
| address_detail | VARCHAR(200) | | 详细地址 |
|
||||
| latitude | NUMERIC(10,7) | | 坐标(部门定位针) |
|
||||
| longitude | NUMERIC(10,7) | | 坐标 |
|
||||
| manager_id | UUID | FK→staff.id, SET NULL | 部门负责人(循环依赖,Application 层维护) |
|
||||
| established_at | DATE | | 成立时间 |
|
||||
| phone | VARCHAR(30) | | 部门联系电话 |
|
||||
| ext_start | INTEGER | | 分机号范围:起始 |
|
||||
| ext_end | INTEGER | | 分机号范围:结束 |
|
||||
| is_active | BOOLEAN | NOT NULL DEFAULT TRUE | FALSE = 已关闭部门,仍可在筛选中显示 |
|
||||
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||
| deleted_at | TIMESTAMPTZ | | 软删除 |
|
||||
|
||||
**关键索引**:
|
||||
```sql
|
||||
CREATE INDEX idx_org_units_parent ON org_units(parent_id) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_org_units_path_prefix ON org_units(path text_pattern_ops); -- 路径前缀查询
|
||||
CREATE INDEX idx_org_units_type ON org_units(type) WHERE deleted_at IS NULL AND is_active = TRUE;
|
||||
```
|
||||
|
||||
**业务注意**:
|
||||
- 查询某部门及所有下级:`WHERE path LIKE '/root_id/{target_id}/%'`
|
||||
- 店组(`group`)的 `parent_id` 必须指向一个 `store` 节点,新增前需校验
|
||||
|
||||
---
|
||||
|
||||
### 3.2 staff — 员工表
|
||||
|
||||
| 字段 | 类型 | 约束 | 业务说明 |
|
||||
|------|------|------|----------|
|
||||
| id | UUID | PK | |
|
||||
| org_unit_id | UUID | NOT NULL, FK→org_units | 当前所属组织节点(门店或店组) |
|
||||
| user_id | INTEGER | UNIQUE, FK→auth_user | Django auth 登录账号 ID |
|
||||
| name | VARCHAR(50) | NOT NULL | 真实姓名 |
|
||||
| nickname | VARCHAR(50) | | 昵称(通讯录/显示名) |
|
||||
| employee_no | VARCHAR(30) | UNIQUE | 员工工号,系统自动生成或手动录入 |
|
||||
| role | VARCHAR(30) | NOT NULL, CHECK | 系统角色枚举:`agent`(经纪人) / `store_manager` / `area_manager` / `admin` / `operator` / `system` |
|
||||
| job_title | VARCHAR(100) | | 职务名称,如「高级业务员」 |
|
||||
| job_category | VARCHAR(50) | | 职务类别,如「置业顾问」(经纪人判定字段) |
|
||||
| job_level | SMALLINT | | 职级(数字) |
|
||||
| supervisor_id | UUID | FK→staff.id, SET NULL | 直属上级 |
|
||||
| status | VARCHAR(20) | NOT NULL DEFAULT 'active' | `active`(在职) / `probation`(试用) / `resigned`(离职) / `frozen`(冻结) |
|
||||
| phone_enc | BYTEA | | AES-256-GCM 加密手机号 |
|
||||
| phone_hash | VARCHAR(64) | | SHA-256 哈希,用于唯一性索引 |
|
||||
| phone_hide | BOOLEAN | NOT NULL DEFAULT FALSE | 通讯录是否隐藏手机号 |
|
||||
| email | VARCHAR(255) | | 邮箱 |
|
||||
| extension | VARCHAR(20) | | 分机号 |
|
||||
| avatar_key | TEXT | | R2/S3 头像路径 |
|
||||
| is_active | BOOLEAN | NOT NULL DEFAULT TRUE | FALSE 时账号不可登录(联动 auth_user.is_active) |
|
||||
| is_system_admin | BOOLEAN | NOT NULL DEFAULT FALSE | 是否为系统管理员(影响权限上限) |
|
||||
| first_joined_at | DATE | | 首次入职日期(计算工龄起点) |
|
||||
| rejoined_at | DATE | | 最近复职日期 |
|
||||
| resigned_at | DATE | | 最近离职日期 |
|
||||
| joined_count | SMALLINT | NOT NULL DEFAULT 1 | 累计入职次数 |
|
||||
| industry_exp_years | SMALLINT | | 行业经验(年) |
|
||||
| mentor_id | UUID | FK→staff.id, SET NULL | 师傅(带教员工) |
|
||||
| business_type | VARCHAR(50) | | 业务类型 |
|
||||
| bank_name | VARCHAR(100) | | 银行名称 |
|
||||
| bank_account | VARCHAR(50) | | 银行卡号(内部财务用) |
|
||||
| partner_no | VARCHAR(50) | | 联号 |
|
||||
| recruit_by_id | UUID | FK→staff.id, SET NULL | 招聘人 |
|
||||
| recruit_source | VARCHAR(50) | | 招聘来源 |
|
||||
| referrer_id | UUID | FK→staff.id, SET NULL | 转介人 |
|
||||
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||
| deleted_at | TIMESTAMPTZ | | 软删除(离职员工仍保留记录) |
|
||||
|
||||
**关键索引**:
|
||||
```sql
|
||||
CREATE UNIQUE INDEX idx_staff_employee_no ON staff(employee_no) WHERE deleted_at IS NULL;
|
||||
CREATE UNIQUE INDEX idx_staff_phone_hash ON staff(phone_hash) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_staff_org_unit ON staff(org_unit_id) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_staff_supervisor ON staff(supervisor_id) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_staff_status ON staff(status) WHERE deleted_at IS NULL;
|
||||
```
|
||||
|
||||
**业务注意**:
|
||||
- `is_active = FALSE` 时对应 `auth_user.is_active` 同步设为 False,通过 Django signal 实现
|
||||
- 离职员工(`status = 'resigned'`)不可硬删除,保留档案以便房源/客源历史关联查询
|
||||
- 经纪人判定:`job_category = '置业顾问'`,部分权限逻辑基于此字段
|
||||
|
||||
---
|
||||
|
||||
### 3.3 staff_personal_info — 员工个人信息扩展表
|
||||
|
||||
| 字段 | 类型 | 约束 | 业务说明 |
|
||||
|------|------|------|----------|
|
||||
| id | UUID | PK | |
|
||||
| staff_id | UUID | UNIQUE, NOT NULL, FK→staff | 1:1 关系 |
|
||||
| gender | VARCHAR(10) | | `male` / `female` / `unknown` |
|
||||
| id_type | VARCHAR(20) | | 证件类型:`id_card`(身份证) / `passport` / `other` |
|
||||
| id_number_enc | BYTEA | | 证件号码(AES 加密) |
|
||||
| id_number_hash | VARCHAR(64) | | SHA-256 哈希(实名认证比对用) |
|
||||
| id_verified | BOOLEAN | NOT NULL DEFAULT FALSE | 是否实名认证通过 |
|
||||
| id_verified_at | TIMESTAMPTZ | | 认证时间 |
|
||||
| birthdate | DATE | | 出生日期 |
|
||||
| native_place | VARCHAR(100) | | 籍贯 |
|
||||
| domicile_type | VARCHAR(20) | | 户籍性质 |
|
||||
| marital_status | VARCHAR(20) | | 婚姻状况 |
|
||||
| political_status | VARCHAR(20) | | 政治面貌 |
|
||||
| has_children | BOOLEAN | | 有无子女 |
|
||||
| education_level | VARCHAR(20) | | 最高学历 |
|
||||
| ethnicity | VARCHAR(20) | | 民族 |
|
||||
| domicile_address | VARCHAR(200) | | 户口所在地 |
|
||||
| residence_address | VARCHAR(200) | | 居住地址 |
|
||||
| work_start_date | DATE | | 参加工作时间 |
|
||||
| emergency_contact | VARCHAR(50) | | 紧急联系人 |
|
||||
| emergency_phone_enc | BYTEA | | 紧急联系人电话(加密) |
|
||||
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||
| updated_by | UUID | FK→staff.id, SET NULL | |
|
||||
|
||||
---
|
||||
|
||||
### 3.4 staff_transfer_logs — 人事异动记录
|
||||
|
||||
| 字段 | 类型 | 约束 | 业务说明 |
|
||||
|------|------|------|----------|
|
||||
| id | UUID | PK | |
|
||||
| staff_id | UUID | NOT NULL, FK→staff, RESTRICT | 被操作员工 |
|
||||
| transfer_type | VARCHAR(30) | NOT NULL, CHECK | 枚举见下方 |
|
||||
| old_value | JSONB | | 变动前的值快照,格式:`{"field": "org_unit_id", "value": "...", "label": "门店A"}` |
|
||||
| new_value | JSONB | | 变动后的值快照 |
|
||||
| transfer_date | DATE | NOT NULL | 异动生效日期(可以是过去日期) |
|
||||
| remarks | VARCHAR(50) | | 备注(最多50字) |
|
||||
| operator_id | UUID | NOT NULL, FK→staff, RESTRICT | 操作人 |
|
||||
| operated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 系统操作时间 |
|
||||
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||
| ⚠️ 无 deleted_at | — | — | 异动记录**不可删除** |
|
||||
|
||||
**transfer_type 枚举**:
|
||||
```
|
||||
onboard = 入职
|
||||
transfer = 调动(含平调/晋升/降职)
|
||||
resign = 离职
|
||||
rejoin = 复职
|
||||
supervisor_change = 上级变动
|
||||
role_change = 角色变更
|
||||
freeze = 账号冻结
|
||||
unfreeze = 账号恢复
|
||||
```
|
||||
|
||||
**关键索引**:
|
||||
```sql
|
||||
CREATE INDEX idx_transfer_logs_staff ON staff_transfer_logs(staff_id, transfer_date DESC);
|
||||
CREATE INDEX idx_transfer_logs_type ON staff_transfer_logs(transfer_type, operated_at DESC);
|
||||
CREATE INDEX idx_transfer_logs_operator ON staff_transfer_logs(operator_id);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.5 staff_reward_punish — 奖惩记录
|
||||
|
||||
| 字段 | 类型 | 约束 | 业务说明 |
|
||||
|------|------|------|----------|
|
||||
| id | UUID | PK | |
|
||||
| staff_id | UUID | NOT NULL, FK→staff | |
|
||||
| rp_date | DATE | NOT NULL | 奖惩日期 |
|
||||
| category | VARCHAR(50) | NOT NULL | 奖惩类别(枚举由 lookup 表维护) |
|
||||
| name | VARCHAR(100) | NOT NULL | 奖惩名称(与类别联动) |
|
||||
| remarks | TEXT | | 备注 |
|
||||
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||
| created_by | UUID | FK→staff.id, SET NULL | |
|
||||
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||
| deleted_at | TIMESTAMPTZ | | 软删除 |
|
||||
|
||||
---
|
||||
|
||||
### 3.6 staff_work_experiences / staff_educations / staff_trainings / staff_family_members
|
||||
|
||||
这四张表结构类似,均为 1:N 附属于 `staff`,存储员工档案中「工作经历」「教育经历」「培训经历」「家庭主要成员」信息。详见下方汇总:
|
||||
|
||||
| 表名 | 关键字段 |
|
||||
|------|---------|
|
||||
| `staff_work_experiences` | staff_id, company, job_title, start_date, end_date, reason, reference_name, reference_phone |
|
||||
| `staff_educations` | staff_id, stage, school, major, start_date, end_date, enrollment_status, degree |
|
||||
| `staff_trainings` | staff_id, training_name, training_date, certificate |
|
||||
| `staff_family_members` | staff_id, relation(称谓), name, birthdate, occupation, work_unit, phone_enc |
|
||||
|
||||
---
|
||||
|
||||
### 3.7 staff_accounts — 员工第三方账号绑定
|
||||
|
||||
| 字段 | 类型 | 约束 | 业务说明 |
|
||||
|------|------|------|----------|
|
||||
| id | UUID | PK | |
|
||||
| staff_id | UUID | NOT NULL, FK→staff | |
|
||||
| platform | VARCHAR(30) | NOT NULL, CHECK | `fonrey`(主账号) / `58anjuke` / `cnreic`(中国网络经纪人) / `wechat_mp`(微信公众号) |
|
||||
| account_no | VARCHAR(100) | | 账号/手机号 |
|
||||
| is_real_name_match | BOOLEAN | | 实名信息一致性(中国网络经纪人专用) |
|
||||
| is_bound | BOOLEAN | NOT NULL DEFAULT FALSE | 是否已绑定 |
|
||||
| bound_at | TIMESTAMPTZ | | 绑定时间 |
|
||||
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
|
||||
|
||||
---
|
||||
|
||||
## 四、枚举常量
|
||||
|
||||
### Staff.role(系统角色)
|
||||
|
||||
| 值 | 含义 | 数据可见范围默认 |
|
||||
|----|------|----------------|
|
||||
| `agent` | 一线经纪人 | 本人/本组 |
|
||||
| `store_manager` | 店长 | 本门店 |
|
||||
| `area_manager` | 区域经理 | 本区域 |
|
||||
| `admin` | 系统管理员 | 全公司 |
|
||||
| `operator` | 运营/行政 | 全公司(只读为主) |
|
||||
| `system` | 系统账号(定时任务用) | — |
|
||||
|
||||
### Staff.status(员工状态)
|
||||
|
||||
```
|
||||
active = 正式在职
|
||||
probation = 试用期
|
||||
resigned = 已离职(不可删除,保留档案)
|
||||
frozen = 账号冻结(在职但无法登录)
|
||||
```
|
||||
|
||||
### OrgUnit.type(组织类型)
|
||||
|
||||
```
|
||||
company = 公司根节点(每个租户唯一)
|
||||
division = 事业部
|
||||
region = 大区
|
||||
area = 区域
|
||||
district = 片区
|
||||
store = 门店(经纪人最小归属单位)
|
||||
group = 店组(门店下的业务小组)
|
||||
functional = 职能部门(行政/财务等)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、查询模式
|
||||
|
||||
### 5.1 查询某部门及所有下级的在职员工
|
||||
|
||||
```sql
|
||||
-- 利用物化路径高效查询子树
|
||||
SELECT s.*
|
||||
FROM staff s
|
||||
JOIN org_units ou ON s.org_unit_id = ou.id
|
||||
WHERE ou.path LIKE '/root_id/{target_org_unit_id}/%'
|
||||
OR ou.id = '{target_org_unit_id}'
|
||||
AND s.deleted_at IS NULL
|
||||
AND s.status != 'resigned';
|
||||
```
|
||||
|
||||
### 5.2 查询员工完整异动历史
|
||||
|
||||
```sql
|
||||
SELECT stl.*,
|
||||
s.name as operator_name,
|
||||
ou.name as operator_org
|
||||
FROM staff_transfer_logs stl
|
||||
JOIN staff s ON stl.operator_id = s.id
|
||||
JOIN org_units ou ON s.org_unit_id = ou.id
|
||||
WHERE stl.staff_id = :staff_id
|
||||
ORDER BY stl.transfer_date DESC, stl.operated_at DESC;
|
||||
```
|
||||
|
||||
### 5.3 获取员工的直接上下级链
|
||||
|
||||
```sql
|
||||
-- 直属上级
|
||||
SELECT supervisor.* FROM staff
|
||||
JOIN staff supervisor ON staff.supervisor_id = supervisor.id
|
||||
WHERE staff.id = :staff_id AND supervisor.deleted_at IS NULL;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、禁止操作
|
||||
|
||||
- ❌ **严禁硬删除 staff 记录**:离职员工需通过 `deleted_at + status = 'resigned'` 软删除,历史房源/跟进日志依赖 `staff.id` 外键
|
||||
- ❌ **严禁删除 staff_transfer_logs**:异动记录为不可变审计日志
|
||||
- ❌ **严禁直接修改 staff.user_id**:账号绑定关系变更需走专门的账号管理流程
|
||||
- ❌ **严禁绕过组织层级约束**:店组不在门店下的数据操作需在 Application 层校验并拒绝
|
||||
- ❌ **严禁明文存储员工手机号和证件号**:必须走 `EncryptedPhoneField` / `EncryptedIDField`
|
||||
574
Project/fonrey/DATA_MODEL/diagram/fonrey-er.svg
Normal file
574
Project/fonrey/DATA_MODEL/diagram/fonrey-er.svg
Normal file
@@ -0,0 +1,574 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2560 1980">
|
||||
<defs>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap');
|
||||
text { font-family: 'JetBrains Mono', 'Noto Sans SC', 'PingFang SC', 'SF Mono', monospace; }
|
||||
</style>
|
||||
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
|
||||
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="#1e293b" stroke-width="0.5"/>
|
||||
</pattern>
|
||||
<!-- Arrow markers per color -->
|
||||
<marker id="arrow-cyan" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
||||
<polygon points="0 0, 10 3.5, 0 7" fill="#22d3ee"/>
|
||||
</marker>
|
||||
<marker id="arrow-emerald" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
||||
<polygon points="0 0, 10 3.5, 0 7" fill="#34d399"/>
|
||||
</marker>
|
||||
<marker id="arrow-violet" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
||||
<polygon points="0 0, 10 3.5, 0 7" fill="#a78bfa"/>
|
||||
</marker>
|
||||
<marker id="arrow-amber" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
||||
<polygon points="0 0, 10 3.5, 0 7" fill="#fbbf24"/>
|
||||
</marker>
|
||||
<marker id="arrow-slate" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
||||
<polygon points="0 0, 10 3.5, 0 7" fill="#94a3b8"/>
|
||||
</marker>
|
||||
<marker id="arrow-orange" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
|
||||
<polygon points="0 0, 10 3.5, 0 7" fill="#fb923c"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<!-- Background -->
|
||||
<rect width="2560" height="1980" fill="#0f172a"/>
|
||||
<rect width="2560" height="1980" fill="url(#grid)"/>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||
<!-- MODULE BOUNDARIES -->
|
||||
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||
|
||||
<!-- Org Module boundary (cyan) -->
|
||||
<rect x="30" y="60" width="310" height="680" rx="12" fill="none" stroke="#22d3ee" stroke-width="1" stroke-dasharray="8,4" opacity="0.6"/>
|
||||
<text x="44" y="80" fill="#22d3ee" font-size="10" font-weight="600">ORG / HR</text>
|
||||
|
||||
<!-- Region+Complex Module boundary (emerald) -->
|
||||
<rect x="360" y="60" width="680" height="1200" rx="12" fill="none" stroke="#34d399" stroke-width="1" stroke-dasharray="8,4" opacity="0.6"/>
|
||||
<text x="374" y="80" fill="#34d399" font-size="10" font-weight="600">REGION & COMPLEX</text>
|
||||
|
||||
<!-- Property Module boundary (violet) -->
|
||||
<rect x="1060" y="60" width="720" height="1560" rx="12" fill="none" stroke="#a78bfa" stroke-width="1" stroke-dasharray="8,4" opacity="0.6"/>
|
||||
<text x="1074" y="80" fill="#a78bfa" font-size="10" font-weight="600">PROPERTY</text>
|
||||
|
||||
<!-- Client Module boundary (amber) -->
|
||||
<rect x="1800" y="60" width="730" height="1200" rx="12" fill="none" stroke="#fbbf24" stroke-width="1" stroke-dasharray="8,4" opacity="0.6"/>
|
||||
<text x="1814" y="80" fill="#fbbf24" font-size="10" font-weight="600">CLIENT</text>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||
<!-- CONNECTION LINES (drawn before boxes) -->
|
||||
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||
|
||||
<!-- OrgUnit → Staff (1:N) -->
|
||||
<line x1="185" y1="220" x2="185" y2="320" stroke="#22d3ee" stroke-width="1.2" marker-end="url(#arrow-cyan)"/>
|
||||
<text x="193" y="275" fill="#22d3ee" font-size="8">1:N</text>
|
||||
|
||||
<!-- Staff → Property (1:N, via created_by) -->
|
||||
<line x1="335" y1="370" x2="1060" y2="370" stroke="#22d3ee" stroke-width="1" stroke-dasharray="4,3" marker-end="url(#arrow-cyan)"/>
|
||||
<text x="690" y="362" fill="#22d3ee" font-size="8">created_by</text>
|
||||
|
||||
<!-- Staff → Client (1:N, via agent) -->
|
||||
<line x1="335" y1="410" x2="1800" y2="410" stroke="#22d3ee" stroke-width="1" stroke-dasharray="4,3" marker-end="url(#arrow-cyan)"/>
|
||||
<text x="1060" y="402" fill="#22d3ee" font-size="8">agent_id</text>
|
||||
|
||||
<!-- District → BusinessArea (1:N) -->
|
||||
<line x1="560" y1="225" x2="560" y2="320" stroke="#34d399" stroke-width="1.2" marker-end="url(#arrow-emerald)"/>
|
||||
<text x="568" y="277" fill="#34d399" font-size="8">1:N</text>
|
||||
|
||||
<!-- District → School (1:N) -->
|
||||
<line x1="700" y1="175" x2="870" y2="175" stroke="#34d399" stroke-width="1.2" marker-end="url(#arrow-emerald)"/>
|
||||
<text x="775" y="167" fill="#34d399" font-size="8">1:N</text>
|
||||
|
||||
<!-- BusinessArea ↔ Complex (N:M via complex_business_areas) -->
|
||||
<line x1="560" y1="420" x2="560" y2="500" stroke="#34d399" stroke-width="1.2" marker-end="url(#arrow-emerald)"/>
|
||||
<text x="568" y="464" fill="#34d399" font-size="8">N:M</text>
|
||||
|
||||
<!-- Complex → Complex_schools join label -->
|
||||
<line x1="700" y1="570" x2="870" y2="400" stroke="#34d399" stroke-width="1" stroke-dasharray="4,3" marker-end="url(#arrow-emerald)"/>
|
||||
<text x="780" y="495" fill="#34d399" font-size="8">N:M</text>
|
||||
|
||||
<!-- Complex → Building (1:N) -->
|
||||
<line x1="560" y1="700" x2="560" y2="790" stroke="#34d399" stroke-width="1.2" marker-end="url(#arrow-emerald)"/>
|
||||
<text x="568" y="749" fill="#34d399" font-size="8">1:N</text>
|
||||
|
||||
<!-- Building → RoomUnit (1:N) -->
|
||||
<line x1="560" y1="980" x2="560" y2="1060" stroke="#34d399" stroke-width="1.2" marker-end="url(#arrow-emerald)"/>
|
||||
<text x="568" y="1024" fill="#34d399" font-size="8">1:N</text>
|
||||
|
||||
<!-- Complex → Property (1:N) -->
|
||||
<line x1="720" y1="600" x2="1060" y2="300" stroke="#a78bfa" stroke-width="1.2" marker-end="url(#arrow-violet)"/>
|
||||
<text x="885" y="445" fill="#a78bfa" font-size="8">1:N</text>
|
||||
|
||||
<!-- Property → PropertyContact (1:N) -->
|
||||
<line x1="1300" y1="390" x2="1300" y2="490" stroke="#a78bfa" stroke-width="1.2" marker-end="url(#arrow-violet)"/>
|
||||
<text x="1308" y="444" fill="#a78bfa" font-size="8">1:N</text>
|
||||
|
||||
<!-- Property → FollowLog (1:N) -->
|
||||
<line x1="1420" y1="300" x2="1570" y2="300" stroke="#a78bfa" stroke-width="1.2" marker-end="url(#arrow-violet)"/>
|
||||
<text x="1482" y="292" fill="#a78bfa" font-size="8">1:N</text>
|
||||
|
||||
<!-- Property → PropertyPhoto (1:N) -->
|
||||
<line x1="1300" y1="670" x2="1300" y2="760" stroke="#a78bfa" stroke-width="1.2" marker-end="url(#arrow-violet)"/>
|
||||
<text x="1308" y="718" fill="#a78bfa" font-size="8">1:N</text>
|
||||
|
||||
<!-- Property → KeyManagement (1:N) -->
|
||||
<line x1="1420" y1="550" x2="1570" y2="550" stroke="#a78bfa" stroke-width="1.2" marker-end="url(#arrow-violet)"/>
|
||||
<text x="1482" y="542" fill="#a78bfa" font-size="8">1:N</text>
|
||||
|
||||
<!-- Property → Commission (1:N) -->
|
||||
<line x1="1420" y1="650" x2="1570" y2="750" stroke="#a78bfa" stroke-width="1.2" marker-end="url(#arrow-violet)"/>
|
||||
<text x="1490" y="695" fill="#a78bfa" font-size="8">1:N</text>
|
||||
|
||||
<!-- Property → Inspection (1:N) -->
|
||||
<line x1="1300" y1="940" x2="1300" y2="1020" stroke="#a78bfa" stroke-width="1.2" marker-end="url(#arrow-violet)"/>
|
||||
<text x="1308" y="984" fill="#a78bfa" font-size="8">1:N</text>
|
||||
|
||||
<!-- Property → Marketing (1:1) -->
|
||||
<line x1="1420" y1="870" x2="1570" y2="960" stroke="#a78bfa" stroke-width="1.2" marker-end="url(#arrow-violet)"/>
|
||||
<text x="1490" y="910" fill="#a78bfa" font-size="8">1:1</text>
|
||||
|
||||
<!-- Property → ListingHistory (1:N) -->
|
||||
<line x1="1180" y1="390" x2="1080" y2="490" stroke="#a78bfa" stroke-width="1.2" marker-end="url(#arrow-violet)"/>
|
||||
<text x="1100" y="435" fill="#a78bfa" font-size="8">1:N</text>
|
||||
|
||||
<!-- Client → ClientRequirement (1:N) -->
|
||||
<line x1="2050" y1="220" x2="2050" y2="310" stroke="#fbbf24" stroke-width="1.2" marker-end="url(#arrow-amber)"/>
|
||||
<text x="2058" y="269" fill="#fbbf24" font-size="8">1:N</text>
|
||||
|
||||
<!-- Client → ClientFollowLog (1:N) -->
|
||||
<line x1="1930" y1="220" x2="1830" y2="310" stroke="#fbbf24" stroke-width="1.2" marker-end="url(#arrow-amber)"/>
|
||||
<text x="1850" y="260" fill="#fbbf24" font-size="8">1:N</text>
|
||||
|
||||
<!-- Client → Viewing (1:N) -->
|
||||
<line x1="2170" y1="220" x2="2270" y2="310" stroke="#fbbf24" stroke-width="1.2" marker-end="url(#arrow-amber)"/>
|
||||
<text x="2210" y="260" fill="#fbbf24" font-size="8">1:N</text>
|
||||
|
||||
<!-- Client → Match (1:N) -->
|
||||
<line x1="2050" y1="490" x2="2050" y2="580" stroke="#fbbf24" stroke-width="1.2" marker-end="url(#arrow-amber)"/>
|
||||
<text x="2058" y="538" fill="#fbbf24" font-size="8">1:N</text>
|
||||
|
||||
<!-- Property → Viewing (1:N) -->
|
||||
<line x1="1780" y1="340" x2="2270" y2="340" stroke="#fb923c" stroke-width="1" stroke-dasharray="4,3" marker-end="url(#arrow-orange)"/>
|
||||
<text x="2020" y="332" fill="#fb923c" font-size="8">1:N</text>
|
||||
|
||||
<!-- Property → Match (1:N) -->
|
||||
<line x1="1780" y1="620" x2="1950" y2="620" stroke="#fb923c" stroke-width="1" stroke-dasharray="4,3" marker-end="url(#arrow-orange)"/>
|
||||
<text x="1845" y="612" fill="#fb923c" font-size="8">1:N</text>
|
||||
|
||||
<!-- MetroStation → Complex (N:M) -->
|
||||
<line x1="620" y1="1280" x2="620" y2="1200" stroke="#34d399" stroke-width="1" stroke-dasharray="4,3" marker-end="url(#arrow-emerald)"/>
|
||||
<text x="628" y="1244" fill="#34d399" font-size="8">N:M</text>
|
||||
|
||||
<!-- MetroLine → MetroStation (1:N) -->
|
||||
<line x1="430" y1="1280" x2="500" y2="1280" stroke="#34d399" stroke-width="1.2" marker-end="url(#arrow-emerald)"/>
|
||||
<text x="452" y="1272" fill="#34d399" font-size="8">1:N</text>
|
||||
|
||||
<!-- ComplexPriceTrend → Complex -->
|
||||
<line x1="870" y1="900" x2="720" y2="660" stroke="#34d399" stroke-width="1" stroke-dasharray="4,3" marker-end="url(#arrow-emerald)"/>
|
||||
<text x="790" y="800" fill="#34d399" font-size="8">1:N</text>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||
<!-- ORG MODULE -->
|
||||
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||
|
||||
<!-- OrgUnit -->
|
||||
<rect x="80" y="100" width="210" height="120" rx="6" fill="#0f172a"/>
|
||||
<rect x="80" y="100" width="210" height="120" rx="6" fill="rgba(8,51,68,0.4)" stroke="#22d3ee" stroke-width="1.5"/>
|
||||
<line x1="80" y1="128" x2="290" y2="128" stroke="#22d3ee" stroke-width="0.5" opacity="0.6"/>
|
||||
<text x="185" y="120" fill="white" font-size="11" font-weight="700" text-anchor="middle">org_units</text>
|
||||
<text x="90" y="145" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||
<text x="90" y="158" fill="#94a3b8" font-size="8">parent_id: uuid (FK→self)</text>
|
||||
<text x="90" y="171" fill="#94a3b8" font-size="8">type: varchar(20)</text>
|
||||
<text x="90" y="184" fill="#94a3b8" font-size="8">name, path, depth</text>
|
||||
<text x="90" y="197" fill="#94a3b8" font-size="8">is_active: bool</text>
|
||||
|
||||
<!-- Staff -->
|
||||
<rect x="80" y="320" width="210" height="150" rx="6" fill="#0f172a"/>
|
||||
<rect x="80" y="320" width="210" height="150" rx="6" fill="rgba(8,51,68,0.4)" stroke="#22d3ee" stroke-width="1.5"/>
|
||||
<line x1="80" y1="348" x2="290" y2="348" stroke="#22d3ee" stroke-width="0.5" opacity="0.6"/>
|
||||
<text x="185" y="340" fill="white" font-size="11" font-weight="700" text-anchor="middle">staff</text>
|
||||
<text x="90" y="365" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||
<text x="90" y="378" fill="#94a3b8" font-size="8">FK org_unit_id</text>
|
||||
<text x="90" y="391" fill="#94a3b8" font-size="8">name: varchar(50)</text>
|
||||
<text x="90" y="404" fill="#94a3b8" font-size="8">phone_enc: text (AES)</text>
|
||||
<text x="90" y="417" fill="#94a3b8" font-size="8">phone_hash: varchar(64)</text>
|
||||
<text x="90" y="430" fill="#94a3b8" font-size="8">user_id: uuid (FK→auth)</text>
|
||||
<text x="90" y="443" fill="#94a3b8" font-size="8">is_active, deleted_at</text>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||
<!-- REGION + COMPLEX MODULE -->
|
||||
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||
|
||||
<!-- District -->
|
||||
<rect x="440" y="100" width="220" height="125" rx="6" fill="#0f172a"/>
|
||||
<rect x="440" y="100" width="220" height="125" rx="6" fill="rgba(6,78,59,0.4)" stroke="#34d399" stroke-width="1.5"/>
|
||||
<line x1="440" y1="128" x2="660" y2="128" stroke="#34d399" stroke-width="0.5" opacity="0.6"/>
|
||||
<text x="550" y="120" fill="white" font-size="11" font-weight="700" text-anchor="middle">districts</text>
|
||||
<text x="450" y="145" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||
<text x="450" y="158" fill="#94a3b8" font-size="8">city: varchar(50)</text>
|
||||
<text x="450" y="171" fill="#94a3b8" font-size="8">name: varchar(50)</text>
|
||||
<text x="450" y="184" fill="#94a3b8" font-size="8">short_name: varchar(20)</text>
|
||||
<text x="450" y="197" fill="#94a3b8" font-size="8">sort_order, is_active</text>
|
||||
|
||||
<!-- BusinessArea -->
|
||||
<rect x="440" y="320" width="240" height="135" rx="6" fill="#0f172a"/>
|
||||
<rect x="440" y="320" width="240" height="135" rx="6" fill="rgba(6,78,59,0.4)" stroke="#34d399" stroke-width="1.5"/>
|
||||
<line x1="440" y1="348" x2="680" y2="348" stroke="#34d399" stroke-width="0.5" opacity="0.6"/>
|
||||
<text x="560" y="340" fill="white" font-size="11" font-weight="700" text-anchor="middle">business_areas</text>
|
||||
<text x="450" y="365" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||
<text x="450" y="378" fill="#94a3b8" font-size="8">FK district_id</text>
|
||||
<text x="450" y="391" fill="#94a3b8" font-size="8">name: varchar(100)</text>
|
||||
<text x="450" y="404" fill="#94a3b8" font-size="8">latitude, longitude</text>
|
||||
<text x="450" y="417" fill="#94a3b8" font-size="8">sort_order, is_active</text>
|
||||
|
||||
<!-- School -->
|
||||
<rect x="700" y="100" width="220" height="135" rx="6" fill="#0f172a"/>
|
||||
<rect x="700" y="100" width="220" height="135" rx="6" fill="rgba(6,78,59,0.4)" stroke="#34d399" stroke-width="1.5"/>
|
||||
<line x1="700" y1="128" x2="920" y2="128" stroke="#34d399" stroke-width="0.5" opacity="0.6"/>
|
||||
<text x="810" y="120" fill="white" font-size="11" font-weight="700" text-anchor="middle">schools</text>
|
||||
<text x="710" y="145" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||
<text x="710" y="158" fill="#94a3b8" font-size="8">FK district_id</text>
|
||||
<text x="710" y="171" fill="#94a3b8" font-size="8">name: varchar(100)</text>
|
||||
<text x="710" y="184" fill="#94a3b8" font-size="8">type: primary/middle/high</text>
|
||||
<text x="710" y="197" fill="#94a3b8" font-size="8">nature: public/private</text>
|
||||
<text x="710" y="210" fill="#94a3b8" font-size="8">level: normal/key/top</text>
|
||||
|
||||
<!-- Complex -->
|
||||
<rect x="440" y="500" width="300" height="200" rx="6" fill="#0f172a"/>
|
||||
<rect x="440" y="500" width="300" height="200" rx="6" fill="rgba(6,78,59,0.4)" stroke="#34d399" stroke-width="1.5"/>
|
||||
<line x1="440" y1="528" x2="740" y2="528" stroke="#34d399" stroke-width="0.5" opacity="0.6"/>
|
||||
<text x="590" y="520" fill="white" font-size="11" font-weight="700" text-anchor="middle">complexes</text>
|
||||
<text x="450" y="545" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||
<text x="450" y="558" fill="#94a3b8" font-size="8">name: varchar(200) [不可直接修改]</text>
|
||||
<text x="450" y="571" fill="#94a3b8" font-size="8">FK district_id</text>
|
||||
<text x="450" y="584" fill="#94a3b8" font-size="8">address, address_summary</text>
|
||||
<text x="450" y="597" fill="#94a3b8" font-size="8">latitude, longitude</text>
|
||||
<text x="450" y="610" fill="#94a3b8" font-size="8">lock_building/room/info: bool</text>
|
||||
<text x="450" y="623" fill="#94a3b8" font-size="8">property_usage_types: varchar[]</text>
|
||||
<text x="450" y="636" fill="#94a3b8" font-size="8">search_vector: tsvector</text>
|
||||
<text x="450" y="649" fill="#94a3b8" font-size="8">developer, property_company</text>
|
||||
<text x="450" y="662" fill="#94a3b8" font-size="8">deleted_at, created_by</text>
|
||||
<text x="450" y="675" fill="#94a3b8" font-size="8">...</text>
|
||||
|
||||
<!-- complex_business_areas join table (small) -->
|
||||
<rect x="440" y="465" width="220" height="30" rx="4" fill="#0f172a"/>
|
||||
<rect x="440" y="465" width="220" height="30" rx="4" fill="rgba(6,78,59,0.2)" stroke="#34d399" stroke-width="1" stroke-dasharray="3,2"/>
|
||||
<text x="550" y="484" fill="#34d399" font-size="8" text-anchor="middle">complex_business_areas (N:M) · is_primary</text>
|
||||
|
||||
<!-- complex_schools join table (small) -->
|
||||
<rect x="700" y="310" width="210" height="30" rx="4" fill="#0f172a"/>
|
||||
<rect x="700" y="310" width="210" height="30" rx="4" fill="rgba(6,78,59,0.2)" stroke="#34d399" stroke-width="1" stroke-dasharray="3,2"/>
|
||||
<text x="805" y="329" fill="#34d399" font-size="8" text-anchor="middle">complex_schools · zone_type</text>
|
||||
|
||||
<!-- Building -->
|
||||
<rect x="440" y="790" width="300" height="185" rx="6" fill="#0f172a"/>
|
||||
<rect x="440" y="790" width="300" height="185" rx="6" fill="rgba(6,78,59,0.4)" stroke="#34d399" stroke-width="1.5"/>
|
||||
<line x1="440" y1="818" x2="740" y2="818" stroke="#34d399" stroke-width="0.5" opacity="0.6"/>
|
||||
<text x="590" y="810" fill="white" font-size="11" font-weight="700" text-anchor="middle">buildings</text>
|
||||
<text x="450" y="835" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||
<text x="450" y="848" fill="#94a3b8" font-size="8">FK complex_id</text>
|
||||
<text x="450" y="861" fill="#94a3b8" font-size="8">name: varchar(50)</text>
|
||||
<text x="450" y="874" fill="#94a3b8" font-size="8">is_standard: bool</text>
|
||||
<text x="450" y="887" fill="#94a3b8" font-size="8">total_floors: smallint</text>
|
||||
<text x="450" y="900" fill="#94a3b8" font-size="8">has_elevator: bool</text>
|
||||
<text x="450" y="913" fill="#94a3b8" font-size="8">built_year: smallint</text>
|
||||
<text x="450" y="926" fill="#94a3b8" font-size="8">property_usage_type</text>
|
||||
<text x="450" y="939" fill="#94a3b8" font-size="8">is_active, created_at</text>
|
||||
<text x="450" y="952" fill="#94a3b8" font-size="8">FK school_id</text>
|
||||
|
||||
<!-- RoomUnit -->
|
||||
<rect x="440" y="1060" width="300" height="155" rx="6" fill="#0f172a"/>
|
||||
<rect x="440" y="1060" width="300" height="155" rx="6" fill="rgba(6,78,59,0.4)" stroke="#34d399" stroke-width="1.5"/>
|
||||
<line x1="440" y1="1088" x2="740" y2="1088" stroke="#34d399" stroke-width="0.5" opacity="0.6"/>
|
||||
<text x="590" y="1080" fill="white" font-size="11" font-weight="700" text-anchor="middle">room_units</text>
|
||||
<text x="450" y="1105" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||
<text x="450" y="1118" fill="#94a3b8" font-size="8">FK building_id</text>
|
||||
<text x="450" y="1131" fill="#94a3b8" font-size="8">floor: smallint</text>
|
||||
<text x="450" y="1144" fill="#94a3b8" font-size="8">floor_name: varchar(20)</text>
|
||||
<text x="450" y="1157" fill="#94a3b8" font-size="8">room_no: varchar(30)</text>
|
||||
<text x="450" y="1170" fill="#94a3b8" font-size="8">display_no: varchar(50)</text>
|
||||
<text x="450" y="1183" fill="#94a3b8" font-size="8">is_standard: bool</text>
|
||||
<text x="450" y="1196" fill="#94a3b8" font-size="8">is_active</text>
|
||||
|
||||
<!-- ComplexPriceTrend -->
|
||||
<rect x="770" y="800" width="250" height="140" rx="6" fill="#0f172a"/>
|
||||
<rect x="770" y="800" width="250" height="140" rx="6" fill="rgba(6,78,59,0.3)" stroke="#34d399" stroke-width="1.5"/>
|
||||
<line x1="770" y1="828" x2="1020" y2="828" stroke="#34d399" stroke-width="0.5" opacity="0.6"/>
|
||||
<text x="895" y="820" fill="white" font-size="11" font-weight="700" text-anchor="middle">complex_price_trends</text>
|
||||
<text x="780" y="845" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||
<text x="780" y="858" fill="#94a3b8" font-size="8">FK complex_id</text>
|
||||
<text x="780" y="871" fill="#94a3b8" font-size="8">record_month: date</text>
|
||||
<text x="780" y="884" fill="#94a3b8" font-size="8">avg_unit_price: numeric(10,2)</text>
|
||||
<text x="780" y="897" fill="#94a3b8" font-size="8">avg_sale_price: numeric(12,2)</text>
|
||||
<text x="780" y="910" fill="#94a3b8" font-size="8">transaction_count: int</text>
|
||||
<text x="780" y="923" fill="#94a3b8" font-size="8">listing_count: int</text>
|
||||
|
||||
<!-- MetroLine -->
|
||||
<rect x="370" y="1270" width="200" height="105" rx="6" fill="#0f172a"/>
|
||||
<rect x="370" y="1270" width="200" height="105" rx="6" fill="rgba(6,78,59,0.3)" stroke="#34d399" stroke-width="1.5"/>
|
||||
<line x1="370" y1="1298" x2="570" y2="1298" stroke="#34d399" stroke-width="0.5" opacity="0.6"/>
|
||||
<text x="470" y="1290" fill="white" font-size="11" font-weight="700" text-anchor="middle">metro_lines</text>
|
||||
<text x="380" y="1315" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||
<text x="380" y="1328" fill="#94a3b8" font-size="8">city: varchar(50)</text>
|
||||
<text x="380" y="1341" fill="#94a3b8" font-size="8">name: varchar(50)</text>
|
||||
<text x="380" y="1354" fill="#94a3b8" font-size="8">color: varchar(7) [HEX]</text>
|
||||
|
||||
<!-- MetroStation -->
|
||||
<rect x="580" y="1270" width="240" height="120" rx="6" fill="#0f172a"/>
|
||||
<rect x="580" y="1270" width="240" height="120" rx="6" fill="rgba(6,78,59,0.3)" stroke="#34d399" stroke-width="1.5"/>
|
||||
<line x1="580" y1="1298" x2="820" y2="1298" stroke="#34d399" stroke-width="0.5" opacity="0.6"/>
|
||||
<text x="700" y="1290" fill="white" font-size="11" font-weight="700" text-anchor="middle">metro_stations</text>
|
||||
<text x="590" y="1315" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||
<text x="590" y="1328" fill="#94a3b8" font-size="8">FK metro_line_id</text>
|
||||
<text x="590" y="1341" fill="#94a3b8" font-size="8">name: varchar(50)</text>
|
||||
<text x="590" y="1354" fill="#94a3b8" font-size="8">latitude, longitude</text>
|
||||
<text x="590" y="1367" fill="#94a3b8" font-size="8">sort_order</text>
|
||||
|
||||
<!-- complex_metro_stations join (small) -->
|
||||
<rect x="580" y="1200" width="240" height="28" rx="4" fill="#0f172a"/>
|
||||
<rect x="580" y="1200" width="240" height="28" rx="4" fill="rgba(6,78,59,0.2)" stroke="#34d399" stroke-width="1" stroke-dasharray="3,2"/>
|
||||
<text x="700" y="1218" fill="#34d399" font-size="8" text-anchor="middle">complex_metro_stations · distance_meters</text>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||
<!-- PROPERTY MODULE -->
|
||||
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||
|
||||
<!-- Property -->
|
||||
<rect x="1080" y="100" width="340" height="290" rx="6" fill="#0f172a"/>
|
||||
<rect x="1080" y="100" width="340" height="290" rx="6" fill="rgba(76,29,149,0.4)" stroke="#a78bfa" stroke-width="1.5"/>
|
||||
<line x1="1080" y1="128" x2="1420" y2="128" stroke="#a78bfa" stroke-width="0.5" opacity="0.6"/>
|
||||
<text x="1250" y="120" fill="white" font-size="11" font-weight="700" text-anchor="middle">properties</text>
|
||||
<text x="1090" y="145" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||
<text x="1090" y="158" fill="#94a3b8" font-size="8">FK complex_id FK building_id</text>
|
||||
<text x="1090" y="171" fill="#94a3b8" font-size="8">FK room_unit_id FK agent_id</text>
|
||||
<text x="1090" y="184" fill="#94a3b8" font-size="8">listing_type: sale/rent/both</text>
|
||||
<text x="1090" y="197" fill="#94a3b8" font-size="8">status: varchar(20)</text>
|
||||
<text x="1090" y="210" fill="#94a3b8" font-size="8">sale_price: numeric(12,2)</text>
|
||||
<text x="1090" y="223" fill="#94a3b8" font-size="8">rent_price: numeric(10,2)</text>
|
||||
<text x="1090" y="236" fill="#94a3b8" font-size="8">floor, total_floors</text>
|
||||
<text x="1090" y="249" fill="#94a3b8" font-size="8">area: numeric(8,2) [m²]</text>
|
||||
<text x="1090" y="262" fill="#94a3b8" font-size="8">bedroom, living, bathroom</text>
|
||||
<text x="1090" y="275" fill="#94a3b8" font-size="8">orientation, decoration</text>
|
||||
<text x="1090" y="288" fill="#94a3b8" font-size="8">search_vector: tsvector</text>
|
||||
<text x="1090" y="301" fill="#94a3b8" font-size="8">is_exclusive: bool</text>
|
||||
<text x="1090" y="314" fill="#94a3b8" font-size="8">completeness_score: int</text>
|
||||
<text x="1090" y="327" fill="#94a3b8" font-size="8">deleted_at, created_by</text>
|
||||
<text x="1090" y="340" fill="#94a3b8" font-size="8">...</text>
|
||||
<text x="1090" y="353" fill="#94a3b8" font-size="7">[89,000+ rows · partitioned by status]</text>
|
||||
<text x="1090" y="370" fill="#94a3b8" font-size="7">UNIQUE (complex_id, building_id, floor, room_no)</text>
|
||||
|
||||
<!-- PropertyContact -->
|
||||
<rect x="1080" y="490" width="280" height="155" rx="6" fill="#0f172a"/>
|
||||
<rect x="1080" y="490" width="280" height="155" rx="6" fill="rgba(76,29,149,0.35)" stroke="#a78bfa" stroke-width="1.5"/>
|
||||
<line x1="1080" y1="518" x2="1360" y2="518" stroke="#a78bfa" stroke-width="0.5" opacity="0.6"/>
|
||||
<text x="1220" y="510" fill="white" font-size="11" font-weight="700" text-anchor="middle">property_contacts</text>
|
||||
<text x="1090" y="535" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||
<text x="1090" y="548" fill="#94a3b8" font-size="8">FK property_id</text>
|
||||
<text x="1090" y="561" fill="#94a3b8" font-size="8">name: varchar(50)</text>
|
||||
<text x="1090" y="574" fill="#94a3b8" font-size="8">phone_enc: text (AES)</text>
|
||||
<text x="1090" y="587" fill="#94a3b8" font-size="8">phone_hash: varchar(64)</text>
|
||||
<text x="1090" y="600" fill="#94a3b8" font-size="8">role: owner/agent/tenant</text>
|
||||
<text x="1090" y="613" fill="#94a3b8" font-size="8">is_primary: bool</text>
|
||||
|
||||
<!-- PropertyPhoto -->
|
||||
<rect x="1080" y="760" width="280" height="155" rx="6" fill="#0f172a"/>
|
||||
<rect x="1080" y="760" width="280" height="155" rx="6" fill="rgba(76,29,149,0.35)" stroke="#a78bfa" stroke-width="1.5"/>
|
||||
<line x1="1080" y1="788" x2="1360" y2="788" stroke="#a78bfa" stroke-width="0.5" opacity="0.6"/>
|
||||
<text x="1220" y="780" fill="white" font-size="11" font-weight="700" text-anchor="middle">property_photos</text>
|
||||
<text x="1090" y="805" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||
<text x="1090" y="818" fill="#94a3b8" font-size="8">FK property_id</text>
|
||||
<text x="1090" y="831" fill="#94a3b8" font-size="8">category: listing/vr/layout</text>
|
||||
<text x="1090" y="844" fill="#94a3b8" font-size="8">file_key: text (R2/S3)</text>
|
||||
<text x="1090" y="857" fill="#94a3b8" font-size="8">is_cover: bool</text>
|
||||
<text x="1090" y="870" fill="#94a3b8" font-size="8">sort_order: smallint</text>
|
||||
<text x="1090" y="883" fill="#94a3b8" font-size="8">width, height, file_size</text>
|
||||
|
||||
<!-- Inspection -->
|
||||
<rect x="1080" y="1020" width="280" height="135" rx="6" fill="#0f172a"/>
|
||||
<rect x="1080" y="1020" width="280" height="135" rx="6" fill="rgba(76,29,149,0.35)" stroke="#a78bfa" stroke-width="1.5"/>
|
||||
<line x1="1080" y1="1048" x2="1360" y2="1048" stroke="#a78bfa" stroke-width="0.5" opacity="0.6"/>
|
||||
<text x="1220" y="1040" fill="white" font-size="11" font-weight="700" text-anchor="middle">property_inspections</text>
|
||||
<text x="1090" y="1065" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||
<text x="1090" y="1078" fill="#94a3b8" font-size="8">FK property_id FK staff_id</text>
|
||||
<text x="1090" y="1091" fill="#94a3b8" font-size="8">inspected_at: timestamptz</text>
|
||||
<text x="1090" y="1104" fill="#94a3b8" font-size="8">status: pending/done</text>
|
||||
<text x="1090" y="1117" fill="#94a3b8" font-size="8">notes: text</text>
|
||||
<text x="1090" y="1130" fill="#94a3b8" font-size="8">attachments: jsonb</text>
|
||||
|
||||
<!-- FollowLog (property) -->
|
||||
<rect x="1440" y="100" width="300" height="155" rx="6" fill="#0f172a"/>
|
||||
<rect x="1440" y="100" width="300" height="155" rx="6" fill="rgba(76,29,149,0.35)" stroke="#a78bfa" stroke-width="1.5"/>
|
||||
<line x1="1440" y1="128" x2="1740" y2="128" stroke="#a78bfa" stroke-width="0.5" opacity="0.6"/>
|
||||
<text x="1590" y="120" fill="white" font-size="11" font-weight="700" text-anchor="middle">property_follow_logs</text>
|
||||
<text x="1450" y="145" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||
<text x="1450" y="158" fill="#94a3b8" font-size="8">FK property_id FK staff_id</text>
|
||||
<text x="1450" y="171" fill="#94a3b8" font-size="8">log_type: call/visit/note...</text>
|
||||
<text x="1450" y="184" fill="#94a3b8" font-size="8">content: text</text>
|
||||
<text x="1450" y="197" fill="#94a3b8" font-size="8">sensitive_view: bool [不可删]</text>
|
||||
<text x="1450" y="210" fill="#94a3b8" font-size="8">created_at, created_by</text>
|
||||
<text x="1450" y="237" fill="#fb7185" font-size="7">⚠ NO DELETE (audit log)</text>
|
||||
|
||||
<!-- KeyManagement -->
|
||||
<rect x="1440" y="490" width="300" height="120" rx="6" fill="#0f172a"/>
|
||||
<rect x="1440" y="490" width="300" height="120" rx="6" fill="rgba(76,29,149,0.35)" stroke="#a78bfa" stroke-width="1.5"/>
|
||||
<line x1="1440" y1="518" x2="1740" y2="518" stroke="#a78bfa" stroke-width="0.5" opacity="0.6"/>
|
||||
<text x="1590" y="510" fill="white" font-size="11" font-weight="700" text-anchor="middle">property_keys</text>
|
||||
<text x="1450" y="535" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||
<text x="1450" y="548" fill="#94a3b8" font-size="8">FK property_id FK holder_id</text>
|
||||
<text x="1450" y="561" fill="#94a3b8" font-size="8">key_no: varchar(50)</text>
|
||||
<text x="1450" y="574" fill="#94a3b8" font-size="8">status: held/returned</text>
|
||||
<text x="1450" y="587" fill="#94a3b8" font-size="8">taken_at, returned_at</text>
|
||||
|
||||
<!-- Commission -->
|
||||
<rect x="1440" y="720" width="300" height="135" rx="6" fill="#0f172a"/>
|
||||
<rect x="1440" y="720" width="300" height="135" rx="6" fill="rgba(76,29,149,0.35)" stroke="#a78bfa" stroke-width="1.5"/>
|
||||
<line x1="1440" y1="748" x2="1740" y2="748" stroke="#a78bfa" stroke-width="0.5" opacity="0.6"/>
|
||||
<text x="1590" y="740" fill="white" font-size="11" font-weight="700" text-anchor="middle">property_commissions</text>
|
||||
<text x="1450" y="765" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||
<text x="1450" y="778" fill="#94a3b8" font-size="8">FK property_id</text>
|
||||
<text x="1450" y="791" fill="#94a3b8" font-size="8">commission_type: exclusive/open</text>
|
||||
<text x="1450" y="804" fill="#94a3b8" font-size="8">rate: numeric(5,4)</text>
|
||||
<text x="1450" y="817" fill="#94a3b8" font-size="8">start_date, end_date</text>
|
||||
<text x="1450" y="830" fill="#94a3b8" font-size="8">signed_at, document_key</text>
|
||||
|
||||
<!-- Marketing -->
|
||||
<rect x="1440" y="940" width="300" height="120" rx="6" fill="#0f172a"/>
|
||||
<rect x="1440" y="940" width="300" height="120" rx="6" fill="rgba(76,29,149,0.35)" stroke="#a78bfa" stroke-width="1.5"/>
|
||||
<line x1="1440" y1="968" x2="1740" y2="968" stroke="#a78bfa" stroke-width="0.5" opacity="0.6"/>
|
||||
<text x="1590" y="960" fill="white" font-size="11" font-weight="700" text-anchor="middle">property_marketing</text>
|
||||
<text x="1450" y="985" fill="#fbbf24" font-size="8">PK id: uuid [1:1 property]</text>
|
||||
<text x="1450" y="998" fill="#94a3b8" font-size="8">FK property_id (UNIQUE)</text>
|
||||
<text x="1450" y="1011" fill="#94a3b8" font-size="8">title: varchar(200)</text>
|
||||
<text x="1450" y="1024" fill="#94a3b8" font-size="8">highlights: text[]</text>
|
||||
<text x="1450" y="1037" fill="#94a3b8" font-size="8">published_at, platforms: jsonb</text>
|
||||
|
||||
<!-- ListingHistory -->
|
||||
<rect x="1080" y="1250" width="300" height="120" rx="6" fill="#0f172a"/>
|
||||
<rect x="1080" y="1250" width="300" height="120" rx="6" fill="rgba(76,29,149,0.35)" stroke="#a78bfa" stroke-width="1.5"/>
|
||||
<line x1="1080" y1="1278" x2="1380" y2="1278" stroke="#a78bfa" stroke-width="0.5" opacity="0.6"/>
|
||||
<text x="1230" y="1270" fill="white" font-size="11" font-weight="700" text-anchor="middle">listing_histories</text>
|
||||
<text x="1090" y="1295" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||
<text x="1090" y="1308" fill="#94a3b8" font-size="8">FK property_id</text>
|
||||
<text x="1090" y="1321" fill="#94a3b8" font-size="8">listed_at, delisted_at</text>
|
||||
<text x="1090" y="1334" fill="#94a3b8" font-size="8">list_price: numeric(12,2)</text>
|
||||
<text x="1090" y="1347" fill="#94a3b8" font-size="8">reason: varchar(50)</text>
|
||||
|
||||
<!-- Arrow from Property to ListingHistory -->
|
||||
<line x1="1230" y1="390" x2="1230" y2="1250" stroke="#a78bfa" stroke-width="1" stroke-dasharray="4,3" marker-end="url(#arrow-violet)"/>
|
||||
<text x="1238" y="820" fill="#a78bfa" font-size="8">1:N</text>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||
<!-- CLIENT MODULE -->
|
||||
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||
|
||||
<!-- Client -->
|
||||
<rect x="1870" y="100" width="320" height="250" rx="6" fill="#0f172a"/>
|
||||
<rect x="1870" y="100" width="320" height="250" rx="6" fill="rgba(120,53,15,0.4)" stroke="#fbbf24" stroke-width="1.5"/>
|
||||
<line x1="1870" y1="128" x2="2190" y2="128" stroke="#fbbf24" stroke-width="0.5" opacity="0.6"/>
|
||||
<text x="2030" y="120" fill="white" font-size="11" font-weight="700" text-anchor="middle">clients</text>
|
||||
<text x="1880" y="145" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||
<text x="1880" y="158" fill="#94a3b8" font-size="8">FK agent_id (staff)</text>
|
||||
<text x="1880" y="171" fill="#94a3b8" font-size="8">client_type: private/public/closed</text>
|
||||
<text x="1880" y="184" fill="#94a3b8" font-size="8">status: active/inactive/converted</text>
|
||||
<text x="1880" y="197" fill="#94a3b8" font-size="8">name: varchar(50)</text>
|
||||
<text x="1880" y="210" fill="#94a3b8" font-size="8">phone_enc: text (AES)</text>
|
||||
<text x="1880" y="223" fill="#94a3b8" font-size="8">phone_hash: varchar(64)</text>
|
||||
<text x="1880" y="236" fill="#94a3b8" font-size="8">activity_level: 1-5 (Celery daily)</text>
|
||||
<text x="1880" y="249" fill="#94a3b8" font-size="8">is_protected: bool [防止转公客]</text>
|
||||
<text x="1880" y="262" fill="#94a3b8" font-size="8">transfer_to_public_type: auto/manual</text>
|
||||
<text x="1880" y="275" fill="#94a3b8" font-size="8">source: varchar(30)</text>
|
||||
<text x="1880" y="288" fill="#94a3b8" font-size="8">deleted_at, created_by</text>
|
||||
<text x="1880" y="301" fill="#94a3b8" font-size="8">...</text>
|
||||
<text x="1880" y="325" fill="#94a3b8" font-size="7">[私客/公客/成交客 三态状态机]</text>
|
||||
|
||||
<!-- ClientRequirement -->
|
||||
<rect x="1960" y="490" width="280" height="155" rx="6" fill="#0f172a"/>
|
||||
<rect x="1960" y="490" width="280" height="155" rx="6" fill="rgba(120,53,15,0.35)" stroke="#fbbf24" stroke-width="1.5"/>
|
||||
<line x1="1960" y1="518" x2="2240" y2="518" stroke="#fbbf24" stroke-width="0.5" opacity="0.6"/>
|
||||
<text x="2100" y="510" fill="white" font-size="11" font-weight="700" text-anchor="middle">client_requirements</text>
|
||||
<text x="1970" y="535" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||
<text x="1970" y="548" fill="#94a3b8" font-size="8">FK client_id</text>
|
||||
<text x="1970" y="561" fill="#94a3b8" font-size="8">req_type: second_hand/new/rent</text>
|
||||
<text x="1970" y="574" fill="#94a3b8" font-size="8">district_ids: uuid[]</text>
|
||||
<text x="1970" y="587" fill="#94a3b8" font-size="8">price_min/max: numeric</text>
|
||||
<text x="1970" y="600" fill="#94a3b8" font-size="8">area_min/max, bedrooms</text>
|
||||
<text x="1970" y="613" fill="#94a3b8" font-size="8">school_ids: uuid[]</text>
|
||||
|
||||
<!-- ClientFollowLog -->
|
||||
<rect x="1820" y="490" width="130" height="155" rx="6" fill="#0f172a"/>
|
||||
<rect x="1820" y="490" width="130" height="155" rx="6" fill="rgba(120,53,15,0.35)" stroke="#fbbf24" stroke-width="1.5"/>
|
||||
<line x1="1820" y1="518" x2="1950" y2="518" stroke="#fbbf24" stroke-width="0.5" opacity="0.6"/>
|
||||
<text x="1885" y="510" fill="white" font-size="10" font-weight="700" text-anchor="middle">client_follow_logs</text>
|
||||
<text x="1828" y="535" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||
<text x="1828" y="548" fill="#94a3b8" font-size="8">FK client_id</text>
|
||||
<text x="1828" y="561" fill="#94a3b8" font-size="8">log_type</text>
|
||||
<text x="1828" y="574" fill="#94a3b8" font-size="8">content: text</text>
|
||||
<text x="1828" y="587" fill="#94a3b8" font-size="8">created_at</text>
|
||||
<text x="1828" y="617" fill="#fb7185" font-size="7">⚠ NO DELETE</text>
|
||||
|
||||
<!-- Viewing -->
|
||||
<rect x="2250" y="290" width="250" height="165" rx="6" fill="#0f172a"/>
|
||||
<rect x="2250" y="290" width="250" height="165" rx="6" fill="rgba(120,53,15,0.35)" stroke="#fbbf24" stroke-width="1.5"/>
|
||||
<line x1="2250" y1="318" x2="2500" y2="318" stroke="#fbbf24" stroke-width="0.5" opacity="0.6"/>
|
||||
<text x="2375" y="310" fill="white" font-size="11" font-weight="700" text-anchor="middle">client_viewings</text>
|
||||
<text x="2260" y="335" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||
<text x="2260" y="348" fill="#94a3b8" font-size="8">FK client_id</text>
|
||||
<text x="2260" y="361" fill="#94a3b8" font-size="8">FK property_id</text>
|
||||
<text x="2260" y="374" fill="#94a3b8" font-size="8">FK agent_id (staff)</text>
|
||||
<text x="2260" y="387" fill="#94a3b8" font-size="8">viewed_at: timestamptz</text>
|
||||
<text x="2260" y="400" fill="#94a3b8" font-size="8">feedback: text</text>
|
||||
<text x="2260" y="413" fill="#94a3b8" font-size="8">rating: smallint</text>
|
||||
<text x="2260" y="426" fill="#94a3b8" font-size="8">status: planned/done/cancelled</text>
|
||||
|
||||
<!-- Match -->
|
||||
<rect x="1960" y="700" width="280" height="150" rx="6" fill="#0f172a"/>
|
||||
<rect x="1960" y="700" width="280" height="150" rx="6" fill="rgba(120,53,15,0.35)" stroke="#fbbf24" stroke-width="1.5"/>
|
||||
<line x1="1960" y1="728" x2="2240" y2="728" stroke="#fbbf24" stroke-width="0.5" opacity="0.6"/>
|
||||
<text x="2100" y="720" fill="white" font-size="11" font-weight="700" text-anchor="middle">client_property_matches</text>
|
||||
<text x="1970" y="745" fill="#fbbf24" font-size="8">PK id: uuid</text>
|
||||
<text x="1970" y="758" fill="#94a3b8" font-size="8">FK client_id</text>
|
||||
<text x="1970" y="771" fill="#94a3b8" font-size="8">FK property_id</text>
|
||||
<text x="1970" y="784" fill="#94a3b8" font-size="8">match_type: system/manual</text>
|
||||
<text x="1970" y="797" fill="#94a3b8" font-size="8">score: numeric(5,2)</text>
|
||||
<text x="1970" y="810" fill="#94a3b8" font-size="8">status: pending/sent/viewed</text>
|
||||
<text x="1970" y="823" fill="#94a3b8" font-size="8">created_at</text>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||
<!-- LEGEND -->
|
||||
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||
|
||||
<rect x="30" y="1850" width="900" height="100" rx="8" fill="rgba(15,23,42,0.8)" stroke="#334155" stroke-width="1"/>
|
||||
<text x="50" y="1873" fill="#94a3b8" font-size="9" font-weight="600">LEGEND</text>
|
||||
|
||||
<!-- Org -->
|
||||
<rect x="50" y="1885" width="14" height="14" rx="2" fill="rgba(8,51,68,0.4)" stroke="#22d3ee" stroke-width="1.5"/>
|
||||
<text x="70" y="1896" fill="#22d3ee" font-size="8">ORG / HR</text>
|
||||
|
||||
<!-- Complex -->
|
||||
<rect x="155" y="1885" width="14" height="14" rx="2" fill="rgba(6,78,59,0.4)" stroke="#34d399" stroke-width="1.5"/>
|
||||
<text x="175" y="1896" fill="#34d399" font-size="8">REGION & COMPLEX</text>
|
||||
|
||||
<!-- Property -->
|
||||
<rect x="310" y="1885" width="14" height="14" rx="2" fill="rgba(76,29,149,0.4)" stroke="#a78bfa" stroke-width="1.5"/>
|
||||
<text x="330" y="1896" fill="#a78bfa" font-size="8">PROPERTY</text>
|
||||
|
||||
<!-- Client -->
|
||||
<rect x="420" y="1885" width="14" height="14" rx="2" fill="rgba(120,53,15,0.4)" stroke="#fbbf24" stroke-width="1.5"/>
|
||||
<text x="440" y="1896" fill="#fbbf24" font-size="8">CLIENT</text>
|
||||
|
||||
<!-- Relationship lines -->
|
||||
<line x1="50" y1="1920" x2="90" y2="1920" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#arrow-slate)"/>
|
||||
<text x="96" y="1924" fill="#94a3b8" font-size="8">Foreign Key (FK)</text>
|
||||
|
||||
<line x1="210" y1="1920" x2="250" y2="1920" stroke="#94a3b8" stroke-width="1" stroke-dasharray="4,3" marker-end="url(#arrow-slate)"/>
|
||||
<text x="256" y="1924" fill="#94a3b8" font-size="8">Soft reference / optional FK</text>
|
||||
|
||||
<rect x="410" y="1913" width="14" height="14" rx="2" fill="rgba(15,23,42,0.5)" stroke="#34d399" stroke-width="1" stroke-dasharray="3,2"/>
|
||||
<text x="430" y="1924" fill="#34d399" font-size="8">Join table (N:M)</text>
|
||||
|
||||
<text x="550" y="1924" fill="#fbbf24" font-size="8">PK Primary Key</text>
|
||||
<text x="640" y="1924" fill="#fb7185" font-size="8">⚠ NO DELETE = append-only audit log</text>
|
||||
|
||||
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||
<!-- TITLE BLOCK -->
|
||||
<!-- ═══════════════════════════════════════════════════════════ -->
|
||||
<text x="1080" y="1930" fill="#475569" font-size="10" text-anchor="middle">Fonrey 房产经纪管理系统 — Entity Relationship Diagram · v1.0 · 2026-04-24 · Schema-per-Tenant (django-tenants)</text>
|
||||
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 40 KiB |
BIN
Project/fonrey/DATA_MODEL/diagram/fonrey-er@2x.png
Normal file
BIN
Project/fonrey/DATA_MODEL/diagram/fonrey-er@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 869 KiB |
Reference in New Issue
Block a user