- 新增 PRD/系统配置/系统配置模块PRD.md(v0.1 Draft) - MVP 范围:US-SETTING-001-A(Lookup Items)、B(房源字段必填规则)、C(客源录入规则) - 新增 PRD/系统配置/系统配置数据模型设计说明_for_Atlas.md - 新增 PRD/系统配置/系统配置参数数据.md(竞品参数数据) - 删除旧版 PRD/系统配置/系统配置.md(已被新PRD替代) - 新增 DATA_MODEL/DATA_MODEL_SETTING.md(系统配置数据模型) - 新增 DATA_MODEL/ENUMS.md(枚举定义与约定) - 新增 AGENTS.md(AI Agent 开发规范) - 更新 PRD/TASK.md:US-SETTING-001 拆分为 A/B/C 三个子任务,修正参考文档路径与验收标准 - 新增 VIBE_CODING_开工前缺失清单.md - 新增 TECH_STACK/房源管理技术方案.md - 更新 DATA_MODEL/DATA_MODEL.md、DATA_MODEL_CLIENT.md、DATA_MODEL_LOGIN.md - 更新 PRD/PRD_MVP.md、PRD/权限管理/权限管理模块PRD.md - 更新 TECH_STACK/TECH_STACK.md、权限管理系统技术方案.md - 更新 UI_DESIGN/preview.html、UI_SYSTEM/UI_SYSTEM.md - 新增 prompt/PRD - 为系统设置生成PRD设计文档.md、更新 prompt 模板
28 KiB
28 KiB
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(收藏夹):经纪人自定义的客源分组收藏夹。
关键业务规则
- 私客手机号唯一性:录入联系人手机号时,系统通过
phone_hash检测是否与现有私客/成交客/公客重复,并在列表顶部提示重复数量。 - 活跃度计算:系统根据「最后跟进日期」自动计算客源活跃度,分为:新配偶(新建)/ 7日活跃 / 30日活跃 / 90日活跃 / 即将过期 / 无效。具体阈值由运营配置。
- 私客自动转公规则:超过配置天数(如 30 天)无跟进记录,系统自动将私客标记为公客(
transfer_to_public_type = 'auto')。 - 状态机:客源状态有严格流转规则(见第四章),不可跳过转台。
- 跟进目的枚举:由
lookup_items表维护,运营可配置,当前已知 23 项(见 Story 8)。 - 号码查看审计:查看联系人明文号码需记录
client_follow_logs(log_type = 'sensitive_view'),不可删除。 - 需求类型独立存储:同一客源可同时有「二手购房」「租房」两类需求,分别存储在独立需求记录中,由
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_urgent=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 | 软删除 | |
| created_by | UUID | FK→staff, SET NULL | |
| updated_by | UUID | FK→staff, SET NULL |
关键索引:
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 | |
| 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) | ||
| VARCHAR(100) | 微信号 | ||
| VARCHAR(20) | QQ号 | ||
| remarks | VARCHAR(200) | 联系人备注,最多200字 | |
| created_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
| updated_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | |
| deleted_at | TIMESTAMPTZ | 软删除(不影响客源本身) | |
| 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 | |
| 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 | |
| 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 |
关键索引:
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 | |
| 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 |
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约束在应用层拦截 - ❌ 严禁明文存储联系人手机号:必须走
EncryptedPhoneField,phone_hash用于索引和重复检测 - ❌ 严禁跳过状态机流转:如私客不可直接跳过「求购」变为「无效」而不生成 status log
- ❌ 严禁在没有
client_type过滤的情况下查询客源列表:私客/公客/成交客数据量均较大,必须按类型隔离查询 - ❌ 严禁查询 clients 时不带
deleted_at IS NULL:软删除过滤必须存在