549 lines
22 KiB
Markdown
549 lines
22 KiB
Markdown
> **For AI assistants**: Read this entire file before writing any code. All decisions here are final. Do not suggest alternatives unless asked.
|
||
# 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**:楼盘软删除过滤必须存在
|