# Fonrey 房产经纪管理系统 — DATA MODEL 设计文档 > **作者**: Backend Architect > **版本**: v1.0 > **日期**: 2026-04-24 > **技术栈**: Django 4.x + PostgreSQL + django-tenants + Redis > **设计目标**: 支撑 89,000+ 房源、多租户隔离、sub-100ms 查询、合规审计 --- ## 一、架构决策总览 (Architecture Decision Records) ### 1.1 多租户策略:Schema-per-Tenant ``` ┌─────────────────────────────────────────────────────────────┐ │ PostgreSQL Instance │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ public schema│ │tenant_abc │ │tenant_xyz │ │ │ │ (shared) │ │ schema │ │ schema │ │ │ │ │ │ │ │ │ │ │ │ - tenants │ │ - properties │ │ - properties │ │ │ │ - domains │ │ - clients │ │ - clients │ │ │ │ │ │ - complexes │ │ - complexes │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` **选型理由**: - `django-tenants` 的 Schema 隔离提供最强的数据安全边界 - 房产经纪公司之间数据绝对不能互通(合规要求) - 每个 Schema 独立索引,避免全局锁竞争 - 支持按租户独立备份/恢复 ### 1.2 核心领域模型关系图 ``` [区域/商圈]──────────────────────────────┐ │ │ [学校管理] │ │ ▼ [楼盘/小区] ──── [楼栋] ─────────► [房源] ◄──── [挂牌历史] │ │ │ ┌────────┼────────┐ │ │ │ │ │ [联系人] [跟进日志] [维护完成度] │ │ │ │ ┌─────┘ ┌────┴──────┐ │ │ │ │ │ [电话查看] [钥匙] [委托] [实勘] │ [客源] ──── [配对记录] ──── [带看记录] │ [员工/组织] ──── [权限] ``` ### 1.3 关键设计原则 | 原则 | 决策 | | ----- | -------------------------------------- | | 主键类型 | `UUID v4`(跨环境安全,避免枚举攻击) | | 软删除 | 所有核心表含 `deleted_at`(历史可追溯) | | 时间戳 | 全部使用 `TIMESTAMPTZ`(含时区) | | 手机号存储 | AES-256-GCM 加密存储,建立 SHA-256 哈希索引 | | 审计字段 | `created_by`, `updated_by` 全表覆盖 | | 枚举值 | 业务枚举用 `VARCHAR` + CHECK,系统枚举用 lookup 表 | | 大文本 | `TEXT` 类型,不设长度(PG 内部优化) | | 金额 | `NUMERIC(12,2)` 万元精度,避免浮点误差 | --- ## 二、领域概览(Domain Overview) 本节用业务语言描述系统的核心领域对象及其关系,作为各子模块数据模型的导读。 ### 核心领域对象 | 领域对象 | 表/子文档 | 业务说明 | |----------|-----------|----------| | **Tenant(租户)** | `public.tenants` | 每家房产经纪公司对应一个租户,数据完全隔离(Schema-per-Tenant) | | **OrgUnit(组织架构)** | `org_units` → [DATA_MODEL_ORG.md](./DATA_MODEL_ORG.md) | 树形组织架构(总部/区域/城市/大区/分公司/门店/团队/虚拟团队),物化路径存储,支持权限继承 | | **Staff(员工)** | `staff` → [DATA_MODEL_ORG.md](./DATA_MODEL_ORG.md) | 经纪人/店长/经理,绑定组织节点,手机号加密存储,与账号(登录)分离 | | **District(城区)** | `districts` → [DATA_MODEL_COMPLEX.md](./DATA_MODEL_COMPLEX.md) | 行政区划,如「静安区」,是区域体系的顶层节点 | | **BusinessArea(商圈)** | `business_areas` → [DATA_MODEL_COMPLEX.md](./DATA_MODEL_COMPLEX.md) | 商圈/板块,从属于城区,一个楼盘可归属多个商圈 | | **School(学校)** | `schools` → [DATA_MODEL_COMPLEX.md](./DATA_MODEL_COMPLEX.md) | 对口学校数据库,是买家购房决策的核心参考,与楼盘多对多关联 | | **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 | 系统核心表,每套二手房源的完整档案,支持出售/出租/出售兼出租三态 | | **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) | 系统/人工推荐的客源↔房源配对 | ### 领域关系快速导航 ``` District (城区) └─ BusinessArea (商圈) └─ Complex (楼盘) ─── School (对口学校) ├─ Building (楼栋) │ └─ RoomUnit (房号) └─ Property (房源) ├─ PropertyContact (联系人/委托方) ├─ FollowLog (跟进日志) ├─ Viewing (带看记录) ──── Client (客源) └─ Match (配对记录) ──────┘ OrgUnit (组织架构) └─ Staff (员工/经纪人) ─── Property / Client / Viewing / Match ``` ### 子文档索引 | 子文档 | 覆盖模块 | 状态 | |--------|----------|------| | [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 张表)、系统设置 | ✅ 完成 | --- ## 三、公共 Schema(Shared / Public) ```sql -- ============================================================ -- 文件: shared_schema.sql -- 用途: django-tenants 公共 Schema,存放租户注册信息 -- ============================================================ -- 租户表(每家房产公司一条记录) CREATE TABLE public.tenants ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), schema_name VARCHAR(63) UNIQUE NOT NULL, -- PG schema 名,最长 63 字符 name VARCHAR(255) NOT NULL, -- 公司名称 short_name VARCHAR(100), -- 简称/品牌名 created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), is_active BOOLEAN NOT NULL DEFAULT TRUE, paid_until DATE, -- 订阅到期日 on_trial BOOLEAN NOT NULL DEFAULT TRUE, extra JSONB NOT NULL DEFAULT '{}' -- 预留扩展字段 ); -- 域名映射表(支持多域名绑定一个租户) CREATE TABLE public.domains ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), domain VARCHAR(253) UNIQUE NOT NULL, -- 含子域名的完整域名 tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE, is_primary BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_domains_tenant ON public.domains(tenant_id); CREATE INDEX idx_domains_primary ON public.domains(tenant_id) WHERE is_primary = TRUE; ``` --- ## 四、租户 Schema(Tenant Schema) 以下所有表均在每个租户的独立 Schema 内创建。 --- ### 3.1 组织人事模块(Organization & HR) > **详细模型** → 见 [`DATA_MODEL_ORG.md`](./DATA_MODEL_ORG.md) > 该文件为权威定义,包含完整字段、枚举、查询模式和禁止操作。 **核心表概览**(开发时以 DATA_MODEL_ORG.md 为准): | 表名 | 说明 | |------|------| | `org_units` | 组织树节点(公司/事业部/大区/区域/片区/门店/店组/职能),物化路径树 | | `staff` | 员工主表,含加密手机号、角色、在职状态、Django auth 绑定 | | `staff_personal_info` | 员工个人信息扩展(证件、学历、婚育等,1:1) | | `staff_transfer_logs` | 人事异动不可变审计日志(入职/调动/离职/复职等) | | `staff_reward_punish` | 奖惩记录 | | `staff_work_experiences` | 工作经历 | | `staff_educations` | 教育经历 | | `staff_trainings` | 培训经历 | | `staff_family_members` | 家庭成员 | | `staff_accounts` | 第三方平台账号绑定(58安居客/中国网络经纪人等) | **关键约束提示**: - `staff.phone_enc` AES-256-GCM 加密,`staff.phone_hash` SHA-256 用于唯一索引 - `staff_transfer_logs` **无 deleted_at**,不可删除 - `org_units` 路径查询:`WHERE path LIKE '/root/{target_id}/%'` - 员工离职:`status = 'resigned'` + `deleted_at` 软删除,记录永久保留 --- ### 3.2 区域与楼盘模块(Region & Complex Management) > **详细模型** → 见 [`DATA_MODEL_COMPLEX.md`](./DATA_MODEL_COMPLEX.md) > 本节仅作概览,开发时以 DATA_MODEL_COMPLEX.md 为权威定义。 **核心表概览**(开发时以 DATA_MODEL_COMPLEX.md 为准): | 表名 | 说明 | 关键字段 | |------|------|----------| | `districts` | 城区/行政区 | `city`, `name`, `short_name`, `sort_order` | | `business_areas` | 商圈/板块(从属于城区) | `district_id`, `name`, `latitude`, `longitude` | | `metro_lines` | 地铁线路 | `city`, `name`, `color` | | `metro_stations` | 地铁站点 | `metro_line_id`, `name`, `latitude`, `longitude` | | `schools` | 学校(对口学区) | `district_id`, `name`, `type`, `nature`, `level` | | `complexes` | 楼盘/小区(房源底座) | `name`, `district_id`, `address`, `latitude/longitude`, `lock_*`, `search_vector` | | `complex_aliases` | 楼盘别名(含系统别名/用户自定义别名) | `complex_id`, `alias`, `is_system` | | `complex_business_areas` | 楼盘↔商圈多对多(含主商圈标识) | `complex_id`, `business_area_id`, `is_primary` | | `complex_schools` | 楼盘↔学校关联(含学区类型) | `complex_id`, `school_id`, `zone_type` | | `complex_metro_stations` | 楼盘↔地铁站关联(含步行距离) | `complex_id`, `station_id`, `distance_meters` | | `buildings` | 楼栋/单元 | `complex_id`, `name`, `is_standard`, `total_floors` | | `room_units` | 房号/结构单元(楼层+房间号) | `building_id`, `floor`, `room_no`, `is_standard` | | `complex_photos` | 楼盘照片(楼盘图/户型图/VR) | `complex_id`, `category`, `file_key`, `is_cover` | | `complex_attachments` | 楼盘附件 | `complex_id`, `file_key`, `file_name` | | `complex_price_trends` | 楼盘价格走势(月度) | `complex_id`, `record_month`, `avg_unit_price` | --- ### 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.17 客源管理(Client Management) > **详细模型** → 见 [`DATA_MODEL_CLIENT.md`](./DATA_MODEL_CLIENT.md) > 该文件为权威定义,包含完整字段、枚举、状态机、查询模式和禁止操作。 **核心表概览**(开发时以 DATA_MODEL_CLIENT.md 为准): | 表名 | 说明 | |------|------| | `clients` | 客源主表(私客/公客/成交客),含加密手机号哈希、活跃度、归属人 | | `client_contacts` | 联系人(1:N),手机号加密+哈希,支持多联系人 | | `client_requirements` | 需求信息(可多类型:二手/新房/租房),含预算/面积/商圈/朝向等偏好 | | `client_follow_logs` | 跟进日志(高写入频率,5种类型,敏感查看类型不可删) | | `client_follow_log_attachments` | 跟进附件(图片/录音,最大20MB) | | `client_viewings` | 带看/预约记录(1:N,含陪看人/合作带看人) | | `client_property_matches` | 智能配房结果(录客配房/系统配房,匹配度评分) | | `client_status_logs` | 状态变更不可变审计日志(改状态/改等级/转公/转成交/转无效等) | | `client_favorite_folders` | 私客收藏夹(经纪人自定义分组) | | `client_folder_items` | 收藏夹与客源的多对多关联 | | `client_school_preferences` | 意向学校(拆表,支持精确查询) | **关键约束提示**: - `client_contacts.phone_hash` 是重复客源检测的唯一依据,录入前必须查重 - `client_status_logs` **无 deleted_at**,不可删除 - 私客超时(配置天数内无跟进)→ Celery 自动转公(`transfer_to_public_type = 'auto'`) - 活跃度 `activity_level` 由 Celery 每日凌晨批量计算,不实时更新 --- ### 3.18 系统设置(System Settings) ```sql -- ============================================================ -- 枚举/选项管理:跟进目的、标签、来源渠道 等运营维护的枚举值 -- ============================================================ CREATE TABLE lookup_categories ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), code VARCHAR(50) UNIQUE NOT NULL, -- 如:follow_purpose, property_source name VARCHAR(100) NOT NULL, module VARCHAR(30) NOT NULL -- property/client/system ); CREATE TABLE lookup_items ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), category_id UUID NOT NULL REFERENCES lookup_categories(id) ON DELETE CASCADE, value VARCHAR(100) NOT NULL, label VARCHAR(100) NOT NULL, -- 显示文本 sort_order INTEGER NOT NULL DEFAULT 0, is_active BOOLEAN NOT NULL DEFAULT TRUE, metadata JSONB NOT NULL DEFAULT '{}', -- 扩展属性 created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_lookup_items_category ON lookup_items(category_id) WHERE is_active = TRUE; CREATE UNIQUE INDEX idx_lookup_items_value ON lookup_items(category_id, value); -- 自定义标签(速销/独家/唯一 等) CREATE TABLE property_tags ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(50) NOT NULL, color VARCHAR(7), -- HEX 颜色 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); -- 收藏(经纪人收藏房源) 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); -- 保护房设置 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() ); -- 筛选方案(保存的搜索条件) CREATE TABLE saved_filters ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), staff_id UUID NOT NULL REFERENCES staff(id) ON DELETE CASCADE, name VARCHAR(100) NOT NULL, module VARCHAR(20) NOT NULL DEFAULT 'property', filter_params JSONB NOT NULL, -- 完整筛选参数 JSON created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_saved_filters_staff ON saved_filters(staff_id, module); -- 号码方修改审批 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'; ``` --- ## 五、关键索引汇总与查询优化策略 ### 4.1 房源列表页核心查询分析 ```sql -- 典型查询:出售状态 + 公盘 + 特定区域 + 价格区间 + 户型筛选 + 按挂牌日期排序 -- 优化方案:复合索引覆盖最高频维度组合 -- 高频组合索引(status + attribute,覆盖 90% 的列表查询) 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 全文搜索触发器(自动维护 search_vector) ```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(); -- 楼盘全文检索向量(含别名,提升模糊搜索精度) CREATE OR REPLACE FUNCTION update_complex_search_vector() RETURNS TRIGGER AS $$ BEGIN NEW.search_vector := setweight(to_tsvector('simple', COALESCE(NEW.name, '')), 'A') || setweight(to_tsvector('simple', COALESCE(NEW.alias, '')), 'B') || setweight(to_tsvector('simple', COALESCE(NEW.address, '')), 'C'); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_complex_search_vector BEFORE INSERT OR UPDATE OF name, alias, address ON complexes FOR EACH ROW EXECUTE FUNCTION update_complex_search_vector(); ``` ### 4.3 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(); ``` --- ## 六、Redis 缓存策略 ### 5.1 缓存 Key 规范 ``` # 格式:{tenant_schema}:{module}:{entity}:{id}:{field} # TTL 单位:秒 # 房源详情(高频读取) {schema}:prop:detail:{property_id} TTL: 300 (5分钟) # 房源联系人(含解密号码,敏感,TTL 短) {schema}:prop:contacts:{property_id} TTL: 60 (1分钟) # 楼盘基础信息(低变更频率) {schema}:complex:base:{complex_id} TTL: 3600 (1小时) # 楼盘名称自动补全候选列表(联想搜索) {schema}:complex:autocomplete:{prefix} TTL: 600 (10分钟) # 员工信息(用于日志快照) {schema}:staff:base:{staff_id} TTL: 1800 (30分钟) # 枚举值/lookup(几乎不变) {schema}:lookup:{category_code} TTL: 86400 (24小时) # 标签列表 {schema}:tags:property TTL: 3600 # 维护完成度(Celery 计算后写入,详情页直接读 Redis) {schema}:prop:completeness:{property_id} TTL: 600 # 房源列表计数(筛选后总条数,避免 COUNT(*) 全扫) {schema}:prop:count:{filter_hash} TTL: 30 (短TTL,保证准确性) ``` ### 5.2 缓存失效策略 ```python # Django Signal 驱动的缓存失效(在 models.py 中注册) # 房源更新 → 失效详情缓存 + 完成度缓存 # 跟进日志新增 → 失效 last_followed_at 缓存 # 联系人更新 → 失效联系人缓存(立即) # 楼盘更新 → 失效楼盘缓存 + 相关房源缓存(批量) # 枚举更新 → 失效对应 lookup 缓存 ``` --- ## 七、Django Model 层设计要点 ### 6.1 抽象基类 ```python # models/base.py import uuid from django.db import models class UUIDPrimaryKeyModel(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) class Meta: abstract = True class TimeStampedModel(UUIDPrimaryKeyModel): created_at = models.DateTimeField(auto_now_add=True, db_index=False) updated_at = models.DateTimeField(auto_now=True) class Meta: abstract = True class SoftDeleteModel(TimeStampedModel): deleted_at = models.DateTimeField(null=True, blank=True, db_index=False) class Meta: abstract = True def soft_delete(self, deleted_by=None): from django.utils import timezone self.deleted_at = timezone.now() self.save(update_fields=['deleted_at', 'updated_at']) class AuditedModel(SoftDeleteModel): created_by = models.ForeignKey( 'staff.Staff', null=True, on_delete=models.SET_NULL, related_name='+', db_column='created_by' ) updated_by = models.ForeignKey( 'staff.Staff', null=True, on_delete=models.SET_NULL, related_name='+', db_column='updated_by' ) class Meta: abstract = True ``` ### 6.2 加密字段 Mixin ```python # utils/encryption.py # 手机号加密:AES-256-GCM + SHA-256 哈希索引 class EncryptedPhoneField: """ 存储时:phone → AES加密 → phone_enc (BYTEA) phone → SHA256 → phone_hash (VARCHAR 64) 查询时:phone_hash 走索引,phone_enc 解密展示 打码展示:前3位明文 + ******* + 后3位 """ pass ``` ### 6.3 Manager 过滤软删除 ```python class ActiveManager(models.Manager): def get_queryset(self): return super().get_queryset().filter(deleted_at__isnull=True) class PropertyManager(ActiveManager): def public(self): return self.get_queryset().filter(attribute='public') def mine(self, staff_id): return self.get_queryset().filter(seller_agent_id=staff_id) ``` --- ## 八、数据量与性能预测 | 表名 | 预估行数 | 增长速度 | 分区策略 | |------|---------|---------|---------| | `properties` | 89,000+ | 中速 | 暂不分区,建议 500k 后按 `created_at` RANGE 分区 | | `follow_logs` | 200万+ | 高速(最高频写入) | 按 `created_at` 月度 RANGE 分区 | | `property_photos` | 500万+ | 高速 | 按 `property_id` HASH 分区(16分区) | | `price_changes` | 50万 | 中速 | 无需分区 | | `listing_histories` | 20万 | 低速 | 无需分区 | | `clients` | 10万+ | 中速 | 暂不分区 | | `viewings` | 100万 | 中速 | 无需分区 | --- ## 九、必须在开发启动前明确的数据架构决策 | 决策项 | 推荐方案 | 风险 | |-------|---------|------| | 小区数据来源 | 预导入基础数据(安居客/链家 API)+ 支持手动新增兜底 | 高:影响录入体验 | | 私盘可见范围 | 录入人所在门店可见(综合业务需求) | 需与权限模块约定 | | 号码查看权限 | 角色级控制:经纪人限查自己相关房源,店长无限制 | 需合规确认 | | 重复房源主键 | 主键:手机号 hash;辅助:(小区+楼栋+单元+房号)组合 | 需双重校验 | | 跟进目的枚举 | 存 lookup_items 表,运营可维护 | 初始化数据需提前收集 | | 手机号加密算法 | AES-256-GCM,密钥存 Django settings(生产用 Vault) | 密钥管理需单独规划 | --- *本文档为 Fonrey 系统 DATA MODEL v1.0,随 PRD 迭代同步更新。* *下一步建议:API 接口规范(URL 设计 + Request/Response Schema)*