> **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) **关联枚举字典**: `DATA_MODEL/ENUMS.md` **最后更新**: 2026-04-27 --- ## 一、文档定位与边界 本文件只定义房源模块的: 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/...` - 数据 API(JSON):`/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 API(P0) | 端点 | 方法 | 鉴权 | 权限码(建议) | 说明 | |---|---|---|---|---| | `/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 的能力(如地图找房、商业地产)只能写“预留”,不得提前实现