初始化
This commit is contained in:
commit
7b8ab4936d
31
.dart_tool/extension_discovery/README.md
Normal file
31
.dart_tool/extension_discovery/README.md
Normal 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.
|
||||
1
.dart_tool/extension_discovery/vs_code.json
Normal file
1
.dart_tool/extension_discovery/vs_code.json
Normal file
@ -0,0 +1 @@
|
||||
{"version":2,"entries":[{"package":"client_proxy_framework","rootUri":"../","packageUri":"lib/"}]}
|
||||
166
.dart_tool/package_config.json
Normal file
166
.dart_tool/package_config.json
Normal 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"
|
||||
}
|
||||
193
.dart_tool/package_graph.json
Normal file
193
.dart_tool/package_graph.json
Normal 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
1
.dart_tool/version
Normal file
@ -0,0 +1 @@
|
||||
3.41.4
|
||||
94
README.md
Normal file
94
README.md
Normal 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
220
docs/payment_flow.md
Normal file
@ -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 秒
|
||||
19
lib/client_proxy_framework.dart
Normal file
19
lib/client_proxy_framework.dart
Normal file
@ -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';
|
||||
51
lib/src/api/api_client.dart
Normal file
51
lib/src/api/api_client.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
45
lib/src/api/api_crypto.dart
Normal file
45
lib/src/api/api_crypto.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
14
lib/src/api/api_response.dart
Normal file
14
lib/src/api/api_response.dart
Normal 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;
|
||||
}
|
||||
190
lib/src/api/proxy_client.dart
Normal file
190
lib/src/api/proxy_client.dart
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
101
lib/src/config/app_config.dart
Normal file
101
lib/src/config/app_config.dart
Normal 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;
|
||||
}
|
||||
|
||||
/// 应用配置接口
|
||||
/// 每个换皮应用实现此接口,提供自己的 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<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;
|
||||
}
|
||||
90
lib/src/config/default_field_mapping.dart
Normal file
90
lib/src/config/default_field_mapping.dart
Normal 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',
|
||||
});
|
||||
61
lib/src/config/field_mapping.dart
Normal file
61
lib/src/config/field_mapping.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
34
lib/src/log/app_logger.dart
Normal file
34
lib/src/log/app_logger.dart
Normal 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);
|
||||
}
|
||||
143
lib/src/services/image_api.dart
Normal file
143
lib/src/services/image_api.dart
Normal 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,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
110
lib/src/services/payment_api.dart
Normal file
110
lib/src/services/payment_api.dart
Normal 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,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
100
lib/src/services/user_api.dart
Normal file
100
lib/src/services/user_api.dart
Normal 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
199
pubspec.lock
Normal 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
15
pubspec.yaml
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user