> **For AI assistants**: Read this entire file before writing any code. All decisions here are final. Do not suggest alternatives unless asked. # Fonrey 客源管理技术方案 **版本**: 1.0 **项目**: Fonrey 房产经纪管理系统 **技术栈**: Django 4.x + HTMX + Alpine.js + PostgreSQL 16 + Redis + Celery + Cloudflare R2 **关联 PRD**: `PRD/客源管理/客源管理模块PRD.md`(v1.4) **关联数据模型**: `DATA_MODEL/DATA_MODEL_CLIENT.md`(本方案不重复 DDL) **关联契约规范**: `TECH_STACK/API_CONTRACT.md`(全局 API 契约权威) **关联枚举字典**: `DATA_MODEL/ENUMS.md` **最后更新**: 2026-04-27 --- ## 变更历史 | 日期 | 变更人 | 变更内容 | |---|---|---| | 2026-04-30 | Atlas | 补充“变更历史”章节(文档治理) | ## 一、文档定位与边界 本文件聚焦客源模块的: 1. 服务边界与模块协作 2. API 端点设计(P0/P1 兼容) 3. HTMX 局刷与页面分片约定 4. 权限与数据范围控制 5. 异步任务、缓存、性能与测试映射 > **不在本文件展开**:数据表字段、DDL、索引、触发器。以 `DATA_MODEL_CLIENT.md` 为唯一权威。 --- ## 二、范围定义(以 TASK.md 为准) ### 2.1 P0 必须覆盖(Phase 1) `US-CLIENT-001 ~ US-CLIENT-017`: - 新增私客、列表筛选、详情、需求编辑、跟进、带看 - 联系人管理、等级/状态变更、转公客/转成交/转无效 - 相关员工管理 - 自动转公、重复检测 ### 2.2 非 P0(仅预留端点,不实现复杂能力) - AI 行为解读深度模型 - 新房推荐高级排序引擎 - 公客/成交客完整运营闭环(P1/P2) --- ## 三、模块架构边界 ## 3.1 模块职责(`apps/client`) - 客源主流程:新增、列表、详情、状态流转 - 客源关联流程:联系人、需求、跟进、带看、智能配房结果 - 质量控制:重复检测、自动转公、操作审计 ## 3.2 外部依赖 | 依赖模块 | 依赖内容 | 依赖方式 | |---|---|---| | `apps/org` | 员工、组织树、归属人/首录人 | FK + Service | | `apps/property` | 配房候选房源、带看房源关联 | 只读查询 + FK | | `apps/permission` | 角色与数据范围权限 | PermissionChecker + ScopeQueryBuilder | | `apps/setting` | 可配置枚举(来源、跟进目的)与查重规则 | TenantSettingsService | | `core/encryption.py` | 手机号/证件号加密与哈希 | 统一工具调用 | | `core/cache.py` | 列表缓存、重复计数缓存、任务进度 | Redis | | `Celery` | 自动转公、重复检测统计、配房重算、导出 | 异步任务 | | `Cloudflare R2` | 跟进图片、带看附件 | 预签名上传 | --- ## 四、API 设计原则 1. 页面路由与数据 API 分离: - 页面:`/client/...` - 数据:`/api/client/...` 2. 列表筛选、Tab 加载、弹窗提交优先 HTMX。 3. 手机号明文永不落库;默认脱敏显示。 4. 重复检测与自动转公走异步批处理 + 局部实时校验组合。 5. 所有状态变更必须写操作日志(不可省略)。 --- ## 五、端点清单(核心) ## 5.1 页面路由(SSR) | 路径 | 方法 | 鉴权 | 说明 | |---|---|---|---| | `/client/list/` | GET | 是 | 客源列表主页面(私客/公客/成交客 Tab 容器) | | `/client/create/` | GET | 是 | 新增私客页面 | | `/client/{client_id}/` | GET | 是 | 客源详情主页面 | | `/client/{client_id}/edit/` | GET | 是 | 编辑客源页面 | | `/client/{client_id}/operation-logs/` | GET | 是 | 客源操作日志页面 | ## 5.2 HTMX 片段端点 | 路径 | 方法 | 用途 | 返回 | |---|---|---|---| | `/client/fragments/list-table/` | POST | 列表筛选/排序/分页局刷 | HTML | | `/client/fragments/repeat-counters/` | GET | 顶部重复统计局刷 | HTML | | `/client/{id}/fragments/tab/{tab}/` | GET | 详情 Tab 懒加载(需求/跟进/带看/配房) | HTML | | `/client/{id}/fragments/contact-panel/` | GET | 联系人面板局刷 | HTML | | `/client/{id}/fragments/staff-panel/` | GET | 相关员工面板局刷 | HTML | | `/client/{id}/fragments/follow-timeline/` | POST | 跟进筛选后局刷 | HTML | > fragment 端点必须校验 `HX-Request=true`,非 HTMX 请求返回 400。 ## 5.3 JSON API(P0 必需) | 端点 | 方法 | 权限 code(建议) | 说明 | |---|---|---|---| | `/api/client/` | POST | `client.private.create.allow` | 新增私客(US-001) | | `/api/client/list/query/` | POST | `client.private.view.scope` | 私客列表筛选(US-002) | | `/api/client/{id}/detail/` | GET | `client.private.view.scope` | 详情聚合(US-004) | | `/api/client/{id}/requirements/` | PATCH | `client.private.edit.allow` | 编辑需求(US-005) | | `/api/client/{id}/follow-logs/` | POST | `client.private.follow.create.allow` | 写入跟进(US-006) | | `/api/client/{id}/follow-logs/query/` | POST | `client.private.follow.view.scope` | 跟进查询(US-006) | | `/api/client/{id}/viewings/` | POST | `client.private.viewing.create.allow` | 新增带看(US-007) | | `/api/client/{id}/viewings/query/` | POST | `client.private.viewing.view.scope` | 带看查询(US-007) | | `/api/client/{id}/contacts/` | POST | `client.private.contact.create.allow` | 新增联系人(US-008) | | `/api/client/{id}/contacts/{contact_id}/` | PATCH | `client.private.contact.edit.allow` | 编辑联系人(US-008) | | `/api/client/{id}/grade/change/` | POST | `client.private.grade.change.allow` | 改等级(US-009) | | `/api/client/{id}/status/change/` | POST | `client.private.status.change.allow` | 改状态(US-010) | | `/api/client/{id}/transfer-public/` | POST | `client.private.transfer_public.allow` | 手动转公客(US-011) | | `/api/client/{id}/transfer-transacted/` | POST | `client.private.transfer_transacted.allow` | 转成交(US-012) | | `/api/client/{id}/mark-invalid/` | POST | `client.private.mark_invalid.allow` | 转无效(US-013) | | `/api/client/{id}/base-info/` | PATCH | `client.private.edit.allow` | 编辑基础信息(US-014) | | `/api/client/{id}/related-staff/` | PATCH | `client.private.related_staff.edit.allow` | 修改首录/归属人(US-015) | | `/api/client/duplicate/check/` | POST | `client.private.create.allow` | 手机号实时重复检测(US-001/017) | | `/api/client/duplicate/summary/` | GET | `client.private.view.scope` | 重复统计(私客-成交、公客)(US-017) | | `/api/client/matches/{id}/query/` | GET | `client.private.view.scope` | 智能配房结果查询(US-020预留,P0可简版) | | `/api/client/export/jobs/` | POST | `client.private.export.scope` | 导出任务创建(US-002) | | `/api/client/export/jobs/{job_id}/` | GET | `client.private.export.scope` | 导出任务状态 | | `/api/client/export/jobs/{job_id}/download/` | GET | `client.private.export.scope` | 导出下载 | --- ## 六、关键 API 规范(请求/响应) ## 6.1 新增私客 `POST /api/client/` ```json { "contacts": [ { "name": "李雷", "gender": "male", "phone_country_code": "+86", "phone": "13800000000", "phone2": null, "wechat": "lilei_wechat" } ], "base_info": { "status": "buying", "property_usage": "residential", "grade": "B", "source": "store_reception" } } ``` 成功 `201`: ```json { "id": "uuid", "client_no": "KY202604270001", "redirect_url": "/client/uuid/", "message": "保存成功" } ``` ## 6.2 列表查询(Keyset) `POST /api/client/list/query/` ```json { "client_type": "private", "tab": "buying", "keyword": "1380000", "filters": { "grade": ["A", "B"], "status": ["buying", "buy_rent"], "budget": {"min": 200, "max": 500} }, "sort": {"field": "last_follow_at", "order": "desc"}, "pagination": {"mode": "keyset", "cursor": "opaque_cursor", "limit": 20} } ``` ## 6.3 手机号重复检测 `POST /api/client/duplicate/check/` ```json { "phone_country_code": "+86", "phone": "13800000000", "scene": "create", "client_id": null } ``` 响应: ```json { "duplicated": true, "scope": "dept", "hits": [ { "client_id": "uuid", "client_type": "private", "owner_name": "王店长", "created_at": "2026-04-27T10:00:00+08:00" } ] } ``` ## 6.4 状态与转化类接口(统一协议) - 改状态:`/status/change/` - 转公:`/transfer-public/` - 转成交:`/transfer-transacted/` - 转无效:`/mark-invalid/` 统一请求字段: ```json { "reason": "客户需求变化", "payload": {} } ``` 额外示例(转成交): ```json { "reason": "已签约", "payload": { "transacted_date": "2026-04-27", "transacted_price": "365.00", "transacted_type": "bought", "property_id": "uuid" } } ``` --- ## 七、HTMX 交互约定 ## 7.1 Header 约定 - 请求:`HX-Request: true` - 成功:`HX-Trigger: {"toast":{"level":"success","message":"操作成功"}}` - 失败:`HX-Trigger: {"toast":{"level":"error","message":"操作失败"}}` - 跳转:`HX-Redirect: /client/{id}/` ## 7.2 模板分片命名 | 场景 | 模板 | |---|---| | 列表 | `templates/client/fragments/list_table.html` | | 重复计数条 | `templates/client/fragments/repeat_counters.html` | | 跟进时间线 | `templates/client/fragments/follow_timeline.html` | | 带看时间线 | `templates/client/fragments/viewing_timeline.html` | | 联系人侧栏 | `templates/client/fragments/contact_panel.html` | | 相关员工侧栏 | `templates/client/fragments/staff_panel.html` | --- ## 八、权限与数据范围 ## 8.1 最小权限矩阵(P0) | 能力 | 权限 code | |---|---| | 查看私客列表范围 | `client.private.view.scope` | | 新增私客 | `client.private.create.allow` | | 编辑私客信息 | `client.private.edit.allow` | | 写跟进 | `client.private.follow.create.allow` | | 查看跟进 | `client.private.follow.view.scope` | | 管理联系人 | `client.private.contact.create.allow` / `client.private.contact.edit.allow` | | 改等级/改状态 | `client.private.grade.change.allow` / `client.private.status.change.allow` | | 转公/转成交/转无效 | `client.private.transfer_public.allow` / `client.private.transfer_transacted.allow` / `client.private.mark_invalid.allow` | | 导出 | `client.private.export.scope` | ## 8.2 数据范围叠加逻辑 最终可见数据 = `权限 scope` ∩ `client_type 过滤` ∩ `业务状态过滤` - 禁止不带 `client_type` 的全量查询 - 私客、公客、成交客必须分流查询 ## 8.3 敏感信息查看审计 查看号码动作必须: 1. 校验查看权限与日限额(若配置) 2. 仅本次响应返回明文 3. 自动写 `client_follow_logs(log_type='sensitive_view')` 4. `sensitive_view` 记录不可删除 --- ## 九、异步任务与缓存策略 ## 9.1 状态机与规则 - `private -> public`(手动转公或自动转公) - `private -> transacted`(转成交) - `private/public/transacted -> invalid`(状态字段,不改变历史) 必须通过 service 校验状态机,禁止跳转。 ### 最低规则(P0) - `buying <-> buy_rent` - `renting <-> buy_rent` - `buying/renting/buy_rent -> paused` - `paused -> buying/renting` - 任意可跟进状态 -> `invalid`(必须填写原因) ## 9.2 Celery 任务 | 任务 | 触发时机 | 说明 | |---|---|---| | `client_auto_transfer_public_task` | 每小时 | 超时未跟进私客自动转公(US-016) | | `client_duplicate_summary_task` | 每日/按需 | 重复统计(US-017) | | `client_match_recompute_task` | 需求变更后 | 重新计算配房结果 | | `client_export_task` | 导出任务创建后 | 异步导出 Excel | > 所有任务必须传入 `tenant_schema_name` 并在任务开头切 schema。 ## 9.3 Redis Key 规范 | Key | TTL | 说明 | |---|---|---| | `{schema}:client:list:query:{hash}` | 60s | 热门列表筛选缓存 | | `{schema}:client:repeat:summary` | 300s | 顶部重复计数缓存 | | `{schema}:client:detail:{id}` | 120s | 详情聚合缓存 | | `{schema}:client:export:{job_id}` | 24h | 导出任务状态 | --- ## 十、性能与可靠性约束 1. 客源列表、跟进时间线全部使用 Keyset 分页。 2. 高频筛选列建立组合索引(见数据模型文档)。 3. 手机号、证件号统一加密 + hash 索引;禁止明文。 4. 附件上传限制:`bmp/jpg/jpeg/png/gif`,20MB/文件。 5. 导出、重复统计、自动转公均走异步,禁止阻塞请求线程。 --- ## 十一、安全与合规 1. 手机号、证件号默认脱敏显示,明文查看需权限与审计。 2. 跟进/转化等关键动作必须记录操作者与来源。 3. 重复检测接口不得泄露超出权限范围的完整客户信息。 --- ## 十二、错误码建议 | code | HTTP | 场景 | |---|---|---| | `CLIENT_NOT_FOUND` | 404 | 客源不存在或无权限 | | `CLIENT_DUPLICATED_PHONE` | 409 | 号码重复(按租户规则) | | `CLIENT_INVALID_TRANSITION` | 400 | 非法状态/类型流转 | | `CLIENT_CONTACT_PHONE_INVALID` | 400 | 联系人电话格式错误 | | `CLIENT_PERMISSION_DENIED` | 403 | 权限不足 | | `CLIENT_EXPORT_JOB_NOT_READY` | 409 | 导出未完成 | --- ## 十三、测试映射(P0) | US | 最低覆盖 | |---|---| | US-CLIENT-001 | 新增成功/必填失败/重复检测提示 | | US-CLIENT-002 | 组合筛选 + Keyset 分页 + 导出 | | US-CLIENT-003 | 批量操作权限与审计 | | US-CLIENT-004~006 | 详情/需求/跟进流程 | | US-CLIENT-007~008 | 带看与联系人新增编辑 | | US-CLIENT-009~013 | 等级/状态/转公/转成交/转无效 | | US-CLIENT-014~015 | 编辑完整信息 + 相关员工变更 | | US-CLIENT-016 | 自动转公定时任务 | | US-CLIENT-017 | 重复统计任务与查询接口 | 测试落点:`tests/integration/client/test_us_client.py` --- ## 十四、落地顺序建议 1. 列表查询 + scope 权限(US-002) 2. 新增私客 + 重复检测(US-001/017) 3. 详情 + 需求/跟进(US-004~006) 4. 联系人/带看(US-007/008) 5. 状态与转化(US-009~013) 6. 自动任务与导出(US-016/017 + 导出) --- ## 十五、文档同步规则 - 枚举变更同步:`DATA_MODEL/ENUMS.md` - 权限 code 变更同步:`DATA_MODEL/DATA_MODEL_PERMISSION.md` - 新增配置项同步:`DATA_MODEL/DATA_MODEL_SETTING.md` - API 变更同步:本文件 + 对应 PRD 验收条目