2026-04-22 23:08:39 +08:00

799 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Client Proxy Framework 使用指南
## 1. 框架概述
`client_proxy_framework` 是一个通用代理 API 框架,用于 Flutter 移动应用与后端 API 的通信。框架已封装好请求/响应的加密解密、字段映射等逻辑,换皮应用只需配置少量参数即可使用。
### 核心特性
- **自动加解密**:请求体和响应体自动进行 AES 加密/解密
- **字段映射**:框架自动将原始字段名转换为 V2 字段名,响应自动转换回来
- **强类型实体**:返回强类型实体类,开发者直接使用映射后的字段
- **统一响应**`EntityResponse<T>` 提供强类型返回值
### 换皮应用:从零搭建
从零创建一个新的换皮应用(`skin_config.json``ClientBootstrap``main` 初始化顺序、Android/iOS 要点等),请阅读 **[《创建新换皮应用 — 步骤清单》](create_new_skin_app.md)**。
### 功能总览与用法
按模块整理的“功能 + 最小调用示例”请见 **[《Framework 功能与用法总览》](framework_feature_usage.md)**。
以首款换皮应用 **FunyMee** 为参考的 **视频首页**`common_info` → 分类 → 按分类拉模板)数据流说明,见 **[《视频首页数据获取流程》](video_home_data_flow.md)**。
**生图 / 任务**(预签名上传、`create-task`、进度轮询、文生图、历史列表与本地封面)的端到端说明,见 **[《生图 / 任务流程》](image_generation_flow.md)**。
示例换皮配置文件 [`lib/src/config/skin_config.example.json`](../lib/src/config/skin_config.example.json) 的字段说明,见 **[《skin_config.example.json 字段说明》](skin_config_example.md)**。
---
## 2. 快速开始
### 2.1 添加依赖
`pubspec.yaml` 中添加:
```yaml
dependencies:
client_proxy_framework:
path: ../client_proxy_framework
```
### 2.2 实现配置
创建一个类继承 `AppConfig`
```dart
import 'package:client_proxy_framework/client_proxy_framework.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';
}
```
### 2.3 初始化
`main.dart` 中初始化:
```dart
void main() {
ApiClient.init(MyAppConfig());
runApp(const MyApp());
}
```
---
## 3. 设计理念
### 3.1 请求流程
```
调用层 (原始字段)
ProxyClient.request()(自动加 `pkg`;默认加 `User_token`
字段映射 (原始 → V2)
AES 加密
发送请求
```
**请求头**`ProxyClient` 默认将 [AppConfig.packageName] 写入映射后的包名字段(原始名 `pkg`);若已设置用户 token默认还会写入 `User_token`。**`UserApi.fastLogin`** 对内层请求使用 `includeUserTokenInHeader: false`(不传 token`pkg` 仍按默认行为传递)。其余接口可按需在 `ProxyClient.request` 上传入上述开关。
**请求体**:各 `*Api` 方法使用与《客户端指南》解密表一致的**原始字段名**(如 `referer``deviceId``fileUrls` / `contentType` / `content`)。
### 3.2 响应流程
```
接收响应
AES 解密
字段映射 (V2 → 原始)
转换为实体类
调用层 (实体类)
```
---
## 4. 配置项详解
### AppConfig 配置
| 配置项 | 必填 | 说明 |
|--------|------|------|
| `appId` | 是 | 应用标识,对应代理请求的 `hero_class` |
| `packageName` | 是 | 应用包名,如 `com.example.app` |
| `aesKey` | 是 | AES 加密密钥,长度需为 16 字符 |
| `preBaseUrl` | 是 | 预发环境域名 |
| `prodBaseUrl` | 是 | 生产环境域名 |
| `proxyPath` | 是 | 代理入口路径 |
| `debugBaseUrlOverride` | 否 | 调试时本地代理地址 |
| `fieldMapping` | 否 | 字段映射表,默认使用 `petsHeroAIFieldMapping` |
---
## 5. API 服务
所有 API 方法使用**原始字段名**,返回**强类型实体**。
### 5.1 UserApi - 用户相关
#### 5.1.1 fastLogin - 设备快速登录
```dart
final res = await UserApi.fastLogin(
deviceId: '设备ID',
sign: 'MD5(deviceId)大写',
app: 'HAndroid', // 必填HIOS / HAndroid
referer: '归因来源', // 可选;`gg` 时常为 Play Install Referrer框架 start 取不到时用 utm_source=google-play&utm_medium=organic
ch: '渠道号', // 可选
type: 'gg', // 可选;未传时默认 ggGoogle Play 归因)
);
if (res.isSuccess) {
final loginInfo = res.data!;
final token = loginInfo.userToken;
final credits = loginInfo.credits;
final userId = loginInfo.userId;
}
```
宿主需在 `FrameworkAuthService.init` 传入的 `AuthServiceCallbacks` 中实现 `deviceId``sign`。**FunyMee 采用的获取方式与签名规则**见 [device_id_and_sign.md](./device_id_and_sign.md)。
**返回实体**: `FastLoginResponse`
- `userToken`: 用户 Token
- `userId`: 用户 ID
- `credits`: 积分
- `avatar`: 头像
- `userName`: 用户名
- `countryCode`: 国家码
- `isVip`: 是否 VIP
- 等等...
#### 5.1.2 getCommonInfo - 获取用户通用信息
```dart
final config = ApiClient.instance.config;
final backendApp = defaultTargetPlatform == TargetPlatform.iOS
? config.backendAppTypeIOS
: config.backendAppTypeAndroid;
final res = await UserApi.getCommonInfo(
app: backendApp, // 必填HIOS / HAndroid 等与后端约定
pkg: config.packageName, // 必填:包名
userId: '用户ID', // 可选
deviceId: '设备ID', // 可选
// client / ch / inviteBy / clientId 可选
);
if (res.isSuccess) {
final info = res.data!;
}
```
**返回实体**: `CommonInfoResponse`
#### 5.1.3 getAccount - 获取账户信息
```dart
final res = await UserApi.getAccount(
app: 'HAndroid',
userId: '用户ID', // 可选
);
if (res.isSuccess) {
final account = res.data!;
final credits = account.credits;
final isVip = account.isVip;
}
```
**返回实体**: `AccountResponse`
- `credits`: 积分
- `avatar`: 头像
- `userName`: 用户名
- `isVip`: 是否 VIP
- `freeTimes`: 免费次数
#### 5.1.4 getAppLanguage - App 语言配置
`GET /v1/config/app-language`(需登录)
```dart
final res = await UserApi.getAppLanguage(
pkg: ApiClient.instance.config.packageName,
lang: 'en', // 可选
);
```
**返回实体**: `AppLanguageConfigResponse``config` 为语言配置 JSON 字符串)
#### 5.1.5 getCreditsPage - 积分分页流水
`ImageApi.getCreditsPageInfo` 相同,路径 `GET /v1/user/credits-page`
```dart
final res = await UserApi.getCreditsPage(page: '1', size: '10', type: '1');
```
#### 5.1.6 getUserPayments - 用户支付记录
```dart
final res = await UserApi.getUserPayments(
app: backendApp,
userId: '用户ID',
);
```
**返回实体**: `UserPaymentsListResponse``items`: `UserPaymentRecord` 列表)
#### 5.1.7 getUnreadMessageCount - 未读消息数
```dart
final res = await UserApi.getUnreadMessageCount(types: '类型列表'); // types 可选
```
**返回实体**: `UnreadMessageCountResponse`
---
### 5.2 PaymentApi - 支付相关
#### 5.2.1 getGooglePayActivities - 获取 Android 商品列表
```dart
final res = await PaymentApi.getGooglePayActivities(
app: 'HAndroid', // 可选;默认 `AppConfig.backendAppTypeAndroid`
client: '客户端', // 可选
country: '国家', // 可选
);
if (res.isSuccess) {
final products = res.data!.productList;
for (final product in products ?? []) {
final id = product.productId;
final price = product.actualAmount;
final bonus = product.bonus;
}
}
```
**返回实体**: `PaymentProductsResponse`
- `productList`: 商品列表
- `productId`: 商品 ID
- `activityId`: 活动 ID
- `actualAmount`: 实际金额
- `originAmount`: 原价
- `bonus`: 赠送积分
- `title`: 标题
#### 5.2.2 getPaymentMethods - 获取支付方式列表
```dart
final res = await PaymentApi.getPaymentMethods(
activityId: 12345, // int与接口一致
country: '国家', // 可选
);
if (res.isSuccess) {
final methods = res.data!.paymentMethods;
for (final method in methods ?? []) {
final pm = method.paymentMethod;
final name = method.name;
final isRecommend = method.recommend;
}
}
```
**返回实体**: `PaymentMethodsResponse`
- `paymentMethods`: 支付方式列表
- `paymentMethod`: 支付方式
- `subPaymentMethod`: 子支付方式
- `name`: 显示名称
- `icon`: 图标
- `recommend`: 是否推荐
#### 5.2.3 createPayment - 创建支付订单
```dart
final res = await PaymentApi.createPayment(
app: 'HAndroid', // HIOS / HAndroid
userId: '用户ID',
activityId: '活动ID',
paymentMethod: '支付方式',
paymentType: '支付子类型', // 可选
);
if (res.isSuccess) {
final order = res.data!;
final orderId = order.orderId;
final payUrl = order.payUrl;
}
```
**返回实体**: `CreatePaymentResponse`
- `orderId`: 订单 ID
- `payUrl`: 支付链接
- `status`: 状态
#### 5.2.4 getPaymentDetailList - 支付订单列表
`GET /v1/payment/getPaymentDetailList`
```dart
final res = await PaymentApi.getPaymentDetailList(
app: 'HAndroid',
userId: '用户ID',
paymentMethod: 'GOOGLEPAY', // 可选
filterStatus: 'SUCCESS', // 可选
);
```
**返回实体**: `PaymentOrderListResponse``orders`: `PaymentOrderSummary` 列表)
#### 5.2.5 getOrderDetail - 获取订单详情
Query 使用原始字段 `id`(值为订单/支付 ID
```dart
final res = await PaymentApi.getOrderDetail(
userId: '用户ID',
orderId: '订单ID',
);
if (res.isSuccess) {
final order = res.data!;
final status = order.status;
}
```
**返回实体**: `OrderDetailResponse`
- `orderId`: 订单 ID
- `status`: 状态
- `amount`: 金额
#### 5.2.6 googlepay - Google Pay 回调
```dart
final res = await PaymentApi.googlepay(
signature: '购买签名',
purchaseData: '购买数据',
orderId: '订单ID',
userId: '用户ID',
);
if (res.isSuccess) {
final result = res.data!;
final status = result.status;
}
```
**返回实体**: `GooglePayCallbackResponse`
- `orderId`: 订单 ID
- `status`: 状态
- `creditsAdded`: 是否已加积分
---
### 5.3 ImageApi - 图片/视频相关
#### 5.3.1 getCategoryList - 获取分类列表
```dart
final res = await ImageApi.getCategoryList();
if (res.isSuccess) {
final categories = res.data!.categories;
}
```
**返回实体**: `CategoryListResponse`
- `categories`: 分类列表
- `id`: ID
- `name`: 名称
- `icon`: 图标
#### 5.3.2 getImg2VideoTasks - 获取任务列表
```dart
final res = await ImageApi.getImg2VideoTasks(
categoryId: 123, // 可选
);
if (res.isSuccess) {
final tasks = res.data!.tasks;
}
```
**返回实体**: `TasksResponse`
#### 5.3.3 getProgress - 查询任务进度
```dart
final res = await ImageApi.getProgress(
app: '应用ID',
taskId: '任务ID',
userId: '用户ID', // 可选
);
if (res.isSuccess) {
final progress = res.data!;
final status = progress.status;
final resultUrl = progress.resultUrl;
}
```
**返回实体**: `ProgressResponse`
- `taskId`: 任务 ID
- `status`: 状态
- `progress`: 进度 (0-100)
- `resultUrl`: 结果 URL
#### 5.3.4 createTask - 创建任务
```dart
final res = await ImageApi.createTask(
userId: '用户ID',
prompt: '提示词', // 可选
resolution: '1024x1024', // 可选
imgUrl: '图片URL', // 可选
allowance: false, // 可选
);
if (res.isSuccess) {
final taskId = res.data!.taskId;
}
```
**返回实体**: `CreateTaskResponse`
- `taskId`: 任务 ID
- `status`: 状态
#### 5.3.5 getMyTasks - 获取我的任务列表
```dart
final res = await ImageApi.getMyTasks(
app: '应用ID',
page: '1', // 可选
pageSize: '10', // 可选
cursor: '游标', // 可选
);
if (res.isSuccess) {
final tasks = res.data!.tasks;
final total = res.data!.total;
}
```
**返回实体**: `MyTasksResponse`
- `tasks`: 任务列表
- `total`: 总数
- `cursor`: 游标
#### 5.3.6 getCreditsPageInfo - 获取积分页面信息
对应 `GET /v1/user/credits-page`(与 `UserApi.getCreditsPage` 相同)。
```dart
final res = await ImageApi.getCreditsPageInfo(
page: '1',
size: '10',
type: '1',
);
// 或UserApi.getCreditsPage(page: '1', size: '10', type: '1');
if (res.isSuccess) {
final info = res.data!;
final total = info.total;
final records = info.records;
final credits = info.credits; // 若后端仍返回单屏字段则可用
}
```
**返回实体**: `CreditsPageInfoResponse`
- `total` / `current` / `pages` / `size` / `records`: 分页与流水(新接口)
- `credits` / `freeTimes` / `isVip` / `vipExpireTime`: 兼容旧字段
- `records` 项见 `CreditRecordItem`
---
### 5.4 FeedbackApi - 举报/反馈相关
#### 5.4.1 getUploadPresignedUrl - 获取上传预签名 URL
```dart
final res = await FeedbackApi.getUploadPresignedUrl(
fileName: 'image.jpg',
);
if (res.isSuccess) {
final result = res.data!;
final uploadUrl = result.uploadUrl;
final filePath = result.filePath;
}
```
**返回实体**: `FeedbackUploadPresignedUrlResponse`
- `uploadUrl`: 上传 URL
- `filePath`: 文件路径
#### 5.4.2 submit - 提交反馈
```dart
final res = await FeedbackApi.submit(
fileUrls: ['https://...'],
content: '反馈内容',
contentType: 'text/plain',
);
if (res.isSuccess) {
final success = res.data!.success;
}
```
**返回实体**: `SubmitFeedbackResponse`
- `success`: 是否成功
- `feedbackId`: 反馈 ID
---
## 6. 响应处理
### 6.1 EntityResponse
所有返回实体类的方法都使用 `EntityResponse<T>`
```dart
final res = await UserApi.fastLogin(...);
// 检查成功
if (res.isSuccess) {
// 访问实体
final data = res.data!;
final token = data.userToken;
} else {
// 处理错误
print('Error: ${res.msg}');
}
```
### EntityResponse 属性
| 属性 | 类型 | 说明 |
|------|------|------|
| `code` | int | 响应码0 表示成功 |
| `msg` | String | 响应消息 |
| `data` | T? | 实体数据 |
| `isSuccess` | bool | 便捷属性code == 0 时为 true |
---
## 7. 设置用户 Token
登录成功后,调用以下方法设置用户 Token
```dart
// 登录成功后
final res = await UserApi.fastLogin(...);
if (res.isSuccess) {
ApiClient.instance.setUserToken(res.data!.userToken);
}
// 登出时
ApiClient.instance.setUserToken(null);
```
---
## 8. 调试模式
框架会自动根据 `kDebugMode` 选择环境:
- **调试模式**:使用 `preBaseUrl`(或 `debugBaseUrlOverride`
- **发布模式**:使用 `prodBaseUrl`
---
## 9. 字段映射
框架自动处理字段映射,调用层使用原始字段名。
### 请求字段映射(原始 → V2
| 原始字段 | V2 字段 |
|----------|---------|
| app | sentinel |
| userId | asset |
| deviceId | origin |
| sign | resolution |
| referer | digest |
| activityId | warrior |
| country | vambrace |
| paymentMethod | resource |
| paymentType | ceremony |
| orderId | federation |
| signature | sample |
| purchaseData | merchant |
| taskId | tree |
| prompt | ledger |
| resolution | guild |
| srcImgUrls | commission |
| fileName1 | gateway |
| contentType | pauldron |
| expectedSize | stronghold |
### 响应字段映射V2 → 原始)
| V2 字段 | 原始字段 |
|---------|----------|
| helm | code/响应码 |
| rampart | msg |
| sidekick | data |
| summon | productList |
| renew | paymentMethods |
| reveal | credits |
| reevaluate | userToken |
---
## 10. 代码示例
### 完整登录流程
```dart
import 'package:client_proxy_framework/client_proxy_framework.dart';
class AuthService {
static Future<bool> login() async {
final deviceId = await getDeviceId();
final sign = md5(deviceId).toUpperCase();
final res = await UserApi.fastLogin(
deviceId: deviceId,
sign: sign,
app: 'HAndroid', // 或 AppConfig.backendAppTypeAndroid / IOS
referer: 'organic',
);
if (res.isSuccess) {
final loginInfo = res.data!;
ApiClient.instance.setUserToken(loginInfo.userToken!);
UserState.setUserId(loginInfo.userId!);
UserState.setCredits(loginInfo.credits ?? 0);
return true;
}
return false;
}
}
```
### 完整支付流程
```dart
class PaymentService {
// 1. 获取商品列表
static Future<List<PaymentProductItem>> getProducts() async {
final res = await PaymentApi.getGooglePayActivities();
return res.data?.productList ?? [];
}
// 2. 获取支付方式
static Future<List<PaymentMethodItem>> getPaymentMethods(int activityId) async {
final res = await PaymentApi.getPaymentMethods(activityId: activityId);
return res.data?.paymentMethods ?? [];
}
// 3. 创建订单
static Future<String?> createOrder({
required String userId,
required String activityId,
required String paymentMethod,
}) async {
final res = await PaymentApi.createPayment(
app: ApiClient.instance.config.backendAppTypeAndroid,
userId: userId,
activityId: activityId,
paymentMethod: paymentMethod,
);
if (res.isSuccess) {
return res.data?.orderId;
}
return null;
}
// 4. Google Pay 回调
static Future<bool> verifyGooglePay({
required String signature,
required String purchaseData,
required String orderId,
required String userId,
}) async {
final res = await PaymentApi.googlepay(
signature: signature,
purchaseData: purchaseData,
orderId: orderId,
userId: userId,
);
return res.isSuccess;
}
}
```
---
## 11. 常见问题
### Q: 如何修改字段映射?
A: 覆盖 `AppConfig.fieldMapping`
```dart
@override
FieldMapping get fieldMapping => const FieldMapping({
'deviceId': 'origin',
'userId': 'asset',
// ... 其他字段
});
```
### Q: 响应 data 为空怎么办?
A: 检查以下几点:
1. `ApiClient.init()` 是否已调用
2. 网络是否正常
3. `appId``aesKey` 等配置是否正确
---
## 12. 文件结构
```
lib/
├── client_proxy_framework.dart # 入口文件
└── src/
├── api/
│ ├── api_client.dart # 全局客户端
│ ├── api_crypto.dart # 加解密工具
│ ├── api_response.dart # 响应对象
│ └── proxy_client.dart # 代理请求客户端(含实体转换)
├── config/
│ ├── app_config.dart # 应用配置抽象类
│ ├── default_field_mapping.dart # 默认字段映射
│ └── field_mapping.dart # 字段映射类
├── entities/
│ ├── entity.dart # 实体基类
│ ├── user_entities.dart # 用户相关实体
│ ├── payment_entities.dart # 支付相关实体
│ ├── image_entities.dart # 图片/视频实体
│ └── feedback_entities.dart # 反馈实体
├── log/
│ └── app_logger.dart # 日志工具
└── services/
├── user_api.dart # 用户 API
├── payment_api.dart # 支付 API
├── image_api.dart # 图片/视频 API
└── feedback_api.dart # 反馈 API
```