# Rental Estimate — 公开租金估算页 `/rental-estimate` 完整说明

> 文档对象:`https://propertylabglobal.com/rental-estimate`(公开,无需登录)
> 代码版本:2026-07-22 · branch `dev`(功能于 2026-07-21 上线)
> 配套文档:`/rental-estimate-admin.md`(管理后台说明)

---

## 1. 这个页面是什么

**租金估算 · 翻新提升** — 一个面向**房东(landlord)**的租金估算工具 + **翻新(renovation)lead-gen** 落地页。

流程:房东定位自己的单位(Google-Maps 式搜索 + 地图拖针)→ 选卧室数 + 面积 + 家私状况 →
确认楼盘名 + 留联系方式 → 系统用**与门户分析同一套租金引擎**(`PropSenseService`,
周边同户型实际整租中位数)算出预估月租区间 → 最后 upsell:「翻新后月租平均可提升 15–35%」,
一键 WhatsApp 联系 PropertyLab 翻新团队。

核心设计:**lead 在第 4 步提交时即入库**(联系方式 + 输入 + 算出的估值一起存),
之后有没有点 WhatsApp 只是转化追踪字段的差别。

- 单页 6 步 wizard,**Alpine.js 3** 驱动,顶部 6 段进度条
- 全中文界面(浅蓝白后台风,字体 Plus Jakarta Sans + Manrope),mobile-first `max-width:560px`
- 底部 sticky 导航条(上一步 / 继续),毛玻璃效果

---

## 2. 文件与路由一览

| 类型 | 位置 |
| --- | --- |
| Routes | `routes/web.php:2965-2968`(public,无 auth) |
| Controller | `app/Http/Controllers/PublicSite/RentalEstimateController.php` |
| View(整页) | `resources/views/public/rental-estimate/index.blade.php` |
| 租金引擎 | `app/Services/InvestHink/PropSenseService`(复用,门户分析同源) |
| Model | `src/InvestHink/RentalEstimateSubmission.php` |
| Migrations | `2026_07_21_000000_create_rental_estimate_submissions_table.php` + `2026_07_21_100000_add_wa_clicked_...` |

### Routes

| Method | URI | Action | Route name | 用途 |
| --- | --- | --- | --- | --- |
| GET | `/rental-estimate` | `page()` | `public.rental-estimate` | 渲染 wizard 页 |
| GET | `/rental-estimate/search?q=` | `search()` | `public.rental-estimate.search` | 楼盘名 autocomplete(JSON) |
| POST | `/rental-estimate/submit` | `submit()` | `public.rental-estimate.submit` | 存 lead + 算租金,返回估值 JSON |
| POST | `/rental-estimate/wa-click` | `waClick()` | `public.rental-estimate.wa-click` | 记录点了 WhatsApp CTA |

### 外部依赖

- **Mapbox GL JS v3.7.0** — 地图(token 从 `config('services.mapbox.token')` 注入;没配 token 页面显示「未配置地图」但流程照常可走)
- **Nominatim**(OpenStreetMap,免 key)— 地区/地址搜索,`countrycodes=my` 限马来西亚
- Alpine.js 3 + Google Fonts(CDN)
- WhatsApp 专线:`601133167831`(controller 常量 `WA_NUMBER`)

---

## 3. 用户流程(6 步 wizard)

前端 Alpine 组件 `rentWizard()`,`step` 1→6,每步都有 `canNext()` 校验,不满足则「继续」按钮禁用。

### 第 1 步 · 定位「你的单位在哪里?」
- 搜索框(350ms debounce,≥2 字符触发)**同时并发查两个源**,结果分组显示:
  - **地点 · Locations**(绿色图标)— Nominatim,最多 4 条,带 Area/City/State 类型标签,按名称去重
  - **楼盘 · Properties**(蓝色图标)— 自家 DB `edgeprop_projects`(`GET /rental-estimate/search`,见 §5),带 `RM xxx psf` 标签
- 选中即取 GPS(6 位小数)+ 地点标签;若「楼盘名称」还没填,自动带入
- Mapbox 地图:选中后落蓝色**可拖动定位针**微调;也可以直接点地图任意处定位(此时清空 place 标签)
- 校验:必须有 lat + lng

### 第 2 步 · 卧室「几间卧室?」
- Pills:1房 / 2房 / 3房 / 4房 / 5房 / Studio(开放式,值为 0)
- 建筑面积 sqft(可留空 — 服务器按房数用默认值:Studio 450 / 1房 650 / 2房 850 / 3房 1100 / 4房 1500 / 5房 2000)

### 第 3 步 · 家私「家私状况?」
- 三张卡:**空屋(无家私)bare** / **部分家私 partial**(厨具+冷气+热水器)/ **全套家私 fully**(拎包入住)

### 第 4 步 · 确认 · 提交「确认楼盘,留下联系方式」
- 楼盘名称输入 + **`<datalist>` autocomplete**(服务器预载最多 ~800 个已知 MY 楼盘名:
  `rental_properties`(未删、非空)∪ `ih_my_projects`(isActive),各取 400、trim、≥3 字符、去纯数字、去重)
- 回显摘要:📍 位置 · 🛏️ 卧室 · 🛋️ 家私 · 面积
- 联系方式:**姓名***、**手机号码***(≥7 位数字)、邮箱(选填)
- 校验:楼盘名 ≥2 字符 + 姓名 + 手机;按「**提交 · 查看租金**」→ `submitLead()`

### 第 5 步 · 市场租金(结果页)
- 提交即跳到本步显示 loading 卡(「正在分析周边同户型实际租金… 约需 5–15 秒」);
  **前端 30 秒 AbortController 超时保护** — 引擎卡住也不会无限转圈,超时显示
  「估算用时较长 —— 我们已收到你的信息,顾问会尽快为你估算并回复。」(lead 服务器端照存)
- **找到估值**(深蓝渐变大卡):预估市场月租 **RM {mid}**、合理区间 RM {low}–{high}/月、
  说明文字(「基于附近 N 套同户型实际租金 · 中位数已按家私状况调整」)
- **样本不足**(灰色卡):「附近同户型样本不足 — 顾问为你精准估算」
- 下方「**周边同户型租金**」对比列表(最多 80 套,按距离排序):楼盘名 + 类型 + 完工年份、
  距离(<1km 显示米)、RM 月租;注明「数据源:PropSense 周边实际整租(3 公里内)· 与门户分析同源」

### 第 6 步 · 翻新 upsell「想让租金更上一层楼?」
- 绿色徽章「↑ 翻新后,月租平均可提升 15–35%」+ PropertyLab 翻新团队介绍(设计、报价到施工一站式)
- 绿色 WhatsApp 大按钮「想翻新,联系我们」(底部导航也有「立即咨询翻新」):
  - 打开 `wa.me/601133167831`,消息**预填**:楼盘、卧室、面积、家私、位置(Google Maps 链接)、预估月租区间、「想了解如何通过翻新提高我的租金」
  - 同时 `waClick()` → POST `/rental-estimate/wa-click`(`keepalive:true`,页面跳走也能送达)记录转化

---

## 4. 租金估算引擎(`computeEstimate()`)

复用 **`PropSenseService::analyze()`** — 与门户 `/portal/dashboard/{id}#rental` 分析**同一数据源**。

1. 面积 <100 sqft 视为空,按房数取 `DEFAULT_SIZE`;layout = `{beds}B` 或 `studio`;
   type 固定 `condo`;price 参数用占位值(`size × 1000` — 租金中位数与价格无关,只为满足接口)
2. 取引擎返回的 `whole_unit_rental`(周边同户型整租 comparable),**过滤**:
   租金 <RM400 或 >RM40,000 或 距离 >3km 的剔除
3. comparable 列表按距离排序,截前 80 条返回前端展示
4. **样本 ≥3 套**才出估值:
   - `mid` = 中位数,`low` = 20 百分位,`high` = 80 百分位
   - 乘**家私系数**:bare ×0.85 / partial ×1.0 / fully ×1.15
   - 全部**四舍五入到 RM50** 的倍数
5. 样本 <3 → `found: false`,提示顾问 24 小时内精准估算(有少量样本仍会列出参考)
6. 引擎抛异常 → catch + `Log::warning('rental-estimate PropSense failed')`,返回「暂时无法读取周边租金数据」——**页面不炸,lead 照存**

---

## 5. 后端逻辑摘要

### `search()` — GET `/rental-estimate/search?q=`
- q <2 字符返回 `[]`
- 查 `edgeprop_projects`:`project_name` 或 `area` LIKE 匹配,必须有经纬度,
  **有 psf_median 的排前面**,按名称排序,**最多 6 条**
- 返回:`{name, subtitle(area, state), psf, lat, lng}`

### `submit()` — POST `/rental-estimate/submit`
1. 校验:必须有 lat/lng(422「请先在第一步定位你的单位」)、姓名 + 手机(422「请填写姓名和手机号码」);furnish 只认 bare/partial/fully,否则回落 partial
2. 先 `computeEstimate()` 算租金
3. **入库 `RentalEstimateSubmission`**(联系方式、楼盘名、place、GPS、beds/size/furnish、
   est_found/low/mid/high、comps_count、IP)— **写库 try/catch 包裹**:存失败只记 log,估值照样返回给用户(体验优先)
4. 返回 `{ok, id, found, mid, low, high, count, comps, note, furnish, layout}` — 前端存 `id` 供 wa-click 用

### `waClick()` — POST `/rental-estimate/wa-click`
- 按 id 找提交:`wa_clicked_at = now()`,`wa_clicks` +1(可累计多次点击)
- try/catch,永远返回 `{ok: true}`

---

## 6. 数据库

**Table `rental_estimate_submissions`**(model `Src\InvestHink\RentalEstimateSubmission`):

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `id` | bigIncrements | |
| `name` | string(120), nullable | 联系人姓名 |
| `email` | string(190), nullable | 选填 |
| `phone` | string(40), nullable | 手机 |
| `property_name` | string(255), nullable | 用户确认的楼盘名 |
| `place` | string(255), nullable | 选中的定位标签 |
| `lat` / `lng` | decimal(10,7), nullable | GPS |
| `beds` | integer, nullable | 0 = Studio |
| `size` | integer, nullable | 建筑面积 sqft |
| `furnish` | string(20), nullable | bare / partial / fully(常量 `FURNISH` 有中文名映射) |
| `est_found` | boolean, default false | 是否算出估值 |
| `est_low` / `est_mid` / `est_high` | integer, nullable | 月租区间(RM,已按家私调整、取整到 50) |
| `comps_count` | integer, nullable | 参与计算的样本数 |
| `ip` | string(45), nullable | 提交 IP |
| `wa_clicked_at` | timestamp, nullable | 首次点 WhatsApp CTA 时间 |
| `wa_clicks` | unsignedInteger, default 0 | 累计点击次数 |
| `created_at` / `updated_at` | timestamps(created_at 有 index) | |

---

## 7. 值得注意的实现细节 / 已知限制

- **提交按钮 = 转化闸门**:看估值必须先留姓名 + 手机 — 这是 lead-gen 的核心设计。
- **双保险防丢 lead**:服务器端「先算后存、存失败照样返回」,前端「30 秒超时照样收尾」— 任何一环失败都不会让用户卡死或 lead 消失。
- **`wa-click` 无所有权校验**:知道 id 就能给任意提交 +1 点击计数(危害仅限统计字段)。
- **无防重复提交** / 无 rate-limit — 同一人可多次提交,后台不做去重(与 property-match admin 的按人合并不同)。
- Nominatim 是免费公共服务,搜地址偶尔慢/限流;楼盘名搜索走自家 DB 不受影响。
- Mapbox token 缺失时地图隐藏,但仍可通过搜索结果取得 GPS 正常走完流程。
- 估算参数写死:type=condo、利率 4.5%、35 年贷款(引擎接口需要,但对租金中位数无影响)。
- 页面 SEO:有 `<meta name="description">`(免费估算市场月租 + 翻新 CTA)。
