commit 7b8ab4936da3cdbbf40313df04c0416bbd5bea69 Author: ivan Date: Tue Mar 24 19:34:04 2026 +0800 初始化 diff --git a/.dart_tool/extension_discovery/README.md b/.dart_tool/extension_discovery/README.md new file mode 100644 index 0000000..9dc6757 --- /dev/null +++ b/.dart_tool/extension_discovery/README.md @@ -0,0 +1,31 @@ +Extension Discovery Cache +========================= + +This folder is used by `package:extension_discovery` to cache lists of +packages that contains extensions for other packages. + +DO NOT USE THIS FOLDER +---------------------- + + * Do not read (or rely) the contents of this folder. + * Do write to this folder. + +If you're interested in the lists of extensions stored in this folder use the +API offered by package `extension_discovery` to get this information. + +If this package doesn't work for your use-case, then don't try to read the +contents of this folder. It may change, and will not remain stable. + +Use package `extension_discovery` +--------------------------------- + +If you want to access information from this folder. + +Feel free to delete this folder +------------------------------- + +Files in this folder act as a cache, and the cache is discarded if the files +are older than the modification time of `.dart_tool/package_config.json`. + +Hence, it should never be necessary to clear this cache manually, if you find a +need to do please file a bug. diff --git a/.dart_tool/extension_discovery/vs_code.json b/.dart_tool/extension_discovery/vs_code.json new file mode 100644 index 0000000..45c4330 --- /dev/null +++ b/.dart_tool/extension_discovery/vs_code.json @@ -0,0 +1 @@ +{"version":2,"entries":[{"package":"client_proxy_framework","rootUri":"../","packageUri":"lib/"}]} \ No newline at end of file diff --git a/.dart_tool/package_config.json b/.dart_tool/package_config.json new file mode 100644 index 0000000..a97cfbe --- /dev/null +++ b/.dart_tool/package_config.json @@ -0,0 +1,166 @@ +{ + "configVersion": 2, + "packages": [ + { + "name": "args", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/args-2.7.0", + "packageUri": "lib/", + "languageVersion": "3.3" + }, + { + "name": "asn1lib", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/asn1lib-1.6.5", + "packageUri": "lib/", + "languageVersion": "3.0" + }, + { + "name": "async", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/async-2.13.0", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "characters", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/characters-1.4.1", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "clock", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/clock-1.1.2", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "collection", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/collection-1.19.1", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "convert", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/convert-3.1.2", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "crypto", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/crypto-3.0.7", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "encrypt", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/encrypt-5.0.3", + "packageUri": "lib/", + "languageVersion": "2.18" + }, + { + "name": "flutter", + "rootUri": "file:///Users/sven/flutter/packages/flutter", + "packageUri": "lib/", + "languageVersion": "3.9" + }, + { + "name": "http", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/http-1.6.0", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "http_parser", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/http_parser-4.1.2", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "js", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/js-0.7.2", + "packageUri": "lib/", + "languageVersion": "3.7" + }, + { + "name": "logger", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/logger-2.7.0", + "packageUri": "lib/", + "languageVersion": "2.17" + }, + { + "name": "material_color_utilities", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/material_color_utilities-0.13.0", + "packageUri": "lib/", + "languageVersion": "3.5" + }, + { + "name": "meta", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/meta-1.17.0", + "packageUri": "lib/", + "languageVersion": "3.5" + }, + { + "name": "path", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/path-1.9.1", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "pointycastle", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/pointycastle-3.9.1", + "packageUri": "lib/", + "languageVersion": "3.2" + }, + { + "name": "sky_engine", + "rootUri": "file:///Users/sven/flutter/bin/cache/pkg/sky_engine", + "packageUri": "lib/", + "languageVersion": "3.9" + }, + { + "name": "source_span", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/source_span-1.10.2", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "string_scanner", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/string_scanner-1.4.1", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "term_glyph", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/term_glyph-1.2.2", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "typed_data", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/typed_data-1.4.0", + "packageUri": "lib/", + "languageVersion": "3.5" + }, + { + "name": "vector_math", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/vector_math-2.2.0", + "packageUri": "lib/", + "languageVersion": "3.1" + }, + { + "name": "web", + "rootUri": "file:///Users/sven/.pub-cache/hosted/pub.dev/web-1.1.1", + "packageUri": "lib/", + "languageVersion": "3.4" + }, + { + "name": "client_proxy_framework", + "rootUri": "../", + "packageUri": "lib/", + "languageVersion": "3.0" + } + ], + "generator": "pub", + "generatorVersion": "3.11.1", + "flutterRoot": "file:///Users/sven/flutter", + "flutterVersion": "3.41.4", + "pubCache": "file:///Users/sven/.pub-cache" +} diff --git a/.dart_tool/package_graph.json b/.dart_tool/package_graph.json new file mode 100644 index 0000000..953be45 --- /dev/null +++ b/.dart_tool/package_graph.json @@ -0,0 +1,193 @@ +{ + "roots": [ + "client_proxy_framework" + ], + "packages": [ + { + "name": "client_proxy_framework", + "version": "1.0.0", + "dependencies": [ + "crypto", + "encrypt", + "flutter", + "http", + "logger" + ], + "devDependencies": [] + }, + { + "name": "logger", + "version": "2.7.0", + "dependencies": [ + "clock", + "meta" + ] + }, + { + "name": "crypto", + "version": "3.0.7", + "dependencies": [ + "typed_data" + ] + }, + { + "name": "encrypt", + "version": "5.0.3", + "dependencies": [ + "args", + "asn1lib", + "clock", + "collection", + "crypto", + "pointycastle" + ] + }, + { + "name": "http", + "version": "1.6.0", + "dependencies": [ + "async", + "http_parser", + "meta", + "web" + ] + }, + { + "name": "flutter", + "version": "0.0.0", + "dependencies": [ + "characters", + "collection", + "material_color_utilities", + "meta", + "sky_engine", + "vector_math" + ] + }, + { + "name": "clock", + "version": "1.1.2", + "dependencies": [] + }, + { + "name": "meta", + "version": "1.17.0", + "dependencies": [] + }, + { + "name": "typed_data", + "version": "1.4.0", + "dependencies": [ + "collection" + ] + }, + { + "name": "pointycastle", + "version": "3.9.1", + "dependencies": [ + "collection", + "convert", + "js" + ] + }, + { + "name": "collection", + "version": "1.19.1", + "dependencies": [] + }, + { + "name": "asn1lib", + "version": "1.6.5", + "dependencies": [] + }, + { + "name": "args", + "version": "2.7.0", + "dependencies": [] + }, + { + "name": "web", + "version": "1.1.1", + "dependencies": [] + }, + { + "name": "http_parser", + "version": "4.1.2", + "dependencies": [ + "collection", + "source_span", + "string_scanner", + "typed_data" + ] + }, + { + "name": "async", + "version": "2.13.0", + "dependencies": [ + "collection", + "meta" + ] + }, + { + "name": "sky_engine", + "version": "0.0.0", + "dependencies": [] + }, + { + "name": "vector_math", + "version": "2.2.0", + "dependencies": [] + }, + { + "name": "material_color_utilities", + "version": "0.13.0", + "dependencies": [ + "collection" + ] + }, + { + "name": "characters", + "version": "1.4.1", + "dependencies": [] + }, + { + "name": "js", + "version": "0.7.2", + "dependencies": [] + }, + { + "name": "convert", + "version": "3.1.2", + "dependencies": [ + "typed_data" + ] + }, + { + "name": "string_scanner", + "version": "1.4.1", + "dependencies": [ + "source_span" + ] + }, + { + "name": "source_span", + "version": "1.10.2", + "dependencies": [ + "collection", + "path", + "term_glyph" + ] + }, + { + "name": "term_glyph", + "version": "1.2.2", + "dependencies": [] + }, + { + "name": "path", + "version": "1.9.1", + "dependencies": [] + } + ], + "configVersion": 1 +} \ No newline at end of file diff --git a/.dart_tool/version b/.dart_tool/version new file mode 100644 index 0000000..b259f47 --- /dev/null +++ b/.dart_tool/version @@ -0,0 +1 @@ +3.41.4 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..94b28d6 --- /dev/null +++ b/README.md @@ -0,0 +1,94 @@ +# client_proxy_framework + +通用代理 API 框架。**接口请求已按原始字段名写好**,不同应用只需**修改映射表**即可接入不同后端。 + +## 设计思路 + +- **业务层**:统一使用**原始字段名**(canonical),如 `deviceId`、`userId`、`credits`、`userToken` +- **映射层**:`FieldMapping` 负责 原始 ↔ V2 互转 +- **换皮应用**:只需提供自己的 `fieldMapping`,无需改业务代码 + +## 使用方式 + +### 1. 添加依赖 + +```yaml +dependencies: + client_proxy_framework: + path: ../client_proxy_framework # 与 app_client_1 同级 +``` + +### 2. 实现配置 + +```dart +class MyAppConfig extends AppConfig { + @override + String get appId => 'YourAppId'; + @override + String get packageName => 'com.yourapp.package'; + @override + String get aesKey => 'your-16-char-key'; + @override + String get preBaseUrl => 'https://pre-api.example.com'; + @override + String get prodBaseUrl => 'https://api.example.com'; + @override + String get proxyPath => '/quester/defender/summoner'; + + // 若后端 V2 字段名不同,覆盖此方法 + @override + FieldMapping get fieldMapping => myCustomFieldMapping; +} +``` + +### 3. 初始化并调用 + +```dart +void main() { + ApiClient.init(MyAppConfig()); + runApp(MyApp()); +} + +// 使用原始字段名调用 +final res = await UserApi.fastLogin( + deviceId: deviceId, + sign: sign, + referer: referer, +); +if (res.isSuccess) { + final data = res.data as Map; + ApiClient.instance.setUserToken(data['userToken']); // 原始字段名 + UserState.setCredits(data['credits']); +} +``` + +## 内置 API 服务(均使用原始字段名) + +| 服务 | 方法示例 | +|------|----------| +| UserApi | fastLogin, referrer, getCommonInfo, getAccount | +| PaymentApi | getGooglePayActivities, getPaymentMethods, createPayment, googlepay | +| ImageApi | getCategoryList, getImg2VideoTasks, getProgress, getUploadPresignedUrl, createTask, getMyTasks | + +## 映射表 + +单一映射表:**原始字段 → 后端字段**。请求时按此转换,响应时自动取反。 + +```dart +const myMapping = FieldMapping({ + 'deviceId': 'origin', + 'userId': 'asset', + 'userToken': 'reevaluate', + 'credits': 'reveal', + // ... 后端文档给的映射表直接填入即可 +}); +``` + +## 可配置项 + +| 配置项 | 说明 | +|--------|------| +| fieldMapping | 字段映射表,换皮应用主要修改此项 | +| proxyKeys | 代理请求体字段名 | +| v2SanctumPath | V2 嵌套路径 | +| responseCodeField / responseMsgField / responseDataField | 响应结构字段名 | diff --git a/docs/payment_flow.md b/docs/payment_flow.md new file mode 100644 index 0000000..5febf83 --- /dev/null +++ b/docs/payment_flow.md @@ -0,0 +1,220 @@ +# 支付流程文档 + +## 1. 概述 + +本文档描述 Android 项目(Flutter)的完整支付流程,包括商品获取、支付方式选择、订单创建、Google Play 内购以及补单机制。 + +--- + +## 2. 支付流程总览 + +``` +用户点击 Buy + │ + ├─ enableThirdPartyPayment === true 且已登录 + │ │ + │ ├─ getPaymentMethods(activityId) 获取支付方式 + │ ├─ 弹窗选择支付方式(_PaymentMethodDialog) + │ ├─ createPayment 创建订单 + │ │ + │ ├─ 若选中的是 Google Pay(resource/ceremony == "GooglePay") + │ │ ├─ 调起 Google Play 内购 + │ │ ├─ 拿到 serverVerificationData + │ │ └─ POST /v1/payment/googlepay 回调验证 + │ │ + │ └─ 否则(其他支付方式) + │ └─ 打开 payUrl 在外部浏览器完成支付 + │ + └─ enableThirdPartyPayment !== true 或未登录 + └─ 仅 Android:直接调起 Google Play 内购 +``` + +--- + +## 3. 支付分支依据 + +| 条件 | 说明 | +|------|------| +| `UserState.enableThirdPartyPayment` | 登录后由 AuthService 从 `/v1/user/common_info` 响应写入 | +| `UserState.userId` | 用户登录后存储的用户 ID | +| **第三方支付** | `enableThirdPartyPayment == true` 且 `userId` 非空 | +| **直接谷歌支付** | 其他情况(未开第三方支付或未登录)| + +--- + +## 4. 商品展示与获取 + +### 4.1 接口 + +- **Android**: `GET /v1/payment/getGooglePayActivities` +- **iOS**: `GET /v1/payment/getApplePayActivities` + +### 4.2 商品字段映射 + +| 字段(API) | 字段(客户端映射) | 说明 | +|-------------|-------------------|------| +| helm | code / productId | Google Play 商品 ID | +| warrior | activityId | 活动 ID,用于创建订单 | +| guardian | actualAmount | 实际金额 | +| curriculum | originAmount | 原价(带划线)| +| forge | bonus | 赠送积分 | +| glossary | title | 标题 | + +### 4.3 代码入口 + +文件:`lib/features/recharge/recharge_screen.dart` +- `_fetchActivities()`: 获取商品列表 +- `_onBuy()`: 用户点击购买入口 + +--- + +## 5. 第三方支付流程 + +### 5.1 步骤 + +1. **获取支付方式**: `POST /v1/payment/get-payment-methods` + - 参数: `warrior` (activityId), `vambrace` (可选,国家) + +2. **弹窗选择**: 展示支付方式列表(`_PaymentMethodSheet`),包含: + - `resource`: 支付方式(如 GOOGLEPAY) + - `ceremony`: 子支付方式 + - `name`: 显示名称 + - `icon`: 图标 URL + - `recommend`: 是否推荐 + +3. **创建订单**: `POST /v1/payment/createPayment` + - 参数: `sentinel`, `asset`(userId), `warrior`(activityId), `resource`, `ceremony` + - 返回: `federation`(订单ID), `convert`(支付URL) + +4. **支付方式分支**: + - **Google Pay**: 调用 `GooglePlayPurchaseService.launchPurchaseAndReturnData()` → 调起内购 → 调用 `PaymentApi.googlepay()` 回调验证 + - **其他方式**: 使用 `url_launcher` 打开 `convert` 支付链接 + +### 5.2 代码位置 + +- 入口: `recharge_screen.dart` → `_runThirdPartyPayment()` +- 创建订单: `_createOrderAndOpenUrl()` +- Google Pay 判断: `_isGooglePay()` + +--- + +## 6. 直接谷歌支付流程 + +仅 Android,且不经过 `getPaymentMethods` 和 `createPayment`(三方支付关闭时): + +1. 调用 `createPayment`(resource=GooglePay, ceremony=GooglePay) +2. 调起 Google Play 内购 +3. 回调验证 + +### 代码位置 + +- `recharge_screen.dart` → `_runGooglePay()` + +--- + +## 7. Google Play 内购统一入口 + +### 7.1 核心方法 + +`GooglePlayPurchaseService.launchPurchaseAndReturnData(productId)` + +- 调起 Google Play 内购 +- 返回 `GooglePayPurchaseResult` 包含: + - `orderId`: Google 订单号 + - `payload.purchaseData`: purchaseData(用于 merchant) + - `payload.signature`: 签名(用于 sample) + - `purchaseDetails`: PurchaseDetails 对象 + +### 7.2 回调验证 + +`PaymentApi.googlepay(sample, merchant, federation, asset)` + +- `sample`: 签名 +- `merchant`: purchaseData +- `federation`: 订单ID +- `asset`: userId + +### 7.3 核销 + +`GooglePlayPurchaseService.completeAndConsumePurchase(purchaseDetails)` + +- 执行 `completePurchase` +- 执行 `consumePurchase`(Android) + +--- + +## 8. 补单机制 + +### 8.1 触发时机 + +- 进入充值页时调用 `GooglePlayPurchaseService.runOrderRecovery()` + +### 8.2 补单流程 + +1. 获取未核销订单: `getUnacknowledgedPurchases()` + - 合并 `queryPastPurchases` 和 `purchaseStream` 的待处理订单 + +2. 对每笔订单: + - 查询本地存储的 `federation` 映射 + - 若存在 federation: 调用 `googlepay` 回调 → 成功后 consume + - 若无 federation: 仅执行 consume 解除「已拥有此内容」 + +3. 补单成功后刷新账户 + +### 8.3 存储映射 + +使用 `SharedPreferences` 存储 `googleOrderId → federation` 映射: +- `saveFederationForGoogleOrderId()` +- `getFederationForGoogleOrderId()` +- `removeFederationForGoogleOrderId()` + +--- + +## 9. API 汇总 + +| 接口 | 方法 | 说明 | +|------|------|------| +| `/v1/payment/getGooglePayActivities` | GET | 获取 Android 商品列表 | +| `/v1/payment/getApplePayActivities` | GET | 获取 iOS 商品列表 | +| `/v1/payment/get-payment-methods` | POST | 获取支付方式列表 | +| `/v1/payment/createPayment` | POST | 创建支付订单 | +| `/v1/payment/getOrderDetail` | GET | 查询订单状态(轮询)| +| `/v1/payment/googlepay` | POST | Google Pay 回调验证 | + +--- + +## 10. 代码文件位置 + +| 功能 | 文件路径 | +|------|----------| +| 充值页面 | `lib/features/recharge/recharge_screen.dart` | +| 支付 API | `lib/core/api/services/payment_api.dart` | +| Google Play 内购服务 | `lib/features/recharge/google_play_purchase_service.dart` | +| 支付方式模型 | `lib/features/recharge/models/payment_method_item.dart` | +| 商品模型 | `lib/features/recharge/models/activity_item.dart` | +| 购买结果模型 | `lib/features/recharge/models/google_pay_purchase_result.dart` | +| WebView 支付页 | `lib/features/recharge/payment_webview_screen.dart` | + +--- + +## 11. 常见问题 + +### 11.1 商品未找到 + +- 原因: 客户端 `helm` (productId) 与 Google Play 后台「产品 ID」不一致 +- 排查: 检查 `docs/google_pay_product_not_found.md` + +### 11.2 补单 + +- 未确认订单可能不会出现在 `queryPastPurchases` 中 +- 应用启动时订阅 `purchaseStream` 接收重新下发 +- 补单会合并两者的待处理订单 + +--- + +## 12. 注意事项 + +- 所有 Google Play 内购统一使用 `launchPurchaseAndReturnData()` 方法 +- 回调验证成功后必须调用 `completePurchase` + `consumePurchase` +- 支付 URL 打开方式取决于 `createPayment` 返回的 `convert` 字段 +- 订单状态轮询: 间隔 1/3/7/15/31/63 秒 diff --git a/lib/client_proxy_framework.dart b/lib/client_proxy_framework.dart new file mode 100644 index 0000000..ca35411 --- /dev/null +++ b/lib/client_proxy_framework.dart @@ -0,0 +1,19 @@ +/// 通用代理 API 框架。 +/// +/// 换皮应用只需: +/// 1. 实现 [AppConfig] 提供 appId、aesKey、baseUrl 等 +/// 2. 在 main 中调用 ApiClient.init(yourConfig) +/// 3. 通过 ApiClient.instance.proxy 发起请求 +library; + +export 'src/api/api_client.dart'; +export 'src/api/api_crypto.dart'; +export 'src/api/api_response.dart'; +export 'src/api/proxy_client.dart'; +export 'src/config/app_config.dart'; +export 'src/config/field_mapping.dart'; +export 'src/config/default_field_mapping.dart'; +export 'src/log/app_logger.dart'; +export 'src/services/image_api.dart'; +export 'src/services/payment_api.dart'; +export 'src/services/user_api.dart'; diff --git a/lib/src/api/api_client.dart b/lib/src/api/api_client.dart new file mode 100644 index 0000000..db5eb47 --- /dev/null +++ b/lib/src/api/api_client.dart @@ -0,0 +1,51 @@ +import '../config/app_config.dart'; +import 'proxy_client.dart'; + +/// 全局 API 客户端 +/// 使用前需调用 [init] 传入应用配置 +class ApiClient { + ApiClient._(); + + static final ApiClient _instance = ApiClient._(); + + static ApiClient get instance => _instance; + + AppConfig? _config; + ProxyClient? _proxy; + + /// 初始化,传入应用配置(必须在首次请求前调用) + static void init(AppConfig config) { + instance._config = config; + instance._proxy = ProxyClient( + config: config, + baseUrlOverride: config.debugBaseUrlOverride, + userToken: null, + ); + } + + /// 获取 ProxyClient,必须先调用 [init] + ProxyClient get proxy { + final p = _proxy; + if (p == null) { + throw StateError( + 'ApiClient not initialized. Call ApiClient.init(config) before use.', + ); + } + return p; + } + + AppConfig get config { + final c = _config; + if (c == null) { + throw StateError( + 'ApiClient not initialized. Call ApiClient.init(config) before use.', + ); + } + return c; + } + + /// 设置用户 Token(登录后调用) + void setUserToken(String? token) { + if (_proxy != null) _proxy!.userToken = token; + } +} diff --git a/lib/src/api/api_crypto.dart b/lib/src/api/api_crypto.dart new file mode 100644 index 0000000..4b5dd57 --- /dev/null +++ b/lib/src/api/api_crypto.dart @@ -0,0 +1,45 @@ +import 'dart:convert'; + +import 'package:encrypt/encrypt.dart'; + +/// AES-128-ECB 加解密 +class ApiCrypto { + ApiCrypto({required String aesKey}) + : _encrypter = Encrypter( + AES( + Key.fromUtf8(aesKey), + mode: AESMode.ecb, + padding: 'PKCS7', + ), + ); + + final Encrypter _encrypter; + + /// AES 加密,返回 Base64 字符串 + String encrypt(String plainText) { + final encrypted = _encrypter.encrypt(plainText); + return encrypted.base64; + } + + /// AES 解密,输入 Base64 字符串 + String decrypt(String base64Cipher) { + final encrypted = Encrypted.fromBase64(base64Cipher); + return _encrypter.decrypt(encrypted); + } + + /// 生成随机 Base64 字符串(用于噪音字段) + static String randomBase64([int byteLength = 16]) { + final bytes = List.generate( + byteLength, (_) => DateTime.now().millisecondsSinceEpoch % 256); + return base64Encode(bytes); + } + + /// 生成 8 位随机字母数字 + static String randomAlnum() { + const chars = + 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + return List.generate(8, + (_) => chars[DateTime.now().microsecondsSinceEpoch % chars.length]) + .join(); + } +} diff --git a/lib/src/api/api_response.dart b/lib/src/api/api_response.dart new file mode 100644 index 0000000..b703a9b --- /dev/null +++ b/lib/src/api/api_response.dart @@ -0,0 +1,14 @@ +/// 统一 API 响应 +class ApiResponse { + ApiResponse({ + required this.code, + this.msg = '', + this.data, + }); + + final int code; + final String msg; + final dynamic data; + + bool get isSuccess => code == 0; +} diff --git a/lib/src/api/proxy_client.dart b/lib/src/api/proxy_client.dart new file mode 100644 index 0000000..c02ebc9 --- /dev/null +++ b/lib/src/api/proxy_client.dart @@ -0,0 +1,190 @@ +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart' as http; + +import '../config/app_config.dart'; +import '../log/app_logger.dart'; +import 'api_crypto.dart'; +import 'api_response.dart'; + +final _proxyLog = AppLogger('ProxyClient'); + +const int _maxLogChunk = 1000; + +void _logLong(String text) { + if (text.isEmpty) return; + final lines = text.split('\n'); + if (lines.length == 1 && text.length <= _maxLogChunk) { + _proxyLog.d(text); + return; + } + final buffer = StringBuffer(); + int chunkIndex = 0; + for (final line in lines) { + final lineWithNewline = buffer.isEmpty ? line : '\n$line'; + if (buffer.length + lineWithNewline.length > _maxLogChunk && + buffer.isNotEmpty) { + chunkIndex++; + _proxyLog.d('(part $chunkIndex)\n$buffer'); + buffer.clear(); + buffer.write(line); + } else { + if (buffer.isNotEmpty) buffer.write('\n'); + buffer.write(line); + } + } + if (buffer.isNotEmpty) { + chunkIndex++; + _proxyLog + .d(chunkIndex > 1 ? '(part $chunkIndex)\n$buffer' : buffer.toString()); + } +} + +void _log(Object? msg) { + if (!kDebugMode) return; + final str = msg?.toString().trim() ?? ''; + if (str.length <= _maxLogChunk) { + _proxyLog.d(str); + return; + } + _logLong(str); +} + +/// 从 json 中按路径取嵌套值,如 ['vault','tome','codex','grimoire','sanctum'] +dynamic _getByPath(Map json, List path) { + dynamic current = json; + for (final key in path) { + if (current is! Map) return null; + current = current[key]; + } + return current; +} + +/// 代理请求客户端 +class ProxyClient { + ProxyClient({ + required this.config, + this.baseUrlOverride, + this.userToken, + }) : _crypto = ApiCrypto(aesKey: config.aesKey); + + final AppConfig config; + final String? baseUrlOverride; + final ApiCrypto _crypto; + + /// 用户 Token,登录后由 [ApiClient.setUserToken] 设置 + String? userToken; + + String get _baseUrl => baseUrlOverride ?? config.baseUrl; + + Map _buildV2Wrapper(Map sanctum) { + final result = Map.from(config.v2FixedValues); + // 构建嵌套:vault.tome.codex.grimoire.sanctum + Map current = result; + final path = config.v2SanctumPath; + for (var i = 0; i < path.length - 1; i++) { + final key = path[i]; + current[key] = {}; + current = current[key] as Map; + } + current[path.last] = sanctum; + + for (final key in config.v2NoiseKeys) { + result[key] = ApiCrypto.randomAlnum(); + } + return result; + } + + /// 发送代理请求 + /// + /// [headers]、[queryParams]、[body] 使用**原始字段名**(canonical), + /// 框架会按 [AppConfig.fieldMapping] 转为 V2 字段名后发送。 + /// 响应 data 会自动从 V2 转回原始字段名。 + Future request({ + required String path, + required String method, + Map? headers, + Map? queryParams, + Map? body, + }) async { + final pk = config.proxyKeys; + final mapping = config.fieldMapping; + + var headersMap = Map.from(headers ?? {}); + if (config.packageName.isNotEmpty) { + headersMap['pkg'] = config.packageName; + } + if (userToken != null && userToken!.isNotEmpty) { + headersMap['User_token'] = userToken!; + } + headersMap = mapping.mapRequest(headersMap); + + var paramsMap = Map.from( + queryParams?.map((k, v) => MapEntry(k, v)) ?? {}, + ); + paramsMap = mapping.mapRequest(paramsMap); + + var sanctum = body ?? {}; + sanctum = mapping.mapRequest(sanctum); + final v2Body = _buildV2Wrapper(sanctum); + + final headersEncoded = jsonEncode(headersMap); + final paramsEncoded = jsonEncode(paramsMap); + final v2BodyEncoded = jsonEncode(v2Body); + + final logStr = + '========== 原始入参 ===========\npath: $path\nmethod: $method\nqueryParams: $paramsEncoded\nbody(sanctum): ${jsonEncode(sanctum)}'; + _log(logStr); + + final proxyBody = { + pk.heroClass: config.appId, + pk.petSpecies: _crypto.encrypt(path), + pk.powerLevel: _crypto.encrypt(method), + pk.questRank: _crypto.encrypt(headersEncoded), + pk.battleScore: _crypto.encrypt(paramsEncoded), + pk.loyaltyIndex: _crypto.encrypt(v2BodyEncoded), + }; + + for (final key in pk.noiseKeys) { + proxyBody[key] = ApiCrypto.randomBase64(); + } + + final url = '$_baseUrl${config.proxyPath}'; + + final response = await http.post( + Uri.parse(url), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode(proxyBody), + ); + + return _parseResponse(response); + } + + ApiResponse _parseResponse(http.Response response) { + try { + final decrypted = _crypto.decrypt(response.body); + final json = jsonDecode(decrypted) as Map; + _log('========== 响应 ===========\n${jsonEncode(json)}'); + + final sanctum = _getByPath(json, config.v2SanctumPath); + final code = sanctum is Map + ? (sanctum[config.responseCodeField] as int? ?? -1) + : (json[config.responseCodeField] as int? ?? -1); + final msg = sanctum is Map + ? (sanctum[config.responseMsgField] as String? ?? '') + : (json[config.responseMsgField] as String? ?? ''); + var data = sanctum is Map + ? sanctum[config.responseDataField] + : json[config.responseDataField]; + + if (data is Map) { + data = config.fieldMapping.mapResponse(data); + } + + return ApiResponse(code: code, msg: msg, data: data); + } catch (e) { + return ApiResponse(code: -1, msg: e.toString()); + } + } +} diff --git a/lib/src/config/app_config.dart b/lib/src/config/app_config.dart new file mode 100644 index 0000000..343d951 --- /dev/null +++ b/lib/src/config/app_config.dart @@ -0,0 +1,101 @@ +import 'package:flutter/foundation.dart'; + +import 'default_field_mapping.dart'; +import 'field_mapping.dart'; + +/// 代理请求体字段名配置 +class ProxyKeysConfig { + const ProxyKeysConfig({ + this.heroClass = 'hero_class', + this.petSpecies = 'pet_species', + this.powerLevel = 'power_level', + this.questRank = 'quest_rank', + this.battleScore = 'battle_score', + this.loyaltyIndex = 'loyalty_index', + this.noiseKeys = const [ + 'billing_addr', + 'utm_term', + 'cluster_id', + 'lsn_value', + 'accuracy_val', + 'dir_path', + ], + }); + + final String heroClass; + final String petSpecies; + final String powerLevel; + final String questRank; + final String battleScore; + final String loyaltyIndex; + final List noiseKeys; +} + +/// 应用配置接口 +/// 每个换皮应用实现此接口,提供自己的 appId、aesKey、baseUrl 等 +abstract class AppConfig { + AppConfig(); + + /// 应用标识(代理请求 hero_class) + String get appId; + + /// 应用包名 + String get packageName; + + /// AES 密钥 + String get aesKey; + + /// 预发环境域名 + String get preBaseUrl; + + /// 生产环境域名 + String get prodBaseUrl; + + /// 代理入口路径 + String get proxyPath; + + /// 调试时本地代理地址(手机无法直连时用电脑转发),null 则直连预发 + String? get debugBaseUrlOverride; + + /// 当前 baseUrl(调试用预发,打包用生产) + String get baseUrl { + if (!kDebugMode) return prodBaseUrl; + return debugBaseUrlOverride ?? preBaseUrl; + } + + /// 代理入口完整 URL + String get proxyUrl => '$baseUrl$proxyPath'; + + /// 代理请求体字段名 + ProxyKeysConfig get proxyKeys => const ProxyKeysConfig(); + + /// V2 包装:sanctum 的嵌套路径,如 ['vault','tome','codex','grimoire','sanctum'] + List get v2SanctumPath => + const ['vault', 'tome', 'codex', 'grimoire', 'sanctum']; + + /// V2 包装:噪音字段名 + List get v2NoiseKeys => + const ['roar', 'clash', 'thunder', 'rumble', 'howl', 'growl']; + + /// V2 包装:固定值(如 arsenal: 4) + Map get v2FixedValues => const {'arsenal': 4}; + + /// 响应 code 字段名 + String get responseCodeField => 'helm'; + + /// 响应 msg 字段名 + String get responseMsgField => 'rampart'; + + /// 响应 data 字段名 + String get responseDataField => 'sidekick'; + + /// 请求头:包名字段名 + String get headerPackageNameField => 'portal'; + + /// 请求头:用户 token 字段名 + String get headerUserTokenField => 'knight'; + + /// 字段映射表(原始字段 ↔ V2 字段) + /// 换皮应用若后端 V2 字段名不同,覆盖此方法返回自己的映射 + FieldMapping get fieldMapping => petsHeroAIFieldMapping; +} diff --git a/lib/src/config/default_field_mapping.dart b/lib/src/config/default_field_mapping.dart new file mode 100644 index 0000000..7f2e102 --- /dev/null +++ b/lib/src/config/default_field_mapping.dart @@ -0,0 +1,90 @@ +import 'field_mapping.dart'; + +/// petsHeroAI 默认字段映射(原始字段 → 后端字段) +/// +/// 单一映射表,后端文档给的格式通常与此一致。 +/// 不同应用若后端字段名不同,只需提供新的 [FieldMapping] 覆盖此表。 +const FieldMapping petsHeroAIFieldMapping = FieldMapping({ + // === 请求头 === + 'pkg': 'portal', + 'User_token': 'knight', + + // === 通用 query === + 'app': 'sentinel', + 'userId': 'asset', + 'ch': 'crest', + 'type': 'accolade', + + // === fast_login body === + 'referer': 'digest', + 'sign': 'resolution', + 'deviceId': 'origin', + + // === 支付 === + 'activityId': 'warrior', + 'country': 'vambrace', + 'orderId': 'federation', + 'payUrl': 'convert', + 'productId': 'helm', + 'paymentMethod': 'resource', + 'paymentType': 'ceremony', + 'signature': 'sample', + 'purchaseData': 'merchant', + + // === 图片/视频 === + 'categoryId': 'insignia', + 'taskId': 'tree', + 'prompt': 'ledger', + 'resolution': 'guild', + 'srcImgUrls': 'commission', + 'fileName1': 'gateway', + 'fileName2': 'action', + 'contentType': 'pauldron', + 'expectedSize': 'stronghold', + 'page': 'trophy', + 'pageSize': 'heatmap', + 'cursor': 'platoon', + 'declaration': 'declaration', + 'quest': 'quest', + 'ext': 'nexus', + 'imgUrl': 'congregation', + 'poseId': 'profit', + 'templateId': 'compendium', + 'notification': 'notification', + 'allowance': 'allowance', + 'cosmos': 'cosmos', + + // === 响应 data(后端字段 → 原始字段,通过取反自动生效)=== + 'userToken': 'reevaluate', + 'credits': 'reveal', + 'avatar': 'realm', + 'userName': 'terminal', + 'countryCode': 'navigate', + 'extConfig': 'surge', + 'appFbConfig': 'evolve', + 'usign': 'retrospect', + 'creditsRecordUrl': 'conquer', + 'tgId': 'concession', + 'tgName': 'defer', + 'email': 'galaxy', + 'forcePayCenter': 'upgrade', + 't2IConfig': 'regulate', + 'h5UrlConfig': 'pursue', + 'payCenterUrl': 'switch', + 'freeBlurTimes': 'vow', + 'firstRegister': 'equip', + 'isVip': 'generate', + 'tags': 'rally', + 'freeTimes': 'decree', + 'subScribeValidTime': 'tokenize', + 'status': 'line', + 'productList': 'summon', + 'paymentMethods': 'renew', + 'guardian': 'guardian', + 'curriculum': 'curriculum', + 'forge': 'forge', + 'tag': 'constrain', + 'img': 'revenue', + 'url': 'digitize', + 'launchImgUrl': 'launchImgUrl', +}); diff --git a/lib/src/config/field_mapping.dart b/lib/src/config/field_mapping.dart new file mode 100644 index 0000000..42d5cc2 --- /dev/null +++ b/lib/src/config/field_mapping.dart @@ -0,0 +1,61 @@ +/// 字段映射配置 +/// +/// 单一映射表:**原始字段名 → 后端字段名**(canonical → V2)。 +/// 请求时按此表转换,响应时自动取反(V2 → 原始)。 +/// 后端给的映射表通常也不区分方向,直接填入即可。 +class FieldMapping { + const FieldMapping(this.mapping); + + /// 原始字段名 → 后端字段名 + /// 如 {'deviceId': 'origin', 'userId': 'asset', 'userToken': 'reevaluate'} + final Map mapping; + + Map get _inverse { + return Map.fromEntries( + mapping.entries.map((e) => MapEntry(e.value, e.key)), + ); + } + + /// 将 Map 的 key 从原始名转为后端名(请求) + Map mapRequest(Map input) { + if (input.isEmpty) return input; + final out = {}; + for (final e in input.entries) { + final key = mapping[e.key] ?? e.key; + out[key] = _mapRequestValue(e.value); + } + return out; + } + + dynamic _mapRequestValue(dynamic value) { + if (value is Map) { + return mapRequest(Map.from(value)); + } + if (value is List) { + return value.map((e) => _mapRequestValue(e)).toList(); + } + return value; + } + + /// 将 Map 的 key 从后端名转为原始名(响应) + Map mapResponse(Map input) { + if (input.isEmpty) return input; + final inv = _inverse; + final out = {}; + for (final e in input.entries) { + final key = inv[e.key] ?? e.key; + out[key] = _mapResponseValue(e.value); + } + return out; + } + + dynamic _mapResponseValue(dynamic value) { + if (value is Map) { + return mapResponse(Map.from(value)); + } + if (value is List) { + return value.map((e) => _mapResponseValue(e)).toList(); + } + return value; + } +} diff --git a/lib/src/log/app_logger.dart b/lib/src/log/app_logger.dart new file mode 100644 index 0000000..74f809f --- /dev/null +++ b/lib/src/log/app_logger.dart @@ -0,0 +1,34 @@ +import 'package:flutter/foundation.dart'; +import 'package:logger/logger.dart'; + +/// 统一应用日志 +class AppLogger { + AppLogger([this.tag = 'App']); + + final String tag; + + static Logger? _logger; + + static Logger get _instance { + _logger ??= Logger( + printer: PrettyPrinter( + methodCount: 0, + errorMethodCount: 6, + lineLength: 80, + colors: true, + printEmojis: true, + dateTimeFormat: DateTimeFormat.onlyTimeAndSinceStart, + ), + level: kReleaseMode ? Level.warning : Level.trace, + ); + return _logger!; + } + + String _msg(Object? message) => '[$tag] $message'; + + void d(Object? message) => _instance.d(_msg(message)); + void i(Object? message) => _instance.i(_msg(message)); + void w(Object? message) => _instance.w(_msg(message)); + void e(Object? message, [Object? error, StackTrace? stackTrace]) => + _instance.e(_msg(message), error: error, stackTrace: stackTrace); +} diff --git a/lib/src/services/image_api.dart b/lib/src/services/image_api.dart new file mode 100644 index 0000000..70c45ea --- /dev/null +++ b/lib/src/services/image_api.dart @@ -0,0 +1,143 @@ +import '../api/api_client.dart'; +import '../api/api_response.dart'; +import '../api/proxy_client.dart'; + +/// 图片/视频相关 API(使用原始字段名) +abstract final class ImageApi { + static ProxyClient get _client => ApiClient.instance.proxy; + + /// 获取图转视频分类列表 + static Future getCategoryList() async { + return _client.request( + path: '/v1/image/img2video/categories', + method: 'GET', + ); + } + + /// 获取图转视频任务列表 + static Future getImg2VideoTasks({int? categoryId}) async { + return _client.request( + path: '/v1/image/img2video/tasks', + method: 'GET', + queryParams: categoryId != null ? {'categoryId': categoryId.toString()} : null, + ); + } + + /// 获取推荐提示词 + static Future getPromptRecommends({ + required String app, + String? ch, + String? userId, + }) async { + return _client.request( + path: '/v1/image/prompt/recomends', + method: 'GET', + queryParams: { + 'app': app, + if (ch != null) 'ch': ch, + if (userId != null) 'userId': userId, + }, + ); + } + + /// 查询任务进度 + static Future getProgress({ + required String app, + required String taskId, + String? userId, + }) async { + return _client.request( + path: '/v1/image/progress', + method: 'GET', + queryParams: { + 'app': app, + 'taskId': taskId, + if (userId != null) 'userId': userId, + }, + ); + } + + /// 获取预签名上传 URL + static Future getUploadPresignedUrl({ + required String fileName1, + String? fileName2, + required String contentType, + required int expectedSize, + }) async { + return _client.request( + path: '/v1/image/upload-presigned-url', + method: 'POST', + body: { + 'fileName1': fileName1, + 'fileName2': fileName2 ?? '', + 'contentType': contentType, + 'expectedSize': expectedSize, + }, + ); + } + + /// 创建生图/视频任务 + static Future createTask({ + required String userId, + String? resolution, + String? srcImgUrls, + String? prompt, + String? cipher, + String? heatmap, + String? imgUrl, + bool allowance = false, + String? ext, + }) async { + return _client.request( + path: '/v1/image/create-task', + method: 'POST', + queryParams: {'userId': userId}, + body: { + if (resolution != null) 'resolution': resolution, + if (srcImgUrls != null) 'srcImgUrls': srcImgUrls, + if (prompt != null) 'prompt': prompt, + if (cipher != null) 'cipher': cipher, + if (heatmap != null) 'heatmap': heatmap, + if (imgUrl != null) 'imgUrl': imgUrl, + if (ext != null) 'ext': ext, + 'allowance': allowance, + }, + ); + } + + /// 获取我的任务列表 + static Future getMyTasks({ + required String app, + String? page, + String? pageSize, + String? cursor, + }) async { + return _client.request( + path: '/v1/image/my-tasks', + method: 'GET', + queryParams: { + 'app': app, + if (page != null) 'page': page, + if (pageSize != null) 'pageSize': pageSize, + if (cursor != null) 'cursor': cursor, + }, + ); + } + + /// 获取积分页面信息 + static Future getCreditsPageInfo({ + required String app, + String? userId, + String? ch, + }) async { + return _client.request( + path: '/v1/image/getCreditsPageInfo', + method: 'GET', + queryParams: { + 'app': app, + if (userId != null) 'userId': userId, + if (ch != null) 'ch': ch, + }, + ); + } +} diff --git a/lib/src/services/payment_api.dart b/lib/src/services/payment_api.dart new file mode 100644 index 0000000..32316c1 --- /dev/null +++ b/lib/src/services/payment_api.dart @@ -0,0 +1,110 @@ +import '../api/api_client.dart'; +import '../api/api_response.dart'; +import '../api/proxy_client.dart'; + +/// 支付相关 API(使用原始字段名) +abstract final class PaymentApi { + static ProxyClient get _client => ApiClient.instance.proxy; + + /// 获取 Google 商品列表 + static Future getGooglePayActivities({ + String? app, + String? shield, + String? country, + String? pkg, + }) async { + return _client.request( + path: '/v1/payment/getGooglePayActivities', + method: 'GET', + queryParams: { + 'app': app ?? ApiClient.instance.config.appId, + 'pkg': pkg ?? ApiClient.instance.config.packageName, + if (shield != null) 'shield': shield, + if (country != null) 'country': country, + }, + ); + } + + /// 获取 Apple 商品列表 + static Future getApplePayActivities({ + String? app, + String? shield, + String? country, + String? pkg, + }) async { + return _client.request( + path: '/v1/payment/getApplePayActivities', + method: 'GET', + queryParams: { + 'app': app ?? ApiClient.instance.config.appId, + 'pkg': pkg ?? ApiClient.instance.config.packageName, + if (shield != null) 'shield': shield, + if (country != null) 'country': country, + }, + ); + } + + /// 获取支付方式列表 + static Future getPaymentMethods({ + required String activityId, + String? country, + }) async { + return _client.request( + path: '/v1/payment/get-payment-methods', + method: 'POST', + body: { + 'activityId': activityId, + if (country != null && country.isNotEmpty) 'country': country, + }, + ); + } + + /// 创建支付订单 + static Future createPayment({ + required String app, + required String userId, + required String activityId, + required String paymentMethod, + String? paymentType, + String? lineage, + String? armor, + }) async { + return _client.request( + path: '/v1/payment/createPayment', + method: 'POST', + queryParams: {'app': app, 'userId': userId}, + body: { + 'app': app, + 'userId': userId, + 'activityId': activityId, + 'paymentMethod': paymentMethod, + if (paymentType != null && paymentType.isNotEmpty) 'paymentType': paymentType, + if (lineage != null) 'lineage': lineage, + if (armor != null) 'armor': armor, + }, + ); + } + + /// Google 支付结果回调 + static Future googlepay({ + required String signature, + required String purchaseData, + required String orderId, + required String userId, + }) async { + return _client.request( + path: '/v1/payment/googlepay', + method: 'POST', + queryParams: { + 'app': ApiClient.instance.config.appId, + 'userId': userId, + }, + body: { + 'signature': signature, + 'purchaseData': purchaseData, + 'orderId': orderId, + 'userId': userId, + }, + ); + } +} diff --git a/lib/src/services/user_api.dart b/lib/src/services/user_api.dart new file mode 100644 index 0000000..c2658f5 --- /dev/null +++ b/lib/src/services/user_api.dart @@ -0,0 +1,100 @@ +import '../api/api_client.dart'; +import '../api/api_response.dart'; +import '../api/proxy_client.dart'; + +/// 用户相关 API(使用原始字段名) +abstract final class UserApi { + static ProxyClient get _client => ApiClient.instance.proxy; + + /// 设备快速登录 + /// 参数:deviceId, sign(MD5(deviceId)大写), referer(归因) + static Future fastLogin({ + required String deviceId, + required String sign, + String? referer, + String? ch, + String? type, + }) async { + return _client.request( + path: '/v1/user/fast_login', + method: 'POST', + queryParams: { + if (ch != null) 'ch': ch, + 'pkg': ApiClient.instance.config.packageName, + if (type != null) 'type': type, + }, + body: { + 'referer': referer ?? '', + 'sign': sign, + 'deviceId': deviceId, + }, + ); + } + + /// 归因上报 + static Future referrer({ + required String app, + required String userId, + required String referer, + required String deviceId, + String? type, + String? pkg, + }) async { + return _client.request( + path: '/v1/user/referrer', + method: 'POST', + queryParams: { + 'app': app, + 'userId': userId, + if (type != null) 'type': type, + 'pkg': pkg ?? ApiClient.instance.config.packageName, + }, + body: { + 'referer': referer, + 'deviceId': deviceId, + }, + ); + } + + /// 获取用户通用信息 + static Future getCommonInfo({ + required String app, + String? shield, + String? userId, + String? ch, + String? item, + String? deviceId, + String? gauntlet, + String? pkg, + }) async { + return _client.request( + path: '/v1/user/common_info', + method: 'GET', + queryParams: { + 'app': app, + if (shield != null) 'shield': shield, + if (userId != null) 'userId': userId, + if (ch != null) 'ch': ch, + if (item != null) 'item': item, + if (deviceId != null) 'deviceId': deviceId, + if (gauntlet != null) 'gauntlet': gauntlet, + if (pkg != null) 'pkg': pkg, + }, + ); + } + + /// 获取用户账户信息 + static Future getAccount({ + required String app, + String? userId, + }) async { + return _client.request( + path: '/v1/user/account', + method: 'GET', + queryParams: { + 'app': app, + if (userId != null) 'userId': userId, + }, + ); + } +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..eb5bd8d --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,199 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + asn1lib: + dependency: transitive + description: + name: asn1lib + sha256: "9a8f69025044eb466b9b60ef3bc3ac99b4dc6c158ae9c56d25eeccf5bc56d024" + url: "https://pub.dev" + source: hosted + version: "1.6.5" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + crypto: + dependency: "direct main" + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + encrypt: + dependency: "direct main" + description: + name: encrypt + sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" + url: "https://pub.dev" + source: hosted + version: "5.0.3" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + http: + dependency: "direct main" + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + js: + dependency: transitive + description: + name: js + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + url: "https://pub.dev" + source: hosted + version: "0.7.2" + logger: + dependency: "direct main" + description: + name: logger + sha256: "25aee487596a6257655a1e091ec2ae66bc30e7af663592cc3a27e6591e05035c" + url: "https://pub.dev" + source: hosted + version: "2.7.0" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + url: "https://pub.dev" + source: hosted + version: "3.9.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" +sdks: + dart: ">=3.9.0-0 <4.0.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..5ad4339 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,15 @@ +name: client_proxy_framework +description: 通用代理 API 框架,支持可配置的 V2 包装、字段映射,换皮应用只需修改配置即可接入。 +version: 1.0.0 +publish_to: 'none' + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + flutter: + sdk: flutter + http: ^1.2.2 + encrypt: ^5.0.3 + crypto: ^3.0.3 + logger: ^2.0.2