新增:设备ID获取说明

This commit is contained in:
ivan 2026-04-17 22:28:33 +08:00
parent 66bbf38c64
commit 4c2d5585e3
6 changed files with 293 additions and 5 deletions

View File

@ -21,6 +21,8 @@
以首款换皮应用 **FunyMee** 为参考的 **视频首页**`common_info` → 分类 → 按分类拉模板)数据流说明,见 **[《视频首页数据获取流程》](video_home_data_flow.md)**。
示例换皮配置文件 [`lib/src/config/skin_config.example.json`](../lib/src/config/skin_config.example.json) 的字段说明,见 **[《skin_config.example.json 字段说明》](skin_config_example.md)**。
---
## 2. 快速开始
@ -155,6 +157,8 @@ if (res.isSuccess) {
}
```
宿主需在 `FrameworkAuthService.init` 传入的 `AuthServiceCallbacks` 中实现 `deviceId``sign`。**FunyMee 采用的获取方式与签名规则**见 [device_id_and_sign.md](./device_id_and_sign.md)。
**返回实体**: `FastLoginResponse`
- `userToken`: 用户 Token
- `userId`: 用户 ID

View File

@ -1,6 +1,6 @@
# 创建新换皮应用 — 步骤清单
本文说明基于 `client_proxy_framework` **从零搭一个换皮应用**的推荐流程。配置以 **`skin_config.json`** 为主;更细的字段说明见 [`new_app_config_template.md`](new_app_config_template.md),原生侧见 [`sdk_integration_guide.md`](sdk_integration_guide.md)。更长篇的上下文与业务对接见 [`skin_app_development_guide.md`](skin_app_development_guide.md)。
本文说明基于 `client_proxy_framework` **从零搭一个换皮应用**的推荐流程。配置以 **`skin_config.json`** 为主;更细的字段说明见 [`new_app_config_template.md`](new_app_config_template.md),原生侧见 [`sdk_integration_guide.md`](sdk_integration_guide.md)。更长篇的上下文与业务对接见 [`skin_app_development_guide.md`](skin_app_development_guide.md)。**`AuthServiceCallbacks` 所需的设备 ID 与 `computeSign`** 见 [`device_id_and_sign.md`](device_id_and_sign.md)。
***
@ -45,6 +45,8 @@ dependencies:
crypto: ^3.0.3 # 常见:与后端约定的 MD5 签名
```
Android 侧通常还需要 [`android_id`](https://pub.dev/packages/android_id)`Settings.Secure.ANDROID_ID`),与「仅用 device_info_plus 读到的 Android `id`」不是同一字段;**完整约定(含兜底 ID、`computeSign`**见 [device_id_and_sign.md](device_id_and_sign.md)。
执行:
```bash
@ -133,7 +135,7 @@ void main() async {
应用侧通常仍需:
1. 实现 **`AuthServiceCallbacks`**`getDeviceId``computeSign``onLoginSuccess` / `onCommonInfoLoaded` / `onLoginFailed`(例如写入本地 `UserState`、路由)。
1. 实现 **`AuthServiceCallbacks`**`getDeviceId``computeSign``onLoginSuccess` / `onCommonInfoLoaded` / `onLoginFailed`(例如写入本地 `UserState`、路由)。**`getDeviceId` / `computeSign` 与 FunyMee、app_client 对齐的参考实现**见 [device_id_and_sign.md](device_id_and_sign.md)。
2. 调用 **`FrameworkAuthService.init(你的回调)`**,再 **`await FrameworkAuthService.start()`**(可参考示例工程里的薄封装 `AuthService.init()`)。
无需再实现 **`AttributionCallbacks`**,除非要自定义 referer 格式;此时用 **`FrameworkAuthService.init(callbacks, attributionCallbacks: 你的实现)`**。
@ -155,6 +157,7 @@ void main() async {
| [`new_app_config_template.md`](new_app_config_template.md) | 按表逐项填写换皮参数(可对照填入 JSON |
| [`sdk_integration_guide.md`](sdk_integration_guide.md) | 原生工程 Adjust / Facebook 等集成要点 |
| [`skin_app_development_guide.md`](skin_app_development_guide.md) | 更长流程、业务/UI 后续对接说明 |
| [`device_id_and_sign.md`](device_id_and_sign.md) | 设备 ID 与 fast_login 签名(宿主参考实现) |
| 框架 `lib/src/config/skin_config.example.json` | JSON 最小结构示例 |
若本清单与旧文档冲突,**以本清单与当前框架代码为准**。

View File

@ -0,0 +1,81 @@
# 设备 ID 与 fast_login 签名(宿主参考实现)
本文档描述 **FunyMee** 等换皮应用在实现 [`AuthServiceCallbacks`](../lib/src/services/auth_service.dart) 时采用的 **`getDeviceId`** 与 **`computeSign`** 规则,与 **app_client** 行为对齐,便于其它宿主复制或迁移到框架内默认实现。
框架本身仍通过接口要求宿主提供上述方法;本文档**不是**框架 API 说明,而是**可复现的实现规范**。
---
## 1. 依赖(宿主 `pubspec.yaml`
| 用途 | 包名 |
|------|------|
| Android`Settings.Secure.ANDROID_ID` | [`android_id`](https://pub.dev/packages/android_id) |
| iOS厂商级设备标识 | [`device_info_plus`](https://pub.dev/packages/device_info_plus) |
| 兜底 ID 持久化 | [`shared_preferences`](https://pub.dev/packages/shared_preferences) |
| 签名 MD5 | [`crypto`](https://pub.dev/packages/crypto)`md5` |
---
## 2. `getDeviceId()` 行为
按 [`defaultTargetPlatform`](https://api.flutter.dev/flutter/foundation/defaultTargetPlatform.html) 分支:
### 2.1 Android
- 使用 **`android_id` 包**的 `AndroidId().getId()`,对应系统 **`Settings.Secure.ANDROID_ID`**。
- 若返回值非空,直接作为 `deviceId`
- **不要**用 `device_info_plus` 的 `AndroidDeviceInfo.id` 当作设备 ID该字段通常为 **ROM 构建号Build ID**,与业务侧期望的 ANDROID_ID 不一致。
### 2.2 iOS
- 使用 `DeviceInfoPlugin().iosInfo`,取 **`identifierForVendor`**IDFV
- 若非空,直接作为 `deviceId`
### 2.3 其它平台或读取失败
- 使用下文 **§3 兜底 ID**,保证同一安装会话内稳定、可持久化。
---
## 3. 兜底设备 IDSharedPreferences
当 Android/iOS 无法取得有效 ID或目标平台非 Android/iOS 时:
1. 读取键名:**`persisted_device_id`**(字符串)。
2. 若已有非空值,直接返回。
3. 否则生成新 ID
- 使用加密安全随机数生成 **16 字节**
- 经 **Base64URL** 编码后,**去掉末尾 `=` 填充**(与现有实现一致)。
4. 写入 SharedPreferences 后返回。
> 注意:不同应用若使用相同键名与存储作用域,行为一致;若宿主需隔离多环境,可自行加前缀或改用独立文件名。
---
## 4. `computeSign(deviceId)`
与后端 **`fast_login`** 约定一致:
- 对 **`deviceId` 的 UTF-8 字节**做 **MD5**
- 结果为 **32 位十六进制字符串****大写**(例如 Dart`md5.convert(utf8.encode(deviceId)).toString().toUpperCase()`)。
该签名对应代理层/V2 包装中的业务字段(如文档中的 `mandate` / `sign` 等,以实际字段映射为准)。
---
## 5. 参考代码位置FunyMee
实现类:**`AppAuthCallbacks`**,文件:
`FunyMee/lib/core/auth/auth_service.dart`
(仅 `getDeviceId``computeSign``_persistedFallbackDeviceId` 与本文档对应;其余如 `onLoginSuccess` 与宿主 `UserState` 绑定,不在此规范范围内。)
---
## 6. 修订记录
| 日期 | 说明 |
|------|------|
| 2026-04-17 | 初版:从 FunyMee `auth_service.dart` 整理 |

View File

@ -3,6 +3,8 @@
本文档说明如何使用 `client_proxy_framework` 从零创建并完成一个换皮应用。
> **推荐优先阅读**[《创建新换皮应用 — 步骤清单》](create_new_skin_app.md) — 与当前框架实现JSON 配置、`ClientBootstrap`、固定 Facebook Channel、默认归因回调等保持同步的精简步骤。
>
> **`getDeviceId` / `computeSign`(与 FunyMee、app_client 对齐)**[device_id_and_sign.md](device_id_and_sign.md)
***
@ -468,6 +470,8 @@ class MainActivity : FlutterActivity() {
## 7. 实现业务代码
认证回调中的 **`getDeviceId`**、**`computeSign`** 与后端 **`fast_login`** 强相关;**与 FunyMee / app_client 一致的字段含义、依赖与兜底策略**见框架文档 [device_id_and_sign.md](device_id_and_sign.md)(下文示例侧重代码结构,生产环境请以该规范为准)。
### 7.1 创建认证服务实现
创建 `lib/core/auth/auth_service.dart`

180
docs/skin_config_example.md Normal file
View File

@ -0,0 +1,180 @@
# `skin_config.example.json` 字段说明
本文档对应仓库内示例文件 [`lib/src/config/skin_config.example.json`](../lib/src/config/skin_config.example.json)。宿主应将复制后的文件放在 **`assets/skin_config.json`**(或自定义路径),并在启动时调用 `ClientBootstrap.initFromAsset(...)`
---
## 1. 根结构概览
| 顶层键 | 必填 | 说明 |
|--------|------|------|
| `app` | 是 | 应用展示名、业务 id、包名 |
| `api` | 是 | 环境域名、代理路径、AES 密钥等 |
| `backend` | 否 | `fast_login` / `common_info` 等接口的 `app` 渠道参数 |
| `videoHome` | 否 | 视频首页「Images」虚拟 Tab 的文案与排序 |
| `proxyKeys` | 否 | 代理请求最外层 JSON 的字段名 |
| `v2` | 否 | 代理 V2 包装壳(层级、业务载荷路径、噪音键) |
| `fieldMapping` | 否 | 逻辑字段名 ↔ 线网/网关字段名;空或省略则恒等映射 |
| `analytics` | 否 | Adjust / Facebook 初始化参数 |
| `adjustEvents` | 否 | 语义化事件名 → Adjust Dashboard **事件 token**(供 `AnalyticsEvents` 等使用) |
| `extConfig` | 否 | `common_info.extConfig` 的键名模式、任务项映射、本地默认值 |
---
## 2. `app`
| 字段 | 类型 | 说明 |
|------|------|------|
| `name` | string | 应用名称(必填,非空) |
| `id` | string | 业务侧应用标识(必填) |
| `packageName` | string | 应用包名;用于请求头 `pkg` 等(必填) |
---
## 3. `backend`
省略时框架使用默认值:`iosAppType``HIOS``androidAppType``HAndroid`
| 字段 | 类型 | 说明 |
|------|------|------|
| `iosAppType` | string | iOS 上部分接口 query `app` 的值 |
| `androidAppType` | string | Android 上同上 |
---
## 4. `videoHome`
用于 [`VideoHomeRuntime`](../lib/src/config/video_home_runtime.dart) 与 [`AppConfig`](../lib/src/config/app_config.dart) 中与首页 Tab 相关的展示配置。
| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `imagesTabLabel` | string | `Images` | 与接口分类并列的「运营位 / Images」Tab 文案 |
| `imagesTabFirst` | bool | `false` | 为 `true` 时该 Tab 排在服务端分类之前 |
---
## 5. `api`
| 字段 | 类型 | 说明 |
|------|------|------|
| `preBaseUrl` | string | **预发**环境 API 根地址(必填) |
| `prodBaseUrl` | string | **生产**环境 API 根地址(必填) |
| `proxyPath` | string | 代理入口路径,如 `/v1/proxy`(必填) |
| `aesKey` | string | AES-128 密钥字符串,当前实现为 **16 字符**(必填) |
| `debugBaseUrlOverride` | string \| null | 仅 **Debug 构建**有效:非空时覆盖 `preBaseUrl`,用于本地/抓包代理 |
| `alwaysUsePreBaseUrl` | bool | 为 `true`**任意构建类型**下 `baseUrl` **始终**为 `preBaseUrl`(便于长期连预发);**Release 若仍访问预发,优先检查此项是否为 `true`** |
### `baseUrl` 选择规则([`SkinConfig.baseUrl`](../lib/src/config/skin_config.dart)
1. 若 `alwaysUsePreBaseUrl == true``preBaseUrl`
2. 否则若 **非** DebugRelease/Profile`prodBaseUrl`
3. 否则Debug`debugBaseUrlOverride ?? preBaseUrl`
---
## 6. `proxyKeys`
定义代理请求明文 JSON 最外层的**键名**;值为密文或内嵌结构,具体与后端约定一致。省略时框架使用内置默认字段名(如 `appId``path``method``headers``params``body` 及默认 `noiseKeys`)。
| 字段 | 说明 |
|------|------|
| `appIdField` / `pathField` / `methodField` / `headerField` / `paramsField` / `bodyField` | 各语义段字段名 |
| `noiseKeys` | 噪音字段名列表,用于 V2 代理外层填充随机串 |
---
## 7. `v2`
与 [`AppConfig`](../lib/src/config/app_config.dart) 中 V2 包装一致:在 `proxyKeys` 体内部再包一层固定壳,业务密文放在 `sanctumPath` 末端。
| 字段 | 说明 |
|------|------|
| `levelField` | 最外层固定层级字段名(如 `level` |
| `levelFixedValue` | 该字段固定整型值 |
| `sanctumPath` | 从根到**业务载荷**的键路径,如 `["wrapper","layer","payload"]` |
| `noiseKeys` | 与 `level` 同层的噪音键名列表 |
---
## 8. `fieldMapping`
**Map逻辑名 → 线网名**。请求发出前将逻辑字段映射为线网字段,响应解析后再映射回逻辑名。示例中 `code``httpCode``msg``message``data``payload` 表示网关使用另一套顶层键名。空对象或省略表示**恒等映射**[`kIdentityFieldMapping`](../lib/src/config/field_mapping.dart))。
---
## 9. `analytics`
传给 [`SkinConfig.buildAnalyticsConfig`](../lib/src/config/skin_config.dart) 以初始化 Adjust / Facebook。占位 token如含 `your_``example.com`)会被识别为无效并**跳过**对应 SDK避免带着假密钥初始化。
| 字段 | 说明 |
|------|------|
| `debugLogs` | 框架侧调试输出开关(与各 SDK 内日志配合) |
| `adjust.appToken` | Adjust App Token |
| `adjust.environment` | `sandbox` / `production`(其它值按 production 处理) |
| `adjust.logLevel` | 如 `verbose` / `off` |
| `adjust.fbAppId` | 可选,供 Adjust 与 Meta 相关能力 |
| `facebook.appId` | Facebook 应用编号 |
| `facebook.clientToken` | 客户端口令;可为空字符串(若业务允许) |
| `facebook.debugLogs` | Facebook SDK 侧调试日志 |
另支持根级 `platformAttribution` 等扩展(见源码 `buildAnalyticsConfig`)。
---
## 10. `adjustEvents`
**键**:框架内使用的**逻辑事件名**(如 `register``purchase``firstPurchase``paymentFailed` 及按档位的价格键)。
**值**Adjust Dashboard 里配置的**事件 token**(字符串)。
未配置的键在调用 `SkinConfig.trackAdjustEvent` 时**不会**上报。示例仅展示 `register` / `purchase`;完整业务档位名需与 [`AnalyticsEvents`](../lib/src/services/analytics_events.dart) 中约定一致。
---
## 11. `extConfig`
描述 `common_info` 返回里 **`extConfig` JSON** 如何解析为 [`ExtConfigData`](../lib/src/config/ext_config_models.dart),以及本地默认值如何与线上下发合并。
### 11.1 `keys`
各**业务语义**对应线网上可能出现的**键名列表**(按顺序取**第一个存在的键**),用于兼容多版本后端键名。示例:
- `showVideoMenu`:如 `go_run``need_wait` — 控制是否展示视频首页顶栏等(需为布尔或可解析为布尔)。
- `forbidScreenshot` / `blockScreenshot`:截屏相关策略。
- `allowThirdPartyPayment`:第三方支付开关。
- `privacyUrl` / `agreementUrl`:链接类字段。
- `items`:运营卡片数组所在键名。
### 11.2 `itemKeys`
列表项**逻辑字段**(如 `image``title``videoUrl`)到线网键名列表的映射;解析 `items` 数组中每一项时使用。详见 [`ExtConfigKeySchema`](../lib/src/config/ext_config_key_schema.dart) 类注释。
### 11.3 `itemKeysHome` / `itemKeysTask`
可选。同一逻辑名在**首页展示**与**创建任务**场景可映射到不同线网键;未配置时回退到 `itemKeys`
### 11.4 `taskItemMapping`
`extConfig.items` 中**单项**组装成与 `GET /v1/image/img2video/tasks` 单项在 `fieldMapping` 之后**同形**的结构,以便走 `TaskItem.fromJson`
**键**:目标路径(点号嵌套,如 `previewVideo.url`)。
**值**:源对象上按顺序尝试的源路径列表。
### 11.5 `itemsApplyFieldMappingBeforeTaskMapping`
- `true`(默认):先对单项做 `fieldMapping.mapResponse`,再按 `taskItemMapping` 取值。
- `false`:在**原始**线网单项上按源路径取值。
### 11.6 `defaultItemTitle`
`extConfig.items` 解析后标题仍为空时填入的占位字符串(对应代码中的 `defaultItemTitleWhenEmpty`)。
### 11.7 `defaults`
**线网键名 → 默认值**,与 `common_info` 返回的 extConfig 对象 **浅合并**:服务器非 null 的顶层键覆盖本地。用于本地兜底 URL、默认开关、空 `items` 等。合并逻辑见 [`SkinExtConfigSection.mergeDefaults`](../lib/src/config/ext_config_key_schema.dart)。
---
## 12. 相关入口
- 初始化:`ClientBootstrap.initFromAsset`[`client_bootstrap.dart`](../lib/src/bootstrap/client_bootstrap.dart)
- 配置模型:`SkinConfig`[`skin_config.dart`](../lib/src/config/skin_config.dart)
- 创建新换皮应用步骤:[create_new_skin_app.md](create_new_skin_app.md)

View File

@ -3,6 +3,22 @@ import 'dart:convert';
import 'credits_balance_parse.dart';
import 'entity.dart';
String? _dynamicToOptionalString(dynamic value) {
if (value == null) return null;
if (value is String) return value;
if (value is List) return value.join(',');
return value.toString();
}
/// URL`skin_config` `avatar` 线 `frequency` fast_login
String? _userAvatarFromJson(Map<String, dynamic> json) {
for (final key in ['avatar', 'frequency']) {
final s = _dynamicToOptionalString(json[key])?.trim();
if (s != null && s.isNotEmpty) return s;
}
return null;
}
/// `extConfig` data JSON Map [json.decode]
String? _wireExtConfigJson(dynamic v) {
if (v == null) return null;
@ -86,7 +102,7 @@ class FastLoginResponse extends Entity {
userToken: _toString(json['userToken']),
userId: _toString(json['userId']),
credits: parseUserCreditsBalance(json),
avatar: _toString(json['avatar']),
avatar: _userAvatarFromJson(json),
userName: _toString(json['userName']),
countryCode: _toString(json['countryCode']),
extConfig: _wireExtConfigJson(json['extConfig']),
@ -213,7 +229,7 @@ class CommonInfoResponse extends Entity {
userToken: _toString(json['userToken']),
userId: _toString(json['userId']),
credits: parseUserCreditsBalance(json),
avatar: _toString(json['avatar']),
avatar: _userAvatarFromJson(json),
userName: _toString(json['userName']),
countryCode: _toString(json['countryCode']),
extConfig: _wireExtConfigJson(json['extConfig']),
@ -287,7 +303,7 @@ class AccountResponse extends Entity {
factory AccountResponse.fromJson(Map<String, dynamic> json) {
return AccountResponse(
credits: parseUserCreditsBalance(json),
avatar: json['avatar'] as String?,
avatar: _userAvatarFromJson(json),
userName: json['userName'] as String?,
isVip: json['isVip'] as bool?,
freeTimes: json['freeTimes'] as int?,