Files
nexus/Project/fonrey/DATA_MODEL/DATA_MODEL_COMPLEX.md

548 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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) | | 小区总建筑面积 |
| plot_area | NUMERIC(12,2) | | 小区占地面积 |
| plot_ratio | NUMERIC(5,2) | | 容积率 |
| green_rate | NUMERIC(5,2) | | 绿化率(% |
| developer | VARCHAR(200) | | 开发商 |
| **物业信息** | | | |
| property_company | VARCHAR(200) | | 物业公司 |
| property_fee | NUMERIC(8,2) | | 物业费(元/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**:楼盘软删除过滤必须存在