# H5 相关实现梳理

## 1. 总体概览

项目里和 H5 相关的能力，当前主要分成 3 条线：

1. 内嵌 H5 页面承载：使用 `webview_flutter` 打开 H5 页面。
2. Flutter 与 H5 双向通讯：通过 JS 注入 + `JavaScriptChannel` 做交互。
3. 外部 H5 / Scheme 回流 App：通过 `app_links` 监听深链。

相关依赖：

- `webview_flutter: 4.4.2`：`pubspec.yaml:61`
- `app_links: ^6.4.1`：`pubspec.yaml:130`
- `url_launcher: ^6.3.1` 已安装：`pubspec.yaml:63`，但本次检索未发现当前 `lib/` 下有明确的 H5 主流程调用。

---

## 2. H5 核心承载页

核心文件：`lib/modules/webview/webview_page.dart`

### 2.1 页面职责

`WebViewPage` 是项目里统一的 H5 容器页，负责：

- 加载 H5 URL
- 给 URL 统一追加 `lang` 参数
- 开启 JS
- 注入 `window.app`
- 接收 H5 发给 Flutter 的消息
- 读取 H5 页面的 `document.title`
- 承接标题栏显隐和样式

### 2.2 加载流程

关键代码位置：

- URL 组装：`lib/modules/webview/webview_page.dart:48-53`
- 开启 JS：`lib/modules/webview/webview_page.dart:55-58`
- 注册消息通道：`lib/modules/webview/webview_page.dart:58-119`
- 页面完成后延迟注入：`lib/modules/webview/webview_page.dart:132-140`
- 加载请求：`lib/modules/webview/webview_page.dart:147`

实际流程：

1. 传入 `widget.url`
2. 若 URL 已有查询参数则追加 `&lang=...`，否则追加 `?lang=...`
3. 使用 `WebViewController` 加载页面
4. `onPageFinished` 后延迟 550ms，再执行 JS 注入
5. 同时读取 `document.title` 作为标题

说明：

- JS 模式为 `JavaScriptMode.unrestricted`
- 延迟注入的注释写明是“等待 Vue3 初始化”
- 页面销毁时会加载 `about:blank`：`lib/modules/webview/webview_page.dart:150-153`

---

## 3. Flutter 与 H5 的通讯方式

### 3.1 H5 -> Flutter

Flutter 侧注册的 JS Channel 名称是：

- `FlutterPageControl`：`lib/modules/webview/webview_page.dart:58-60`

H5 发送消息后，Flutter 根据字符串内容执行不同动作。

### 已支持的消息协议

| H5 消息 | Flutter 行为 | 代码位置 |
| --- | --- | --- |
| `close_page` | 关闭当前 WebView 页 | `lib/modules/webview/webview_page.dart:62-63` |
| `view_user_info:{userId}` | 跳到个人资料页 | `lib/modules/webview/webview_page.dart:64-75` |
| `go_to_room:{roomId}` | 进入房间 | `lib/modules/webview/webview_page.dart:76-87` |
| `private_chat:{userId}` | 打开私聊会话 | `lib/modules/webview/webview_page.dart:88-110` |
| `uploadImgFile` | 调起图片选择，并把图片路径再注入给 H5 | `lib/modules/webview/webview_page.dart:111-112`、`184-206` |
| `editingRoom` | 跳转房间管理搜索页 | `lib/modules/webview/webview_page.dart:113-114` |
| `editingUser` | 跳转用户管理搜索页 | `lib/modules/webview/webview_page.dart:115-116` |

### H5 侧当前更稳妥的调用方式

从 Flutter 代码来看，当前真正注册的 channel 只有 `FlutterPageControl`，因此 H5 若要主动发消息，建议按下面方式理解：

```js
FlutterPageControl.postMessage('close_page')
FlutterPageControl.postMessage('go_to_room:123456')
FlutterPageControl.postMessage('view_user_info:10001')
FlutterPageControl.postMessage('private_chat:10001')
```

### 3.2 Flutter -> H5

Flutter 会在页面加载完成后给 H5 注入 `window.app` 对象：`lib/modules/webview/webview_page.dart:156-181`

### 注入的方法

| 方法 | 作用 | 代码位置 |
| --- | --- | --- |
| `window.app.getAccessOrigin()` | 返回一段 JSON 字符串，里面包含鉴权和设备/渠道信息 | `lib/modules/webview/webview_page.dart:162-170` |
| `window.app.getAuth()` | 当前默认等价于 `getAccessOrigin()` | `lib/modules/webview/webview_page.dart:173-175` |
| `window.app.sendToFlutter(data)` | 试图把消息回传给 Flutter | `lib/modules/webview/webview_page.dart:177-180` |

### `getAccessOrigin()` 返回内容

其返回值是 JSON 字符串，不是 JS 对象，H5 需要自行 `JSON.parse(...)`。

大致结构如下：

```js
{
  "Authorization": "Bearer <token>",
  "Req-Lang": "<lang>",
  "Req-App-Intel": "build=<build>;version=<version>;model=<model>;channel=<channel>;Req-Imei=<imei>",
  "Req-Sys-Origin": "origin=<origin>;originChild=<originChild>"
}
```

这些值来自：

- Token：`AccountStorage`，见 `lib/shared/data_sources/sources/local/user_manager.dart:23-55`
- 语言、版本、渠道、设备、来源：`SCGlobalConfig`，见 `lib/app/constants/sc_global_config.dart:24-43`
- 设备唯一标识：`SCDeviceIdUtils.getDeviceId()`，见 `lib/shared/tools/sc_deviceId_utils.dart:31-62`

### 图片上传相关注入

当 H5 发出 `uploadImgFile` 后，Flutter 会调起图片选择器，然后重新注入另一组方法：`lib/modules/webview/webview_page.dart:184-206`

这时会新增：

- `window.app.getImagePath()`：返回所选图片本地路径的 JSON 字符串

并且会把：

- `window.app.getAuth()`

改写成：

- 返回 `window.app.getImagePath()`

也就是说，图片选择之后，`getAuth()` 不再表示鉴权信息，而变成了“图片路径”。

---

## 4. H5 路由与承载入口

### 4.1 统一路由

项目提供统一路由：

- 路由常量：`SCMainRoute.webViewPage = '/main/webViewPage'`  
  位置：`lib/modules/index/main_route.dart:42`

- 路由解析逻辑：  
  `lib/modules/index/main_route.dart:185-198`

解析行为：

- `url` 会先 `Uri.decodeComponent`
- `title` 直接透传
- `showTitle` 直接从 query 里取字符串

### 4.2 标题栏显隐规则

`WebViewPage` 默认 `showTitle = "true"`：`lib/modules/webview/webview_page.dart:27-31`

但通过路由进入时：

- `showTitle: params['showTitle']?.first ?? ""`：`lib/modules/index/main_route.dart:191-195`

因此目前有两种进入方式：

1. 直接 `WebViewPage(...)`
   - 不传 `showTitle` 时，标题栏默认显示
2. 通过 `SCMainRoute.webViewPage?...`
   - 通常会显式传 `showTitle=false`
   - 若未来有人漏传 `showTitle`，最终拿到的是空字符串 `""`，标题栏也会被隐藏

---

## 5. 当前已接入的 H5 页面入口

### 5.1 登录页 / 关于页协议页

这两处是直接 new `WebViewPage`，默认显示标题栏。

| 场景 | URL 来源 | 代码位置 |
| --- | --- | --- |
| 登录页《Terms of Service》 | `SCGlobalConfig.userAgreementUrl` | `lib/modules/auth/login_page.dart:311-324` |
| 登录页《Privacy Policy》 | `SCGlobalConfig.privacyAgreementUrl` | `lib/modules/auth/login_page.dart:350-363` |
| 设置-关于-《Terms of Service》 | `SCGlobalConfig.userAgreementUrl` | `lib/modules/user/settings/about/about_page.dart:80-90` |
| 设置-关于-《Privacy Policy》 | `SCGlobalConfig.privacyAgreementUrl` | `lib/modules/user/settings/about/about_page.dart:112-123` |

### 5.2 首页 Party 榜单 H5

这些入口都走统一路由，并显式 `showTitle=false`。

| 入口 | URL 来源 | 代码位置 |
| --- | --- | --- |
| 财富榜 | `SCGlobalConfig.wealthRankUrl` | `lib/modules/home/popular/party/sc_home_party_page.dart:295-300` |
| 房间榜 | `SCGlobalConfig.roomRankUrl` | `lib/modules/home/popular/party/sc_home_party_page.dart:330-335` |
| 魅力榜 | `SCGlobalConfig.charmRankUrl` | `lib/modules/home/popular/party/sc_home_party_page.dart:365-370` |

### 5.3 我的页面角色中心 H5

角色身份先通过 `/app/h5/identity` 拉取，再决定展示哪些入口。

身份拉取：

- `lib/services/auth/user_profile_manager.dart:90-95`
- `lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart:539-551`

角色字段定义：

- `lib/shared/business_logic/models/res/sc_user_identity_res.dart:6-87`

当前角色入口：

| 身份条件 | 打开页面 | 代码位置 |
| --- | --- | --- |
| `anchor && agent` | `agencyCenterUrl` | `lib/modules/user/me_page2.dart:295-308` |
| `anchor && !agent` | `hostCenterUrl` | `lib/modules/user/me_page2.dart:309-322` |
| `!admin && bdLeader` | `bdLeaderUrl` | `lib/modules/user/me_page2.dart:325-339` |
| `!admin && bd` | `bdCenterUrl` | `lib/modules/user/me_page2.dart:340-353` |
| `freightAgent` | `coinSellerUrl` | `lib/modules/user/me_page2.dart:356-370` |
| `admin` | `adminUrl` | `lib/modules/user/me_page2.dart:372-386` |

### 5.4 Banner H5

Banner 打开逻辑在：

- `lib/shared/tools/sc_banner_utils.dart:10-27`

规则：

1. `item.content == ENTER_ROOM` 时，直接进房间
2. 否则只要 `params` 是 `http://` 或 `https://` 开头，就用内嵌 WebView 打开

注意：

- 这里只校验是不是 URL，没有域名白名单

### 5.5 消息中心 / 通知里的外链 H5

只要 `message.extraData.link` 以 `http` / `https` 开头，就会直接进入统一 WebView：

- 活动消息：`lib/modules/chat/activity/message_activity_page.dart:268-277`
- 通知消息：`lib/modules/chat/noti/message_notifcation_page.dart:280-289`

这两处同样没有域名白名单。

---

## 6. H5 URL 配置清单

配置接口定义在：

- `lib/app/config/app_config.dart:21-66`
- `lib/app/constants/sc_global_config.dart:7-18`
- `lib/app/config/configs/sc_variant1_config.dart:26-71`

当前 `variant1` 中的 H5 URL：

| 配置项 | URL | 当前使用情况 |
| --- | --- | --- |
| `privacyAgreementUrl` | `https://h5.haiyihy.com/privacy.html` | 已使用 |
| `userAgreementUrl` | `https://h5.haiyihy.com/service.html` | 已使用 |
| `anchorAgentUrl` | `https://h5.haiyihy.com/apply/index.html` | 本次检索未发现引用 |
| `hostCenterUrl` | `https://h5.haiyihy.com/host-center/index.html` | 已使用 |
| `bdCenterUrl` | `https://h5.haiyihy.com/bd-center/index.html` | 已使用 |
| `bdLeaderUrl` | `https://h5.haiyihy.com/bd-leader-center/index.html` | 已使用 |
| `coinSellerUrl` | `https://h5.haiyihy.com/coin-seller/index.html` | 已使用 |
| `adminUrl` | `https://h5.haiyihy.com/admin-center/index.html` | 已使用 |
| `agencyCenterUrl` | `https://h5.haiyihy.com/agency-center/index.html` | 已使用 |
| `gamesKingUrl` | `https://h5.haiyihy.com/games-king/index.html` | 本次检索未发现引用 |
| `wealthRankUrl` | `https://h5.haiyihy.com/ranking/index.html?first=Wealth` | 已使用 |
| `charmRankUrl` | `https://h5.haiyihy.com/ranking/index.html?first=Charm` | 已使用 |
| `roomRankUrl` | `https://h5.haiyihy.com/ranking/index.html?first=Room` | 已使用 |
| `inviteNewUserUrl` | `https://h5.haiyihy.com/invitation/invite-new-user/index.html` | 本次检索未发现引用 |

---

## 7. `/app/h5/identity` 接口

这个接口是 H5 相关链路里的关键接口。

### 7.1 接口位置

- 未授权校验时直接请求：`lib/shared/data_sources/sources/remote/net/api.dart:49-89`
- 路径映射表：`lib/shared/data_sources/sources/remote/net/network_client.dart:199`
- Repository 包装：`lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart:539-551`

### 7.2 当前用途

1. 拉取当前用户身份，用于决定“我的”页面展示哪些 H5 入口
2. 在网络层发生 401 时，用它来复核当前会话是否仍然有效

### 7.3 身份字段

当前模型里可见的角色字段包括：

- `agent`
- `anchor`
- `bd`
- `admin`
- `superAdmin`
- `bdLeader`
- `superFreightAgent`
- `manager`
- `freightAgent`

定义位置：`lib/shared/business_logic/models/res/sc_user_identity_res.dart:6-87`

---

## 8. 外部 H5 / Deep Link 回流 App

### 8.1 依赖与处理器

深链能力不是走 WebView，而是走 `app_links`：

- 依赖：`pubspec.yaml:130`
- 处理器：`lib/shared/tools/sc_deep_link_handler.dart:1-61`

`SCDeepLinkHandler` 做了两件事：

1. 启动时读取 `initialLink`
2. 运行时监听 `uriLinkStream`

### 8.2 App 根层接入

`main.dart` 已初始化深链监听：

- 挂载处理器：`lib/main.dart:246`
- `initState` 调用：`lib/main.dart:248-257`
- 深链初始化：`lib/main.dart:266-274`

但是当前真正处理链接的 `_handleLink(Uri uri)` 里只有日志和简单取参：

- `lib/main.dart:276-281`

也就是说：

- 深链基础设施已经接好了
- 但业务路由尚未真正实现

### 8.3 Android 配置

AndroidManifest 里已配置：

- `https://h5.yumi.com`
- `yumi://app`

位置：`android/app/src/main/AndroidManifest.xml:55-64`

### 8.4 iOS 配置现状

本次检索结果里：

- 未发现 `applinks:` Associated Domains
- 未发现针对 `yumi://app` 的明确 `CFBundleURLSchemes`
- `Info.plist` 中 `CFBundleURLTypes` 目前只有一个空壳配置：`ios/Runner/Info.plist:38-44`

因此至少从当前代码检索结果看：

- Android 侧有显式深链声明
- iOS 侧未看到与当前 H5 域名 / scheme 对应的完整配置

---

## 9. WebView 样式与测试

WebView 页面样式做了策略抽象：

- 接口定义：`lib/app/config/business_logic_strategy.dart:1451-1491`
- Base 实现：`lib/app/config/strategies/base_business_logic_strategy.dart:2024-2078`
- Variant1 实现：`lib/app/config/strategies/variant1_business_logic_strategy.dart:2003-2057`

测试覆盖位置：

- `test/business_logic_strategy_test.dart:1122-1183`

当前测试主要覆盖：

- 普通 WebView 页面的颜色策略
- 游戏 WebView 页面的颜色策略
- Variant1 对应策略返回值

额外说明：

- `WebViewPage` 里有 `_progressBar(...)` 方法：`lib/modules/webview/webview_page.dart:275-283`
- 但当前 `build` 里没有真正挂载这条进度条

---

## 10. 当前实现里的注意点 / 风险点

### 10.1 JS Channel 名称与注入 helper 名称不一致

Flutter 注册的是：

- `FlutterPageControl`

但注入到 H5 的 helper 写的是：

- `FlutterApp.postMessage(data)`：`lib/modules/webview/webview_page.dart:178-179`、`202-203`

本次检索未发现项目里其他地方定义了 `FlutterApp` JS 对象。

这意味着：

- 如果 H5 直接调用 `window.app.sendToFlutter(data)`，大概率会找不到 `FlutterApp`
- 当前更像是 H5 需要自己直接调用 `FlutterPageControl.postMessage(...)`

### 10.2 `uploadImgFile` 会覆盖 `getAuth()`

在初始注入时：

- `getAuth()` 等于 `getAccessOrigin()`

在用户选图后：

- `getAuth()` 被改成 `getImagePath()`

这会导致 H5 侧如果沿用 `getAuth()` 获取鉴权信息，选图后语义会发生变化。

### 10.3 缺少域名白名单，且注入内容包含鉴权信息

当前以下入口只检查“是不是 http/https”，没有限制域名：

- Banner：`lib/shared/tools/sc_banner_utils.dart:18-24`
- 活动消息：`lib/modules/chat/activity/message_activity_page.dart:269-276`
- 通知消息：`lib/modules/chat/noti/message_notifcation_page.dart:281-288`

而 `WebViewPage` 会对已加载页面统一注入：

- Bearer Token
- 语言
- 设备标识
- 构建版本
- 渠道和来源信息

因此当前实现等于：

- 只要某个外链能被塞进 Banner 或消息链接
- 该页面理论上就能拿到 `window.app.getAccessOrigin()` 这组数据

这是当前 H5 链路里最需要重点关注的安全点。

### 10.4 Android 深链域名与业务配置域名不一致

当前业务配置里的 H5 域名主要是：

- `h5.haiyihy.com`

但 AndroidManifest 里声明的可验证 Host 是：

- `h5.yumi.com`

位置分别在：

- `lib/app/config/configs/sc_variant1_config.dart:26-71`
- `android/app/src/main/AndroidManifest.xml:59-64`

如果期望 H5 页面通过系统级深链回流 App，这个域名不一致需要单独确认。

### 10.5 Deep Link 已接入，但业务处理未完成

`SCDeepLinkHandler` 已经能收到链接，但 `main.dart` 里的 `_handleLink` 还没有落业务跳转逻辑。

当前状态更接近：

- “能收到”
- “还没消费”

### 10.6 `lang` 参数是无条件追加

当前 URL 处理逻辑只判断是否已有 `?`，不会判断原 URL 是否已经存在 `lang` 参数：

- `lib/modules/webview/webview_page.dart:48-53`

如果某些 H5 页面本来就自带 `lang`，最终可能出现重复参数。

---

## 11. 给 H5 同学的当前可参考约定

如果只根据当前 Flutter 实现来对接，比较稳妥的方式是：

### 读取鉴权/设备信息

```js
const auth = JSON.parse(window.app.getAccessOrigin())
```

### 向 Flutter 发消息

```js
FlutterPageControl.postMessage('close_page')
FlutterPageControl.postMessage('view_user_info:10001')
FlutterPageControl.postMessage('go_to_room:123456')
FlutterPageControl.postMessage('private_chat:10001')
FlutterPageControl.postMessage('uploadImgFile')
```

### 选图后读取图片路径

```js
const imageInfo = JSON.parse(window.app.getImagePath())
```

注意：

- `window.app.sendToFlutter(...)` 目前从代码上看不可靠，因为它引用的是 `FlutterApp`
- `window.app.getAuth()` 在选图前后语义不同，不建议 H5 继续把它当成固定鉴权接口

---

## 12. 本次检索范围

本次重点检查了以下文件/模块：

- `lib/modules/webview/webview_page.dart`
- `lib/modules/index/main_route.dart`
- `lib/shared/tools/sc_banner_utils.dart`
- `lib/modules/auth/login_page.dart`
- `lib/modules/user/settings/about/about_page.dart`
- `lib/modules/home/popular/party/sc_home_party_page.dart`
- `lib/modules/user/me_page2.dart`
- `lib/modules/chat/activity/message_activity_page.dart`
- `lib/modules/chat/noti/message_notifcation_page.dart`
- `lib/app/config/app_config.dart`
- `lib/app/config/configs/sc_variant1_config.dart`
- `lib/app/constants/sc_global_config.dart`
- `lib/shared/data_sources/sources/remote/net/api.dart`
- `lib/shared/data_sources/sources/remote/net/network_client.dart`
- `lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart`
- `lib/shared/business_logic/models/res/sc_user_identity_res.dart`
- `lib/services/auth/user_profile_manager.dart`
- `lib/shared/tools/sc_deep_link_handler.dart`
- `lib/main.dart`
- `android/app/src/main/AndroidManifest.xml`
- `ios/Runner/Info.plist`
- `test/business_logic_strategy_test.dart`
