diff --git a/Project/fonrey/DATA_MODEL/DATA_MODEL.md b/Project/fonrey/DATA_MODEL/DATA_MODEL.md
index 35e324a6..d8ac29c0 100644
--- a/Project/fonrey/DATA_MODEL/DATA_MODEL.md
+++ b/Project/fonrey/DATA_MODEL/DATA_MODEL.md
@@ -87,7 +87,7 @@
| **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 | 系统核心表,每套二手房源的完整档案,支持出售/出租/出售兼出租三态 |
+| **Property(房源)** | `properties` → [DATA_MODEL_PROPERTY.md](./DATA_MODEL_PROPERTY.md) | 系统核心表,每套二手房源的完整档案,支持出售/出租/出售兼出租三态 |
| **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) | 系统/人工推荐的客源↔房源配对 |
@@ -117,7 +117,7 @@ OrgUnit (组织架构)
| [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 张表)、系统设置 | ✅ 完成 |
+| [DATA_MODEL_PROPERTY.md](./DATA_MODEL_PROPERTY.md) | 房源管理(properties 及配套 22 张表,含跟进/钥匙/委托/实勘/营销/产证/完成度等) | ✅ 完成 |
---
@@ -220,765 +220,46 @@ CREATE INDEX idx_domains_primary ON public.domains(tenant_id) WHERE is_primary =
---
-### 3.3 房源核心模块(Property Core)
-
-```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')),
- -- 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')),
- -- 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')),
- -- 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')),
- -- 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(5)
- CHECK (grade IN ('A_urgent','A','B','C','D')),
- -- A_urgent=A(急迫), A=A, B=B(较强), C=C(一般), D=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')),
- -- 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字)
-
- -- ── 相关方(冗余存储 UUID,完整信息查 staff 表)──
- 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), -- 房源来源渠道(由运营维护枚举)
-
- -- ── 维护完成度(冗余缓存,定期重算)──
- 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
-);
-
--- ── 索引策略(针对高频查询路径设计)──
-
--- 1. 最核心的列表页:按状态 + 属性 + 类型过滤
-CREATE INDEX idx_properties_status_attr ON properties(status, attribute, property_type)
- WHERE deleted_at IS NULL;
-
--- 2. 区域筛选(通过 complex 表 JOIN 优化)
-CREATE INDEX idx_properties_complex ON properties(complex_id)
- WHERE deleted_at IS NULL;
-
--- 3. 价格排序(出售最常用)
-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');
-
--- 4. 面积区间筛选
-CREATE INDEX idx_properties_area ON properties(area)
- WHERE deleted_at IS NULL;
-
--- 5. 挂牌日期倒序(最新挂牌)
-CREATE INDEX idx_properties_listed_at ON properties(listed_at DESC NULLS LAST)
- WHERE deleted_at IS NULL;
-
--- 6. 最后跟进日期(超时未跟进功能)
-CREATE INDEX idx_properties_last_followed ON properties(last_followed_at DESC NULLS LAST)
- WHERE deleted_at IS NULL;
-
--- 7. 户型筛选
-CREATE INDEX idx_properties_bedroom ON properties(bedroom_count)
- WHERE deleted_at IS NULL;
-
--- 8. 等级筛选
-CREATE INDEX idx_properties_grade ON properties(grade)
- WHERE deleted_at IS NULL;
-
--- 9. 完成度排序(引导补全信息)
-CREATE INDEX idx_properties_completeness ON properties(completeness_score)
- WHERE deleted_at IS NULL;
-
--- 10. 全文搜索
-CREATE INDEX idx_properties_search ON properties USING gin(search_vector);
-
--- 11. 与我相关(相关方快速定位)
-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;
-
--- 12. 复合索引:列表默认排序(状态 + 挂牌时间)
-CREATE INDEX idx_properties_list_default ON properties(status, listed_at DESC NULLS LAST)
- WHERE deleted_at IS NULL;
-```
-
----
-
-### 3.4 房源联系人(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;
-```
-
----
-
-### 3.5 挂牌历史(Listing History)
-
-```sql
--- ============================================================
--- 挂牌历史:记录房源每次上架的完整快照
--- 设计重点:不可删除(合规),仅追加
--- ============================================================
-
-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, -- 存储经纪人姓名+门店(防止变更后丢失)
-
- 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';
-```
-
----
-
-### 3.6 调价记录(Price Change Log)
-
-```sql
--- ============================================================
--- 调价记录:支持折线图展示,不可删除
--- ============================================================
-
-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
-);
-
-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);
-```
-
----
-
-### 3.7 跟进日志(Follow-up Logs)
-
-```sql
--- ============================================================
--- 跟进日志:系统最高写入频率的表,按 property_id 分区预留
--- 6 种类型:写入跟进/修改跟进/敏感信息跟进/敏感信息查看/其他跟进/系统日志
--- ============================================================
-
-CREATE TABLE follow_logs (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- 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')),
- -- written=写入跟进(经纪人主动写)
- -- modified=修改跟进(字段变更自动生成)
- -- sensitive_op=敏感信息跟进(相关方保护变更)
- -- sensitive_view=敏感信息查看(查看号码等)
- -- other=其他跟进(钥匙/新增联系人等)
- -- system=系统日志
-
- -- 写入跟进专用字段
- purpose VARCHAR(50), -- 跟进目的(由运营维护枚举值)
- content TEXT, -- 跟进内容,最少6字最多500字
- 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": "售价"}
- -- 支持多字段同时变更
-
- -- 系统标签(显示在日志时间线上的 tag)
- 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}
-
- -- 是否可删除(敏感信息查看类型 = FALSE)
- is_deletable BOOLEAN NOT NULL DEFAULT TRUE,
-
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
- deleted_at TIMESTAMPTZ -- 仅 is_deletable=TRUE 时才能软删
-);
-
--- 核心索引:时间线展示
-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');
-
--- 跟进日志附件(一条跟进可附多张图)
-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, -- R2/S3 存储路径
- 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);
-
--- 跟进录音(独立存储,支持音频文件)
-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,
- duration_seconds INTEGER,
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
-);
-```
-
----
-
-### 3.8 钥匙管理(Key Management)
-
-```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;
-
--- 钥匙附件
-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()
-);
-```
-
----
-
-### 3.9 委托管理(Commission Management)
-
-```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, -- 独家委托/非独家委托(运营维护枚举)
- 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,
-
- -- 签约方式(选择后动态展示委托书模板)
- signing_method VARCHAR(50),
-
- -- 委托人(产权人)信息
- owner_type VARCHAR(20) NOT NULL DEFAULT 'owner'
- CHECK (owner_type IN ('owner','authorized_third')),
- -- 从 property_contacts 中选择
- 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,
-
- 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';
-
--- 委托附件(身份证/房产证/委托书 等)
-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);
-```
-
----
-
-### 3.10 实勘管理(Field Survey)
-
-```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';
-
--- 实勘照片(按空间分类)
-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, -- R2/S3 路径
- 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);
-```
-
----
-
-### 3.11 房源图片管理(Property Photos)
-
-```sql
--- ============================================================
--- 房源图片:与实勘照片分离存储,经纪人自主上传和管理
--- 封面限1张,全景类型单独处理
--- ============================================================
-
-CREATE TABLE property_photos (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- 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,
-
- 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_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;
-```
-
----
-
-### 3.12 房源附件(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);
-```
-
----
-
-### 3.13 房源营销信息(Property Marketing)
-
-```sql
-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
-);
-```
-
----
-
-### 3.14 产证信息(Property Certificate)
-
-```sql
-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
-);
-```
-
----
-
-### 3.15 楼盘基本信息扩展(Complex Property Info)
-
-```sql
--- 补充:楼盘与房源通过 complex_id 关联,楼盘信息首次填写后修改需走楼盘管理系统
--- 楼盘价格走势(用于楼盘详情页展示)
-CREATE TABLE complex_price_trends (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- complex_id UUID NOT NULL REFERENCES complexes(id) ON DELETE CASCADE,
- record_month DATE NOT NULL, -- 月份(取该月1日存储)
- avg_sale_price NUMERIC(10,2), -- 月均售价(万元/套)
- avg_unit_price NUMERIC(10,2), -- 月均单价(元/m²)
- transaction_count INTEGER, -- 成交套数
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
-);
-
-CREATE UNIQUE INDEX idx_complex_price_trend_month
- ON complex_price_trends(complex_id, record_month);
-```
-
----
-
-### 3.16 维护完成度评分(Completeness Scoring)
-
-```sql
--- ============================================================
--- 维护完成度:不直接存完整计算明细(减少宽表),
--- 以触发器/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,
-
- -- 各维度得分(满分见 PRD 8.2)
- 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()
-);
-```
+### 3.3 房源模块(Property Management)
+
+> **详细模型** → 见 [`DATA_MODEL_PROPERTY.md`](./DATA_MODEL_PROPERTY.md)
+> 本节仅作概览,开发时以 DATA_MODEL_PROPERTY.md 为权威定义。
+
+**核心表概览**(开发时以 DATA_MODEL_PROPERTY.md 为准):
+
+| 表名 | 说明 | 关键字段 |
+|------|------|----------|
+| `properties` | 房源主表(系统核心,89,000+ 数据量) | `status`, `attribute`, `property_type`, `complex_id`, `sale_price`, `area`, `grade`, `completeness_score`, `search_vector` |
+| `property_contacts` | 业主/联系人(手机号 AES 加密+哈希索引) | `property_id`, `phone_enc`, `phone_hash`, `identity`, `is_number_holder` |
+| `listing_histories` | 挂牌历史快照(不可删除) | `property_id`, `listing_type`, `status`, `sale_price`, `seller_agent_snapshot` |
+| `price_changes` | 调价记录(不可删除) | `property_id`, `old_sale_price`, `new_sale_price`, `change_reason`, `changed_by` |
+| `follow_logs` | 跟进日志(6种类型,最高写入频率) | `property_id`, `log_type`, `content`, `is_deletable`, `operator_id` |
+| `follow_log_attachments` | 跟进附件(图片) | `follow_log_id`, `file_key`, `file_type` |
+| `follow_log_recordings` | 跟进录音 | `follow_log_id`, `file_key`, `duration_seconds` |
+| `property_keys` | 钥匙管理(机械钥匙/密码) | `property_id`, `key_type`, `holder_id`, `is_active` |
+| `key_attachments` | 钥匙附件 | `key_id`, `file_key` |
+| `commissions` | 委托管理(独家/非独家) | `property_id`, `commission_type`, `period_start`, `status` |
+| `commission_attachments` | 委托附件(身份证/产证/委托书) | `commission_id`, `category`, `file_key` |
+| `field_surveys` | 实勘管理(GPS 打卡) | `property_id`, `status`, `gps_latitude`, `gps_longitude`, `created_by` |
+| `survey_photos` | 实勘照片(按空间分类) | `survey_id`, `category`, `file_key`, `is_vr_screenshot` |
+| `property_photos` | 房源图片(经纪人管理,封面唯一约束) | `property_id`, `category`, `is_cover`, `file_key` |
+| `property_attachments` | 房源附件 | `property_id`, `category`, `file_key` |
+| `property_marketing` | 营销信息(1:1,卖点/业主心态/介绍) | `property_id`, `marketing_title`, `core_selling_points` |
+| `property_certificates` | 产证信息(1:1) | `property_id`, `cert_no`, `owner_name`, `land_nature` |
+| `property_completeness` | 维护完成度快照(1:1,Celery 异步计算) | `property_id`, `total_score`, `score_survey`, `score_commission`, ... |
+| `property_tags` | 标签字典(系统预置+运营自定义) | `name`, `color`, `is_system` |
+| `property_tag_relations` | 房源↔标签多对多 | `property_id`, `tag_id` |
+| `property_favorites` | 经纪人收藏房源 | `staff_id`, `property_id` |
+| `property_protections` | 保护房设置(1:1) | `property_id`, `is_protected`, `start_at`, `end_at` |
+| `number_holder_approvals` | 号码方变更审批 | `property_id`, `applicant_id`, `status` |
+
+**关键约束提示**:
+- `property_contacts.phone_hash` 是重复房源检测的主要依据,录入前必须查重
+- `listing_histories` / `price_changes` **无 deleted_at**,不可删除
+- `follow_logs` 中 `is_deletable=FALSE`(`sensitive_view` 类型)不可软删
+- `completeness_score` 只由 Celery 任务写入,Application 层禁止直接更新
+- `last_followed_at` 由触发器 `trg_update_last_followed` 自动维护
+- `property_photos.is_cover` 唯一约束:每套房源仅一张封面
---
diff --git a/Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md b/Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md
new file mode 100644
index 00000000..1d7ac7e0
--- /dev/null
+++ b/Project/fonrey/DATA_MODEL/DATA_MODEL_PROPERTY.md
@@ -0,0 +1,1168 @@
+# 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_urgent` | A(急迫) |
+| `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_urgent','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
+);
+
+-- ── 索引策略 ──
+
+-- 核心列表过滤: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 PRIMARY KEY DEFAULT gen_random_uuid(),
+ 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,
+
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ deleted_at TIMESTAMPTZ -- 仅 is_deletable=TRUE 时可软删
+);
+
+-- 时间线展示(核心)
+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 PRIMARY KEY DEFAULT gen_random_uuid(),
+ 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,
+
+ 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_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)*
diff --git a/Project/fonrey/DATA_MODEL/diagram/fonrey-er.drawio b/Project/fonrey/DATA_MODEL/diagram/fonrey-er.drawio
new file mode 100644
index 00000000..d259a57c
--- /dev/null
+++ b/Project/fonrey/DATA_MODEL/diagram/fonrey-er.drawio
@@ -0,0 +1,860 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Project/fonrey/DATA_MODEL/fonrey-data-model.xml b/Project/fonrey/DATA_MODEL/fonrey-data-model.xml
index 90f81918..221ada31 100644
--- a/Project/fonrey/DATA_MODEL/fonrey-data-model.xml
+++ b/Project/fonrey/DATA_MODEL/fonrey-data-model.xml
@@ -1,774 +1,368 @@
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/Project/fonrey/DATA_MODEL/fonrey-data-model.xml.bak b/Project/fonrey/DATA_MODEL/fonrey-data-model.xml.bak
new file mode 100644
index 00000000..64e2e512
--- /dev/null
+++ b/Project/fonrey/DATA_MODEL/fonrey-data-model.xml.bak
@@ -0,0 +1,774 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/wiki/index.md b/wiki/index.md
index 48dd659f..7c0d8cae 100644
--- a/wiki/index.md
+++ b/wiki/index.md
@@ -4,6 +4,7 @@
- [Overview](overview.md) — living synthesis
## Sources
+- [2026-04-24] [CTP Topic 56 Automated Infrastructure Testing](sources/ctp-topic-56-automated-infrastructure-testing.md)
- [2026-04-24] [Public Cloud Learning Sessions - Ollie Workflow and The Demand Process - 20240416](sources/public-cloud-learning-sessions-ollie-workflow-and-the-demand-process-20240416-16.md)
- [2026-04-24] [CTP Topic 33 An Introduction to GitOps](sources/ctp-topic-33-an-introduction-to-gitops.md)
- [2026-04-24] [CTP Topic 3 Deploy and maintain infrastructure](sources/ctp-topic-3-deploy-and-maintain-infrastructure.md)
@@ -413,7 +414,6 @@
- [2026-04-19] [public-cloud-learning-sessions-reducing-cloud-costs-20250318-170100-meeting-reco](sources/public-cloud-learning-sessions-reducing-cloud-costs-20250318-170100-meeting-reco.md) — (expected: wiki/sources/public-cloud-learning-sessions-reducing-cloud-costs-20250318-170100-meeting-reco.md — source missing)
- [2026-04-19] [ctp-topic-13-cloud-finops-micro-focus-policies-best-practices-to-optimize-the-co](sources/ctp-topic-13-cloud-finops-micro-focus-policies-best-practices-to-optimize-the-co.md) — (expected: wiki/sources/ctp-topic-13-cloud-finops-micro-focus-policies-best-practices-to-optimize-the-co.md — source missing)
- [2026-04-19] [ctp-topic-15-working-with-renovatebot](sources/ctp-topic-15-working-with-renovatebot.md) — (expected: wiki/sources/ctp-topic-15-working-with-renovatebot.md — source missing)
-- [2026-04-19] [ctp-topic-56-automated-infrastructure-testing](sources/ctp-topic-56-automated-infrastructure-testing.md) — (expected: wiki/sources/ctp-topic-56-automated-infrastructure-testing.md — source missing)
- [Your-AI-Isn-t-Stupid---It-Just-Needs-a-Better-Harness--Lychee-Technology-Engineering-Blog](sources/Your-AI-Isn-t-Stupid---It-Just-Needs-a-Better-Harness--Lychee-Technology-Engineering-Blog.md) — (expected: wiki/sources/Your-AI-Isn-t-Stupid---It-Just-Needs-a-Better-Harness--Lychee-Technology-Engineering-Blog.md — source missing)
- [Expose-hermes-agent-as-an-OpenAI-compatible-API-for-any-frontend](sources/Expose-hermes-agent-as-an-OpenAI-compatible-API-for-any-frontend.md) — (expected: wiki/sources/Expose-hermes-agent-as-an-OpenAI-compatible-API-for-any-frontend.md — source missing)
- [zk-steward](sources/zk-steward.md) — (expected: wiki/sources/zk-steward.md — source missing)
diff --git a/wiki/log.md b/wiki/log.md
index c95ccaf9..82666c90 100644
--- a/wiki/log.md
+++ b/wiki/log.md
@@ -2389,3 +2389,15 @@
- Key Entities 中提及的 Victor Etkin 仅出现 1 次,不满足 ≥2 次条件,以 wikilink 形式记录于 Source page
- Key Concepts 中 Kubernetes/Atlantis 已有 wikilink 指向其他 Source page
- 冲突检测:与 ctp-topic-39(Atlantis 不支持 EKS)存在 Atlantis + Kubernetes 实践约束差异,已记录于 Source page Contradictions
+
+## [2026-04-24] ingest | CTP Topic 56 Automated Infrastructure Testing
+- Source file: Cloud & DevOps/Public-Cloud-Learning-Sessions/06_CI_CD_GitOps/ctp-topic-56-automated-infrastructure-testing.md
+- Status: ✅ 成功摄入
+- Summary: Mark Francis 主讲自动化基础设施测试,倡导将 TerraTest(Golang 框架)应用于 Terraform IaC 的 apply → test → destroy 自动化验证循环;核心主张集成测试超越语法检查,TDD 应用于 IaC 领域,测试作为首要开发步骤;价值观:"让机器做重复的事,把人脑留给复杂的人类问题"
+- Concepts identified: [[Infrastructure Testing]], [[TerraTest]], [[Test-Driven Development (TDD)]], [[IaC Testing Framework]]
+- Source page: wiki/sources/ctp-topic-56-automated-infrastructure-testing.md
+- Notes:
+ - index.md 更新:新增条目于 CTP Topic 33 (GitOps) 之后
+ - overview.md 更新:新增条目于 Cloud Transformation & DevOps 章节,GitOps 和 CI/CD Pipeline 质量保障层
+ - Key Entities 中 Mark Francis 仅出现 1 次,以 wikilink 形式记录于 Source page
+ - 冲突检测:待发现相关冲突内容,Contradictions 暂置空占位
diff --git a/wiki/overview.md b/wiki/overview.md
index 34f067a9..990065a0 100644
--- a/wiki/overview.md
+++ b/wiki/overview.md
@@ -49,7 +49,9 @@ Cloud Transformation Programme (CTP) materials cover AWS landing zones, EKS, Ter
**[[ctp-topic-33-an-introduction-to-gitops]]**(CTP Topic 33):Victor Etkin 讲解 GitOps 方法论入门——GitOps 将软件开发原则应用于部署流程,解决部署失败和配置不一致问题。四大原则:声明式配置、版本控制、CD 流程分离、自修复协调器;核心工具仅需 Git。GitOps Controller 持续比对 Git 声明的期望状态与系统实际状态,自动调和偏差。Pull 模型(代理同时监控 Git 和目标系统)比 Push 模型安全性更高,是 GitOps 推荐模式。CI 专注代码构建和分析,CD 专注二进制部署,两者解耦增强安全性。幂等平台(如 Kubernetes)是 CD 流程顺利运行的必要条件。Git 提交日志天然构成合规审计追踪。属 [[GitOps]] 概念层核心来源,与 [[ctp-topic-32-using-atlantis-cicd-for-infrastructure-deployments]](Atlantis 工具)和 [[ctp-topic-2-git]](Git 基础)共同构成 CI/CD/GitOps 完整知识链路。
-**[[ctp-topic-21-supply-chain-security-in-micro-focus]]**(CTP Topic 21):Micro Focus 产品安全小组 Shlomi Ben-Hur 主讲的软件供应链安全新方法——核心议题:在云转型背景下,软件供应链安全已成为企业安全战略的重中之重。供应链(产品层面)涵盖源码管理(SCM)、构建组件(CI)、制品库到最终交付系统(CD)的所有环节,Micro Focus 内部存在 17 种不同 SCM 工具的极高多样性。主要驱动因素:SolarWinds 攻击事件(通过渗透构建过程注入恶意代码)、美国网络安全行政命令、以及向 AWS/SaaS 迁移带来的开放性风险。核心转变:从过去 99% 关注研发安全(代码扫描/渗透测试)转向全生命周期安全防护;供应链安全成为 SDL(安全开发生命周期)的第五大支柱,强调必须同时确保 CI 过程(构建环境/自动化服务器)和 CD 过程(交付系统)的完整性,防止黑客在任何环节篡改二进制文件。属 [[Supply Chain Security(供应链安全)]] 在 [[Micro Focus]] 云转型场景的核心实践,与 [[DevSecOps]](开发安全运维一体化)高度关联。
+**[[ctp-topic-56-automated-infrastructure-testing]]**(CTP Topic 56):Mark Francis 主讲自动化基础设施测试——将软件测试原则应用于 Terraform IaC 代码,通过 TerraTest(Golang 框架)实现 apply → test → destroy 自动化验证循环。核心主张:集成测试超越语法检查,验证实际部署行为是否符合预期;倡导测试驱动开发(TDD)应用于 IaC 领域,先写测试再实现功能;提议将测试编写作为基础设施开发的首要步骤,移除手动验证,追求自动化验证套件和更高的部署信心。核心价值观:"让机器做重复的事,把人脑留给复杂的人类问题"。属 [[GitOps]] 和 [[CI/CD Pipeline]] 的质量保障层,与 [[ctp-topic-33-an-introduction-to-gitops]](GitOps 概念)和 [[ctp-topic-32-using-atlantis-cicd-for-infrastructure-deployments]](Atlantis 工具)共同构成完整的 IaC 质量保障链路。
+
+**[[ctp-topic-21-supply-chain-security-in-micro-focus]]**(CTP Topic 21):
**[[ctp-topic-24-micro-focus-product-privacy-framework]]**(CTP Topic 24):Micro Focus 产品隐私框架在云转型中的应用——PSAC(产品安全顾问委员会)与法律顾问合作,将 GDPR/CCPA 等晦涩法律条款翻译为约 110 项低级别技术要求;隐私框架是 STLC(安全开发生命周期)中 13 个安全与隐私轨道之一;通过五类需求(架构类/文档类/法律类/实现类/SAS 运营类)和成熟度模型(0-4 级)评估产品隐私合规状态;通过"蜘蛛图"直观展示产品在安全去标识化、被遗忘权、数据可移植性等 KPI 上的合规现状;最终产出标准化《产品隐私设置文档》,确保客户获得一致的隐私信息参考。属 [[Product Privacy Framework(产品隐私框架)]] 在 [[Micro Focus]] 云转型场景的核心实践,与 [[Micro Focus Security Development Life Cycle (STLC) Overview]](STLC 整体架构)直接关联。
diff --git a/wiki/sources/ctp-topic-56-automated-infrastructure-testing.md b/wiki/sources/ctp-topic-56-automated-infrastructure-testing.md
new file mode 100644
index 00000000..443f2d06
--- /dev/null
+++ b/wiki/sources/ctp-topic-56-automated-infrastructure-testing.md
@@ -0,0 +1,58 @@
+---
+title: "CTP Topic 56 Automated Infrastructure Testing"
+type: source
+tags:
+ - Testing
+ - IaC
+ - Automation
+ - CTP
+ - Terraform
+ - TerraTest
+ - TDD
+sources: []
+last_updated: 2026-04-14
+---
+
+## Source File
+- [[Cloud & DevOps/Public-Cloud-Learning-Sessions/06_CI_CD_GitOps/ctp-topic-56-automated-infrastructure-testing.md]]
+
+## Summary(用中文描述)
+- 核心主题:自动化基础设施测试——将软件测试原则应用于 Terraform IaC 代码,通过 TerraTest 框架实现基础设施的 Apply → Test → Destroy 自动化验证循环。
+- 问题域:传统 Terraform 验证仅做语法检查,无法验证实际部署后的行为是否符合预期;手动测试耗时且不可重复;缺乏测试的基础设施代码变更信心不足。
+- 方法/机制:
+ - TerraTest(Golang 库):自动执行 apply → test → destroy 生命周期,输出结构化测试结果
+ - 测试驱动开发(TDD):先写测试,再实现功能,确保测试先行且全面覆盖
+ - 提议的新工作流:将测试编写作为基础设施开发的首要步骤,移除手动验证环节
+- 结论/价值:自动化测试虽然前期投入时间,但长期回报是减少 Bug、提升部署信心、积累可重复的测试套件;"让机器做重复的事,把人脑留给复杂的人类问题"
+
+## Key Claims(用中文描述)
+- 集成测试对于验证已部署基础设施的功能至关重要,超越了语法检查,确保实际部署与预期相符。
+- TerraTest 通过自动化 apply-test-destroy 循环简化了测试流程,降低了基础设施测试的门槛。
+- 测试驱动开发(TDD)在基础设施即代码领域的应用:先写测试,再实现功能,聚焦开发并积累全面测试套件。
+- 提议的工作流将测试编写作为核心步骤,移除手动验证,追求自动化验证套件和更高的部署信心。
+- 长期收益(减少 Bug、提升信心)远超前期投入困难;测试应被视为一等公民。
+
+## Key Quotes
+> "I think the bottom quote, just I think let's leave the repetitive things for the computers to do and use our brains for the complex human things."
+> — Mark Francis,核心价值观:重复性工作交给机器,人脑专注于复杂的人类问题
+
+> "I'm just extending the value of putting stuff as code."
+> — Mark Francis,将测试代码化的价值延伸
+
+## Key Concepts
+- [[Infrastructure Testing(基础设施测试)]]:对 Terraform 等 IaC 工具部署的实际基础设施资源进行验证,而非仅检查语法或计划输出
+- [[TerraTest]]:HashiCorp 官方出品的 Golang 基础设施测试框架,支持 apply-test-destroy 自动化循环
+- [[Test-Driven Development(TDD)]]:先写测试用例,再实现功能,确保测试覆盖全面且聚焦开发过程
+- [[IaC Testing Framework]]:专门针对基础设施即代码的测试工具链,包括语法检查、计划验证、集成测试等多个层次
+
+## Key Entities
+- [[Mark Francis]]:CTP Topic 56 讲师,主讲自动化基础设施测试实践
+
+## Connections
+- [[ctp-topic-33-an-introduction-to-gitops]] ← extends ← [[ctp-topic-56-automated-infrastructure-testing.md]]
+- [[ctp-topic-9-ci-cd-with-gruntwork]] ← depends_on ← [[ctp-topic-56-automated-infrastructure-testing.md]]
+- [[ctp-topic-3-deploy-and-maintain-infrastructure]] ← extends ← [[ctp-topic-56-automated-infrastructure-testing.md]]
+- [[ctp-topic-32-using-atlantis-cicd-for-infrastructure-deployments]] ← related_to ← [[ctp-topic-56-automated-infrastructure-testing.md]]
+
+## Contradictions
+- (待发现:如有相关页面引用与本页面观点冲突的内容,将在此记录)