docs/api目录文档分类和补全 --------- Co-authored-by: guke <guke@autohome.com.cn> Reviewed-on: #111
8.6 KiB
H2b — 首页 feed 真接入设计(H5 端 + 安卓 getLocation)
仓库:
shaguabijia-app-server(分支feat/h5-sgbridge-gk)+shaguabijia-app-android日期:2026-06-30 里程碑:H5-WebView 改造方案 M3(首页)子项 H2b 配套:docs/H5-WebView改造方案.md、原生参考ui/home/HomeViewModel.kt+HomeScreen.kt
1. 目标
把 h5/home/index.html 里写死的 7 张 mock feed 卡换成真实 /api/v1/meituan/* 数据,三个排序 tab(智能推荐 / 距离最近 / 销量最高)接通后端,「抢」按钮接换链拉起美团 —— 忠实移植原生 HomeViewModel + HomeScreen 的 feed 逻辑(全功能,非精简版)。
已确认决策:
- D1:本轮一并接安卓真实定位 —— 安卓
SGBridge.kt加getLocation(),H5 据此拿经纬度;拿不到落北京默认。 - D2:对齐原生全功能 —— 触底翻页 + 每 tab 缓存 + 三态 + 去重 + 防竞态 + 未授权不打接口。
2. 范围(两仓改动;后端零改动)
| 仓 | 文件 | 改动 |
|---|---|---|
| app-server | h5/shared/bridge.js |
新增 getLocation()(查询类,同步返 {longitude, latitude};无桥 mock 北京) |
| app-server | h5/home/index.html |
feed 模块重写:渲染真实卡 + 三 tab 接端点 + 翻页/缓存/三态 + 抢按钮 |
| android | ui/webview/SGBridge.kt |
新增 @JavascriptInterface fun getLocation(): String(返宿主缓存的 lastLocation JSON,空串=拿不到) |
| android | ui/webview/WebViewScreen.kt |
取 FusedLocationProviderClient.lastLocation(3s 超时)缓存,经构造器 locationProvider 注入 SGBridge |
后端不动: /api/v1/meituan/feed、/top-sales、/referral-link 及 CouponCard 均已就绪。
3. 数据契约(已存在,只读)
POST /api/v1/meituan/feed{longitude, latitude, page, page_size?, tab}→{items: CouponCard[], has_next, page, status}tab="rec"智能推荐(离线库,佣金≥3%,分页);tab="distance"距离最近(实时打美团,一次性返回has_next=false)。
POST /api/v1/meituan/top-sales{page, page_size?, platform?}→{items, has_next, status}(销量最高,离线库,不传经纬度)。POST /api/v1/meituan/referral-link{product_view_sign, platform, biz_line, link_type_list:[1,3]}→{link, link_map}(link_map["3"]=deeplink,["1"]=H5 长链)。- 三个端点均不鉴权;
status:ok有数据 /empty暂无 /degraded上游失败已降级。
4. tab → 端点映射(对齐 HomeViewModel.FeedSort + loadPage)
tab(data-key) |
label | 端点 | 备注 |
|---|---|---|---|
rec(默认激活) |
智能推荐 | /feed tab=rec |
传经纬度;离线库,坐标实际不影响结果 |
distance |
距离最近 | /feed tab=distance |
传经纬度;一次性,has_next=false 天然不触底翻页 |
sales |
销量最高 | /top-sales |
不传经纬度;离线库按销量降序 |
现有点击处理器(行 6664)里的
asc/desc方向翻转是 mock 残留,后端不支持方向 → 删除,改为真实拉数据。
5. 定位坐标流程(D1)
bridge.js getLocation()(新增,查询类同步):
function getLocation() {
if (hasNative && native.getLocation) {
var o = safeParse(native.getLocation(), null);
if (o && typeof o.longitude === 'number' && typeof o.latitude === 'number') return o;
return null; // 原生拿不到(空串/未授权)→ null,调用方落默认
}
return { longitude: 116.404, latitude: 39.928 }; // 浏览器 mock:北京
}
H5 home:
- 进页 /
visibilitychange可见时取var c = SGBridge.getLocation() || {longitude:116.404, latitude:39.928}(北京默认,对齐原生HomeViewModel初值)。 - 坐标变化 → 清空所有 tab 缓存并重拉当前 tab 第一页(对齐
updateLocation)。
android SGBridge.getLocation(): String: 返宿主缓存的 {"longitude":..,"latitude":..} 或 ""。@JavascriptInterface 同步、不能等异步 GPS,故由 WebViewScreen 预取 FusedLocationProviderClient.lastLocation(镜像 HomeScreen.awaitLastLocation,3s 超时)缓存到 remember 状态,经构造器 locationProvider: () -> Pair<Double,Double>? 注入。未授权 / 拿不到 → ""(H5 落北京默认,rec/sales 不受影响,distance 暂北京中心,回前台拿到后刷新转真实)。
6. 卡片渲染(1:1 照 FeedCard HomeScreen.kt:1694,复用现有 .feed-card 标记/CSS)
| 卡片元素 | CSS | CouponCard 字段 | 规则 |
|---|---|---|---|
| 头图 | .feed-img img@src |
head_image_url |
直用(后端已缩放转 WebP) |
| 品类徽章 | .feed-cat-badge |
platform |
1→「外卖」cat-waimai;否则→「团购」cat-tuangou |
| 店名 | .feed-title .shop-name |
brand_name |
空则不渲染店名 + 分隔符「|」 |
| 标题 | .feed-title(分隔符后) |
name + available_poi_num |
name+(available_poi_num? 「|N店通用」);CSS 已 2 行省略 |
| 距离·店名 | .feed-meta .meta-left |
distance_text + poi_name |
distanceText+" "+poiName(rec 无距离→仅店名) |
| 销量 | .feed-meta .meta-right |
sale_volume |
直用(可空,空则不渲染) |
| 优惠标签 | .feed-tag |
price_label / rank_label |
二者非空中随机取一(按 sign 固定);都空则不渲染 |
| 现价 | .feed-price |
sell_price |
splitPrice→整数 + 小数;¥+整数+<small>.x</small> |
| 原价 | .feed-original |
original_price |
¥{original_price}(删除线) |
| 抢 | .feed-grab |
— | 点击→换链→openMeituan |
⚠️ 与 mock 的差异(随原生): mock 卡的
.feed-tag显示「比团购省 X 元」,但原生FeedCard只用price_label/rank_label,不展示「比团购省」。已确认(2026-06-30):随原生去掉「比团购省」。通则:原生实现与 H5 原型冲突时,一律以原生为准(原生是已上线、经产品校准的真实行为)。
7. 列表行为(照 HomeViewModel)
- 翻页:
currentPage/hasNext,触底(滚到底部阈值)loadNextPage;append时按product_view_sign去重再拼接;distance/sales的has_next由后端控制。feedJob/AbortController在跑则不重入。 - 每 tab 缓存:
{coupons, page, hasNext, loadedAt},TTL 5 分钟。切 tab 命中新鲜缓存 → 直接恢复列表+分页、不转圈;过期/无缓存 → 重置拉第一页。坐标变化清空全部缓存(rec/distance 依赖坐标)。 - 三态(复用 H2a 容器):
loading:首次进 tab 转圈(用feedLoadedOnce区分「首次加载中」vs「已加载但空」,防空态闪现)。empty:已加载且列表空 → 复用#homeFeedNoDealEmpty(「附近暂无…去比价」)。degraded:status=degraded→ 「服务繁忙,下拉重试」提示。
- 防竞态: 切 tab
abort旧请求(旧 tab 响应晚到不覆盖新 tab)。 - 未授权定位: 三 tab 都显示
#homeFeedEmpty(「去授权」),切 tab 只切高亮不打接口(对齐selectTabWithoutLoad,尤其 distance 避免拿默认坐标空打美团)。复用 H2aapplyLocationState联动。
8. 「抢」流程(照 HomeScreen.kt:615 + HomeViewModel.getReferralLink)
点「抢」→ POST /referral-link {product_view_sign, platform, biz_line, link_type_list:[1,3]} → deeplink = link_map["3"],h5 = link_map["1"] || link → SGBridge.openMeituan(deeplink || h5);两者皆空 → SGBridge.toast('暂时无法跳转')。请求中给按钮加 loading/防连点。
9. 调用方式
feed/top-sales/referral-link 均不鉴权 → 用裸 fetch + 相对 /api/v1(对齐 home 现有 /api/v1/platform/* 调法,无需引 api.js)。POST、Content-Type: application/json。
10. 验收 / 测试
- 浏览器(无桥,mock 北京): 三 tab 切换各自拉到数据;触底翻页累加;切回命中缓存不转圈;空/降级态文案正确;点抢 console 打印
openMeituan。 - 真机: 距离最近按真实定位由近及远;rec/sales 正常;点抢拉起美团 App / H5。未授权时三 tab 均「去授权」空态、不打接口;授权回前台自动出 feed。
- 后端: 现有
pytest(meituan feed 冒烟)仍通过(本设计不改后端)。
11. 风险
| 风险 | 缓解 |
|---|---|
getLocation 同步 vs GPS 异步 |
宿主预取并缓存 lastLocation,getLocation() 同步返缓存;首次可能空→落北京默认,回前台刷新转真实 |
| 安卓改动跨仓 | 安卓侧仅 +1 桥方法 + 宿主取定位;H5 用 SGBridge.getLocation() 抽象,安卓未上线时浏览器/真机均落北京默认,H5 不阻塞 |
| distance 一次性返回被误触底翻页 | has_next=false 天然挡住;翻页逻辑只认 hasNext |
| 缓存与定位变化不一致 | 坐标变化统一清空全部 tab 缓存 |