gcx/docs/guides/01-Flutter移动端开发指南.md

26 KiB
Raw Permalink Blame History

Genex Flutter 移动端开发指南

Consumer App消费者端+ Merchant App商户核销端


1. 技术栈概览

技术 版本 用途
Flutter 3.x 跨平台UI框架
Dart 3.x 开发语言
Riverpod 2.x 状态管理Provider替代方案编译安全
GoRouter 最新 声明式路由、深链接
Dio 5.x HTTP客户端
Freezed 最新 不可变数据模型代码生成
Hive/Isar 最新 本地持久化
WebSocket 内置 AI Agent实时通信、行情推送
flutter_localizations 内置 国际化

2. 项目架构Clean Architecture + Riverpod

2.1 目录结构

genex_mobile/
├── lib/
│   ├── main.dart                    # 应用入口
│   ├── app/
│   │   ├── app.dart                 # MaterialApp配置
│   │   ├── router.dart              # GoRouter路由定义
│   │   └── theme/                   # 主题配置
│   ├── core/
│   │   ├── constants/               # 常量API地址、术语映射表
│   │   ├── network/                 # Dio配置、拦截器、错误处理
│   │   ├── storage/                 # 本地存储封装
│   │   ├── utils/                   # 工具类
│   │   └── extensions/              # Dart扩展方法
│   ├── features/                    # 按功能模块组织
│   │   ├── auth/                    # 注册/登录
│   │   │   ├── data/
│   │   │   │   ├── datasources/     # 远程/本地数据源
│   │   │   │   ├── models/          # DTOFreezed生成
│   │   │   │   └── repositories/    # Repository实现
│   │   │   ├── domain/
│   │   │   │   ├── entities/        # 领域实体
│   │   │   │   ├── repositories/    # Repository接口
│   │   │   │   └── usecases/        # 用例
│   │   │   └── presentation/
│   │   │       ├── providers/       # Riverpod Providers
│   │   │       ├── pages/           # 页面Widget
│   │   │       └── widgets/         # 模块专用组件
│   │   ├── coupons/                 # 券浏览/购买/持有
│   │   ├── trading/                 # 二级市场交易
│   │   ├── wallet/                  # 余额/资产/交易记录
│   │   ├── redeem/                  # 券使用/核销
│   │   ├── transfer/                # P2P转赠
│   │   ├── profile/                 # 个人中心/KYC
│   │   ├── ai_agent/                # AI Agent对话/建议
│   │   └── merchant/                # 商户端核销(共享模块)
│   ├── shared/
│   │   ├── widgets/                 # 全局共享组件
│   │   ├── providers/               # 全局Provider
│   │   └── models/                  # 共享数据模型
│   └── l10n/                        # 国际化资源
├── test/                            # 单元/Widget测试
├── integration_test/                # 集成测试
└── pubspec.yaml

2.2 Clean Architecture 分层

┌────────────────────────────────────────┐
│         Presentation Layer             │
│  Pages → Widgets → Riverpod Providers  │
├────────────────────────────────────────┤
│           Domain Layer                 │
│  Entities → UseCases → Repository(接口)│
├────────────────────────────────────────┤
│            Data Layer                  │
│  Models(DTO) → DataSources → Repo实现  │
└────────────────────────────────────────┘

依赖方向Presentation → Domain ← DataDomain层不依赖任何外层


3. 核心模块实现

3.1 术语映射(全局执行)

所有面向用户的UI文本必须使用Web2术语禁止出现区块链术语。

// lib/core/constants/terminology.dart
class Terminology {
  // 用户界面术语 → 底层技术术语
  static const Map<String, String> mapping = {
    '我的账户': '链上钱包地址',
    '我的券': 'ERC-721/1155 NFT资产',
    '我的余额': '链上稳定币(USDC)余额',
    '转赠给朋友': 'P2P链上转移',
    '购买': '链上原子交换',
    '核销/使用': '合约兑付(Redemption)',
    '订单号': '交易哈希(TX Hash)',
    '安全验证': '链上签名(MPC钱包后台执行)',
  };
}

3.2 认证模块

// lib/features/auth/domain/entities/user.dart
@freezed
class User with _$User {
  const factory User({
    required String id,
    required String phone,        // 或 email
    required KycLevel kycLevel,   // L0/L1/L2/L3
    required WalletMode walletMode, // standard/pro
    String? displayName,
    String? avatar,
  }) = _User;
}

enum KycLevel { L0, L1, L2, L3 }
enum WalletMode { standard, pro }
// lib/features/auth/domain/usecases/register_usecase.dart
class RegisterUseCase {
  final AuthRepository _repo;
  RegisterUseCase(this._repo);

  /// 注册:手机号/邮箱 → 后台自动创建MPC钱包用户无感知
  Future<Either<Failure, User>> call(RegisterParams params) {
    return _repo.register(
      phone: params.phone,
      email: params.email,
      password: params.password,
    );
  }
}

3.3 券资产模块

// lib/features/coupons/domain/entities/coupon.dart
@freezed
class Coupon with _$Coupon {
  const factory Coupon({
    required String id,           // 券ID链上唯一
    required String issuerName,   // 发行方名称
    required double faceValue,    // 面值
    required double currentPrice, // 当前市场价
    required DateTime expiryDate, // 到期日期
    required CouponStatus status, // 可用/已使用/已过期
    required CouponType type,     // utility/security
    String? imageUrl,
    String? description,
    List<String>? usageConditions,
  }) = _Coupon;
}

enum CouponStatus { available, used, expired, listed }
enum CouponType { utility, security }
// lib/features/coupons/presentation/providers/coupon_providers.dart
final couponListProvider = FutureProvider.autoDispose<List<Coupon>>((ref) {
  final repo = ref.watch(couponRepositoryProvider);
  return repo.getMyCoupons();
});

final couponDetailProvider = FutureProvider.autoDispose.family<Coupon, String>(
  (ref, couponId) {
    final repo = ref.watch(couponRepositoryProvider);
    return repo.getCouponDetail(couponId);
  },
);

3.4 交易模块

// lib/features/trading/domain/entities/order.dart
@freezed
class TradeOrder with _$TradeOrder {
  const factory TradeOrder({
    required String orderId,      // 订单号映射TX Hash
    required String couponId,
    required OrderSide side,      // buy/sell
    required double price,
    required OrderStatus status,
    required DateTime createdAt,
  }) = _TradeOrder;
}

enum OrderSide { buy, sell }
enum OrderStatus { pending, matched, settled, cancelled }

Utility Track价格校验前端 + 后端双重验证)

// lib/features/trading/presentation/providers/sell_provider.dart
class SellNotifier extends StateNotifier<SellState> {
  // Utility Track券卖出价 ≤ 面值
  bool validatePrice(double sellPrice, double faceValue, CouponType type) {
    if (type == CouponType.utility && sellPrice > faceValue) {
      return false; // 消费型券不允许溢价
    }
    return true;
  }
}

3.5 P2P转赠

// 转赠流程:输入朋友手机号 → 翻译层解析为链上地址 → 链上P2P转移
class TransferUseCase {
  final TransferRepository _repo;
  TransferUseCase(this._repo);

  Future<Either<Failure, TransferResult>> call({
    required String couponId,
    required String recipientPhone,  // 手机号(非链上地址)
  }) {
    return _repo.transferByPhone(
      couponId: couponId,
      recipientPhone: recipientPhone,
    );
    // 后端翻译层:手机号 → 链上地址 → Gas代付 → 链上P2P转移
  }
}

4. AI Agent 集成

4.1 架构

┌──────────────────────────────┐
│      AI Agent UI Layer       │
│  悬浮按钮 / 对话面板 / 建议条  │
├──────────────────────────────┤
│      AI Agent SDK            │
│  对话管理 / 上下文组装 / 流式  │
├──────────────────────────────┤
│      WebSocket连接            │
│  Agent Gateway API           │
└──────────────────────────────┘

4.2 悬浮入口按钮

// lib/features/ai_agent/presentation/widgets/ai_fab.dart
class AiAgentFab extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final unreadCount = ref.watch(aiUnreadCountProvider);
    return Positioned(
      right: 16,
      bottom: 80,
      child: GestureDetector(
        onTap: () => _showAgentPanel(context),
        onLongPress: () => _showQuickActions(context),
        child: Stack(
          children: [
            CircleAvatar(
              radius: 28,
              child: Icon(Icons.smart_toy_outlined),
            ),
            if (unreadCount > 0)
              Positioned(
                right: 0, top: 0,
                child: Badge(count: unreadCount),
              ),
          ],
        ),
      ),
    );
  }
}

4.3 对话面板(流式输出)

// lib/features/ai_agent/presentation/pages/agent_chat_panel.dart
class AgentChatPanel extends ConsumerStatefulWidget {
  @override
  _AgentChatPanelState createState() => _AgentChatPanelState();
}

class _AgentChatPanelState extends ConsumerState<AgentChatPanel> {
  final _channel = WebSocketChannel.connect(
    Uri.parse('wss://api.gogenex.com/agent/ws'),
  );

  void _sendMessage(String text) {
    final context = _buildContext(); // 组装上下文
    _channel.sink.add(jsonEncode({
      'type': 'chat',
      'message': text,
      'context': context,
    }));
  }

  Map<String, dynamic> _buildContext() {
    return {
      'user_profile': ref.read(userProfileProvider).toJson(),
      'current_page': GoRouter.of(context).location,
      'coupon_portfolio': ref.read(myCouponsProvider).toSummary(),
      'recent_actions': ref.read(recentActionsProvider),
    };
  }
}

4.4 消费者端AI场景

场景 实现方式
智能选券推荐 首页Feed中插入AI推荐卡片
价格顾问 券详情页底部AI分析标签
到期管理 我的券列表AI排序+推送提醒
出售定价 出售页面AI建议价格+解释
自然语言搜索 对话面板返回筛选券卡片

5. 商户端核销模块

5.1 核销方式

方式 场景 技术
扫码核销 门店扫消费者券码 Camera → QR解码 → API调用
输码核销 手动输入券码 文本输入 → API调用
离线核销 网络不可用 本地验证 → 队列缓存 → 联网同步

5.2 离线核销实现

// lib/features/merchant/data/services/offline_redeem_service.dart
class OfflineRedeemService {
  final _queue = HiveBox<PendingRedemption>('offline_queue');
  final _localValidator = LocalCouponValidator();

  /// 离线核销:本地验证 + 缓存 + 联网自动同步
  Future<RedeemResult> redeemOffline(String couponCode) async {
    // 1. 本地验证(预下载的有效券列表 + 签名验证)
    final isValid = await _localValidator.validate(couponCode);
    if (!isValid) return RedeemResult.invalid();

    // 2. 本地标记已核销,加入同步队列
    await _queue.add(PendingRedemption(
      couponCode: couponCode,
      timestamp: DateTime.now(),
      storeId: currentStoreId,
    ));

    return RedeemResult.pendingSync();
  }

  /// 联网后自动同步
  Future<void> syncPendingRedemptions() async {
    final pending = _queue.values.toList();
    for (final item in pending) {
      try {
        await _api.confirmRedemption(item);
        await _queue.delete(item.key);
      } catch (_) {
        // 重试逻辑
      }
    }
  }
}

6. 网络层

6.1 Dio配置

// lib/core/network/api_client.dart
class ApiClient {
  late final Dio _dio;

  ApiClient() {
    _dio = Dio(BaseOptions(
      baseUrl: 'https://api.gogenex.com/api/v1',
      connectTimeout: Duration(seconds: 10),
      receiveTimeout: Duration(seconds: 30),
    ));

    _dio.interceptors.addAll([
      AuthInterceptor(),      // JWT Token注入
      LoggingInterceptor(),   // 日志
      RetryInterceptor(),     // 重试策略
      ErrorInterceptor(),     // 统一错误处理
    ]);
  }
}

6.2 错误处理

// lib/core/network/failure.dart
@freezed
class Failure with _$Failure {
  const factory Failure.network({String? message}) = NetworkFailure;
  const factory Failure.server({required int code, String? message}) = ServerFailure;
  const factory Failure.auth({String? message}) = AuthFailure;
  const factory Failure.validation({required Map<String, String> errors}) = ValidationFailure;
}

7. 状态管理模式Riverpod

// 只读数据查询
final couponListProvider = FutureProvider.autoDispose<List<Coupon>>((ref) async {
  return ref.watch(couponRepoProvider).getAll();
});

// 可变状态管理
final cartProvider = StateNotifierProvider<CartNotifier, CartState>((ref) {
  return CartNotifier(ref.watch(orderRepoProvider));
});

// 异步操作
final purchaseProvider = FutureProvider.family<Order, PurchaseParams>((ref, params) {
  return ref.watch(orderRepoProvider).purchase(params);
});

8. 应用构建配置

8.1 多环境配置

// lib/core/config/env.dart
enum Environment { dev, staging, prod }

class AppConfig {
  static late Environment env;
  static String get apiBase => switch (env) {
    Environment.dev => 'https://dev-api.gogenex.com',
    Environment.staging => 'https://staging-api.gogenex.com',
    Environment.prod => 'https://api.gogenex.com',
  };
}

8.2 Flavor构建

# Consumer App
flutter run --flavor consumer -t lib/main_consumer.dart

# Merchant App
flutter run --flavor merchant -t lib/main_merchant.dart

Consumer和Merchant共享核心模块core/shared/),通过不同入口和路由配置区分功能。


9. 测试策略

层级 工具 覆盖目标
单元测试 flutter_test + mocktail UseCase、Repository、Provider
Widget测试 flutter_test 关键页面组件
集成测试 integration_test 购买流程、核销流程、转赠流程
Golden测试 golden_toolkit UI快照回归
// test/features/trading/sell_notifier_test.dart
void main() {
  test('Utility券不允许溢价出售', () {
    final notifier = SellNotifier();
    expect(
      notifier.validatePrice(110.0, 100.0, CouponType.utility),
      false, // 110 > 面值100拒绝
    );
    expect(
      notifier.validatePrice(85.0, 100.0, CouponType.utility),
      true, // 85 < 面值100允许
    );
  });
}

10. 性能优化

策略 实现
图片懒加载 cached_network_image + CDN
列表虚拟化 ListView.builder + 分页加载
离线缓存 Hive本地数据库 + 增量同步
启动优化 延迟初始化非核心服务
包体积 Tree-shaking + 延迟加载
内存管理 autoDispose Provider自动释放

11. 安全规范

  • JWT Token存储于Flutter Secure Storage非SharedPreferences
  • 敏感操作大额交易、转赠需二次验证PIN/生物识别)
  • API通信全链路HTTPSCertificate Pinning
  • 本地数据加密存储Hive加密Box
  • 禁止在日志中输出敏感信息Token、用户手机号
  • ProGuard/R8混淆Android

12. 发布流程

开发 → 提交PR → CI自动测试 → Code Review → 合入main
                                              ↓
                                    自动构建GitHub Actions / GitLab CI
                                              ↓
                              ┌────────────────┼────────────────┐
                              ↓                ↓                ↓
                        Dev Build        Staging Build      Prod Build
                        (内部测试)       (TestFlight/Beta)   (App Store/Play Store)

13. Pro模式加密原生用户

13.1 Pro模式切换

// lib/features/profile/presentation/pages/pro_mode_settings.dart
class ProModeSettings extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final user = ref.watch(userProfileProvider);

    return Column(children: [
      // 风险提示确认
      RiskDisclosureCard(
        text: '切换至Pro模式后您将自行管理钱包私钥。'
              '平台无法帮您恢复丢失的私钥或资产。',
      ),

      // 开启Pro模式
      SwitchListTile(
        title: Text('开启Pro模式'),
        value: user.walletMode == WalletMode.pro,
        onChanged: (enabled) async {
          if (enabled) {
            // 必须确认风险提示
            final confirmed = await showRiskConfirmDialog(context);
            if (confirmed) {
              await ref.read(walletProvider.notifier).switchToProMode();
            }
          } else {
            // Pro→标准需将资产转回平台托管
            await ref.read(walletProvider.notifier).switchToStandard();
          }
        },
      ),

      if (user.walletMode == WalletMode.pro) ...[
        // 链上地址显示
        CopyableAddressField(address: user.chainAddress),
        // WalletConnect连接
        WalletConnectButton(),
        // MetaMask连接
        MetaMaskConnectButton(),
        // 交易哈希查看入口
        ListTile(
          title: Text('查看链上交易'),
          onTap: () => launchUrl(explorerUrl),
        ),
      ],
    ]);
  }
}

13.2 助记词备份与社交恢复

// lib/features/wallet/domain/usecases/backup_usecase.dart
class BackupSeedPhraseUseCase {
  /// Pro模式开启时强制提示备份助记词
  Future<Either<Failure, void>> call() async {
    final mnemonic = await _walletService.generateMnemonic();
    // 显示12词助记词用户手动抄写
    // 验证确认随机抽3个词验证
    return _walletService.confirmBackup(mnemonic);
  }
}

// 社交恢复Guardian机制
class SocialRecoveryUseCase {
  /// 预设3-5个可信联系人多数确认即可恢复
  Future<Either<Failure, void>> setupGuardians(List<String> guardianPhones) async {
    if (guardianPhones.length < 3 || guardianPhones.length > 5) {
      return Left(Failure.validation(errors: {'guardians': '需要3-5个守护人'}));
    }
    return _repo.setupSocialRecovery(guardianPhones);
  }

  /// 发起恢复(需>50%守护人确认)
  Future<Either<Failure, void>> initiateRecovery() async {
    return _repo.initiateRecovery();
  }
}

// AA钱包ERC-4337集成
class AAWalletService {
  /// 邮箱/手机号作为恢复入口
  Future<void> recoverViaEmail(String email) async {
    // ERC-4337 Account Abstraction恢复流程
  }
}

14. 提取到外部钱包

// lib/features/wallet/presentation/pages/extract_to_external.dart
class ExtractToExternalPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Column(children: [
      // KYC L2+才可提取
      KycGuard(requiredLevel: KycLevel.L2, child: Column(children: [
        // 输入外部钱包地址EVM兼容
        TextField(
          decoration: InputDecoration(labelText: '外部钱包地址0x...'),
          controller: _addressController,
        ),
        // 风险提示
        WarningCard(
          text: '提取后平台将不再托管该券。'
                '您需自行保管钱包私钥,平台无法冻结、恢复或干预该券。',
        ),
        // 选择要提取的券
        CouponSelector(onSelected: (coupons) => setState(() => _selected = coupons)),
        // 确认提取
        ElevatedButton(
          onPressed: () => _confirmExtract(ref),
          child: Text('确认提取'),
        ),
      ])),
    ]);
  }
}

15. 个人中心完整模块

15.1 转赠记录

// lib/features/transfer/presentation/pages/transfer_history.dart
class TransferHistoryPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final transfers = ref.watch(transferHistoryProvider);

    return transfers.when(
      data: (list) => ListView.builder(
        itemCount: list.length,
        itemBuilder: (_, i) => TransferHistoryCard(
          direction: list[i].direction, // sent / received
          recipientDisplay: list[i].recipientPhone ?? '外部钱包',
          couponName: list[i].couponName,
          timestamp: list[i].createdAt,
          status: list[i].status, // completed / pending
        ),
      ),
      loading: () => SkeletonList(),
      error: (e, _) => ErrorWidget(e),
    );
  }
}

15.2 我的余额/充值/提现

// lib/features/wallet/domain/entities/balance.dart
@freezed
class WalletBalance with _$WalletBalance {
  const factory WalletBalance({
    required double totalBalance,       // 总余额(链上+链下聚合,美元显示)
    required double availableBalance,   // 可用余额
    required double frozenBalance,      // 冻结金额(挂单中/待结算)
    required double withdrawable,       // 可提现
  }) = _WalletBalance;
}

// 充值/提现流程
class DepositUseCase {
  Future<Either<Failure, DepositResult>> call(DepositParams params) {
    // 银行卡/信用卡/Apple Pay → 法币入金 → 后台转换USDC
    return _repo.deposit(params);
  }
}

class WithdrawUseCase {
  Future<Either<Failure, WithdrawResult>> call(WithdrawParams params) {
    // USDC → 法币 → 银行卡T+1到账
    return _repo.withdraw(params);
  }
}

15.3 我的订单

// lib/features/trading/presentation/pages/order_history.dart
class OrderHistoryPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final orders = ref.watch(orderHistoryProvider);
    return DefaultTabController(
      length: 3,
      child: Column(children: [
        TabBar(tabs: [
          Tab(text: '全部'),
          Tab(text: '买入'),
          Tab(text: '卖出'),
        ]),
        Expanded(child: TabBarView(children: [
          OrderList(orders: orders),
          OrderList(orders: orders.where((o) => o.side == OrderSide.buy)),
          OrderList(orders: orders.where((o) => o.side == OrderSide.sell)),
        ])),
      ]),
    );
  }
}

15.4 换手机号

// lib/features/profile/domain/usecases/change_phone.dart
class ChangePhoneUseCase {
  /// 换手机号KYC身份验证人脸+证件)后迁移账户
  Future<Either<Failure, void>> call(ChangePhoneParams params) async {
    // 1. 验证新手机号未注册
    // 2. KYC身份验证人脸识别 + 证件对比)
    // 3. 旧手机号验证码确认
    // 4. 更新映射表(旧手机→新手机,链上地址不变)
    return _repo.changePhone(params);
  }
}

16. 离线核销增强

// lib/features/merchant/data/services/offline_redeem_service.dart
// 补充:离线核销限额与冲突处理

class OfflineRedeemConfig {
  static const maxSingleAmount = 500.0;  // 单笔离线核销限额$500
  static const maxDailyAmount = 5000.0;  // 单日离线核销限额$5,000
  static const maxDailyCount = 50;       // 单日离线核销笔数限制
}

class OfflineConflictResolver {
  /// 冲突处理:同一张券被两个门店离线核销
  /// 以先上链者为准,后者自动退回并通知
  Future<void> resolveConflict(PendingRedemption local, ChainRedemption chain) async {
    if (chain.redeemedBy != local.storeId) {
      // 该券已被其他门店核销,本地核销无效
      await _notifyStore(local.storeId, '券${local.couponCode}已被其他门店核销');
      await _queue.delete(local.key);
    }
  }
}

17. 争议与投诉

// lib/features/support/domain/entities/ticket.dart
@freezed
class SupportTicket with _$SupportTicket {
  const factory SupportTicket({
    required String id,
    required TicketCategory category,  // transaction/account/coupon
    required String description,
    required TicketStatus status,
    String? orderId,                   // 关联订单号
    List<String>? attachments,
  }) = _SupportTicket;
}

enum TicketCategory { transaction, account, coupon, compliance, other }
enum TicketStatus { open, inProgress, waitingUser, resolved, closed }

18. 消息通知

// lib/features/notification/domain/entities/notification.dart
@freezed
class AppNotification with _$AppNotification {
  const factory AppNotification({
    required String id,
    required NotificationType type,
    required String title,
    required String body,
    required DateTime createdAt,
    required bool isRead,
    String? deepLink,              // 点击跳转(如券详情、订单详情)
  }) = _AppNotification;
}

enum NotificationType {
  tradeComplete,        // 交易完成
  couponExpiringSoon,   // 券即将过期
  priceAlert,           // 价格变动
  transferReceived,     // 收到转赠
  systemAnnouncement,   // 系统公告
  issuerNotice,         // 发行方公告
}

文档版本: v2.0 基于: Genex 券交易平台 - 软件需求规格说明书 v4.1 技术栈: Flutter 3.x + Riverpod + Clean Architecture 更新: 补充Pro模式/助记词/社交恢复/AA钱包/外部钱包提取/转赠记录/余额/订单/换手机号/离线核销增强/争议投诉/消息通知