2026-04-22 11:13:54 +08:00

19 KiB
Raw Blame History

Client Proxy Framework 使用指南

1. 框架概述

client_proxy_framework 是一个通用代理 API 框架,用于 Flutter 移动应用与后端 API 的通信。框架已封装好请求/响应的加密解密、字段映射等逻辑,换皮应用只需配置少量参数即可使用。

核心特性

  • 自动加解密:请求体和响应体自动进行 AES 加密/解密
  • 字段映射:框架自动将原始字段名转换为 V2 字段名,响应自动转换回来
  • 强类型实体:返回强类型实体类,开发者直接使用映射后的字段
  • 统一响应EntityResponse<T> 提供强类型返回值

换皮应用:从零搭建

从零创建一个新的换皮应用(skin_config.jsonClientBootstrapmain 初始化顺序、Android/iOS 要点等),请阅读 《创建新换皮应用 — 步骤清单》

功能总览与用法

按模块整理的“功能 + 最小调用示例”请见 《Framework 功能与用法总览》

以首款换皮应用 FunyMee 为参考的 视频首页common_info → 分类 → 按分类拉模板)数据流说明,见 《视频首页数据获取流程》

生图 / 任务(预签名上传、create-task、进度轮询、文生图、历史列表与本地封面)的端到端说明,见 《生图 / 任务流程》

示例换皮配置文件 lib/src/config/skin_config.example.json 的字段说明,见 《skin_config.example.json 字段说明》


2. 快速开始

2.1 添加依赖

pubspec.yaml 中添加:

dependencies:
  client_proxy_framework:
    path: ../client_proxy_framework

2.2 实现配置

创建一个类继承 AppConfig

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

void main() {
  ApiClient.init(MyAppConfig());
  runApp(const MyApp());
}

3. 设计理念

3.1 请求流程

调用层 (原始字段)
    ↓
ProxyClient.request()(自动加 `pkg`;默认加 `User_token`
    ↓
字段映射 (原始 → V2)
    ↓
AES 加密
    ↓
发送请求

请求头ProxyClient 会将 [AppConfig.packageName] 写入映射后的包名字段(原始名 pkg);若已设置用户 token默认还会写入 User_tokenUserApi.fast_login 等无需登录态接口内部使用 includeUserTokenInHeader: false,避免把旧 token 打进 filter_type。其余请求也可在直接调用 ProxyClient.request 时传入该参数。

请求体:各 *Api 方法使用与《客户端指南》解密表一致的原始字段名(如 refererdeviceIdfileUrls / 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 - 设备快速登录

final res = await UserApi.fastLogin(
  deviceId: '设备ID',
  sign: 'MD5(deviceId)大写',
  app: 'HAndroid',       // 必填HIOS / HAndroid
  referer: '归因来源',    // 可选
  ch: '渠道号',           // 可选
  type: 'fb',            // 可选;未传时默认 fb
);

if (res.isSuccess) {
  final loginInfo = res.data!;
  final token = loginInfo.userToken;
  final credits = loginInfo.credits;
  final userId = loginInfo.userId;
}

宿主需在 FrameworkAuthService.init 传入的 AuthServiceCallbacks 中实现 deviceIdsignFunyMee 采用的获取方式与签名规则device_id_and_sign.md

返回实体: FastLoginResponse

  • userToken: 用户 Token
  • userId: 用户 ID
  • credits: 积分
  • avatar: 头像
  • userName: 用户名
  • countryCode: 国家码
  • isVip: 是否 VIP
  • 等等...

5.1.2 getCommonInfo - 获取用户通用信息

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 - 获取账户信息

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(需登录)

final res = await UserApi.getAppLanguage(
  pkg: ApiClient.instance.config.packageName,
  lang: 'en', // 可选
);

返回实体: AppLanguageConfigResponseconfig 为语言配置 JSON 字符串)

5.1.5 getCreditsPage - 积分分页流水

ImageApi.getCreditsPageInfo 相同,路径 GET /v1/user/credits-page

final res = await UserApi.getCreditsPage(page: '1', size: '10', type: '1');

5.1.6 getUserPayments - 用户支付记录

final res = await UserApi.getUserPayments(
  app: backendApp,
  userId: '用户ID',
);

返回实体: UserPaymentsListResponseitems: UserPaymentRecord 列表)

5.1.7 getUnreadMessageCount - 未读消息数

final res = await UserApi.getUnreadMessageCount(types: '类型列表'); // types 可选

返回实体: UnreadMessageCountResponse


5.2 PaymentApi - 支付相关

5.2.1 getGooglePayActivities - 获取 Android 商品列表

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 - 获取支付方式列表

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 - 创建支付订单

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

final res = await PaymentApi.getPaymentDetailList(
  app: 'HAndroid',
  userId: '用户ID',
  paymentMethod: 'GOOGLEPAY', // 可选
  filterStatus: 'SUCCESS',    // 可选
);

返回实体: PaymentOrderListResponseorders: PaymentOrderSummary 列表)

5.2.5 getOrderDetail - 获取订单详情

Query 使用原始字段 id(值为订单/支付 ID

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 回调

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 - 获取分类列表

final res = await ImageApi.getCategoryList();

if (res.isSuccess) {
  final categories = res.data!.categories;
}

返回实体: CategoryListResponse

  • categories: 分类列表
    • id: ID
    • name: 名称
    • icon: 图标

5.3.2 getImg2VideoTasks - 获取任务列表

final res = await ImageApi.getImg2VideoTasks(
  categoryId: 123,  // 可选
);

if (res.isSuccess) {
  final tasks = res.data!.tasks;
}

返回实体: TasksResponse

5.3.3 getProgress - 查询任务进度

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 - 创建任务

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 - 获取我的任务列表

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 相同)。

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

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 - 提交反馈

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>

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

// 登录成功后
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. 代码示例

完整登录流程

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

完整支付流程

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

@override
FieldMapping get fieldMapping => const FieldMapping({
  'deviceId': 'origin',
  'userId': 'asset',
  // ... 其他字段
});

Q: 响应 data 为空怎么办?

A: 检查以下几点:

  1. ApiClient.init() 是否已调用
  2. 网络是否正常
  3. appIdaesKey 等配置是否正确

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