初始化
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