diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 8276453..cd712d5 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -14,6 +14,13 @@ + + + `,监听后刷新首页 Tab / Grid | +| `ExtConfigRuntime.commonInfoSucceeded` | `true` / `false` / `null`:是否成功拉到 common_info(**建议仅在为 `true` 时展示核心业务 UI**) | +| `ExtConfigData` | `showVideoMenu`、`allowScreenshot`、`allowThirdPartyPayment`、`privacyUrl`、`agreementUrl`、`items` | +| `ExtConfigItem` | 单项:`image`、`image_fix`、`img_need`、`cost`、`title`、`params` / `detail`;`taskExt` ⇒ `params ?? detail` | +| `kExtConfigItemsCategoryId` | 固定 `-1`,作「静态 items Tab」分类 id | +| `mergeHomeTabsWithExtConfigItems()` | `showVideoMenu == true` 时在 API Tab 列表 **末尾** 追加静态 Tab | +| `ClientBootstrap.skin.extConfigKeySchema` 等 | `SkinConfig` 从 JSON `extConfig.keys` 注入;宿主可只改配置不换代码 | + +**默认 wire 键(可在 `skin_config.extConfig.keys` 中整体改写):** + +| 语义 | 默认候选键(首个存在则生效) | +|------|------------------------------| +| 展示顶部 Video Tab 栏 + items 固定最后一格 | `go_run`、`need_wait` | +| 允许截屏 | `screen`;若无则看 `safe_area`(`true` ⇒ 不允许截屏) | +| 允许第三方支付 | `san_fang`、`lucky` | +| 隐私 / 协议 URL | `privacy`、`agreement` | +| items 数组 | `items` | + +**`itemKeys` 默认(可省略 `imageFix`,仍解析 `image_fix`):** `image`、`image_fix`、`img_need`、`cost`、`title`、`params`、`detail`。 + +示例(与当前后端约定一致时可原样使用): + +```json +{ + "go_run": false, + "screen": false, + "san_fang": false, + "privacy": "https://example.com/privacy", + "agreement": "https://example.com/terms", + "items": [ + { + "image": "https://cdn.example.com/a.png", + "image_fix": "https://cdn.example.com/a.png", + "img_need": 2, + "cost": 1, + "title": "BananaTask", + "params": "animal_expression" + } + ] +} +``` + +首页逻辑建议(对齐 `app_client`):`await FrameworkAuthService.loginComplete` 后判断 `ExtConfigRuntime.commonInfoSucceeded.value == true` 再进入主页;`showVideoMenu == true` 时展示顶部 Tab,分类列表用 `mergeHomeTabsWithExtConfigItems` 把静态 Tab 放在最后,**该 Tab 的 Grid 数据源为 `ExtConfigRuntime.data.value?.items`**。第三方支付入口用 `allowThirdPartyPayment`;截屏策略用 `shouldPreventCapture` 或自行根据 `allowScreenshot` 调用宿主侧防护(框架不强制依赖 `screen_secure`)。 + +--- + +## 6. 请求与字段映射(框架行为) + +- 宿主**只使用** `UserApi`、`ImageApi`、`PaymentApi`、`FeedbackApi` 等,**Body/Query 使用业务原始字段名**(如 `deviceId`、`taskId`)。 +- `ProxyClient` 负责:按 `skin_config.fieldMapping` 做键名映射、V2 包装、AES、噪音字段等。 +- 查阅某一字段的密文键名:在 [FunyMeeAI_client_guide.md](./FunyMeeAI_client_guide.md) 或 `skin_config.json` 的 `fieldMapping` 中搜索原始键。 + +--- + +## 7. API 与数据实体总表 + +以下均定义于 `client_proxy_framework/lib/src/services/*.dart`,实体在 `entities/`。 + +### 7.1 用户 + +| 方法 | Path | 说明 | +|------|------|------| +| `UserApi.fastLogin` | `POST /v1/user/fast_login` | 设备登录 | +| `UserApi.referrer` | `POST /v1/user/referrer` | 归因上报 | +| `UserApi.getCommonInfo` | `GET /v1/user/common_info` | 通用信息(首页配置、积分、扩展 JSON) | +| `UserApi.getAppLanguage` | `GET /v1/config/app-language` | 语言配置 | +| `UserApi.getAccount` | `GET /v1/user/account` | 账户信息 | +| `UserApi.getCreditsPage` | `GET /v1/user/credits-page` | **积分分页流水**(`page/size/type`) | +| `UserApi.getUserPayments` | `GET /v1/user/payments` | **支付/订单记录列表** | +| `UserApi.getUnreadMessageCount` | `GET /v1/user/unread-message-count` | 未读消息 | +| `UserApi.deleteAccount` | `GET /v1/user/delete` | **注销** | + +核心实体:`FastLoginResponse`、`CommonInfoResponse`、`AccountResponse`、`CreditsPageInfoResponse`(含 `CreditRecordItem`)、`UserPaymentsListResponse`。 + +### 7.2 图片 / 任务 + +| 方法 | Path | 说明 | +|------|------|------| +| `ImageApi.getCategoryList` | `GET /v1/image/img2video/categories` | 分类 | +| `ImageApi.getImg2VideoTasks` | `GET /v1/image/img2video/tasks` | 图生视频任务模板列表 | +| `ImageApi.getPromptRecommends` | `GET /v1/image/prompt/recomends` | 推荐提示词 | +| `ImageApi.createTxt2Img` | `POST /v1/image/txt2img_create` | 文生图 | +| `ImageApi.createImg2VideoPose` | `POST /v1/image/img2video_pose_task` | 图生视频姿态任务 | +| `ImageApi.getProgress` | `GET /v1/image/progress` | **任务进度** | +| `ImageApi.getImg2VideoPoseTemplates` | `GET /v1/image/img2Video_pose_template` | 姿态模板 | +| `ImageApi.getUploadPresignedUrl` | `POST /v1/image/upload-presigned-url` | **用户图预签名上传** | +| `ImageApi.createTask` | `POST /v1/image/create-task` | **主创建任务**(多参数,与后端协定) | +| `ImageApi.getMyTasks` | `GET /v1/image/my-tasks` | **我的任务列表** | + +核心实体:`CreateTaskResponse`、`ProgressResponse`(`status`、`progress`、`resultUrl`)、`MyTasksResponse` / `MyTaskItem`(`taskId`、`status`、`createTime`、`resultUrl` 等)、`UploadPresignedUrlResponse`。 + +**与 app_client 对齐(框架内、无 UI)** + +- **上传图 ↔ 任务 id 本地缓存**:创建任务成功后调用 `TaskUploadCoverStore.saveAfterCreateTaskResponse(response:, source:)`(或 `saveAfterCreateTaskBody` / `saveForTask`)。文件在应用 support 目录下 `gallery_upload_covers/`,默认 25h 过期清理,文件名与数字 `taskId` 一致(如 `123.jpg`)。 +- **我的任务列表**:仍用 `ImageApi.getMyTasks`。`MyTasksResponse` 同时兼容列表键 `tasks` / `intensify`、`hasNext` / `manifest`;`MyTaskItem` / `CreateTaskResponse` 中任务 id 兼容 `taskId` / `tree` / `id`(解密后的 business 字段)。 +- **与 app_client Gallery 相同的行模型**:`GalleryTaskItem` / `GalleryMediaItem` 及 `listingDisplayFromApi` 等见 `gallery_task_models.dart`;原始列表 Map 可用 `ImageTaskHistory.parseGalleryTasksFromData` 解析,本机封面路径用 `ImageTaskHistory.localCoverPathsForGalleryTasks`(或与 `MyTaskItem` 对应的 `localCoverPathsForMyTaskItems`)在刷新列表后合并。 + +**FunyMee 推荐直接调用的封装(框架已导出)** + +- **`compressImageForUpload` / `CompressImageForUploadOptions`**:上传前压图。 +- **`ImagePresignedUploadCreateTaskFlow.run`**:压图(可关)→ `getUploadPresignedUrl` → HTTP PUT → `createTask` → 可选 `TaskUploadCoverStore`;`UploadPresignedUrlResponse` 支持 `putHeaders` 等与 PUT 合并;若后端只要 `imgUrl` 可设 `createTaskUseImgUrlOnly: true`。 +- **`UserAccountRefresh.fetchAndNotify`**:`getAccount` + 回调,无 UI 状态。 +- **`AdjustService.obtainReferrerForUpload`**:返回 `ReferrerForUpload`(`digest` + `source`),供 `UserApi.referrer` 等与参考产品一致。 +- **`ensureDeviceMemoryProfileInitialized`**(默认通道 `client_proxy_framework/device_memory`,插件已接 Android `ActivityManager.totalMem`)、`deviceGridMaxConcurrentVideos` 等。 +- **`VideoThumbnailCache.instance`**:远程视频缩略图 / 海报帧缓存。 + +### 7.3 支付 + +| 方法 | Path | 说明 | +|------|------|------| +| `PaymentApi.getGooglePayActivities` | `GET /v1/payment/getGooglePayActivities` | Android 商品活动 | +| `PaymentApi.getApplePayActivities` | `GET /v1/payment/getApplePayActivities` | iOS 商品活动 | +| `PaymentApi.getPaymentMethods` | `POST /v1/payment/get-payment-methods` | 某活动支付方式 | +| `PaymentApi.createPayment` | `POST /v1/payment/createPayment` | 创建订单 | +| `PaymentApi.getPaymentDetailList` / `getOrderDetail` 等 | 见 `payment_api.dart` | 订单详情、列表 | + +内购验证与 `PurchaseDetails` 流封装见 `PaymentService`(`client_proxy_framework`)。 + +### 7.4 反馈(举报) + +| 方法 | Path | 说明 | +|------|------|------| +| `FeedbackApi.getUploadPresignedUrl` | `POST /v1/feedback/upload-presigned-url` | 截图/凭证上传 | +| `FeedbackApi.submit` | `POST /v1/feedback/submit` | 提交(`fileUrls`、`contentType`、`content`) | + +实体:`SubmitFeedbackResponse`、`FeedbackUploadPresignedUrlResponse`。 + +--- + +## 8. 分屏实施规格 + +| 页面 | 主要 API / 数据源 | 关键实体与注意事项 | +|------|-------------------|-------------------| +| **FunyMee Home** | `getCommonInfo`(框架内已拉);Tab/静态列表用 **`ExtConfigRuntime` / `ExtConfigData`**;其它首页结构可解析 `t2IConfig` | 见 **§5.5**;静态 Tab id 使用 `kExtConfigItemsCategoryId` | +| **生成图片** | `getUploadPresignedUrl` + HTTP PUT 到 `uploadUrl`;`createTask` 或 `createTxt2Img` / 业务指定路径 | 传 `userId`、`app`;参数以后台为准 | +| **生成中** | `getProgress` 轮询 | `taskId` 来自创建响应;`status` / `progress` 与 client_guide 状态枚举对齐 | +| **已完成 / 可下载 / 单图** | 同一任务不同 UI 态;数据来自 `ProgressResponse` 或 `MyTaskItem` | 下载前校验 `resultUrl`;**举报入口放此组页面** | +| **My History** | `getMyTasks`(分页 `page` / `pageSize` / `cursor` 按后端) | 展示 `createTime`、`resultUrl`、状态;无远程封面时可合并 `TaskUploadCoverStore`;过期逻辑见 §10 | +| **Credit Record** | `getCreditsPage` | `CreditRecordItem.createTime` 为整型时间戳(秒或毫秒需与后端确认) | +| **Purchase Point** | `getGooglePayActivities` / `getApplePayActivities` → `getPaymentMethods` → `createPayment` + 平台 IAB | 对齐 `PaymentService` 与订单恢复 | +| **个人中心** | `getAccount`;展示 `common_info` 冗余字段 | 跳转购买、注销;**无举报** | +| **注销** | 两步 UI + `deleteAccount` | 成功后清除本地 token、回首页或登录流 | +| **举报** | `FeedbackApi` 预签名上传 + `submit` | `contentType` 与后台枚举一致(见 client_guide) | +| **全屏 Loading** | 无 API | 覆盖路由栈或全局 `Overlay` | + +--- + +## 9. 生图状态机与轮询 + +推荐流程: + +1. **创建**:`createTask`(或文生图等)→ 取 `taskId`;若有用户上传的本地待传文件,成功后调用 `TaskUploadCoverStore.saveAfterCreateTaskResponse` 与任务 id 关联(见 §7.2)。 +2. **轮询**:循环 `ImageApi.getProgress(app:, taskId:, userId:)`,间隔 1–3s,带** backoff** 与**页面 dispose 取消**。 +3. **状态映射**(字符串以**后端文档为准**,此处为占位): + - `pending` / `processing` → 生成中页 + - `success` / `completed` 且 `resultUrl` 有效 → 可下载 / 单图 + - `failed` → 错误态 UI + 重试/客服 +4. **离开 App 再进入**:用 `getMyTasks` 或持久化 `taskId` 恢复轮询。 + +详细状态码表:在 **client_guide** 搜索 `progress` / `task` / `status`。 + +--- + +## 10. 历史 24 小时与倒计时 + +- 列表项展示 **创建时间**:优先 `MyTaskItem.createTime`(格式由后端决定,ISO8601 或时间戳用 `intl`/自建解析)。 +- **「剩余可下载时间」**:若接口**未**直接返回过期时间戳,则采用产品规则: + `deadline = createdAt + 24h`,剩余 = `deadline - DateTime.now()`;展示与 **My History** 画板一致(天/时/分)。 +- 过期后:隐藏或置灰 Download,可选调用后台是否仍返回 `resultUrl` 以决定客户端行为。 + +--- + +## 11. 积分流水与支付记录 + +- **积分流水(Credit Record 页)**:`UserApi.getCreditsPage(page:, size:, type:)`。 + **`type` 含义必须与后台确认**(过滤消费/充值等);分页用 `total/current/pages`。 +- **支付记录(若单独 Tab)**:`UserApi.getUserPayments(app:, userId:)`,实体 `UserPaymentsListResponse`。 + +--- + +## 12. 内购(积分购买) + +### 12.1 编排层(推荐,`package:client_proxy_framework/...` 已导出 `payment_flow.dart`) + +| 类型 | 作用 | +|------|------| +| `PaymentSettlementSink` | 宿主实现 `onPaymentSettled`:**仅此一处**拉 `common_info` / 更新 `UserState` / 埋点 | +| `PaymentSettlement` / `PaymentFlowOutcomeType` | 成功 / 失败 / 取消 / 超时 等统一结果 | +| `PaymentFlowCatalog.loadStoreActivities` | 按平台调 `getGooglePayActivities` / `getApplePayActivities` | +| `PaymentApi.getPaymentMethods` | 仍直接调用;选完后走三方或内购 | +| `ThirdPartyCheckoutCoordinator.createOrder` | 封装 `createPayment`,得到 `orderId` + `payUrl` | +| `ThirdPartyCheckoutCoordinator.openPayUrlIfPresent` | 需传入宿主 `PaymentCheckoutUrlLauncher`(WebView / `launchUrl`) | +| `ThirdPartyPaymentWatch` | 宿主在打开收银台后调用 `start(orderId:)`,`stop()` 停止;终态只触发一次 `sink` | +| `NativeIapCoordinator.purchaseGooglePlay` | **仅 Android**:`createPayment` → 拉起购买 → `googlepay` → consume(含 `federation` 映射) | + +**宿主策略**:根据 `ExtConfigRuntime.data` 的 `allowThirdPartyPayment`(或自研规则)选择调用 `ThirdPartyCheckoutCoordinator` + `ThirdPartyPaymentWatch`,或 `NativeIapCoordinator.purchaseGooglePlay`;**不要在框架里写死分支**。 + +### 12.2 底层 API(仍可直接用) + +1. 拉商品活动:`PaymentApi.getGooglePayActivities` / `getApplePayActivities`。 +2. 用户选档位 → `PaymentApi.getPaymentMethods(activityId: int)`。 +3. `PaymentApi.createPayment`、`PaymentApi.googlepay`、`PaymentService` 补单等:**编排类内部已组合**,单独对接时仍以 client_guide 为准。 + +--- + +## 13. 举报(反馈) + +1. **入口**:仅从 **生图完成后的结果页**(已完成 / 可下载 / 单图)进入 `Y9WlO` 对应界面。 +2. 可选图片:`FeedbackApi.getUploadPresignedUrl` → PUT 文件 → 得到 `fileUrl` 列表。 +3. `FeedbackApi.submit(fileUrls:, content:, contentType:)`;`content` 可含任务 id、原因枚举等约定字段。 + +--- + +## 14. 注销账号 + +1. UI:`SYt0O` → `yxwpg`。 +2. 调用 `UserApi.deleteAccount(app:, userId:)`(GET);**确认是否需额外 body/query**。 +3. 清理:`ApiClient` 侧 token、本地 `SharedPreferences`、再 `runApp` 或导航到首页。 + +--- + +## 15. 主题、字体与资源 + +- 颜色/圆角:对齐 Pencil;主强调色与 Tab 下划线可参考 `#c99304`。 +- 字体:`Inter` + `Bonheur Royale`(见历史卡片 Download 标签);在 `pubspec.yaml` 注册 `fonts:` 并放入 `assets/fonts/`。 +- 首页背景:设计使用 `首页.png`,可复制为 `assets/images/` 或完全用 `LinearGradient` 复刻 `suXxr`。 + +--- + +## 16. 权限与合规 + +- **相册/存储**:保存生成图、选图上传前申请权限(iOS `NSPhotoLibraryUsageDescription` 等)。 +- **网络**:ATS / 明文规则按环境配置。 +- **Adjust / Facebook**:按 skin_guide 填 `AndroidManifest.xml`、`Info.plist`。 +- **隐私政策 URL**:若 `common_info` / `h5UrlConfig` 下发,在个人中心打开。 + +--- + +## 17. 调试、环境与联调 + +- 切换预发布:`skin_config.api.alwaysUsePreBaseUrl: true` 或 `debugBaseUrlOverride`。 +- 抓包:注意请求体为代理包装后的 JSON;**解密验证用 client_guide + 后端工具**。 +- `Logger` / `AnalyticsService` 调试开关见 `skin_config.analytics.debugLogs`。 + +--- + +## 18. 测试与上线检查清单 + +- [ ] 冷启动:无崩溃,`loginComplete` 后首屏可请求接口 +- [ ] 生图全链路:创建 → 进度 → 结果 → 下载(或保存相册) +- [ ] 历史分页与 24h 展示正确 +- [ ] 积分流水 `type`、分页正确 +- [ ] 内购:沙盒成功、积分刷新、Adjust 事件(若配置) +- [ ] 举报:仅结果页可见、提交成功 +- [ ] 注销:账号不可用、本地状态清空 +- [ ] 弱网 / 后台恢复 / 任务恢复 + +--- + +## 19. 导航关系 + +```mermaid +flowchart TB + Home[FunyMee Home] + GenFlow[生成图片→生成中] + GenResult[生图完成: 已完成/可下载/单图] + Hist[My History] + Credit[Credit Record] + Buy[Purchase Point] + Profile[个人中心] + Report[举报] + Del1[注销 步骤1] + Del2[注销 步骤2] + + Home --> GenFlow + GenFlow --> GenResult + GenResult --> Report + Home --> Hist + Home --> Profile + Hist --- Credit + Profile --> Buy + Profile --> Del1 --> Del2 +``` + +--- + +## 20. 外部文档索引 + +| 文档 | 用途 | +|------|------| +| [skin_app_development_guide.md](./skin_app_development_guide.md) | 换皮、Android/iOS、SDK、`AppAuthCallbacks` 模板 | +| [FunyMeeAI_client_guide.md](./FunyMeeAI_client_guide.md) | 代理/V2/接口细节、错误码 | +| [new_app_config_template.md](./new_app_config_template.md) | 配置模板说明 | +| `assets/skin_config.json` | 本应用实际配置 | +| `../client_proxy_framework/lib/src/services/*.dart` | API 源码与注释 | + +--- + +## 21. 修订记录 + +| 日期 | 说明 | +|------|------| +| 2026-04-07 | 初版:页面清单与启动流程 | +| 2026-04-07 | 举报入口:生图完成页 | +| 2026-04-07 | **完整版**:API 总表、分屏规格、状态机、支付/举报/注销、`skin_config`、工程结构、测试清单;明确与 client_guide / skin_guide 的闭环关系 | +| 2026-04-07 | 框架增加 `ExtConfigData` / `ExtConfigRuntime`:解析 `go_run`/`screen`/`san_fang` 与旧键名,common_info 成功后更新;手册 §5.5 | +| 2026-04-07 | `skin_config.extConfig`:`keys`/`itemKeys`/`defaults`;`ExtConfigKeySchema`;与 common_info 浅合并 | +| 2026-04-07 | 支付编排:`PaymentSettlementSink`、`ThirdPartyCheckoutCoordinator`、`ThirdPartyPaymentWatch`、`NativeIapCoordinator`;`CreatePaymentResponse.federation` | diff --git a/docs/new_app_config_template.md b/docs/new_app_config_template.md index cb38e89..6032ba0 100644 --- a/docs/new_app_config_template.md +++ b/docs/new_app_config_template.md @@ -58,6 +58,29 @@ *** +## 五点五、extConfig(`skin_config.json`,换皮即配) + +在 **`skin_config.json` 根级**增加 `extConfig` 后,无需改代码即可指定: + +1. **逻辑字段** ↔ 线上下发 JSON **键名** 的候选列表(兼容多版本后端键名)。 +2. **`defaults`**:本地默认对象(与 `common_info.extConfig` **同一套 wire 键**);`common_info` 成功后会与服务器 JSON **浅合并**,**服务器顶层键覆盖同名键**。 + +典型结构见仓库内 `assets/skin_config.json` 的 `extConfig` 示例;框架内类型为 `ExtConfigKeySchema` / `SkinExtConfigSection`(`client_proxy_framework`)。 + +| 子节 | 说明 | +|------|------| +| `keys.showVideoMenu` | 字符串数组,如 `["go_run","need_wait"]`,依次为布尔字段候选键 | +| `keys.allowScreenshot` | 直接表示「允许截屏」的键,如 `["screen"]` | +| `keys.blockScreenshot` | 为 `true` 时表示**禁止**截屏的键,如 `["safe_area"]` | +| `keys.allowThirdPartyPayment` | 如 `["san_fang","lucky"]` | +| `keys.privacyUrl` / `agreementUrl` / `items` | 隐私、协议 URL、items 数组所在键名 | +| `itemKeys` | **固定列表项字段**(不含业务上可省略的兜底图时,可不写 `imageFix`,框架默认 `image_fix`):`image`、`imgNeed`、`cost`、`title`、`params`、`detail`;`imageFix` 可选 | +| `defaults` | 与线上下发相同键名的对象,可放静态 `items` 列表供无网或后台未下发时使用 | + +省略整个 `extConfig` 时,解析行为与内置默认键名一致(见 FunyMee 开发手册 §5.5)。 + +*** + ## 六、Adjust SDK 配置 | 配置项 | 值 | 说明 | diff --git a/docs/skin_app_development_guide.md b/docs/skin_app_development_guide.md index 5c5fc15..4b60d89 100644 --- a/docs/skin_app_development_guide.md +++ b/docs/skin_app_development_guide.md @@ -198,6 +198,7 @@ dependencies: crypto: ^3.0.3 logger: ^2.0.2 shared_preferences: ^2.2.2 + android_id: ^0.5.1 device_info_plus: ^11.1.0 ``` @@ -472,13 +473,30 @@ class MainActivity : FlutterActivity() { ```dart import 'dart:convert'; +import 'dart:math'; + +import 'package:android_id/android_id.dart'; import 'package:client_proxy_framework/client_proxy_framework.dart'; import 'package:crypto/crypto.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import '../user/user_state.dart'; +const _prefsKeyFallbackDeviceId = 'persisted_device_id'; + +Future _persistedFallbackDeviceId() async { + final prefs = await SharedPreferences.getInstance(); + var id = prefs.getString(_prefsKeyFallbackDeviceId); + if (id != null && id.isNotEmpty) return id; + final random = Random.secure(); + final bytes = List.generate(16, (_) => random.nextInt(256)); + id = base64UrlEncode(bytes).replaceAll('=', ''); + await prefs.setString(_prefsKeyFallbackDeviceId, id); + return id; +} + /// 归因回调实现 class AppAttributionCallbacks implements AttributionCallbacks { @override @@ -523,18 +541,25 @@ class AppAttributionCallbacks implements AttributionCallbacks { /// 认证回调实现 class AppAuthCallbacks implements AuthServiceCallbacks { + /// 与 app_client 一致:Android 用 Settings.Secure.ANDROID_ID(`android_id` 包); + /// `device_info_plus` 的 `AndroidDeviceInfo.id` 是 ROM 构建号,不能作为设备 ID。 + /// iOS 用 `identifierForVendor`;读失败或其它平台用 SharedPreferences 持久化随机 id。 @override Future getDeviceId() async { - final deviceInfo = DeviceInfoPlugin(); switch (defaultTargetPlatform) { case TargetPlatform.android: - final android = await deviceInfo.androidInfo; - return android.id; + final androidId = await const AndroidId().getId(); + if (androidId != null && androidId.isNotEmpty) { + return androidId; + } + return _persistedFallbackDeviceId(); case TargetPlatform.iOS: - final ios = await deviceInfo.iosInfo; - return ios.identifierForVendor ?? 'ios-unknown'; + final ios = await DeviceInfoPlugin().iosInfo; + final idfv = ios.identifierForVendor; + if (idfv != null && idfv.isNotEmpty) return idfv; + return _persistedFallbackDeviceId(); default: - return 'device-${DateTime.now().millisecondsSinceEpoch}'; + return _persistedFallbackDeviceId(); } } diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 3e0763f..ab2fb97 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -76,5 +76,9 @@ TODO: Add Facebook Client Token FacebookDisplayName FunyMee AI + FacebookAutoLogAppEventsEnabled + + FacebookAdvertiserIDCollectionEnabled + diff --git a/lib/core/auth/auth_service.dart b/lib/core/auth/auth_service.dart index 875a86f..f46dfef 100644 --- a/lib/core/auth/auth_service.dart +++ b/lib/core/auth/auth_service.dart @@ -1,25 +1,47 @@ import 'dart:convert'; +import 'dart:math'; +import 'package:android_id/android_id.dart'; import 'package:client_proxy_framework/client_proxy_framework.dart'; import 'package:crypto/crypto.dart' show md5; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import '../user/user_state.dart'; +const _prefsKeyFallbackDeviceId = 'persisted_device_id'; + +Future _persistedFallbackDeviceId() async { + final prefs = await SharedPreferences.getInstance(); + var id = prefs.getString(_prefsKeyFallbackDeviceId); + if (id != null && id.isNotEmpty) return id; + final random = Random.secure(); + final bytes = List.generate(16, (_) => random.nextInt(256)); + id = base64UrlEncode(bytes).replaceAll('=', ''); + await prefs.setString(_prefsKeyFallbackDeviceId, id); + return id; +} + class AppAuthCallbacks implements AuthServiceCallbacks { + /// 与 app_client 一致:Android 用 Settings.Secure.ANDROID_ID(android_id 包,非 Build.ID); + /// iOS 用 identifierForVendor;失败或其它平台用 SharedPreferences 持久化随机 id。 @override Future getDeviceId() async { - final deviceInfo = DeviceInfoPlugin(); switch (defaultTargetPlatform) { case TargetPlatform.android: - final android = await deviceInfo.androidInfo; - return android.id; + final androidId = await const AndroidId().getId(); + if (androidId != null && androidId.isNotEmpty) { + return androidId; + } + return _persistedFallbackDeviceId(); case TargetPlatform.iOS: - final ios = await deviceInfo.iosInfo; - return ios.identifierForVendor ?? 'ios-unknown'; + final ios = await DeviceInfoPlugin().iosInfo; + final idfv = ios.identifierForVendor; + if (idfv != null && idfv.isNotEmpty) return idfv; + return _persistedFallbackDeviceId(); default: - return 'device-${DateTime.now().millisecondsSinceEpoch}'; + return _persistedFallbackDeviceId(); } } diff --git a/pubspec.lock b/pubspec.lock index ffdbd80..5d2c01a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,6 +9,22 @@ packages: url: "https://pub.dev" source: hosted version: "5.5.1" + android_id: + dependency: "direct main" + description: + name: android_id + sha256: "50d62501d623a7e7358b3ceffd1bdd9b420292eba66cec8347d33ed10791f28e" + url: "https://pub.dev" + source: hosted + version: "0.5.1" + archive: + dependency: transitive + description: + name: archive + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff + url: "https://pub.dev" + source: hosted + version: "4.0.9" args: dependency: transitive description: @@ -64,6 +80,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + url: "https://pub.dev" + source: hosted + version: "1.0.0" collection: dependency: transitive description: @@ -175,6 +199,22 @@ packages: description: flutter source: sdk version: "0.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + hooks: + dependency: transitive + description: + name: hooks + sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388 + url: "https://pub.dev" + source: hosted + version: "1.0.2" http: dependency: "direct main" description: @@ -191,6 +231,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + image: + dependency: transitive + description: + name: image + sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce + url: "https://pub.dev" + source: hosted + version: "4.8.0" in_app_purchase: dependency: transitive description: @@ -223,6 +271,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.8+1" + jni: + dependency: transitive + description: + name: jni + sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f + url: "https://pub.dev" + source: hosted + version: "1.0.0" + jni_flutter: + dependency: transitive + description: + name: jni_flutter + sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6" + url: "https://pub.dev" + source: hosted + version: "1.0.1" js: dependency: transitive description: @@ -279,6 +343,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.7.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" matcher: dependency: transitive description: @@ -303,6 +375,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.17.0" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" + url: "https://pub.dev" + source: hosted + version: "0.17.6" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + url: "https://pub.dev" + source: hosted + version: "9.3.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" path: dependency: transitive description: @@ -311,6 +407,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "914a07484c4380e572998d30486e77e0d9cd2faec72fee268086d07bf7f302c9" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + url: "https://pub.dev" + source: hosted + version: "2.6.0" path_provider_linux: dependency: transitive description: @@ -335,6 +455,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" platform: dependency: transitive description: @@ -367,6 +495,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.9.1" + posix: + dependency: transitive + description: + name: posix + sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" + url: "https://pub.dev" + source: hosted + version: "6.5.0" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" shared_preferences: dependency: "direct main" description: @@ -492,6 +636,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + video_thumbnail: + dependency: transitive + description: + name: video_thumbnail + sha256: "181a0c205b353918954a881f53a3441476b9e301641688a581e0c13f00dc588b" + url: "https://pub.dev" + source: hosted + version: "0.5.6" vm_service: dependency: transitive description: @@ -532,6 +684,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" sdks: dart: ">=3.11.1 <4.0.0" - flutter: ">=3.35.0" + flutter: ">=3.38.4" diff --git a/pubspec.yaml b/pubspec.yaml index 46f967b..29de4ce 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,6 +18,7 @@ dependencies: crypto: ^3.0.3 logger: ^2.0.2 shared_preferences: ^2.2.2 + android_id: ^0.5.1 device_info_plus: ^11.1.0 dev_dependencies: