> **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_PROPERTY) > **权威定义**:本文件是房源模块所有表结构的唯一权威来源。 > **主文档引用**:`DATA_MODEL.md` §3.3–§3.16 为本文件的概览摘要,开发以本文件为准。 > **版本**:v1.0 | **日期**:2026-04-24 --- ## 目录 1. [模块说明](#1-模块说明) 2. [表清单](#2-表清单) 3. [枚举值总览](#3-枚举值总览) 4. [DDL 定义](#4-ddl-定义) - 4.1 [properties(房源主表)](#41-properties房源主表) - 4.2 [property_contacts(联系人)](#42-property_contacts联系人) - 4.3 [listing_histories(挂牌历史)](#43-listing_histories挂牌历史) - 4.4 [price_changes(调价记录)](#44-price_changes调价记录) - 4.5 [follow_logs(跟进日志)](#45-follow_logs跟进日志) - 4.6 [follow_log_attachments(跟进附件)](#46-follow_log_attachments跟进附件) - 4.7 [follow_log_recordings(跟进录音)](#47-follow_log_recordings跟进录音) - 4.8 [property_keys(钥匙管理)](#48-property_keys钥匙管理) - 4.9 [key_attachments(钥匙附件)](#49-key_attachments钥匙附件) - 4.10 [commissions(委托管理)](#410-commissions委托管理) - 4.11 [commission_attachments(委托附件)](#411-commission_attachments委托附件) - 4.12 [field_surveys(实勘管理)](#412-field_surveys实勘管理) - 4.13 [survey_photos(实勘照片)](#413-survey_photos实勘照片) - 4.14 [property_photos(房源图片)](#414-property_photos房源图片) - 4.15 [property_attachments(房源附件)](#415-property_attachments房源附件) - 4.16 [property_marketing(营销信息)](#416-property_marketing营销信息) - 4.17 [property_certificates(产证信息)](#417-property_certificates产证信息) - 4.18 [property_completeness(维护完成度)](#418-property_completeness维护完成度) - 4.19 [property_tags / property_tag_relations(标签)](#419-property_tags--property_tag_relations标签) - 4.20 [property_favorites(收藏)](#420-property_favorites收藏) - 4.21 [property_protections(保护房)](#421-property_protections保护房) - 4.22 [number_holder_approvals(号码方审批)](#422-number_holder_approvals号码方审批) 5. [触发器](#5-触发器) 6. [查询模式参考](#6-查询模式参考) 7. [禁止操作](#7-禁止操作) --- ## 1 模块说明 **房源(Property)** 是 Fonrey 系统的核心领域对象,代表一套二手房源的完整档案。 核心业务规则: | 规则 | 说明 | |------|------| | 多态交易状态 | 一套房源可同时处于出售、出租或租售三态(`status`) | | 流通属性 | 公盘/私盘/特盘/封盘(`attribute`),控制可见范围 | | 相关方体系 | 首录方/号码方/出售方/实买方,通过 `staff_id` 关联 | | 联系人加密 | 业主/联系人手机号 AES-256-GCM 加密,SHA-256 哈希索引 | | 跟进日志不可删 | `sensitive_view` 类型跟进 `is_deletable=FALSE`,合规强制 | | 挂牌历史不可删 | `listing_histories` 无 `deleted_at`,append-only | | 调价记录不可删 | `price_changes` 无 `deleted_at`,append-only | | 完成度异步计算 | `completeness_score` 由 Celery 任务更新,不实时 | --- ## 2 表清单 | # | 表名 | 说明 | 是否可删除 | |---|------|------|----------| | 1 | `properties` | 房源主表 | 软删除(`deleted_at`) | | 2 | `property_contacts` | 业主/联系人(手机号加密) | 软删除 | | 3 | `listing_histories` | 挂牌历史快照 | **不可删除** | | 4 | `price_changes` | 调价记录 | **不可删除** | | 5 | `follow_logs` | 跟进日志(6种类型) | 部分不可删(`sensitive_view`) | | 6 | `follow_log_attachments` | 跟进附件(图片) | 随日志联级 | | 7 | `follow_log_recordings` | 跟进录音 | 随日志联级 | | 8 | `property_keys` | 钥匙管理 | 软删除(`is_active=FALSE`) | | 9 | `key_attachments` | 钥匙附件 | 随钥匙联级 | | 10 | `commissions` | 委托管理 | 状态驱动(`status='cancelled'`) | | 11 | `commission_attachments` | 委托附件 | 随委托联级 | | 12 | `field_surveys` | 实勘管理 | 软删除 | | 13 | `survey_photos` | 实勘照片(按空间分类) | 随实勘联级 | | 14 | `property_photos` | 房源图片(经纪人管理) | 软删除 | | 15 | `property_attachments` | 房源附件 | 直接删除 | | 16 | `property_marketing` | 营销信息(1:1) | 随房源联级 | | 17 | `property_certificates` | 产证信息(1:1) | 随房源联级 | | 18 | `property_completeness` | 维护完成度快照(1:1) | 随房源联级 | | 19 | `property_tags` | 标签字典 | 系统标签不可删 | | 20 | `property_tag_relations` | 房源↔标签多对多 | 随房源/标签联级 | | 21 | `property_favorites` | 经纪人收藏房源 | 直接删除 | | 22 | `property_protections` | 保护房设置(1:1) | 随房源联级 | | 23 | `number_holder_approvals` | 号码方变更审批 | 随房源联级 | --- ## 3 枚举值总览 ### property_type(房源类型) | 值 | 说明 | |----|------| | `residential` | 住宅 | | `villa` | 别墅 | | `commercial_residential` | 商住 | | `shop` | 商铺 | | `office` | 写字楼 | | `other` | 其他 | ### status(交易状态) | 值 | 说明 | |----|------| | `for_sale` | 出售 | | `for_rent` | 出租 | | `for_sale_rent` | 租售 | | `suspended` | 暂缓 | | `sold_elsewhere` | 他售 | | `rented_elsewhere` | 他租 | | `sold` | 成交 | | `unlisted` | 未挂牌 | ### attribute(流通属性) | 值 | 说明 | |----|------| | `public` | 公盘 | | `private` | 私盘 | | `special` | 特盘 | | `sealed` | 封盘 | ### orientation(朝向) | 值 | 说明 | |----|------| | `east` | 东 | | `south` | 南 | | `west` | 西 | | `north` | 北 | | `southeast` | 东南 | | `northeast` | 东北 | | `east_west` | 东西 | | `south_north` | 南北 | | `northwest` | 西北 | | `southwest` | 西南 | ### decoration(装修情况) | 值 | 说明 | |----|------| | `rough` | 毛坯 | | `plain` | 清水 | | `simple` | 简装 | | `medium` | 中装 | | `fine` | 精装 | | `luxury` | 豪装 | ### grade(等级) | 值 | 说明 | |----|------| | `A` | A(急迫) | | `B` | B(较强) | | `C` | C(一般) | | `D` | D(较弱) | ### follow_log.log_type(跟进日志类型) | 值 | 说明 | 可删除 | |----|------|--------| | `written` | 经纪人主动写入跟进 | 是 | | `modified` | 字段变更自动生成 | 是 | | `sensitive_op` | 敏感信息操作跟进 | 否 | | `sensitive_view` | 敏感信息查看(查看号码等) | **否** | | `other` | 其他(钥匙/新增联系人等) | 是 | | `system` | 系统日志 | 是 | --- ## 4 DDL 定义 ### 4.1 properties(房源主表) ```sql -- ============================================================ -- 房源主表:系统最核心的表,全部筛选/排序/搜索围绕此表展开 -- 设计重点:89,000+ 数据量,复合索引策略,分区预留 -- ============================================================ CREATE TABLE properties ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- ── 基础分类 ── property_type VARCHAR(20) NOT NULL CHECK (property_type IN ('residential','villa','commercial_residential', 'shop','office','other')), -- ── 交易状态 ── status VARCHAR(20) NOT NULL DEFAULT 'for_sale' CHECK (status IN ('for_sale','for_rent','for_sale_rent', 'suspended','sold_elsewhere','rented_elsewhere', 'sold','unlisted')), -- ── 流通属性 ── attribute VARCHAR(20) NOT NULL DEFAULT 'public' CHECK (attribute IN ('public','private','special','sealed')), private_reason TEXT, -- 私盘/封盘必填说明 -- ── 位置信息 ── complex_id UUID NOT NULL REFERENCES complexes(id) ON DELETE RESTRICT, building_id UUID REFERENCES buildings(id) ON DELETE SET NULL, block_no VARCHAR(30), -- 栋/幢/弄号 unit_no VARCHAR(30), -- 单元号 room_no VARCHAR(30), -- 房号/门牌号 floor SMALLINT NOT NULL, -- 所在楼层 total_floors SMALLINT NOT NULL, -- 总楼层 CONSTRAINT chk_floor CHECK (floor > 0 AND floor <= total_floors), -- ── 户型 ── bedroom_count SMALLINT NOT NULL DEFAULT 0, -- 室 living_room_count SMALLINT NOT NULL DEFAULT 0, -- 厅 bathroom_count SMALLINT NOT NULL DEFAULT 0, -- 卫 kitchen_count SMALLINT NOT NULL DEFAULT 0, -- 厨 balcony_count SMALLINT NOT NULL DEFAULT 0, -- 阳台数 -- ── 面积 ── area NUMERIC(8,2) NOT NULL, -- 建筑面积 m² inner_area NUMERIC(8,2), -- 套内面积 m² -- ── 价格 ── sale_price NUMERIC(12,2), -- 挂牌售价(万元) sale_bottom_price NUMERIC(12,2), -- 售底价(万元,内部可见) sale_record_price NUMERIC(12,2), -- 备案/核验价(万元) rent_price NUMERIC(10,2), -- 挂牌租价(元/月) -- ── 基础物理属性 ── orientation VARCHAR(10) CHECK (orientation IN ('east','south','west','north', 'southeast','northeast','east_west', 'south_north','northwest','southwest')), decoration VARCHAR(10) CHECK (decoration IN ('rough','plain','simple','medium', 'fine','luxury')), has_elevator BOOLEAN, built_year SMALLINT, -- ── 用途 ── usage_type VARCHAR(30), -- 住宅/商住/商业/普通住宅/花园洋房 等 usage_subtype VARCHAR(30), -- 细分用途 -- ── 商铺专属 ── shop_frontage NUMERIC(6,2), -- 开间(米) shop_depth NUMERIC(6,2), -- 进深(米) shop_height NUMERIC(6,2), -- 层高(米) shop_location VARCHAR(20) CHECK (shop_location IS NULL OR shop_location IN ('street','mall','residential', 'ground_floor','complex')), -- ── 房屋状态 ── house_status VARCHAR(20) CHECK (house_status IN ('owner_occupied','vacant', 'tenant_occupied','unknown')), viewing_time VARCHAR(20) CHECK (viewing_time IN ('anytime','by_appointment','inconvenient')), -- ── 等级与标签 ── grade VARCHAR(10) CHECK (grade IN ('A','B','C','D')), -- ── 交易属性 ── ownership_years VARCHAR(30), -- 房本年限:不满2年/满2年/满5年 等 ownership_years_detail VARCHAR(20), -- 满五/不满五 ownership_nature VARCHAR(20) CHECK (ownership_nature IS NULL OR ownership_nature IN ('commercial','reform_housing', 'collective','economic')), is_only_house BOOLEAN, -- 唯一住房 payment_method VARCHAR(30) CHECK (payment_method IS NULL OR payment_method IN ('full','mortgage','installment','advance')), tax_included VARCHAR(10) CHECK (tax_included IS NULL OR tax_included IN ('each_party','net','inclusive')), has_mortgage BOOLEAN, has_loan BOOLEAN, has_seal BOOLEAN, has_restriction BOOLEAN, original_price NUMERIC(12,2), -- 原购价(万元) sale_reason TEXT, -- 售房原因(最多200字) -- ── 营销备注 ── remarks TEXT, -- 房源备注(最多500字) -- ── 相关方 ── first_recorder_id UUID REFERENCES staff(id) ON DELETE SET NULL, -- 首录方 number_holder_id UUID REFERENCES staff(id) ON DELETE SET NULL, -- 号码方 seller_agent_id UUID REFERENCES staff(id) ON DELETE SET NULL, -- 出售方 buyer_agent_id UUID REFERENCES staff(id) ON DELETE SET NULL, -- 实买方 -- ── 来源 ── source VARCHAR(50), -- 房源来源渠道(lookup_items 维护) -- ── 维护完成度(冗余缓存,Celery 定期重算)── completeness_score SMALLINT NOT NULL DEFAULT 0, -- 0-100 -- ── 时间轨迹 ── listed_at TIMESTAMPTZ, -- 最近一次挂牌时间 last_followed_at TIMESTAMPTZ, -- 最后跟进时间(冗余,加速排序) created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), deleted_at TIMESTAMPTZ, created_by UUID REFERENCES staff(id) ON DELETE SET NULL, updated_by UUID REFERENCES staff(id) ON DELETE SET NULL, -- ── 全文检索向量 ── search_vector TSVECTOR, -- ── 乐观锁 ── version INTEGER NOT NULL DEFAULT 1 -- 每次 UPDATE 必须 +1;应用层检测 0 行受影响时抛 ConflictError ); -- ── 索引策略 ── -- 核心列表过滤:status + attribute + type CREATE INDEX idx_properties_status_attr ON properties(status, attribute, property_type) WHERE deleted_at IS NULL; -- 区域筛选 CREATE INDEX idx_properties_complex ON properties(complex_id) WHERE deleted_at IS NULL; -- 售价排序 CREATE INDEX idx_properties_sale_price ON properties(sale_price DESC NULLS LAST) WHERE deleted_at IS NULL AND status IN ('for_sale','for_sale_rent'); -- 面积区间 CREATE INDEX idx_properties_area ON properties(area) WHERE deleted_at IS NULL; -- 挂牌时间倒序 CREATE INDEX idx_properties_listed_at ON properties(listed_at DESC NULLS LAST) WHERE deleted_at IS NULL; -- 超时跟进检测 CREATE INDEX idx_properties_last_followed ON properties(last_followed_at DESC NULLS LAST) WHERE deleted_at IS NULL; -- 户型筛选 CREATE INDEX idx_properties_bedroom ON properties(bedroom_count) WHERE deleted_at IS NULL; -- 等级筛选 CREATE INDEX idx_properties_grade ON properties(grade) WHERE deleted_at IS NULL; -- 完成度排序 CREATE INDEX idx_properties_completeness ON properties(completeness_score) WHERE deleted_at IS NULL; -- 全文搜索 CREATE INDEX idx_properties_search ON properties USING gin(search_vector); -- 相关方快速定位 CREATE INDEX idx_properties_seller_agent ON properties(seller_agent_id) WHERE deleted_at IS NULL; CREATE INDEX idx_properties_number_holder ON properties(number_holder_id) WHERE deleted_at IS NULL; -- 列表默认排序(status + listed_at) CREATE INDEX idx_properties_list_default ON properties(status, listed_at DESC NULLS LAST) WHERE deleted_at IS NULL; -- 高频复合索引(status + attribute + complex_id + sale_price) CREATE INDEX idx_properties_list_composite ON properties (status, attribute, complex_id, sale_price DESC NULLS LAST) WHERE deleted_at IS NULL; -- 个人仪表板(与我相关) CREATE INDEX idx_properties_my_properties ON properties (seller_agent_id, status, listed_at DESC NULLS LAST) WHERE deleted_at IS NULL; ``` --- ### 4.2 property_contacts(联系人) ```sql -- ============================================================ -- 业主/联系人:手机号加密存储,哈希值支持重复检测 -- 安全要点:任何查看明文号码的行为均触发审计日志 -- ============================================================ CREATE TABLE property_contacts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), property_id UUID NOT NULL REFERENCES properties(id) ON DELETE CASCADE, name VARCHAR(50) NOT NULL, gender VARCHAR(10) NOT NULL DEFAULT 'male' CHECK (gender IN ('male','female')), identity VARCHAR(20) NOT NULL DEFAULT 'contact' CHECK (identity IN ('owner','contact','subletter', 'tenant','agent','corporate')), -- owner=业主, contact=联系人, subletter=二房东, tenant=租客, -- agent=代理人, corporate=企业法人 -- 手机号:加密存储 + 哈希索引 phone_enc BYTEA NOT NULL, -- AES-256-GCM 加密 phone_hash VARCHAR(64) NOT NULL, -- SHA-256(phone),去重查询 phone2_enc BYTEA, phone2_hash VARCHAR(64), wechat VARCHAR(100), qq VARCHAR(20), remarks TEXT, -- 号码方标记(关联审批流) is_number_holder BOOLEAN NOT NULL DEFAULT FALSE, number_holder_approved_at TIMESTAMPTZ, sort_order INTEGER NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), deleted_at TIMESTAMPTZ, created_by UUID REFERENCES staff(id) ON DELETE SET NULL, updated_by UUID REFERENCES staff(id) ON DELETE SET NULL ); CREATE INDEX idx_contacts_property ON property_contacts(property_id) WHERE deleted_at IS NULL; -- 关键:手机号哈希全局索引(重复房源检测) CREATE INDEX idx_contacts_phone_hash ON property_contacts(phone_hash) WHERE deleted_at IS NULL; CREATE INDEX idx_contacts_phone2_hash ON property_contacts(phone2_hash) WHERE phone2_hash IS NOT NULL AND deleted_at IS NULL; ``` --- ### 4.3 listing_histories(挂牌历史) ```sql -- ============================================================ -- 挂牌历史:记录房源每次上架的完整快照 -- 注意:无 deleted_at,不可删除(append-only,合规要求) -- ============================================================ CREATE TABLE listing_histories ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), property_id UUID NOT NULL REFERENCES properties(id) ON DELETE RESTRICT, listing_type VARCHAR(20) NOT NULL CHECK (listing_type IN ('for_sale','for_rent')), status VARCHAR(20) NOT NULL DEFAULT 'active' CHECK (status IN ('active','ended')), -- 价格快照 sale_price NUMERIC(12,2), rent_price NUMERIC(10,2), sale_unit_price NUMERIC(10,2), -- 元/m²,计算字段 -- 交易信息快照 ownership_years VARCHAR(30), is_only_house BOOLEAN, tax_included VARCHAR(10), sale_reason TEXT, -- 经纪人快照(防止人员变动后丢失历史数据) seller_agent_id UUID REFERENCES staff(id) ON DELETE SET NULL, seller_agent_snapshot JSONB, -- {name, store_group, org_unit_name} started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), ended_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -- 无 deleted_at:此表记录不可删除 ); CREATE INDEX idx_listing_histories_property ON listing_histories(property_id); CREATE INDEX idx_listing_histories_active ON listing_histories(property_id) WHERE status = 'active'; ``` --- ### 4.4 price_changes(调价记录) ```sql -- ============================================================ -- 调价记录:支持折线图展示,不可删除(append-only) -- ============================================================ CREATE TABLE price_changes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), property_id UUID NOT NULL REFERENCES properties(id) ON DELETE RESTRICT, old_sale_price NUMERIC(12,2), new_sale_price NUMERIC(12,2), old_bottom_price NUMERIC(12,2), new_bottom_price NUMERIC(12,2), old_record_price NUMERIC(12,2), new_record_price NUMERIC(12,2), old_rent_price NUMERIC(10,2), new_rent_price NUMERIC(10,2), change_reason TEXT NOT NULL, -- 最多200字 changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), changed_by UUID NOT NULL REFERENCES staff(id) ON DELETE RESTRICT -- 无 deleted_at:不可删除 ); CREATE INDEX idx_price_changes_property ON price_changes(property_id); CREATE INDEX idx_price_changes_time ON price_changes(property_id, changed_at DESC); ``` --- ### 4.5 follow_logs(跟进日志) ```sql -- ============================================================ -- 跟进日志:系统最高写入频率的表 -- 6 种类型:written / modified / sensitive_op / sensitive_view / other / system -- sensitive_view 类型:is_deletable=FALSE,合规不可删 -- ============================================================ CREATE TABLE follow_logs ( id UUID NOT NULL DEFAULT gen_random_uuid(), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 分区键,必须在最前声明 property_id UUID NOT NULL REFERENCES properties(id) ON DELETE CASCADE, log_type VARCHAR(30) NOT NULL CHECK (log_type IN ('written','modified','sensitive_op', 'sensitive_view','other','system')), -- 写入跟进专用字段 purpose VARCHAR(50), -- 跟进目的(lookup_items 维护) content TEXT, -- 最少6字,最多500字 -- AI 辅助判断标签 ai_tag VARCHAR(20) CHECK (ai_tag IS NULL OR ai_tag IN ('ai_for_sale','ai_not_for_sale')), -- 修改跟进专用字段 change_detail JSONB, -- 格式:{"field": "sale_price", "old": 850, "new": 800, "label": "售价"} -- 系统显示标签 log_tag VARCHAR(50), -- 如:查看号码/图片下载/改状态/改价格/改等级/修改相关方 -- 可见性控制 is_public BOOLEAN NOT NULL DEFAULT TRUE, -- FALSE = 仅本人及上级可见 -- 操作人 operator_id UUID REFERENCES staff(id) ON DELETE SET NULL, operator_snapshot JSONB, -- {name, role, org_unit_name, store_group} -- 是否可删除(sensitive_view = FALSE,合规强制) is_deletable BOOLEAN NOT NULL DEFAULT TRUE, deleted_at TIMESTAMPTZ, -- 仅 is_deletable=TRUE 时可软删 PRIMARY KEY (id, created_at) -- 分区表主键必须包含分区键 ) PARTITION BY RANGE (created_at); -- ── 按月自动建分区(由 partition_maintenance_task Celery 任务维护)── -- 示例:初始建立当前月 + 下一个月的分区 CREATE TABLE follow_logs_2026_04 PARTITION OF follow_logs FOR VALUES FROM ('2026-04-01') TO ('2026-05-01'); CREATE TABLE follow_logs_2026_05 PARTITION OF follow_logs FOR VALUES FROM ('2026-05-01') TO ('2026-06-01'); -- 默认分区:兜底,防止超出已建分区范围导致写入失败 CREATE TABLE follow_logs_default PARTITION OF follow_logs DEFAULT; -- 时间线展示(核心) CREATE INDEX idx_follow_logs_property_time ON follow_logs(property_id, created_at DESC) WHERE deleted_at IS NULL; -- 6个 Tab 过滤 CREATE INDEX idx_follow_logs_type ON follow_logs(property_id, log_type, created_at DESC) WHERE deleted_at IS NULL; -- 操作员过滤 CREATE INDEX idx_follow_logs_operator ON follow_logs(operator_id, created_at DESC) WHERE deleted_at IS NULL; -- 合规审计(敏感类型专用) CREATE INDEX idx_follow_logs_sensitive ON follow_logs(property_id, created_at DESC) WHERE log_type IN ('sensitive_view','sensitive_op'); ``` --- ### 4.6 follow_log_attachments(跟进附件) ```sql CREATE TABLE follow_log_attachments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), follow_log_id UUID NOT NULL REFERENCES follow_logs(id) ON DELETE CASCADE, file_key TEXT NOT NULL, -- Cloudflare R2 存储路径 file_name VARCHAR(255) NOT NULL, file_size INTEGER NOT NULL, -- bytes file_type VARCHAR(10) CHECK (file_type IN ('bmp','jpg','png','svg','gif')), sort_order SMALLINT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_follow_attachments_log ON follow_log_attachments(follow_log_id); ``` --- ### 4.7 follow_log_recordings(跟进录音) ```sql CREATE TABLE follow_log_recordings ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), follow_log_id UUID NOT NULL REFERENCES follow_logs(id) ON DELETE CASCADE, file_key TEXT NOT NULL, -- Cloudflare R2 存储路径 duration_seconds INTEGER, -- 录音时长(秒) created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_follow_recordings_log ON follow_log_recordings(follow_log_id); ``` --- ### 4.8 property_keys(钥匙管理) ```sql CREATE TABLE property_keys ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), property_id UUID NOT NULL REFERENCES properties(id) ON DELETE CASCADE, key_type VARCHAR(20) NOT NULL CHECK (key_type IN ('mechanical','password')), -- 钥匙持有方 holder_id UUID REFERENCES staff(id) ON DELETE SET NULL, holder_snapshot JSONB, -- {name, store_group} storage_unit_id UUID REFERENCES org_units(id) ON DELETE SET NULL, -- 保管部门 -- 他司钥匙标记 is_other_agency BOOLEAN NOT NULL DEFAULT FALSE, other_agency_info VARCHAR(30), -- 最多30字 remarks TEXT, -- 最多200字 is_active BOOLEAN NOT NULL DEFAULT TRUE, -- FALSE=已归还/失效 created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by UUID REFERENCES staff(id) ON DELETE SET NULL ); CREATE INDEX idx_property_keys_property ON property_keys(property_id) WHERE is_active = TRUE; ``` --- ### 4.9 key_attachments(钥匙附件) ```sql CREATE TABLE key_attachments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), key_id UUID NOT NULL REFERENCES property_keys(id) ON DELETE CASCADE, file_key TEXT NOT NULL, file_name VARCHAR(255) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_key_attachments_key ON key_attachments(key_id); ``` --- ### 4.10 commissions(委托管理) ```sql CREATE TABLE commissions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), property_id UUID NOT NULL REFERENCES properties(id) ON DELETE CASCADE, commission_type VARCHAR(50) NOT NULL, -- 独家委托/非独家委托(lookup_items 维护) period_start DATE NOT NULL, period_end DATE, is_open_ended BOOLEAN NOT NULL DEFAULT FALSE, -- 无固定结束日期 -- 委托经纪人 agent_id UUID REFERENCES staff(id) ON DELETE SET NULL, agent_snapshot JSONB, -- {name, store_group} signing_method VARCHAR(50), -- 签约方式(选择后动态展示委托书模板) -- 委托人(产权人)信息 owner_type VARCHAR(20) NOT NULL DEFAULT 'owner' CHECK (owner_type IN ('owner','authorized_third')), property_owner_contact_id UUID REFERENCES property_contacts(id) ON DELETE SET NULL, owner_name VARCHAR(50), owner_id_type VARCHAR(20), -- 身份证/护照 等 owner_id_number VARCHAR(50), -- 证件号(明文,仅供参考) owner_id_number_enc BYTEA, -- 证件号 AES-256-GCM 加密 remarks TEXT, -- 最多200字 status VARCHAR(20) NOT NULL DEFAULT 'active' CHECK (status IN ('active','expired','cancelled')), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by UUID REFERENCES staff(id) ON DELETE SET NULL ); CREATE INDEX idx_commissions_property ON commissions(property_id); CREATE INDEX idx_commissions_active ON commissions(property_id) WHERE status = 'active'; ``` --- ### 4.11 commission_attachments(委托附件) ```sql CREATE TABLE commission_attachments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), commission_id UUID NOT NULL REFERENCES commissions(id) ON DELETE CASCADE, category VARCHAR(20) NOT NULL CHECK (category IN ('id_card','property_cert', 'commission_letter','other')), file_key TEXT NOT NULL, file_name VARCHAR(255) NOT NULL, file_size INTEGER, sort_order SMALLINT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_commission_attachments_commission ON commission_attachments(commission_id); ``` --- ### 4.12 field_surveys(实勘管理) ```sql CREATE TABLE field_surveys ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), property_id UUID NOT NULL REFERENCES properties(id) ON DELETE CASCADE, status VARCHAR(10) NOT NULL DEFAULT 'draft' CHECK (status IN ('draft','submitted')), -- GPS 定位(实勘打卡) gps_latitude NUMERIC(10,7), gps_longitude NUMERIC(10,7), gps_accuracy NUMERIC(6,2), -- 精度(米) description TEXT, -- 实勘说明,最多200字 submitted_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by UUID NOT NULL REFERENCES staff(id) ON DELETE RESTRICT ); CREATE INDEX idx_field_surveys_property ON field_surveys(property_id); CREATE INDEX idx_field_surveys_submitted ON field_surveys(property_id) WHERE status = 'submitted'; ``` --- ### 4.13 survey_photos(实勘照片) ```sql CREATE TABLE survey_photos ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), survey_id UUID NOT NULL REFERENCES field_surveys(id) ON DELETE CASCADE, category VARCHAR(20) NOT NULL CHECK (category IN ('layout','living_room','dining_room', 'bedroom','bathroom','kitchen', 'entrance','balcony','study', 'indoor_other','outdoor')), file_key TEXT NOT NULL, -- Cloudflare R2 路径 thumbnail_key TEXT, -- 缩略图路径 file_size INTEGER, width INTEGER, height INTEGER, sort_order SMALLINT NOT NULL DEFAULT 0, is_vr_screenshot BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_survey_photos_survey ON survey_photos(survey_id); CREATE INDEX idx_survey_photos_category ON survey_photos(survey_id, category); ``` --- ### 4.14 property_photos(房源图片) ```sql -- ============================================================ -- 房源图片:经纪人自主上传和管理,与实勘照片分离存储 -- 封面限1张(唯一约束),全景类型单独处理 -- ============================================================ CREATE TABLE property_photos ( id UUID NOT NULL DEFAULT gen_random_uuid(), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 分区键 property_id UUID NOT NULL REFERENCES properties(id) ON DELETE CASCADE, category VARCHAR(20) NOT NULL CHECK (category IN ('cover','entrance','living_room', 'dining_room','bedroom','bathroom', 'kitchen','balcony','study', 'indoor_other','outdoor','panorama')), file_key TEXT NOT NULL, -- R2/S3 原图路径 thumbnail_key TEXT, -- Cloudflare Images 生成的缩略图 file_name VARCHAR(255), file_size INTEGER, width INTEGER, height INTEGER, is_cover BOOLEAN NOT NULL DEFAULT FALSE, sort_order SMALLINT NOT NULL DEFAULT 0, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by UUID REFERENCES staff(id) ON DELETE SET NULL, PRIMARY KEY (id, created_at) -- 分区表主键必须包含分区键 ) PARTITION BY RANGE (created_at); CREATE TABLE property_photos_2026_04 PARTITION OF property_photos FOR VALUES FROM ('2026-04-01') TO ('2026-05-01'); CREATE TABLE property_photos_2026_05 PARTITION OF property_photos FOR VALUES FROM ('2026-05-01') TO ('2026-06-01'); CREATE TABLE property_photos_default PARTITION OF property_photos DEFAULT; CREATE INDEX idx_property_photos_property ON property_photos(property_id); CREATE INDEX idx_property_photos_cover ON property_photos(property_id) WHERE is_cover = TRUE; CREATE INDEX idx_property_photos_category ON property_photos(property_id, category); -- 每套房源只能有一张封面 CREATE UNIQUE INDEX idx_property_photos_unique_cover ON property_photos(property_id) WHERE is_cover = TRUE; ``` --- ### 4.15 property_attachments(房源附件) ```sql CREATE TABLE property_attachments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), property_id UUID NOT NULL REFERENCES properties(id) ON DELETE CASCADE, category VARCHAR(20) NOT NULL DEFAULT 'other' CHECK (category IN ('id_card','property_cert', 'commission_letter','other')), file_key TEXT NOT NULL, file_name VARCHAR(255) NOT NULL, file_size INTEGER NOT NULL, file_type VARCHAR(50), -- MIME type sort_order SMALLINT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_by UUID REFERENCES staff(id) ON DELETE SET NULL ); CREATE INDEX idx_property_attachments_property ON property_attachments(property_id); CREATE INDEX idx_property_attachments_category ON property_attachments(property_id, category); ``` --- ### 4.16 property_marketing(营销信息) ```sql -- 1:1 扩展表,营销标题/核心卖点/业主心态/户型介绍/小区介绍 CREATE TABLE property_marketing ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), property_id UUID NOT NULL UNIQUE REFERENCES properties(id) ON DELETE CASCADE, marketing_title VARCHAR(30), -- 营销标题,0-30字 core_selling_points TEXT, -- 核心卖点,最多200字 owner_attitude TEXT, -- 业主心态,最多200字 layout_description TEXT, -- 户型介绍,最多200字 complex_description TEXT, -- 小区介绍,最多200字 -- AI 生成标记 ai_generated_points BOOLEAN NOT NULL DEFAULT FALSE, ai_generated_attitude BOOLEAN NOT NULL DEFAULT FALSE, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_by UUID REFERENCES staff(id) ON DELETE SET NULL ); ``` --- ### 4.17 property_certificates(产证信息) ```sql -- 1:1 扩展表,存储产权证相关信息 CREATE TABLE property_certificates ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), property_id UUID NOT NULL UNIQUE REFERENCES properties(id) ON DELETE CASCADE, owner_name VARCHAR(100), owner_id_number VARCHAR(50), -- 身份证号/统一社会信用代码 owner_cert_type VARCHAR(20), -- 身份证/护照/营业执照 property_location VARCHAR(500), -- 房屋坐落(产权证书上的地址),最多50字 cert_status VARCHAR(30), -- 产证状态 cert_no VARCHAR(100), -- 产证号 first_registered_at DATE, -- 首次登记时间 ownership_nature VARCHAR(30), -- 权属性质 land_nature VARCHAR(30), -- 土地性质 updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_by UUID REFERENCES staff(id) ON DELETE SET NULL ); ``` --- ### 4.18 property_completeness(维护完成度) ```sql -- ============================================================ -- 维护完成度快照(1:1) -- 各维度得分由 Celery 任务异步计算后写入,非实时 -- properties.completeness_score 为冗余总分,供列表排序用 -- ============================================================ CREATE TABLE property_completeness ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), property_id UUID NOT NULL UNIQUE REFERENCES properties(id) ON DELETE CASCADE, score_core_info SMALLINT NOT NULL DEFAULT 0, -- 重点信息 满分8 score_attachment SMALLINT NOT NULL DEFAULT 0, -- 附件 满分8 score_survey SMALLINT NOT NULL DEFAULT 0, -- 实勘 满分16 score_vr SMALLINT NOT NULL DEFAULT 0, -- VR 满分8 score_key SMALLINT NOT NULL DEFAULT 0, -- 钥匙 满分10 score_commission SMALLINT NOT NULL DEFAULT 0, -- 委托 满分10 score_verification SMALLINT NOT NULL DEFAULT 0, -- 验证 满分7 score_follow_up SMALLINT NOT NULL DEFAULT 0, -- 跟进 满分8 score_viewing SMALLINT NOT NULL DEFAULT 0, -- 带看 满分8 score_other SMALLINT NOT NULL DEFAULT 0, -- 其他 满分7 total_score SMALLINT NOT NULL DEFAULT 0, -- 总分 0-100 calculated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); ``` --- ### 4.19 property_tags / property_tag_relations(标签) ```sql -- 标签字典(系统预置 + 运营自定义) CREATE TABLE property_tags ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(50) NOT NULL, color VARCHAR(7), -- HEX 颜色,如 #FF5733 is_system BOOLEAN NOT NULL DEFAULT FALSE, -- 系统预置标签不可删除 sort_order INTEGER NOT NULL DEFAULT 0, is_active BOOLEAN NOT NULL DEFAULT TRUE ); -- 房源 ↔ 标签 多对多 CREATE TABLE property_tag_relations ( property_id UUID NOT NULL REFERENCES properties(id) ON DELETE CASCADE, tag_id UUID NOT NULL REFERENCES property_tags(id) ON DELETE CASCADE, PRIMARY KEY (property_id, tag_id) ); CREATE INDEX idx_property_tags_property ON property_tag_relations(property_id); CREATE INDEX idx_property_tags_tag ON property_tag_relations(tag_id); ``` --- ### 4.20 property_favorites(收藏) ```sql -- 经纪人收藏房源(快速访问) CREATE TABLE property_favorites ( staff_id UUID NOT NULL REFERENCES staff(id) ON DELETE CASCADE, property_id UUID NOT NULL REFERENCES properties(id) ON DELETE CASCADE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (staff_id, property_id) ); CREATE INDEX idx_property_favorites_staff ON property_favorites(staff_id); ``` --- ### 4.21 property_protections(保护房) ```sql -- 1:1,标记某套房源是否受保护(防止被他人抢单/公盘化) CREATE TABLE property_protections ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), property_id UUID NOT NULL UNIQUE REFERENCES properties(id) ON DELETE CASCADE, is_protected BOOLEAN NOT NULL DEFAULT FALSE, reason TEXT, start_at TIMESTAMPTZ, end_at TIMESTAMPTZ, set_by UUID REFERENCES staff(id) ON DELETE SET NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); ``` --- ### 4.22 number_holder_approvals(号码方审批) ```sql -- 号码方变更审批流:经纪人申请,上级审批 CREATE TABLE number_holder_approvals ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), property_id UUID NOT NULL REFERENCES properties(id) ON DELETE CASCADE, contact_id UUID NOT NULL REFERENCES property_contacts(id) ON DELETE CASCADE, applicant_id UUID NOT NULL REFERENCES staff(id) ON DELETE RESTRICT, approver_id UUID REFERENCES staff(id) ON DELETE SET NULL, status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','approved','rejected')), remarks TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), decided_at TIMESTAMPTZ ); CREATE INDEX idx_number_holder_approvals_status ON number_holder_approvals(status) WHERE status = 'pending'; CREATE INDEX idx_number_holder_approvals_property ON number_holder_approvals(property_id); ``` --- ## 5 触发器 ### 5.1 房源全文搜索向量自动维护 ```sql CREATE OR REPLACE FUNCTION update_property_search_vector() RETURNS TRIGGER AS $$ BEGIN NEW.search_vector := setweight(to_tsvector('simple', COALESCE(NEW.block_no, '') || ' ' || COALESCE(NEW.unit_no, '') || ' ' || COALESCE(NEW.room_no, '')), 'A') || setweight(to_tsvector('simple', COALESCE(NEW.remarks, '')), 'C'); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_property_search_vector BEFORE INSERT OR UPDATE OF block_no, unit_no, room_no, remarks ON properties FOR EACH ROW EXECUTE FUNCTION update_property_search_vector(); ``` ### 5.2 last_followed_at 自动维护 ```sql -- 写入跟进日志时,自动更新 properties.last_followed_at(加速超时检测排序) CREATE OR REPLACE FUNCTION update_property_last_followed() RETURNS TRIGGER AS $$ BEGIN IF NEW.log_type = 'written' THEN UPDATE properties SET last_followed_at = NEW.created_at, updated_at = NOW() WHERE id = NEW.property_id; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_update_last_followed AFTER INSERT ON follow_logs FOR EACH ROW EXECUTE FUNCTION update_property_last_followed(); ``` --- ## 6 查询模式参考 ### 6.1 房源列表页(高频) ```sql -- 出售公盘,按挂牌时间倒序 SELECT p.id, p.status, p.attribute, p.sale_price, p.area, p.bedroom_count, p.floor, p.total_floors, p.completeness_score, p.listed_at FROM properties p WHERE p.status = 'for_sale' AND p.attribute = 'public' AND p.deleted_at IS NULL ORDER BY p.listed_at DESC NULLS LAST LIMIT 20 OFFSET 0; -- 命中索引:idx_properties_list_default ``` ### 6.2 区域筛选(楼盘 + 商圈下钻) ```sql -- 某商圈下所有出售房源 SELECT p.* FROM properties p JOIN complexes c ON c.id = p.complex_id JOIN complex_business_areas cba ON cba.complex_id = c.id WHERE cba.business_area_id = :business_area_id AND p.status IN ('for_sale','for_sale_rent') AND p.deleted_at IS NULL ORDER BY p.listed_at DESC NULLS LAST; -- 命中索引:idx_properties_complex + complex_business_areas 索引 ``` ### 6.3 与我相关(经纪人仪表板) ```sql SELECT * FROM properties WHERE seller_agent_id = :my_staff_id AND status NOT IN ('sold','unlisted') AND deleted_at IS NULL ORDER BY listed_at DESC NULLS LAST; -- 命中索引:idx_properties_my_properties ``` ### 6.4 重复房源检测(录入前必查) ```sql -- 通过联系人手机号哈希检测 SELECT pc.property_id, p.status, p.deleted_at FROM property_contacts pc JOIN properties p ON p.id = pc.property_id WHERE pc.phone_hash = SHA256(:input_phone) AND pc.deleted_at IS NULL; -- 命中索引:idx_contacts_phone_hash ``` ### 6.5 跟进日志时间线(详情页) ```sql -- 房源详情页跟进 Tab(全部类型,时间倒序) SELECT fl.*, fla.file_key, fla.file_name FROM follow_logs fl LEFT JOIN follow_log_attachments fla ON fla.follow_log_id = fl.id WHERE fl.property_id = :property_id AND fl.deleted_at IS NULL ORDER BY fl.created_at DESC; -- 命中索引:idx_follow_logs_property_time ``` --- ## 7 禁止操作 | 操作 | 影响表 | 原因 | |------|--------|------| | `DELETE FROM listing_histories` | `listing_histories` | 挂牌历史不可删除,合规审计要求 | | `DELETE FROM price_changes` | `price_changes` | 调价记录不可删除 | | `UPDATE follow_logs SET deleted_at = NOW() WHERE is_deletable = FALSE` | `follow_logs` | 敏感信息查看类型日志不可删除 | | 直接修改 `properties.completeness_score` | `properties` | 完成度分数只由 Celery 任务计算更新,禁止 Application 层直接写入 | | 直接修改 `properties.last_followed_at` | `properties` | 由触发器 `trg_update_last_followed` 自动维护 | | 删除 `property_tags WHERE is_system = TRUE` | `property_tags` | 系统预置标签不可删除 | | 明文存储手机号 | `property_contacts` | 手机号必须 AES-256-GCM 加密后存 `phone_enc`,同时更新 `phone_hash` | --- *DATA_MODEL_PROPERTY.md — Fonrey 房产经纪管理系统房源模块数据模型 v1.0* *下一步:API 接口规范(房源模块 URL 设计 + Request/Response Schema)*