初始化

This commit is contained in:
ivan 2026-03-24 19:34:04 +08:00
commit 7b8ab4936d
21 changed files with 1878 additions and 0 deletions

View File

@ -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.

View File

@ -0,0 +1 @@
{"version":2,"entries":[{"package":"client_proxy_framework","rootUri":"../","packageUri":"lib/"}]}

View File

@ -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"
}

View File

@ -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
}

1
.dart_tool/version Normal file
View File

@ -0,0 +1 @@
3.41.4

94
README.md Normal file
View File

@ -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<String, dynamic>;
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 | 响应结构字段名 |

220
docs/payment_flow.md Normal file
View File

@ -0,0 +1,220 @@
# 支付流程文档
## 1. 概述
本文档描述 Android 项目Flutter的完整支付流程包括商品获取、支付方式选择、订单创建、Google Play 内购以及补单机制。
---
## 2. 支付流程总览
```
用户点击 Buy
├─ enableThirdPartyPayment === true 且已登录
│ │
│ ├─ getPaymentMethods(activityId) 获取支付方式
│ ├─ 弹窗选择支付方式_PaymentMethodDialog
│ ├─ createPayment 创建订单
│ │
│ ├─ 若选中的是 Google Payresource/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 秒

View File

@ -0,0 +1,19 @@
/// API
///
///
/// 1. [AppConfig] appIdaesKeybaseUrl
/// 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';

View File

@ -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;
}
}

View File

@ -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<int>.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();
}
}

View File

@ -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;
}

View File

@ -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<String, dynamic> json, List<String> path) {
dynamic current = json;
for (final key in path) {
if (current is! Map<String, dynamic>) 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<String, dynamic> _buildV2Wrapper(Map<String, dynamic> sanctum) {
final result = Map<String, dynamic>.from(config.v2FixedValues);
// vault.tome.codex.grimoire.sanctum
Map<String, dynamic> current = result;
final path = config.v2SanctumPath;
for (var i = 0; i < path.length - 1; i++) {
final key = path[i];
current[key] = <String, dynamic>{};
current = current[key] as Map<String, dynamic>;
}
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<ApiResponse> request({
required String path,
required String method,
Map<String, String>? headers,
Map<String, String>? queryParams,
Map<String, dynamic>? body,
}) async {
final pk = config.proxyKeys;
final mapping = config.fieldMapping;
var headersMap = Map<String, dynamic>.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<String, dynamic>.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 = <String, dynamic>{
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<String, dynamic>;
_log('========== 响应 ===========\n${jsonEncode(json)}');
final sanctum = _getByPath(json, config.v2SanctumPath);
final code = sanctum is Map<String, dynamic>
? (sanctum[config.responseCodeField] as int? ?? -1)
: (json[config.responseCodeField] as int? ?? -1);
final msg = sanctum is Map<String, dynamic>
? (sanctum[config.responseMsgField] as String? ?? '')
: (json[config.responseMsgField] as String? ?? '');
var data = sanctum is Map<String, dynamic>
? sanctum[config.responseDataField]
: json[config.responseDataField];
if (data is Map<String, dynamic>) {
data = config.fieldMapping.mapResponse(data);
}
return ApiResponse(code: code, msg: msg, data: data);
} catch (e) {
return ApiResponse(code: -1, msg: e.toString());
}
}
}

View File

@ -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<String> noiseKeys;
}
///
/// appIdaesKeybaseUrl
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<String> get v2SanctumPath =>
const ['vault', 'tome', 'codex', 'grimoire', 'sanctum'];
/// V2
List<String> get v2NoiseKeys =>
const ['roar', 'clash', 'thunder', 'rumble', 'howl', 'growl'];
/// V2 arsenal: 4
Map<String, dynamic> 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;
}

View File

@ -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',
});

View File

@ -0,0 +1,61 @@
///
///
/// ** **canonical V2
/// V2
///
class FieldMapping {
const FieldMapping(this.mapping);
///
/// {'deviceId': 'origin', 'userId': 'asset', 'userToken': 'reevaluate'}
final Map<String, String> mapping;
Map<String, String> get _inverse {
return Map.fromEntries(
mapping.entries.map((e) => MapEntry(e.value, e.key)),
);
}
/// Map key
Map<String, dynamic> mapRequest(Map<String, dynamic> input) {
if (input.isEmpty) return input;
final out = <String, dynamic>{};
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<String, dynamic>.from(value));
}
if (value is List) {
return value.map((e) => _mapRequestValue(e)).toList();
}
return value;
}
/// Map key
Map<String, dynamic> mapResponse(Map<String, dynamic> input) {
if (input.isEmpty) return input;
final inv = _inverse;
final out = <String, dynamic>{};
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<String, dynamic>.from(value));
}
if (value is List) {
return value.map((e) => _mapResponseValue(e)).toList();
}
return value;
}
}

View File

@ -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);
}

View File

@ -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<ApiResponse> getCategoryList() async {
return _client.request(
path: '/v1/image/img2video/categories',
method: 'GET',
);
}
///
static Future<ApiResponse> getImg2VideoTasks({int? categoryId}) async {
return _client.request(
path: '/v1/image/img2video/tasks',
method: 'GET',
queryParams: categoryId != null ? {'categoryId': categoryId.toString()} : null,
);
}
///
static Future<ApiResponse> 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<ApiResponse> 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<ApiResponse> 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<ApiResponse> 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<ApiResponse> 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<ApiResponse> 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,
},
);
}
}

View File

@ -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<ApiResponse> 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<ApiResponse> 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<ApiResponse> 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<ApiResponse> 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<ApiResponse> 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,
},
);
}
}

View File

@ -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<ApiResponse> 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<ApiResponse> 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<ApiResponse> 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<ApiResponse> getAccount({
required String app,
String? userId,
}) async {
return _client.request(
path: '/v1/user/account',
method: 'GET',
queryParams: {
'app': app,
if (userId != null) 'userId': userId,
},
);
}
}

199
pubspec.lock Normal file
View File

@ -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"

15
pubspec.yaml Normal file
View File

@ -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