7 changed files with 669 additions and 0 deletions
@ -0,0 +1,175 @@ |
|||
import 'package:get/get.dart'; |
|||
import 'package:fluwx/fluwx.dart'; |
|||
import '../../modules/login/controllers/weixinLogin.dart'; |
|||
|
|||
/// 支付结果回调 |
|||
typedef PaymentResultCallback = void Function(bool success, String? message); |
|||
|
|||
/// 支付服务 |
|||
class PayService extends GetxService { |
|||
late FlWX _flwx; |
|||
|
|||
@override |
|||
void onInit() { |
|||
super.onInit(); |
|||
_flwx = FlWX(); |
|||
} |
|||
|
|||
/// 微信支付 |
|||
/// |
|||
/// [paymentData] 支付参数,包含以下字段: |
|||
/// - appId: 应用ID |
|||
/// - partnerId: 商户号 |
|||
/// - prepayId: 预支付交易会话ID |
|||
/// - packageValue: 扩展字段,固定值"Sign=WXPay" |
|||
/// - nonceStr: 随机字符串 |
|||
/// - timestamp: 时间戳 |
|||
/// - sign: 签名 |
|||
/// |
|||
/// [onResult] 支付结果回调 |
|||
Future<bool> wechatPay({ |
|||
required Map<String, dynamic> paymentData, |
|||
PaymentResultCallback? onResult, |
|||
}) async { |
|||
try { |
|||
// 检查微信是否安装 |
|||
if (!await _flwx.isInstalled) { |
|||
onResult?.call(false, '微信未安装'); |
|||
return false; |
|||
} |
|||
|
|||
// 将支付数据转换为Payment对象 |
|||
final payment = paymentData.toPayment(); |
|||
if (payment == null) { |
|||
onResult?.call(false, '支付参数格式错误'); |
|||
return false; |
|||
} |
|||
|
|||
// 调用微信支付 |
|||
final result = await _flwx.pay( |
|||
payment, |
|||
onSuccess: () { |
|||
onResult?.call(true, '支付成功'); |
|||
}, |
|||
onResponse: (response) { |
|||
if (response.errCode == 0) { |
|||
onResult?.call(true, '支付成功'); |
|||
} else if (response.errCode == -2) { |
|||
onResult?.call(false, '用户取消支付'); |
|||
} else { |
|||
onResult?.call(false, '支付失败: ${response.errStr}'); |
|||
} |
|||
}, |
|||
); |
|||
|
|||
return result; |
|||
} catch (e) { |
|||
onResult?.call(false, '支付异常: $e'); |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
/// 创建微信支付订单 |
|||
/// |
|||
/// [orderInfo] 订单信息,包含: |
|||
/// - orderId: 订单ID |
|||
/// - amount: 支付金额(分) |
|||
/// - description: 商品描述 |
|||
/// - userId: 用户ID |
|||
/// |
|||
/// 返回支付参数,用于调用 [wechatPay] 方法 |
|||
Future<Map<String, dynamic>?> createWechatPayOrder({ |
|||
required String orderId, |
|||
required int amount, |
|||
required String description, |
|||
String? userId, |
|||
}) async { |
|||
try { |
|||
// TODO: 这里需要调用后端API创建支付订单 |
|||
// 示例代码,实际需要根据后端接口调整 |
|||
|
|||
// final response = await Api.createPayOrder({ |
|||
// 'orderId': orderId, |
|||
// 'amount': amount, |
|||
// 'description': description, |
|||
// 'userId': userId, |
|||
// 'payType': 'wechat', |
|||
// }); |
|||
|
|||
// if (response['success'] == true) { |
|||
// return response['data'] as Map<String, dynamic>; |
|||
// } |
|||
|
|||
// 临时返回示例数据结构,实际使用时需要删除 |
|||
return { |
|||
'appId': 'your_app_id', |
|||
'partnerId': 'your_partner_id', |
|||
'prepayId': 'prepay_id_from_server', |
|||
'packageValue': 'Sign=WXPay', |
|||
'nonceStr': 'random_string', |
|||
'timestamp': DateTime.now().millisecondsSinceEpoch ~/ 1000, |
|||
'sign': 'signature_from_server', |
|||
}; |
|||
} catch (e) { |
|||
print('创建支付订单失败: $e'); |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
/// 查询支付结果 |
|||
/// |
|||
/// [orderId] 订单ID |
|||
/// 返回支付状态:success, failed, pending |
|||
Future<String?> queryPaymentStatus(String orderId) async { |
|||
try { |
|||
// TODO: 调用后端API查询支付状态 |
|||
// final response = await Api.queryPaymentStatus({'orderId': orderId}); |
|||
// return response['status'] as String?; |
|||
|
|||
// 临时返回,实际使用时需要删除 |
|||
return 'pending'; |
|||
} catch (e) { |
|||
print('查询支付状态失败: $e'); |
|||
return null; |
|||
} |
|||
} |
|||
|
|||
/// 完整的支付流程 |
|||
/// |
|||
/// [orderId] 订单ID |
|||
/// [amount] 支付金额(分) |
|||
/// [description] 商品描述 |
|||
/// [userId] 用户ID |
|||
/// [onResult] 支付结果回调 |
|||
Future<bool> processWechatPayment({ |
|||
required String orderId, |
|||
required int amount, |
|||
required String description, |
|||
String? userId, |
|||
PaymentResultCallback? onResult, |
|||
}) async { |
|||
try { |
|||
// 1. 创建支付订单 |
|||
final paymentData = await createWechatPayOrder( |
|||
orderId: orderId, |
|||
amount: amount, |
|||
description: description, |
|||
userId: userId, |
|||
); |
|||
|
|||
if (paymentData == null) { |
|||
onResult?.call(false, '创建支付订单失败'); |
|||
return false; |
|||
} |
|||
|
|||
// 2. 调用微信支付 |
|||
return await wechatPay( |
|||
paymentData: paymentData, |
|||
onResult: onResult, |
|||
); |
|||
} catch (e) { |
|||
onResult?.call(false, '支付流程异常: $e'); |
|||
return false; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
import 'package:get/get.dart'; |
|||
import '../controllers/payment_controller.dart'; |
|||
|
|||
class PaymentBinding extends Bindings { |
|||
@override |
|||
void dependencies() { |
|||
Get.lazyPut<PaymentController>(() => PaymentController()); |
|||
} |
|||
} |
|||
@ -0,0 +1,85 @@ |
|||
import 'package:get/get.dart'; |
|||
import '../../../data/services/pay_service.dart'; |
|||
|
|||
class PaymentController extends GetxController { |
|||
final PayService _payService = Get.find<PayService>(); |
|||
|
|||
// 支付状态 |
|||
final RxBool isLoading = false.obs; |
|||
final RxString paymentStatus = ''.obs; |
|||
final RxString errorMessage = ''.obs; |
|||
|
|||
// 测试订单信息 |
|||
final RxString testOrderId = ''.obs; |
|||
final RxDouble testAmount = 0.01.obs; // 测试金额 1分钱 |
|||
|
|||
@override |
|||
void onInit() { |
|||
super.onInit(); |
|||
// 生成测试订单ID |
|||
testOrderId.value = 'test_${DateTime.now().millisecondsSinceEpoch}'; |
|||
} |
|||
|
|||
/// 测试微信支付 |
|||
Future<void> testWechatPayment() async { |
|||
try { |
|||
isLoading.value = true; |
|||
paymentStatus.value = '正在发起支付...'; |
|||
errorMessage.value = ''; |
|||
|
|||
// 调用支付服务,使用回调处理结果 |
|||
final launched = await _payService.processWechatPayment( |
|||
orderId: testOrderId.value, |
|||
amount: (testAmount.value * 100).toInt(), // 转换为分 |
|||
description: '微信支付测试', |
|||
onResult: (success, message) { |
|||
// 支付结果回调 |
|||
isLoading.value = false; |
|||
|
|||
if (success) { |
|||
paymentStatus.value = '支付成功!'; |
|||
errorMessage.value = ''; |
|||
Get.snackbar( |
|||
'支付成功', |
|||
message ?? '微信支付测试完成', |
|||
snackPosition: SnackPosition.TOP, |
|||
); |
|||
} else { |
|||
paymentStatus.value = '支付失败'; |
|||
errorMessage.value = message ?? '未知错误'; |
|||
Get.snackbar( |
|||
'支付失败', |
|||
message ?? '未知错误', |
|||
snackPosition: SnackPosition.TOP, |
|||
); |
|||
} |
|||
}, |
|||
); |
|||
|
|||
if (!launched) { |
|||
// 如果支付没有成功启动 |
|||
isLoading.value = false; |
|||
paymentStatus.value = '支付启动失败'; |
|||
errorMessage.value = '无法启动微信支付'; |
|||
Get.snackbar('支付启动失败', '无法启动微信支付'); |
|||
} |
|||
} catch (e) { |
|||
isLoading.value = false; |
|||
paymentStatus.value = '支付异常'; |
|||
errorMessage.value = e.toString(); |
|||
Get.snackbar('支付异常', e.toString()); |
|||
} |
|||
} |
|||
|
|||
/// 重置状态 |
|||
void resetStatus() { |
|||
paymentStatus.value = ''; |
|||
errorMessage.value = ''; |
|||
testOrderId.value = 'test_${DateTime.now().millisecondsSinceEpoch}'; |
|||
} |
|||
|
|||
/// 更新测试金额 |
|||
void updateTestAmount(double amount) { |
|||
testAmount.value = amount; |
|||
} |
|||
} |
|||
@ -0,0 +1,365 @@ |
|||
import 'package:flutter/material.dart'; |
|||
import 'package:get/get.dart'; |
|||
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
|||
import '../controllers/payment_controller.dart'; |
|||
|
|||
class PaymentView extends GetView<PaymentController> { |
|||
const PaymentView({Key? key}) : super(key: key); |
|||
|
|||
@override |
|||
Widget build(BuildContext context) { |
|||
final isDarkMode = Theme.of(context).brightness == Brightness.dark; |
|||
|
|||
return Scaffold( |
|||
backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], |
|||
appBar: AppBar( |
|||
backgroundColor: isDarkMode ? Colors.black : Colors.white, |
|||
elevation: 0.5, |
|||
title: Text( |
|||
'微信支付测试', |
|||
style: TextStyle( |
|||
fontSize: 18.sp, |
|||
fontWeight: FontWeight.w600, |
|||
color: isDarkMode ? Colors.white : Colors.black, |
|||
), |
|||
), |
|||
centerTitle: true, |
|||
leading: IconButton( |
|||
icon: Icon( |
|||
Icons.arrow_back_ios, |
|||
color: isDarkMode ? Colors.white : Colors.black, |
|||
), |
|||
onPressed: () => Get.back(), |
|||
), |
|||
), |
|||
body: SingleChildScrollView( |
|||
padding: EdgeInsets.all(16.w), |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
_buildTestInfoCard(isDarkMode), |
|||
SizedBox(height: 20.h), |
|||
_buildAmountSelector(isDarkMode), |
|||
SizedBox(height: 20.h), |
|||
_buildPaymentButton(isDarkMode), |
|||
SizedBox(height: 20.h), |
|||
_buildStatusCard(isDarkMode), |
|||
SizedBox(height: 20.h), |
|||
_buildInstructions(isDarkMode), |
|||
], |
|||
), |
|||
), |
|||
); |
|||
} |
|||
|
|||
/// 测试信息卡片 |
|||
Widget _buildTestInfoCard(bool isDarkMode) { |
|||
return Container( |
|||
padding: EdgeInsets.all(16.w), |
|||
decoration: BoxDecoration( |
|||
color: isDarkMode ? Colors.grey[900] : Colors.white, |
|||
borderRadius: BorderRadius.circular(12.r), |
|||
boxShadow: [ |
|||
BoxShadow( |
|||
color: Colors.black.withOpacity(0.05), |
|||
blurRadius: 10, |
|||
offset: const Offset(0, 2), |
|||
), |
|||
], |
|||
), |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Text( |
|||
'测试订单信息', |
|||
style: TextStyle( |
|||
fontSize: 16.sp, |
|||
fontWeight: FontWeight.w600, |
|||
color: isDarkMode ? Colors.white : Colors.black, |
|||
), |
|||
), |
|||
SizedBox(height: 12.h), |
|||
Obx(() => _buildInfoRow('订单ID', controller.testOrderId.value, isDarkMode)), |
|||
SizedBox(height: 8.h), |
|||
_buildInfoRow('商品描述', '微信支付测试', isDarkMode), |
|||
SizedBox(height: 8.h), |
|||
Obx(() => _buildInfoRow('支付金额', '¥${controller.testAmount.value.toStringAsFixed(2)}', isDarkMode)), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
|
|||
/// 信息行 |
|||
Widget _buildInfoRow(String label, String value, bool isDarkMode) { |
|||
return Row( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
SizedBox( |
|||
width: 80.w, |
|||
child: Text( |
|||
'$label:', |
|||
style: TextStyle( |
|||
fontSize: 14.sp, |
|||
color: isDarkMode ? Colors.grey[400] : Colors.grey[600], |
|||
), |
|||
), |
|||
), |
|||
Expanded( |
|||
child: Text( |
|||
value, |
|||
style: TextStyle( |
|||
fontSize: 14.sp, |
|||
color: isDarkMode ? Colors.white : Colors.black, |
|||
), |
|||
), |
|||
), |
|||
], |
|||
); |
|||
} |
|||
|
|||
/// 金额选择器 |
|||
Widget _buildAmountSelector(bool isDarkMode) { |
|||
final amounts = [0.01, 0.10, 1.00, 10.00]; |
|||
|
|||
return Container( |
|||
padding: EdgeInsets.all(16.w), |
|||
decoration: BoxDecoration( |
|||
color: isDarkMode ? Colors.grey[900] : Colors.white, |
|||
borderRadius: BorderRadius.circular(12.r), |
|||
boxShadow: [ |
|||
BoxShadow( |
|||
color: Colors.black.withOpacity(0.05), |
|||
blurRadius: 10, |
|||
offset: const Offset(0, 2), |
|||
), |
|||
], |
|||
), |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Text( |
|||
'选择测试金额', |
|||
style: TextStyle( |
|||
fontSize: 16.sp, |
|||
fontWeight: FontWeight.w600, |
|||
color: isDarkMode ? Colors.white : Colors.black, |
|||
), |
|||
), |
|||
SizedBox(height: 12.h), |
|||
Wrap( |
|||
spacing: 12.w, |
|||
runSpacing: 8.h, |
|||
children: amounts.map((amount) { |
|||
return Obx(() => GestureDetector( |
|||
onTap: () => controller.updateTestAmount(amount), |
|||
child: Container( |
|||
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h), |
|||
decoration: BoxDecoration( |
|||
color: controller.testAmount.value == amount |
|||
? Colors.blue[600] |
|||
: (isDarkMode ? Colors.grey[800] : Colors.grey[200]), |
|||
borderRadius: BorderRadius.circular(20.r), |
|||
), |
|||
child: Text( |
|||
'¥${amount.toStringAsFixed(2)}', |
|||
style: TextStyle( |
|||
fontSize: 14.sp, |
|||
color: controller.testAmount.value == amount |
|||
? Colors.white |
|||
: (isDarkMode ? Colors.white : Colors.black), |
|||
), |
|||
), |
|||
), |
|||
)); |
|||
}).toList(), |
|||
), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
|
|||
/// 支付按钮 |
|||
Widget _buildPaymentButton(bool isDarkMode) { |
|||
return Obx(() => SizedBox( |
|||
width: double.infinity, |
|||
height: 50.h, |
|||
child: ElevatedButton( |
|||
onPressed: controller.isLoading.value ? null : controller.testWechatPayment, |
|||
style: ElevatedButton.styleFrom( |
|||
backgroundColor: Colors.green[600], |
|||
foregroundColor: Colors.white, |
|||
shape: RoundedRectangleBorder( |
|||
borderRadius: BorderRadius.circular(12.r), |
|||
), |
|||
elevation: 2, |
|||
), |
|||
child: controller.isLoading.value |
|||
? Row( |
|||
mainAxisAlignment: MainAxisAlignment.center, |
|||
children: [ |
|||
SizedBox( |
|||
width: 20.w, |
|||
height: 20.w, |
|||
child: const CircularProgressIndicator( |
|||
strokeWidth: 2, |
|||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white), |
|||
), |
|||
), |
|||
SizedBox(width: 12.w), |
|||
const Text('处理中...'), |
|||
], |
|||
) |
|||
: Row( |
|||
mainAxisAlignment: MainAxisAlignment.center, |
|||
children: [ |
|||
Icon(Icons.payment, size: 20.w), |
|||
SizedBox(width: 8.w), |
|||
Text( |
|||
'发起微信支付', |
|||
style: TextStyle( |
|||
fontSize: 16.sp, |
|||
fontWeight: FontWeight.w600, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
), |
|||
)); |
|||
} |
|||
|
|||
/// 状态卡片 |
|||
Widget _buildStatusCard(bool isDarkMode) { |
|||
return Obx(() { |
|||
if (controller.paymentStatus.value.isEmpty) { |
|||
return const SizedBox.shrink(); |
|||
} |
|||
|
|||
final isError = controller.errorMessage.value.isNotEmpty; |
|||
|
|||
return Container( |
|||
padding: EdgeInsets.all(16.w), |
|||
decoration: BoxDecoration( |
|||
color: isError |
|||
? (isDarkMode ? Colors.red[900] : Colors.red[50]) |
|||
: (isDarkMode ? Colors.green[900] : Colors.green[50]), |
|||
borderRadius: BorderRadius.circular(12.r), |
|||
border: Border.all( |
|||
color: isError ? Colors.red[300]! : Colors.green[300]!, |
|||
width: 1, |
|||
), |
|||
), |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Row( |
|||
children: [ |
|||
Icon( |
|||
isError ? Icons.error : Icons.check_circle, |
|||
color: isError ? Colors.red[600] : Colors.green[600], |
|||
size: 20.w, |
|||
), |
|||
SizedBox(width: 8.w), |
|||
Text( |
|||
'支付状态', |
|||
style: TextStyle( |
|||
fontSize: 16.sp, |
|||
fontWeight: FontWeight.w600, |
|||
color: isError ? Colors.red[600] : Colors.green[600], |
|||
), |
|||
), |
|||
], |
|||
), |
|||
SizedBox(height: 8.h), |
|||
Text( |
|||
controller.paymentStatus.value, |
|||
style: TextStyle( |
|||
fontSize: 14.sp, |
|||
color: isDarkMode ? Colors.white : Colors.black, |
|||
), |
|||
), |
|||
if (controller.errorMessage.value.isNotEmpty) ...[ |
|||
SizedBox(height: 8.h), |
|||
Text( |
|||
'错误信息: ${controller.errorMessage.value}', |
|||
style: TextStyle( |
|||
fontSize: 12.sp, |
|||
color: Colors.red[600], |
|||
), |
|||
), |
|||
], |
|||
SizedBox(height: 12.h), |
|||
SizedBox( |
|||
width: double.infinity, |
|||
child: OutlinedButton( |
|||
onPressed: controller.resetStatus, |
|||
style: OutlinedButton.styleFrom( |
|||
side: BorderSide( |
|||
color: isError ? Colors.red[600]! : Colors.green[600]!, |
|||
), |
|||
), |
|||
child: Text( |
|||
'重新测试', |
|||
style: TextStyle( |
|||
color: isError ? Colors.red[600] : Colors.green[600], |
|||
), |
|||
), |
|||
), |
|||
), |
|||
], |
|||
), |
|||
); |
|||
}); |
|||
} |
|||
|
|||
/// 使用说明 |
|||
Widget _buildInstructions(bool isDarkMode) { |
|||
return Container( |
|||
padding: EdgeInsets.all(16.w), |
|||
decoration: BoxDecoration( |
|||
color: isDarkMode ? Colors.grey[900] : Colors.blue[50], |
|||
borderRadius: BorderRadius.circular(12.r), |
|||
border: Border.all( |
|||
color: Colors.blue[200]!, |
|||
width: 1, |
|||
), |
|||
), |
|||
child: Column( |
|||
crossAxisAlignment: CrossAxisAlignment.start, |
|||
children: [ |
|||
Row( |
|||
children: [ |
|||
Icon( |
|||
Icons.info_outline, |
|||
color: Colors.blue[600], |
|||
size: 20.w, |
|||
), |
|||
SizedBox(width: 8.w), |
|||
Text( |
|||
'使用说明', |
|||
style: TextStyle( |
|||
fontSize: 16.sp, |
|||
fontWeight: FontWeight.w600, |
|||
color: Colors.blue[600], |
|||
), |
|||
), |
|||
], |
|||
), |
|||
SizedBox(height: 12.h), |
|||
Text( |
|||
'1. 选择测试金额(建议使用0.01元进行测试)\n' |
|||
'2. 点击"发起微信支付"按钮\n' |
|||
'3. 系统将调用微信支付SDK\n' |
|||
'4. 在微信中完成支付流程\n' |
|||
'5. 返回应用查看支付结果\n\n' |
|||
'注意:这是测试环境,实际不会产生真实扣费', |
|||
style: TextStyle( |
|||
fontSize: 14.sp, |
|||
color: isDarkMode ? Colors.grey[300] : Colors.grey[700], |
|||
height: 1.5, |
|||
), |
|||
), |
|||
], |
|||
), |
|||
); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue