Files
nexus/Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md
2026-04-30 06:57:02 +08:00

30 KiB
Raw Blame History

For AI assistants: Read this entire file before writing any code. All decisions here are final. Do not suggest alternatives unless asked.

Fonrey — 客源管理数据模型DATA_MODEL_CLIENT

所属系统: Fonrey 房产经纪管理系统
版本: v1.0
日期: 2026-04-24
关联模块: apps/client/ — 私客、公客、成交客、跟进记录、带看、智能配房


一、领域概览Domain Overview

核心概念

  • Client客源:有购房/租房意向或历史成交记录的客户。核心实体与房源Property是系统业务闭环的两端。
  • 客源类型
    • 私客private:经纪人独占跟进的意向客户,是本期核心。
    • 公客public:私客超时未跟进或手动转公后,进入全公司共享客源池。
    • 成交客transacted:已完成购房/租房成交的客户,用于复购/转介绍跟进。
  • ClientContact联系人:一个客源可有多个联系人,每个联系人有独立手机号。手机号加密存储,用于重复检测(「私客与成交客重复」)。
  • ClientRequirement需求信息:购房/租房的详细偏好。一个客源可同时有「二手」「新房」「租房」三种需求类型(分别对应独立的需求记录)。
  • ClientFollowLog跟进日志:经纪人与客户每次沟通的书面记录,是客源活跃度计算的数据来源。
  • Viewing带看记录:与 Property 模块共享此表,记录经纪人带客户看房的过程。见主 DATA_MODEL.md 3.17 节。
  • ClientPropertyMatch智能配房:系统按需求自动匹配的房源列表,分「录客配房」和「系统配房」两种来源。
  • ClientFavoriteFolder收藏夹:经纪人自定义的客源分组收藏夹。

关键业务规则

  1. 私客手机号唯一性:录入联系人手机号时,系统通过 phone_hash 检测是否与现有私客/成交客/公客重复,并在列表顶部提示重复数量。
  2. 活跃度计算:系统根据「最后跟进日期」自动计算客源活跃度,分为:新配偶(新建)/ 7日活跃 / 30日活跃 / 90日活跃 / 即将过期 / 无效。具体阈值由运营配置。
  3. 私客自动转公规则:超过配置天数(如 30 天)无跟进记录,系统自动将私客标记为公客(transfer_to_public_type = 'auto')。
  4. 状态机:客源状态有严格流转规则(见第四章),不可跳过转台。
  5. 跟进目的枚举:由 lookup_items 表维护,运营可配置,当前已知 23 项(见 Story 8
  6. 号码查看审计:查看联系人明文号码需记录 client_follow_logslog_type = 'sensitive_view'),不可删除。
  7. 需求类型独立存储:同一客源可同时有「二手购房」「租房」两类需求,分别存储在独立需求记录中,由 client_requirements.requirement_type 区分。

二、实体关系

Client (客源主表)
  │
  ├── 1:N ── ClientContact       (联系人,多个号码)
  ├── 1:N ── ClientRequirement   (需求信息,可多类型)
  ├── 1:N ── ClientFollowLog     (跟进日志,高写入频率)
  ├── 1:N ── ClientViewing       (带看预约)
  ├── 1:N ── ClientPropertyMatch (智能配房结果)
  ├── 1:1 ── ClientActivityCache (活跃度缓存,异步计算)
  ├── N:M ── ClientFavoriteFolder (通过 client_folder_items 关联)
  └── 1:N ── ClientStatusLog     (状态变更日志,不可删)

ClientFavoriteFolder
  └── 1:N ── ClientFolderItem    (收藏夹中的客源)

Staff (员工)
  ├── first_recorder_id → Client (首录人)
  └── owner_id → Client         (归属人)

三、Schema 定义

3.1 clients — 客源主表

字段 类型 约束 业务说明
id UUID PK 主键(系统生成,业务无关)
client_no VARCHAR(30) UNIQUE, NOT NULL 系统生成的客源编号,格式由运营配置(如 KY20260424001
client_type VARCHAR(20) NOT NULL DEFAULT 'private' private=私客 / public=公客 / transacted=成交客
status VARCHAR(20) NOT NULL DEFAULT 'buying' 见下方枚举
grade VARCHAR(5) NOT NULL DEFAULT 'C' A=A急迫 / B=较强 / C=一般 / D=较弱 / E=暂不关注
property_usage VARCHAR(30) NOT NULL DEFAULT 'residential' residential=住宅 / villa=别墅 / commercial_residential=商住 / shop=商铺 / office=写字楼 / other=其他
buying_purpose VARCHAR(20)[] 购房目的多选:rigid=刚需 / investment=投资 / school_district=学区 / upgrade=改善 / commercial=商用 / other=其他
payment_method VARCHAR(30) full=全额 / mortgage=商业贷款 / mortgage_fund=商贷+公积金 / fund=公积金
properties_owned VARCHAR(20) none=无 / local_none=本地无外地有 / local_has=本地有
has_loan_record BOOLEAN 有无贷款记录
id_type VARCHAR(20) 证件类型:id_card / passport / hk_macao / other
id_number_enc BYTEA 证件号码AES 加密)
source VARCHAR(50) 客户来源lookup_items 维护)
remarks TEXT 备注最多200字
is_starred BOOLEAN NOT NULL DEFAULT FALSE 是否收藏(快速标记,详细收藏夹用 client_folder_items
is_pinned BOOLEAN NOT NULL DEFAULT FALSE 是否置顶(列表顶部置顶)
is_big_value BOOLEAN NOT NULL DEFAULT FALSE 是否大价值客户(影响筛选展示)
is_protected BOOLEAN NOT NULL DEFAULT FALSE 是否保护客(影响转公逻辑)
prefers_new_house BOOLEAN 偏好新房(用于筛选)
transfer_to_public_type VARCHAR(20) 转公客方式:manual=手动转公 / auto=自动转公(超时) / marketing_jump=营销客跳公 / resource_public=资料客素公
transferred_public_at TIMESTAMPTZ 进入公客池时间
invalid_reason VARCHAR(30) 无效原因:invalid_phone=号码无效 / peer_agent=同行 / ad=广告推销 / no_intent=无意向 / other
invalidated_at TIMESTAMPTZ 标记无效时间
transacted_at DATE 成交日期
transacted_property_id UUID FK→properties, SET NULL 成交关联的房源
transacted_price NUMERIC(12,2) 成交价格(万元)
transacted_type VARCHAR(20) 成交类型:bought=我购 / rented=我租
transacted_property_type VARCHAR(20) 成交房源类型:second_hand=二手 / new_house=新房
first_recorder_id UUID FK→staff, SET NULL 首录人
owner_id UUID FK→staff, SET NULL 归属人(私客独占跟进人)
org_unit_id UUID FK→org_units, SET NULL 归属部门(冗余,加速筛选)
activity_level VARCHAR(20) new_matched=新配偶 / active_7d / active_30d / active_90d / expiring / frozen / invalid(异步计算)
last_active_at TIMESTAMPTZ 最后有效跟进时间(触发器维护)
last_follow_at TIMESTAMPTZ 最后跟进时间(冗余,列表排序用)
commission_date DATE 委托日期
entrust_count SMALLINT NOT NULL DEFAULT 1 委托次数(成交后再委托则累加)
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() 记录创建时间(系统自动)
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() 记录最后更新时间(系统自动)
deleted_at TIMESTAMPTZ 软删除时间戳NULL=未删除非NULL=已软删除
created_by UUID FK→staff, SET NULL 创建人(操作员工)
updated_by UUID FK→staff, SET NULL 最后修改人(操作员工)
version INTEGER NOT NULL DEFAULT 1 乐观锁版本号;每次 UPDATE +1应用层检测 0 行受影响时抛 ConflictError

关键索引

CREATE UNIQUE INDEX idx_clients_client_no ON clients(client_no) WHERE deleted_at IS NULL;
CREATE INDEX idx_clients_type_status ON clients(client_type, status) WHERE deleted_at IS NULL;
CREATE INDEX idx_clients_owner ON clients(owner_id) WHERE deleted_at IS NULL;
CREATE INDEX idx_clients_org_unit ON clients(org_unit_id) WHERE deleted_at IS NULL;
CREATE INDEX idx_clients_activity ON clients(activity_level, last_active_at DESC) WHERE deleted_at IS NULL;
CREATE INDEX idx_clients_grade ON clients(grade) WHERE deleted_at IS NULL;
CREATE INDEX idx_clients_transferred_at ON clients(transferred_public_at DESC) WHERE client_type = 'public';
CREATE INDEX idx_clients_last_follow ON clients(last_follow_at DESC NULLS LAST) WHERE deleted_at IS NULL;

3.2 client_contacts — 联系人表

字段 类型 约束 业务说明
id UUID PK 主键(系统生成,业务无关)
client_id UUID NOT NULL, FK→clients, CASCADE 所属客源(关联 clients 表,联系人随客源级联删除)
sort_order SMALLINT NOT NULL DEFAULT 0 联系人1为主联系人sort_order=0
name VARCHAR(50) NOT NULL 联系人姓名
gender VARCHAR(10) NOT NULL DEFAULT 'male' male=先生 / female=女士
phone_enc BYTEA NOT NULL AES-256-GCM 加密手机号电话1
phone_hash VARCHAR(64) NOT NULL SHA-256 哈希(重复检测)
phone_country_code VARCHAR(10) NOT NULL DEFAULT '+86' 国际区号
phone_is_invalid BOOLEAN NOT NULL DEFAULT FALSE 是否被标记为无效号码
phone2_enc BYTEA 备用电话2
phone2_hash VARCHAR(64) 备用电话2哈希SHA-256用于重复检测
wechat VARCHAR(100) 微信号
qq VARCHAR(20) QQ号
remarks VARCHAR(200) 联系人备注最多200字
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() 记录创建时间(系统自动)
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() 记录最后更新时间(系统自动)
deleted_at TIMESTAMPTZ 软删除时间戳NULL=未删除(不影响客源本身)
created_by UUID FK→staff, SET NULL 创建人(操作员工)

关键索引

-- 关键:手机号哈希全局唯一索引(用于重复客源检测)
CREATE INDEX idx_client_contacts_phone_hash ON client_contacts(phone_hash) WHERE deleted_at IS NULL;
CREATE INDEX idx_client_contacts_phone2_hash ON client_contacts(phone2_hash) WHERE phone2_hash IS NOT NULL AND deleted_at IS NULL;
CREATE INDEX idx_client_contacts_client ON client_contacts(client_id) WHERE deleted_at IS NULL;

业务注意

  • sort_order = 0 的联系人为主联系人,姓名用于客源姓名显示
  • 手机号标记无效(phone_is_invalid = TRUE)时,不影响记录存在,但该号码不再参与重复检测
  • 联系人软删除后客源仍保留,但若所有联系人均被删则客源实际上无有效号码

3.3 client_requirements — 需求信息表

一个客源可同时有多类需求(二手购房、新房、租房),每类需求独立一条记录。

字段 类型 约束 业务说明
id UUID PK 主键(系统生成,业务无关)
client_id UUID NOT NULL, FK→clients, CASCADE 所属客源(关联 clients 表,需求随客源级联删除)
requirement_type VARCHAR(20) NOT NULL second_hand=二手 / new_house=新房 / rental=租房
is_primary BOOLEAN NOT NULL DEFAULT TRUE 是否为主需求(用于列表展示)
budget_min NUMERIC(12,2) 最低预算(万元/元,依据需求类型)
budget_max NUMERIC(12,2) 最高预算
area_min NUMERIC(8,2) 最小面积(㎡)
area_max NUMERIC(8,2) 最大面积
bedroom_counts SMALLINT[] 可接受卧室数:如 [2,3](多选)
floor_preferences VARCHAR(20)[] 楼层偏好多选:no_first=不要一层 / low=低楼层 / mid=中楼层 / high=高楼层 / no_top=不要顶层
orientations VARCHAR(10)[] 朝向多选:east/south/west/north
decorations VARCHAR(10)[] 装修偏好多选(枚举同 properties.decoration
building_age_ranges VARCHAR(20)[] 楼龄多选:within_5y/5_10y/10_15y/15_20y/over_20y
intent_district_ids UUID[] 意向行政区 ID 数组
intent_business_area_ids UUID[] 意向商圈 ID 数组
intent_complex_names TEXT 意向小区文本逗号分隔最多500字
transportation VARCHAR(50) 交通要求最多50字
intent_school_names TEXT 意向学校(文本,逗号分隔)
school_enrollment_date DATE 入学时间月份精度取该月1日存储
traffic_preference TEXT 交通备注
requirement_notes VARCHAR(200) 需求备注最多200字
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() 记录创建时间(系统自动)
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() 记录最后更新时间(系统自动)

关键索引

CREATE INDEX idx_client_requirements_client ON client_requirements(client_id);
CREATE INDEX idx_client_requirements_type ON client_requirements(requirement_type, client_id);
-- 智能配房时按预算/面积范围查询
CREATE INDEX idx_client_requirements_budget ON client_requirements(budget_min, budget_max);
CREATE INDEX idx_client_requirements_area ON client_requirements(area_min, area_max);

3.4 client_follow_logs — 客源跟进日志

follow_logs(房源跟进)结构类似,独立存储以避免跨模块混淆。

字段 类型 约束 业务说明
id UUID PK 主键(系统生成,业务无关)
client_id UUID NOT NULL, FK→clients, CASCADE 所属客源(关联 clients 表,跟进日志随客源级联删除)
log_type VARCHAR(30) NOT NULL 见下方枚举
purpose VARCHAR(50) 跟进目的lookup_items 维护23项
content TEXT 跟进内容最少6字最多500字
log_tag VARCHAR(50) 跟进标签:has_recording=有录音 / has_photo=有图片 / not_satisfied=对房源不满意 / still_considering=还在考虑 / ready_to_deposit=可交定金
change_detail JSONB 修改跟进专用,格式:{"field": "grade", "old": "C", "new": "B", "label": "等级"}
is_public BOOLEAN NOT NULL DEFAULT TRUE FALSE=仅本人及上级可见
is_deletable BOOLEAN NOT NULL DEFAULT TRUE 敏感信息查看类型为 FALSE不可删除
operator_id UUID FK→staff, SET NULL 操作人
operator_snapshot JSONB {name, store_group, role}(防止人员调动后显示异常)
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() 记录创建时间(系统自动)
deleted_at TIMESTAMPTZ 软删除时间戳;仅 is_deletable=TRUE 时可软删

log_type 枚举

written       = 写入跟进(经纪人主动写)
modified      = 修改跟进(字段变更自动生成)
sensitive_view= 敏感信息查看(查看号码等,不可删)
other         = 其他跟进(系统自动:新增私客/状态变更等)
system        = 系统日志

关键索引

CREATE INDEX idx_client_follow_logs_client_time ON client_follow_logs(client_id, created_at DESC) WHERE deleted_at IS NULL;
CREATE INDEX idx_client_follow_logs_type ON client_follow_logs(client_id, log_type, created_at DESC) WHERE deleted_at IS NULL;
CREATE INDEX idx_client_follow_logs_operator ON client_follow_logs(operator_id, created_at DESC) WHERE deleted_at IS NULL;
-- 不可删记录(合规审计)
CREATE INDEX idx_client_follow_sensitive ON client_follow_logs(client_id, created_at DESC) WHERE log_type = 'sensitive_view';

3.5 client_follow_log_attachments — 跟进附件

字段 类型 约束 业务说明
id UUID PK 主键(系统生成,业务无关)
follow_log_id UUID NOT NULL, FK→client_follow_logs, CASCADE 所属跟进日志(附件随日志级联删除)
file_key TEXT NOT NULL R2/S3 存储路径
file_name VARCHAR(255) NOT NULL 原始文件名(用于展示和下载)
file_size INTEGER NOT NULL bytes最大 20MB
file_type VARCHAR(10) CHECK bmp/jpg/png/gif
has_location BOOLEAN NOT NULL DEFAULT FALSE 是否含 GPS 位置信息
sort_order SMALLINT NOT NULL DEFAULT 0 附件排序顺序
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() 记录创建时间(系统自动)

3.6 client_viewings — 带看记录(客源侧视图)

字段 类型 约束 业务说明
id UUID PK 主键(系统生成,业务无关)
client_id UUID NOT NULL, FK→clients, RESTRICT 所属客源(带看记录仅软删除,不随客源删除)
property_id UUID NOT NULL, FK→properties, RESTRICT 带看房源(房源删除时保留带看记录)
viewing_type VARCHAR(20) NOT NULL DEFAULT 'viewing' appointment=预约 / viewing=带看 / revisit=复看 / empty=空看
agent_id UUID FK→staff, SET NULL 主带看经纪人
companion_ids UUID[] 陪看人员 ID 数组最多5人
cooperator_ids UUID[] 合作带看人 ID 数组最多5人
scheduled_at TIMESTAMPTZ 预约时间
viewing_start_at TIMESTAMPTZ 实际带看开始时间
viewing_end_at TIMESTAMPTZ 结束时间
situation TEXT 带看情况必填≥6字
client_intent VARCHAR(20) 客户意向:interested=感兴趣 / not_interested=不感兴趣 / negotiating=谈判中 / cancelled=取消
viewing_progress SMALLINT 带看进度1=一看2=二看...,冗余字段,触发器维护)
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() 记录创建时间(系统自动)
deleted_at TIMESTAMPTZ 软删除时间戳;带看记录可软删除
created_by UUID FK→staff, SET NULL 创建人(操作员工)

关键索引

CREATE INDEX idx_client_viewings_client ON client_viewings(client_id, viewing_start_at DESC) WHERE deleted_at IS NULL;
CREATE INDEX idx_client_viewings_property ON client_viewings(property_id) WHERE deleted_at IS NULL;
CREATE INDEX idx_client_viewings_agent ON client_viewings(agent_id) WHERE deleted_at IS NULL;

3.7 client_property_matches — 智能配房

字段 类型 约束 业务说明
id UUID PK 主键(系统生成,业务无关)
client_id UUID NOT NULL, FK→clients, CASCADE 所属客源
property_id UUID NOT NULL, FK→properties, CASCADE 匹配的房源
match_source VARCHAR(20) NOT NULL DEFAULT 'recorded' recorded=录客配房(基于录入需求) / system=系统配房(算法推荐)
match_group VARCHAR(30) 分组:quality_layout=优质户型 / price_reduced=降价 / hot=热门 / newly_listed=新上
match_score NUMERIC(5,2) 匹配度评分0-100
match_reasons JSONB 匹配原因详情,格式:[{"key": "budget", "match": true}, ...]
status VARCHAR(20) NOT NULL DEFAULT 'suggested' suggested=待推送 / shared=已分享 / rejected=已反馈不合适 / viewed=客户已查看
shared_at TIMESTAMPTZ 分享时间
feedback VARCHAR(50) 反馈原因lookup_items 维护)
calculated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() 配房计算时间
created_by UUID FK→staff, SET NULL 触发配房操作的员工录客配房时记录系统配房可为NULL

关键索引

CREATE UNIQUE INDEX idx_client_matches_pair ON client_property_matches(client_id, property_id);
CREATE INDEX idx_client_matches_client ON client_property_matches(client_id, match_source, match_group);
CREATE INDEX idx_client_matches_status ON client_property_matches(client_id, status) WHERE status != 'rejected';

3.8 client_status_logs — 状态变更日志(不可删)

字段 类型 约束 业务说明
id UUID PK 主键(系统生成,业务无关)
client_id UUID NOT NULL, FK→clients, RESTRICT 所属客源状态日志永久保留RESTRICT 防止删除客源)
change_type VARCHAR(30) NOT NULL status_change=改状态 / grade_change=改等级 / to_public=转公客 / to_transacted=转成交 / to_invalid=转无效 / owner_change=改归属人 / source_change=改来源
old_value JSONB 变更前快照,格式:{"status": "buying", "label": "求购"}
new_value JSONB 变更后快照
reason TEXT 变更理由改状态必填最多200字
operator_id UUID NOT NULL, FK→staff, RESTRICT 操作人(必填,状态变更审计用)
operated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() 操作时间(系统自动记录)
⚠️ 无 deleted_at 此表记录不可删除

关键索引

CREATE INDEX idx_client_status_logs_client ON client_status_logs(client_id, operated_at DESC);
CREATE INDEX idx_client_status_logs_type ON client_status_logs(change_type, operated_at DESC);

3.9 client_favorite_folders — 私客收藏夹

字段 类型 约束 业务说明
id UUID PK 主键(系统生成,业务无关)
staff_id UUID NOT NULL, FK→staff, CASCADE 收藏夹所属经纪人
name VARCHAR(10) NOT NULL 收藏夹名称最多10字
is_default BOOLEAN NOT NULL DEFAULT FALSE 系统默认收藏夹
sort_order INTEGER NOT NULL DEFAULT 0 收藏夹显示顺序(升序)
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() 记录创建时间(系统自动)
deleted_at TIMESTAMPTZ 软删除时间戳NULL=未删除
CREATE INDEX idx_favorite_folders_staff ON client_favorite_folders(staff_id) WHERE deleted_at IS NULL;
-- 每个经纪人只能有一个默认收藏夹
CREATE UNIQUE INDEX idx_favorite_folders_default ON client_favorite_folders(staff_id) WHERE is_default = TRUE AND deleted_at IS NULL;

3.10 client_folder_items — 收藏夹中的客源

字段 类型 约束 业务说明
folder_id UUID NOT NULL, FK→client_favorite_folders, CASCADE 所属收藏夹
client_id UUID NOT NULL, FK→clients, CASCADE 被收藏的客源
added_at TIMESTAMPTZ NOT NULL DEFAULT NOW() 加入收藏夹时间
PRIMARY KEY (folder_id, client_id) 联合主键(同一客源在同一收藏夹只能出现一次)
CREATE INDEX idx_folder_items_client ON client_folder_items(client_id);

3.11 client_school_preferences — 意向学校(多对多)

单独拆表便于学校搜索,避免文本字段模糊查询。

字段 类型 约束 业务说明
id UUID PK 主键(系统生成,业务无关)
requirement_id UUID NOT NULL, FK→client_requirements, CASCADE 所属需求(意向学校随需求级联删除)
school_id UUID FK→schools, SET NULL 从学校表选择,允许为 NULL自由输入
school_name VARCHAR(100) NOT NULL 学校名称(当 school_id 为 NULL 时为手动输入)
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() 记录创建时间(系统自动)
CREATE INDEX idx_school_prefs_requirement ON client_school_preferences(requirement_id);

四、枚举常量

clients.status客源状态

buying         = 求购(私客活跃态)
renting        = 求租(私客活跃态)
buy_or_rent    = 租购(私客活跃态)
suspended      = 暂缓(暂时无需求,不计入活跃统计)
bought         = 已购(成交客:我购)
rented_done    = 已租(成交客:我租)
public         = 公客(已转入公客池)
invalid        = 无效(号码无效/无意向等)

状态流转规则

buying/renting/buy_or_rent
  → suspended    (改状态操作,可逆)
  → public       (手动转公 or 超时自动转公,不可逆)
  → bought/rented_done (转成交,不可逆)
  → invalid      (转无效,需经理审批后可恢复)

clients.grade等级

A         = A(急迫)
B         = B(较强)
C         = C(一般,默认值)
D         = D(较弱)
E         = E(暂不关注)

client_status_logs.change_type变更类型

status_change     = 改状态(含改等级时同时改状态的情况)
grade_change      = 改等级
to_public         = 转公客manual=手动 or auto=自动)
to_transacted     = 转成交(记录成交信息)
to_invalid        = 转无效(含无效原因)
owner_change      = 改归属人
source_change     = 改来源
merge             = 合并客源(被合并的记录保留日志)

clients.activity_level活跃度分层系统计算

含义 触发条件(示例,以运营配置为准)
new_matched 新匹配 录入后 3 天内
active_7d 7日活跃 最后跟进在 7 天内
active_30d 30日活跃 最后跟进在 30 天内
active_90d 90日活跃 最后跟进在 90 天内
expiring 即将过期 距自动转公还有 N 天
frozen 冻结(暂缓) status = suspended
invalid 无效 status = invalid

五、查询模式

5.1 私客列表页(求购 Tab核心查询

-- 典型:当前经纪人名下 + 求购状态 + 等级筛选 + 按最后跟进排序
SELECT c.id, c.status, c.grade, c.activity_level,
       c.last_follow_at, c.commission_date, c.buying_purpose,
       cc.name AS contact_name,   -- JOIN 主联系人
       s.name AS owner_name, ou.name AS org_unit_name,
       COUNT(cpm.id) AS match_count   -- 智能配房数量
FROM clients c
JOIN client_contacts cc ON cc.client_id = c.id AND cc.sort_order = 0 AND cc.deleted_at IS NULL
JOIN staff s ON s.id = c.owner_id
JOIN org_units ou ON ou.id = c.org_unit_id
LEFT JOIN client_property_matches cpm ON cpm.client_id = c.id AND cpm.status != 'rejected'
WHERE c.client_type = 'private'
  AND c.owner_id = :current_staff_id       -- 与我相关
  AND c.status IN ('buying', 'buy_or_rent')
  AND c.deleted_at IS NULL
GROUP BY c.id, cc.name, s.name, ou.name
ORDER BY c.last_follow_at DESC NULLS LAST
LIMIT 20 OFFSET :offset;

5.2 重复客源检测(录入/编辑时触发)

-- 手机号哈希碰撞检测(私客、成交客、公客三池同时检查)
SELECT c.id, c.client_type, c.status, c.client_no,
       cc.name AS contact_name
FROM client_contacts cc
JOIN clients c ON cc.client_id = c.id
WHERE cc.phone_hash = :new_phone_hash
  AND cc.deleted_at IS NULL
  AND c.deleted_at IS NULL
  AND c.status != 'invalid';

5.3 活跃度批量更新Celery 定时任务,每日凌晨执行)

-- 更新活跃度以7日活跃为例
UPDATE clients
SET activity_level = 'active_7d',
    updated_at = NOW()
WHERE client_type = 'private'
  AND status NOT IN ('invalid', 'public', 'bought', 'rented_done')
  AND last_follow_at >= NOW() - INTERVAL '7 days'
  AND deleted_at IS NULL;

5.4 私客自动转公超时无跟进Celery 定时任务)

-- 查询应自动转公的私客阈值由运营配置假设30天
SELECT id FROM clients
WHERE client_type = 'private'
  AND status IN ('buying', 'renting', 'buy_or_rent')
  AND last_follow_at < NOW() - INTERVAL '30 days'
  AND is_protected = FALSE
  AND deleted_at IS NULL;
-- 后续在 Application 层批量更新 client_type='public', transfer_to_public_type='auto'

六、触发器

6.1 last_follow_at 自动维护

-- 每次写入跟进日志时,自动更新 clients.last_follow_at
CREATE OR REPLACE FUNCTION update_client_last_follow()
RETURNS TRIGGER AS $$
BEGIN
    IF NEW.log_type = 'written' THEN
        UPDATE clients
        SET last_follow_at = NEW.created_at,
            last_active_at = NEW.created_at,
            updated_at = NOW()
        WHERE id = NEW.client_id;
    END IF;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_client_last_follow
    AFTER INSERT ON client_follow_logs
    FOR EACH ROW EXECUTE FUNCTION update_client_last_follow();

6.2 viewing_progress 自动维护

-- 每次新增带看记录时,自动更新 clients 的带看进度冗余字段
CREATE OR REPLACE FUNCTION update_client_viewing_progress()
RETURNS TRIGGER AS $$
BEGIN
    UPDATE clients
    SET updated_at = NOW()
    WHERE id = NEW.client_id;
    -- Application 层根据 COUNT(viewings) 计算具体进度
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_client_viewing_progress
    AFTER INSERT ON client_viewings
    FOR EACH ROW EXECUTE FUNCTION update_client_viewing_progress();

七、禁止操作

  • 严禁硬删除 clients 记录:无效/转公客/成交客均通过 status 和 soft delete 处理,历史跟进/带看依赖外键
  • 严禁删除 client_status_logs:状态变更为不可变审计日志
  • 严禁删除 log_type='sensitive_view' 的跟进记录:必须通过 is_deletable=FALSE 约束在应用层拦截
  • 严禁明文存储联系人手机号:必须走 EncryptedPhoneFieldphone_hash 用于索引和重复检测
  • 严禁跳过状态机流转:如私客不可直接跳过「求购」变为「无效」而不生成 status log
  • 严禁在没有 client_type 过滤的情况下查询客源列表:私客/公客/成交客数据量均较大,必须按类型隔离查询
  • 严禁查询 clients 时不带 deleted_at IS NULL:软删除过滤必须存在