chore: sync local project changes

This commit is contained in:
Shen Wei
2026-04-28 16:39:21 +08:00
parent 365caa800a
commit e4cf7f8485
27 changed files with 13691 additions and 1317 deletions

View File

@@ -1942,6 +1942,134 @@
---
## 6. 技术考量
### 6.0 核心技术设计
#### 6.0.1 客源状态机
客源在生命周期内的状态流转是系统的核心业务逻辑,必须在后端通过状态机严格控制,禁止跨状态直接跳转。
```
┌─────────────────────────────────────────┐
│ ▼
[录入] ──► 私客(求购/求租/暂缓)──► 公客 ──► 成交客
│ │
│ 手动转公/自动掉公 │ 转私客(若有权限)
│ │
▼ ▼
无效客 无效客
```
| 状态 | 允许流转目标 | 触发方式 |
|------|------------|---------|
| 私客(求购) | 求租、暂缓、公客、成交客、无效 | 手动操作 / 掉公规则 |
| 私客(求租) | 求购、暂缓、公客、成交客、无效 | 手动操作 / 掉公规则 |
| 私客(暂缓) | 求购、求租、公客、无效 | 手动操作 |
| 公客 | 无效 | 手动操作(暂不支持公转私) |
| 成交客 | — | 终态,不可流转 |
| 无效客 | — | 终态,不可流转 |
**实现要求**
- 所有状态变更须通过后端 API 的状态机校验,前端不直接修改状态字段
- 每次状态变更自动写入 `ClientStatusChangeLog` 审计表(变更人、变更前后状态、时间、理由)
- 状态变更失败须返回明确错误码,前端展示友好提示
---
#### 6.0.2 重复客源检测算法
重复检测是客源管理的核心数据质量保障。采用**异步检测 + 前端提示**策略,不阻塞录入流程。
**检测触发时机**
1. 录入时:提交表单后,后端异步触发检测,结果以橙色提示横幅展示
2. 编辑手机号时:号码变更保存后重新触发
3. 批量导入后:全量扫描,生成重复报告
**去重匹配规则(按优先级)**
| 优先级 | 匹配维度 | 匹配逻辑 | 说明 |
|--------|---------|---------|------|
| P0 | 手机号精确匹配 | `phone_normalized = normalize(input_phone)` | 去除空格、+86、国际区号后精确比对 |
| P1 | 手机号模糊匹配 | 去除格式差异后匹配 | 处理 `135****8888` 与 `13500008888` 等录入差异 |
| P2 | 姓名 + 意向商圈组合 | 同名 + 相同意向区域 | 辅助人工判断,不自动合并 |
**去重处理规则**
- **私客与私客重复**:提示「该手机号已存在于 [员工姓名] 名下私客,请确认是否为同一客户」,由操作人决定是否继续录入或转为该员工协作方
- **私客与公客重复**:提示「该手机号已在公客池中,请确认业务归属后操作」
- **合并客户**:合并以**首录时间更早**的记录为主数据,跟进记录、带看记录全部合并至主记录,被合并记录保留为「已合并」历史副本,不可恢复
**数据模型**
```python
class ClientDuplicateLog(TenantModel):
source_client = ForeignKey(Client, related_name='duplicate_source')
target_client = ForeignKey(Client, related_name='duplicate_target')
match_type = CharField(choices=['phone_exact', 'phone_fuzzy', 'name_area'])
detected_at = DateTimeField(auto_now_add=True)
resolved_by = ForeignKey(User, null=True)
resolution = CharField(choices=['merged', 'ignored', 'pending'])
```
---
#### 6.0.3 私客掉公(自动转公)机制
掉公规则由运营配置,初期建议默认值如下(待业务确认后写入系统配置模块):
| 触发条件 | 默认值 | 是否可配置 |
|---------|--------|----------|
| 私客无跟进天数 | 30 天 | 是(系统配置模块) |
| 触发检测频率 | 每日凌晨 02:00 Celery 定时任务 | 否 |
| 提前预警天数 | 到期前 3 天,系统消息通知归属人 | 是 |
**掉公流程**
```
Celery Beat每日 02:00
└─► 扫描last_follow_up_at < NOW() - 掉公天数 AND status='private'
└─► 批量更新状态为 'public'
└─► 写入 ClientStatusChangeLogoperator='system'
└─► 触发消息推送:「您的 N 个客源已自动转入公客池」
```
**边界情况**
- 暂缓状态的私客**不参与**掉公计算
- 成交客、无效客**不参与**掉公计算
- 掉公后归属人变更为 `NULL`,首录人保留
---
#### 6.0.4 敏感号码权限与留痕
手机号码为核心敏感数据,访问须满足以下要求:
| 场景 | 展示方式 | 查看权限 | 留痕要求 |
|------|---------|---------|---------|
| 列表页 | `+86 135****8888` 打码 | 无需额外权限 | 不留痕 |
| 详情页联系人面板 | `+86 135****8888` 打码 | 无需额外权限 | 不留痕 |
| 点击「查看号码」 | 展示完整号码 10 秒后重新打码 | 需「查看客源完整号码」权限 | **必须留痕**:写入操作日志 |
| 编辑联系人 | 打码显示 + 「查看号码」链接 | 查看需权限,编辑需「编辑客源联系人」权限 | 查看留痕 |
**实现要求**
- 号码存储使用字段级加密AES-256与房源模块同等安全标准
- 查看完整号码的 API 须独立端点(不含在标准详情接口内),方便审计
- 操作日志异步写入,不影响查看响应时间
---
#### 6.0.5 列表性能策略
当前截图显示公客池已有 28,878 条,私客总量持续增长,列表性能须在设计阶段预防:
| 策略 | 说明 |
|------|------|
| 服务端分页 | 所有列表接口默认 20 条/页,禁止前端全量加载 |
| 筛选条件下推 | 所有筛选条件传至 Django ORM生成 SQL WHERE 子句,不做内存过滤 |
| 索引设计 | 联合索引:`(tenant_id, status,归属人, 最后跟进时间)`;手机号哈希索引(辅助去重查询) |
| 慢查询监控 | 超过 500ms 的查询写入慢日志,上线前需通过 EXPLAIN ANALYZE 验证核心列表查询 |
| 智能配房批量计算 | 不在请求链路内实时计算,通过 Celery 定时或客源更新时异步触发,结果缓存至 Redis |
---
### 6.1 依赖关系
| 系统/模块 | 依赖原因 | 优先级风险 |