Sync: expand data model and gitops notes
This commit is contained in:
574
Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md
Normal file
574
Project/fonrey/DATA_MODEL/DATA_MODEL_CLIENT.md
Normal file
@@ -0,0 +1,574 @@
|
||||
# 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_logs`(`log_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_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 | |
|
||||
|
||||
**关键索引**:
|
||||
```sql
|
||||
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) | | |
|
||||
| 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 | | 软删除(不影响客源本身) |
|
||||
| created_by | UUID | FK→staff, SET NULL | |
|
||||
|
||||
**关键索引**:
|
||||
```sql
|
||||
-- 关键:手机号哈希全局唯一索引(用于重复客源检测)
|
||||
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() | |
|
||||
|
||||
**关键索引**:
|
||||
```sql
|
||||
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 = 系统日志
|
||||
```
|
||||
|
||||
**关键索引**:
|
||||
```sql
|
||||
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 | |
|
||||
|
||||
**关键索引**:
|
||||
```sql
|
||||
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 | |
|
||||
|
||||
**关键索引**:
|
||||
```sql
|
||||
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 | — | — | 此表记录**不可删除** |
|
||||
|
||||
**关键索引**:
|
||||
```sql
|
||||
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 | | |
|
||||
|
||||
```sql
|
||||
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) | | |
|
||||
|
||||
```sql
|
||||
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() | |
|
||||
|
||||
```sql
|
||||
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_urgent = A(急迫)
|
||||
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)核心查询
|
||||
|
||||
```sql
|
||||
-- 典型:当前经纪人名下 + 求购状态 + 等级筛选 + 按最后跟进排序
|
||||
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 重复客源检测(录入/编辑时触发)
|
||||
|
||||
```sql
|
||||
-- 手机号哈希碰撞检测(私客、成交客、公客三池同时检查)
|
||||
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 定时任务,每日凌晨执行)
|
||||
|
||||
```sql
|
||||
-- 更新活跃度(以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 定时任务)
|
||||
|
||||
```sql
|
||||
-- 查询应自动转公的私客(阈值由运营配置,假设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 自动维护
|
||||
|
||||
```sql
|
||||
-- 每次写入跟进日志时,自动更新 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 自动维护
|
||||
|
||||
```sql
|
||||
-- 每次新增带看记录时,自动更新 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`**:软删除过滤必须存在
|
||||
Reference in New Issue
Block a user