Files
nexus/Project/fonrey/TECH_STACK/房源管理技术方案.md
2026-04-30 20:33:51 +08:00

452 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
> **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`v2.1
**关联数据模型**: `DATA_MODEL/DATA_MODEL_PROPERTY.md`(本方案不重复 DDL
**关联契约规范**: `TECH_STACK/API_CONTRACT.md`(全局 API 契约权威)
**关联枚举字典**: `DATA_MODEL/ENUMS.md`
**最后更新**: 2026-04-27
---
## 变更历史
| 日期 | 变更人 | 变更内容 |
|---|---|---|
| 2026-04-30 | Atlas | 补充“变更历史”章节(文档治理) |
## 一、文档定位与边界
本文件只定义房源模块的:
1. 服务边界与模块协作
2. API 端点设计(重点)
3. HTMX 局刷协议
4. 权限接入、异步任务、缓存与性能约束
5. 测试与验收映射
> **不在本文件展开**表结构字段、索引、DDL、触发器。请以 `DATA_MODEL_PROPERTY.md` 为唯一权威来源。
---
## 二、范围定义(以 TASK.md 为准)
本方案覆盖 `PRD/TASK.md` 中房源 P0 User Story
- US-PROPERTY-001录入二手住宅出售/出租)
- US-PROPERTY-002查看与筛选房源列表
- US-PROPERTY-003查看房源详情
- US-PROPERTY-004写入与查看跟进记录
- US-PROPERTY-005管理房源图片上传/分类/排序)
- US-PROPERTY-006管理业主联系人
- US-PROPERTY-007调整房源价格
- US-PROPERTY-008变更房源状态
---
## 三、模块架构边界
## 3.1 模块职责
`apps/property` 负责:
- 房源主流程:新增、列表检索、详情展示、状态与价格维护
- 房源协作数据:联系人、跟进、图片
- 房源审计轨迹:挂牌历史、调价记录、敏感查看记录(通过服务层写入)
## 3.2 外部依赖
| 依赖模块 | 依赖内容 | 依赖方式 |
|---|---|---|
| `apps/complex` | 小区/楼盘基础信息、联想搜索 | Service 调用 + 外键引用 |
| `apps/org` | 员工组织信息(相关方、操作人) | 外键 + Service 查询 |
| `apps/permission` | RBAC/Scope 权限判定 | PermissionChecker + ScopeQueryBuilder |
| `apps/setting` | 可配置枚举(来源、跟进目的等) | lookup_items 读取缓存 |
| `core/encryption.py` | 手机号加密/脱敏/哈希 | 统一工具调用,禁止明文 |
| `core/cache.py` | 筛选缓存、任务进度、短期详情缓存 | Redis Key 带租户前缀 |
| `Celery` | 导出、图片处理、完成度重算 | 异步任务 |
| `Cloudflare R2` | 房源图片与附件对象存储 | 预签名上传 + 回写元数据 |
## 3.3 分层约束(必须遵守)
- `views.py` 仅做:参数校验、权限门禁、调用 service、组织响应
- 业务规则全部下沉到 `services/`
- 所有写操作必须落审计(至少 follow_logs 或 change_logs
- 耗时 >500ms 的流程必须异步化
---
## 四、API 设计原则
1. **页面端点与数据端点分离**
- 页面SSR + HTMX 容器):`/property/...`
- 数据 APIJSON`/api/property/...`
2. **HTMX 局刷优先**列表筛选、Tab 内容、弹窗提交走局部刷新。
3. **列表性能目标**89k 房源规模下,`p95 < 2s`(索引 + Keyset 分页 + 预加载)。
4. **统一错误协议**
- JSON`{"error":"...","code":"SNAKE_CASE"}`
- HTMX返回片段 + `HX-Trigger` Toast
5. **权限前置**:所有 API 在 service 执行前完成权限与 scope 过滤。
6. **敏感信息最小暴露**:默认打码;明文查看必须具备权限并记录审计。
---
## 五、端点清单(核心)
## 5.1 页面路由SSR + HTMX 容器)
| 路径 | 方法 | 鉴权 | 说明 |
|---|---|---|---|
| `/property/list/` | GET | 是 | 房源列表主页面(包含筛选栏与列表容器) |
| `/property/create/` | GET | 是 | 新增房源页面P0: 住宅) |
| `/property/{property_id}/` | GET | 是 | 房源详情主页面(多 Tab 容器) |
| `/property/{property_id}/edit/` | GET | 是 | 编辑房源页面(非弹窗) |
## 5.2 HTMX 片段端点
| 路径 | 方法 | 用途 | 返回 |
|---|---|---|---|
| `/property/fragments/list-table/` | GET | 列表筛选/排序/翻页局刷 | HTML 片段 |
| `/property/fragments/filter-panel/` | GET | 筛选项联动刷新(区域→商圈等) | HTML 片段 |
| `/property/{id}/fragments/tab/{tab_name}/` | GET | 详情页 Tab 懒加载(跟进/相册/附件等) | HTML 片段 |
| `/property/{id}/fragments/contact-panel/` | GET | 联系人侧栏局刷 | HTML 片段 |
| `/property/{id}/fragments/follow-timeline/` | GET | 跟进时间线局刷(筛选后) | HTML 片段 |
| `/property/{id}/fragments/photo-grid/` | GET | 相册宫格局刷 | HTML 片段 |
> 所有 fragment 端点必须校验 `HX-Request=true`,非 HTMX 请求返回 400。
## 5.3 JSON APIP0
| 端点 | 方法 | 鉴权 | 权限码(建议) | 说明 |
|---|---|---|---|---|
| `/api/property/` | POST | 是 | `property.create.allow` | 新增房源US-001 |
| `/api/property/list/query/` | POST | 是 | `property.list.view.scope` | 列表查询US-002 |
| `/api/property/{id}/detail/` | GET | 是 | `property.list.view.scope` | 详情聚合数据US-003 |
| `/api/property/{id}/status/change/` | POST | 是 | `property.status.change.allow` | 改状态US-008 |
| `/api/property/{id}/price/change/` | POST | 是 | `property.price.change.allow` | 调价US-007 |
| `/api/property/{id}/follow-logs/` | POST | 是 | `property.follow.create.allow` | 新增跟进US-004 |
| `/api/property/{id}/follow-logs/query/` | POST | 是 | `property.follow.view.scope` | 跟进查询US-004 |
| `/api/property/{id}/contacts/` | POST | 是 | `property.contact.create.allow` | 新增联系人US-006 |
| `/api/property/{id}/contacts/{contact_id}/` | PATCH | 是 | `property.contact.edit.allow` | 编辑联系人US-006 |
| `/api/property/{id}/contacts/same-owner/` | GET | 是 | `property.list.view.scope` | 同业主其他房源US-006 |
| `/api/property/{id}/owner-phone/view/` | POST | 是 | `property.owner_phone.view.daily_limit` | 查看号码(审计+额度) |
| `/api/property/{id}/photos/upload-token/` | POST | 是 | `property.photo.upload.allow` | 获取 R2 预签名US-005 |
| `/api/property/{id}/photos/commit/` | POST | 是 | `property.photo.upload.allow` | 上传回执写库US-005 |
| `/api/property/{id}/photos/reorder/` | POST | 是 | `property.photo.edit.allow` | 图片排序US-005 |
| `/api/property/{id}/photos/category/batch/` | POST | 是 | `property.photo.edit.allow` | 批量改分类US-005 |
| `/api/property/{id}/photos/{photo_id}/set-cover/` | POST | 是 | `property.photo.edit.allow` | 设封面US-005 |
| `/api/property/export/jobs/` | POST | 是 | `property.list.export.scope` | 创建导出任务US-002 |
| `/api/property/export/jobs/{job_id}/` | GET | 是 | `property.list.export.scope` | 查询导出任务状态 |
| `/api/property/export/jobs/{job_id}/download/` | GET | 是 | `property.list.export.scope` | 下载导出结果 |
---
## 六、关键 API 规范(请求/响应)
## 6.1 新增房源
`POST /api/property/`
```json
{
"property_type": "residential",
"trade_type": "sale_rent",
"complex_id": "uuid",
"building_no": "5",
"unit_no": "2",
"room_no": "1102",
"floor_current": 11,
"floor_total": 33,
"area": "89.50",
"layout": {"bedroom": 3, "living": 2, "bathroom": 2, "kitchen": 1, "balcony": 1},
"sale_price": "368.00",
"rent_price": null,
"owner_contacts": [
{
"name": "张三",
"identity": "owner",
"phone": "13800000000",
"gender": "male"
}
],
"related_staff": {
"first_agent_id": "uuid",
"number_agent_id": "uuid",
"sale_agent_id": "uuid"
}
}
```
成功:`201`
```json
{
"id": "uuid",
"code": "FY202604270001",
"redirect_url": "/property/uuid/",
"message": "保存成功"
}
```
## 6.2 列表查询
`POST /api/property/list/query/`
```json
{
"keyword": "阳光花园",
"filters": {
"district_ids": ["uuid"],
"status": ["sale", "rent"],
"attribute": ["public"],
"price_sale": {"min": 200, "max": 500}
},
"sort": {"field": "updated_at", "order": "desc"},
"pagination": {"mode": "keyset", "cursor": "opaque_cursor", "limit": 20}
}
```
成功:`200`
```json
{
"items": [{"id": "uuid", "title": "阳光花园 5-2-1102", "status": "sale"}],
"next_cursor": "opaque_cursor_2",
"total_estimate": 89432
}
```
## 6.3 改状态
`POST /api/property/{id}/status/change/`
```json
{
"from_status": "sale",
"to_status": "paused",
"reason": "业主暂不出售"
}
```
校验规则:
- 必须符合状态机(例如 sale -> paused/other_sale/deal
- reason 必填,<= 50 字
## 6.4 调价
`POST /api/property/{id}/price/change/`
```json
{
"sale_price": "350.00",
"bottom_price": "338.00",
"reason": "业主急售"
}
```
成功后联动:
- 更新当前挂牌价
- 追加 `price_changes` 记录
- 触发详情页价格区域局刷
## 6.5 写跟进
`POST /api/property/{id}/follow-logs/`
```json
{
"follow_type": "write_follow",
"purpose": "owner_follow",
"content": "今日电话沟通业主接受350万附近报价。",
"visibility": "team",
"attachments": ["object_key_1", "object_key_2"]
}
```
校验规则:
- content: 6~500 字
- 附件图片最多 10 张,单张 <=20MB
## 6.6 导出任务
1) 创建:`POST /api/property/export/jobs/`
```json
{"query_snapshot": {...}, "columns": ["code", "title", "status", "sale_price"]}
```
2) 查询:`GET /api/property/export/jobs/{job_id}/`
```json
{"status":"running", "progress":65}
```
3) 下载:`GET /api/property/export/jobs/{job_id}/download/`
- 任务完成后返回一次性下载 URL
---
## 七、HTMX 交互约定(房源模块)
## 7.1 请求头与响应头
- 请求必须带:`HX-Request: true`
- 成功提示:`HX-Trigger: {"toast":{"level":"success","message":"保存成功"}}`
- 失败提示:`HX-Trigger: {"toast":{"level":"error","message":"保存失败"}}`
- 跳转:`HX-Redirect: /property/{id}/`
## 7.2 片段模板命名
| 场景 | 模板 |
|---|---|
| 列表表格 | `templates/property/fragments/list_table.html` |
| 跟进时间线 | `templates/property/fragments/follow_timeline.html` |
| 联系人面板 | `templates/property/fragments/contact_panel.html` |
| 相册宫格 | `templates/property/fragments/photo_grid.html` |
## 7.3 推荐前端触发方式
- 筛选提交:`hx-post="/property/fragments/list-table/"`
- 切 Tab`hx-get="/property/{id}/fragments/tab/follow/"`
- 弹窗提交:`hx-post` + `hx-target` 当前弹窗容器,成功后触发父容器刷新
---
## 八、权限与数据范围设计
## 8.1 P0 必需权限项(最小集合)
> 与 `DATA_MODEL_PERMISSION.md` 对齐;若 `permission_defs` 尚未落库,则按下列 code 补种子。
| 权限 code | 类型 | 说明 |
|---|---|---|
| `property.list.view.scope` | SCOPE | 查看房源范围 |
| `property.list.export.scope` | SCOPE | 导出房源范围 |
| `property.create.allow` | BOOLEAN | 新增房源 |
| `property.list.edit.allow` | BOOLEAN | 编辑房源基础信息 |
| `property.price.change.allow` | BOOLEAN | 调价 |
| `property.status.change.allow` | BOOLEAN | 改状态 |
| `property.follow.view.scope` | SCOPE | 查看跟进范围 |
| `property.follow.create.allow` | BOOLEAN | 新增跟进 |
| `property.contact.create.allow` | BOOLEAN | 新增联系人 |
| `property.contact.edit.allow` | BOOLEAN | 编辑联系人 |
| `property.photo.upload.allow` | BOOLEAN | 上传图片 |
| `property.photo.edit.allow` | BOOLEAN | 改分类/排序/封面 |
| `property.owner_phone.view.daily_limit` | INTEGER | 每日查看号码上限0=不限制) |
## 8.2 Scope 与业务属性叠加
最终查询范围 = `权限 scope 过滤``业务状态过滤``房源属性规则(公盘/私盘/特盘/封盘)`
- 权限系统不直接改写 `properties.attribute`
- 房源属性由业务规则决定可见性,权限只决定“理论上可看范围”
## 8.3 查看号码审计
调用 `/owner-phone/view/` 时必须:
1. 校验 `daily_limit`
2. 解密返回明文(仅本次)
3. 自动写入 `follow_logs``sensitive_view`
4. `sensitive_view` 记录不可删除
---
## 九、异步任务与缓存策略
## 9.1 Celery 任务
| 任务 | 触发时机 | 说明 |
|---|---|---|
| `property_export_task` | 创建导出任务后 | 按查询快照导出 Excel 到 R2 |
| `property_photo_postprocess_task` | 图片上传 commit 后 | 生成缩略图、提取元数据 |
| `property_completeness_recalc_task` | 调价/状态变更/跟进/图片更新后 | 异步重算维护完成度 |
> 所有任务参数必须包含 `tenant_schema_name`,任务开头显式切 schema。
## 9.2 Redis Key 规范
| Key | TTL | 说明 |
|---|---|---|
| `{schema}:property:list:query:{hash}` | 60s | 热门筛选结果短缓存 |
| `{schema}:property:detail:{id}` | 120s | 详情聚合缓存 |
| `{schema}:property:export:{job_id}` | 24h | 导出任务状态 |
| `{schema}:property:owner_phone:view:{staff_id}:{date}` | 24h | 每日查看号码计数 |
---
## 十、性能与可靠性约束
1. 列表查询强制 Keyset 分页(禁止 OFFSET
2. 所有筛选字段必须走已建索引(见 `DATA_MODEL_PROPERTY.md`)。
3. 大列表默认返回精简字段,详情页再按 Tab 懒加载。
4. 导出走异步任务,前端轮询任务状态,禁止同步导出。
5. 批量写操作使用事务,失败要回滚并返回结构化错误。
---
## 十一、安全与合规
- 手机号、微信等敏感信息:入库加密、展示脱敏
- API 全链路 HTTPS
- 操作审计必须包含:操作者、时间、旧值/新值、来源 IP
- 文件上传白名单:`bmp/jpg/jpeg/png/gif/svg`P0 与 PRD 对齐)
- 上传大小限制20MB/文件
- 防刷:列表查询、号码查看、导出任务创建均需限流
---
## 十二、错误码建议
| code | HTTP | 场景 |
|---|---|---|
| `PROPERTY_NOT_FOUND` | 404 | 房源不存在或无访问权限 |
| `PROPERTY_INVALID_STATE_TRANSITION` | 400 | 非法状态流转 |
| `PROPERTY_PRICE_INVALID` | 400 | 价格参数非法 |
| `PROPERTY_FOLLOW_CONTENT_TOO_SHORT` | 400 | 跟进内容不足 6 字 |
| `PROPERTY_PHONE_VIEW_LIMIT_EXCEEDED` | 429 | 查看号码超限 |
| `PROPERTY_EXPORT_JOB_NOT_READY` | 409 | 导出未完成即下载 |
| `PROPERTY_PERMISSION_DENIED` | 403 | 权限不足 |
---
## 十三、测试映射P0
| User Story | 最低测试覆盖 |
|---|---|
| US-PROPERTY-001 | 新增成功 / 必填校验失败 / 无权限403 |
| US-PROPERTY-002 | 关键词+组合筛选 / Keyset 分页 / 导出任务创建 |
| US-PROPERTY-003 | 详情加载 / 号码默认脱敏 / 查看号码审计 |
| US-PROPERTY-004 | 跟进写入成功 / 内容长度校验 / 时间线筛选 |
| US-PROPERTY-005 | 上传签名获取 / commit 落库 / 排序与封面设置 |
| US-PROPERTY-006 | 联系人新增编辑 / 同业主房源查询 |
| US-PROPERTY-007 | 调价成功并写历史 / 理由缺失失败 |
| US-PROPERTY-008 | 合法流转成功 / 非法流转失败 |
**测试强制要求**
- 集成测试使用 `TenantClient`
- HTMX 请求必须带 `HTTP_HX_REQUEST=true`
- 权限三态200 / 403 / 302
- Celery 在测试环境 eager 执行
---
## 十四、落地顺序建议(供开发阶段使用)
1. 先搭建列表查询 + Scope 权限过滤US-002
2. 再打通新增与详情主链路US-001,003
3. 上线状态变更与调价US-007,008
4. 补齐跟进、联系人、图片US-004,005,006
5. 最后接入导出异步与性能压测
---
## 十五、文档同步规则
- 枚举变更:同步 `DATA_MODEL/ENUMS.md`
- 权限 code 变更:同步 `DATA_MODEL/DATA_MODEL_PERMISSION.md`
- API 变更:同步本文件与对应 PRD 验收条目
- 超出 P0 的能力(如地图找房、商业地产)只能写“预留”,不得提前实现