26 KiB
26 KiB
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/ # DTO(Freezed生成)
│ │ │ │ └── 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 ← Data(Domain层不依赖任何外层)
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通信全链路HTTPS,Certificate 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钱包/外部钱包提取/转赠记录/余额/订单/换手机号/离线核销增强/争议投诉/消息通知