> **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/` — 楼盘/小区、楼栋、结构(楼层+房号)、区域、学校 --- ## 变更历史 | 日期 | 变更人 | 变更内容 | |---|---|---| | 2026-04-30 | Atlas | 补充“变更历史”章节(文档治理) | ## 一、领域概览(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 | FALSE=已停用(不在筛选项中展示) | | 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 | FALSE=已停用(不在筛选项中展示) | --- ### 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 | FALSE=已停用(不在筛选项中展示) | ```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 | FALSE=已停用(不在筛选项中展示) | | 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) | | 地铁站经度坐标(WGS84) | | **物业属性** | | | | | 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 | | 软删除时间戳;NULL=未删除,非NULL=已软删除 | | created_by | UUID | FK→staff, SET NULL | 创建人(操作员工) | | updated_by | UUID | FK→staff, SET NULL | 最后更新人(操作员工) | | version | INTEGER | NOT NULL DEFAULT 1 | 乐观锁版本号;每次 UPDATE +1;应用层检测 0 行受影响时抛 ConflictError | **关键索引**: ```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 | FALSE=已停用(楼栋被删除或合并) | | 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 | | 图片宽度(px) | | height | INTEGER | | 图片高度(px) | | 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 | R2/S3 存储路径 | | file_name | VARCHAR(255) | NOT NULL | 原始文件名 | | file_size | INTEGER | | 文件大小(bytes) | | 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**:楼盘软删除过滤必须存在