From 77ce33bac9201120884ce391647506b250c59bf4 Mon Sep 17 00:00:00 2001 From: NIGGER SLAYER Date: Wed, 15 Apr 2026 12:53:01 +0800 Subject: [PATCH] appid --- docs/h5-summary.md | 546 ++++++++++++++++++ .../config/configs/sc_variant1_config.dart | 2 +- 2 files changed, 547 insertions(+), 1 deletion(-) create mode 100644 docs/h5-summary.md diff --git a/docs/h5-summary.md b/docs/h5-summary.md new file mode 100644 index 0000000..f2e47a1 --- /dev/null +++ b/docs/h5-summary.md @@ -0,0 +1,546 @@ +# 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 ", + "Req-Lang": "", + "Req-App-Intel": "build=;version=;model=;channel=;Req-Imei=", + "Req-Sys-Origin": "origin=;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` diff --git a/lib/app/config/configs/sc_variant1_config.dart b/lib/app/config/configs/sc_variant1_config.dart index b053ae9..8b1c3bb 100644 --- a/lib/app/config/configs/sc_variant1_config.dart +++ b/lib/app/config/configs/sc_variant1_config.dart @@ -77,7 +77,7 @@ class SCVariant1Config implements AppConfig { String get tencentImAppid => '20036101'; @override - String get agoraRtcAppid => 'ceb9e2620d454bca9725f7a7f11d4019'; + String get agoraRtcAppid => '4b5e5cea3b86476caf7f7a57d05b82d1'; @override num get gameAppid => 9999999999; // 需要注册新的游戏服务账户并获取独立App ID