feat(client): 购买卡按 render_type 分发——redirect 外跳/qr 内嵌二维码/重选取消订单
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -84,6 +84,9 @@ class LicenseRepository {
|
||||
return data?['canceled'] as bool? ?? false;
|
||||
} on DioException catch (e) {
|
||||
final status = e.response?.statusCode;
|
||||
// 409 分支防御性保留:本后端实现里非 pending 单取消返回 200 +
|
||||
// canceled:false(业务性失败,非 HTTP 错误),故 409 实际永不触发;
|
||||
// 保留兜底以防后端未来改走标准 HTTP 语义时前端仍能优雅降级。
|
||||
if (status == 404 || status == 409) return false;
|
||||
throw AppException(
|
||||
e.response?.data?['error'] as String? ?? '取消订单失败,请稍后重试',
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../core/config/license_plans.dart';
|
||||
@@ -18,6 +19,7 @@ import '../../core/theme/app_dims.g.dart';
|
||||
import '../../core/theme/app_fonts.dart';
|
||||
import '../../core/theme/app_tokens.dart';
|
||||
import '../../core/theme/context_tokens.dart';
|
||||
import '../../core/utils/money.dart';
|
||||
import '../../providers/license_provider.dart';
|
||||
import '../../widgets/ds/ds_atoms.dart';
|
||||
import '../../widgets/ds/ds_toast.dart';
|
||||
@@ -89,10 +91,23 @@ class _PurchaseCardState extends ConsumerState<PurchaseCard> {
|
||||
final order = await ref
|
||||
.read(licenseRepositoryProvider)
|
||||
.createPurchase(_plan.bizCode, clientType: _clientType);
|
||||
// 外部浏览器打开收银台:桌面端进网页支付,移动端由系统浏览器打开
|
||||
// wap 收银台自动拉起支付宝 App
|
||||
await launchUrl(Uri.parse(order.payUrl),
|
||||
mode: LaunchMode.externalApplication);
|
||||
// pay v2 收银台会话按 render_type 分发:redirect 外跳外部浏览器(桌面进
|
||||
// 网页支付,移动端 wap 收银台自动拉起支付宝 App);qr 不外跳,本卡片
|
||||
// waiting 态内嵌二维码(pay 侧当前未实现该分支,向前预留);未知类型
|
||||
// 视为客户端版本落后于协议,提示升级并复位,不进入 waiting 态。
|
||||
switch (order.renderType) {
|
||||
case 'redirect':
|
||||
await launchUrl(Uri.parse(order.redirectUrl),
|
||||
mode: LaunchMode.externalApplication);
|
||||
case 'qr':
|
||||
break;
|
||||
default:
|
||||
if (!mounted) return;
|
||||
setState(() => _submitting = false);
|
||||
showDsToast(context, '当前版本暂不支持该支付方式,请升级应用',
|
||||
bg: context.tokens.danger);
|
||||
return;
|
||||
}
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_order = order;
|
||||
@@ -141,8 +156,17 @@ class _PurchaseCardState extends ConsumerState<PurchaseCard> {
|
||||
}
|
||||
|
||||
/// 回到套餐选择(仅停止本地轮询,不影响已支付订单到账)。
|
||||
/// waiting 态下的订单仍是 pending(重新选择/放弃本单),fire-and-forget
|
||||
/// 取消,防止 pending 单堆积;取消失败静默——服务端 reconcile 任务兜底
|
||||
/// 清理超时单。success 态点「完成」不带 pending 单,不触发取消。
|
||||
void _backToPick() {
|
||||
_pollTimer?.cancel();
|
||||
final otn = _order?.outTradeNo;
|
||||
if (_phase == _Phase.waiting && otn != null && otn.isNotEmpty) {
|
||||
ref.read(licenseRepositoryProvider).cancelPurchase(otn).catchError((_) {
|
||||
return false;
|
||||
});
|
||||
}
|
||||
setState(() {
|
||||
_order = null;
|
||||
_newExpiresAt = null;
|
||||
@@ -320,6 +344,9 @@ class _PurchaseCardState extends ConsumerState<PurchaseCard> {
|
||||
}
|
||||
|
||||
Widget _buildWaiting(AppTokens t) {
|
||||
final isQr = _order?.renderType == 'qr';
|
||||
final amountMinor = _order?.amountMinor ?? 0;
|
||||
final amountText = amountMinor > 0 ? '¥${yuanFromMinor(amountMinor)}' : '—';
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -332,10 +359,31 @@ class _PurchaseCardState extends ConsumerState<PurchaseCard> {
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text('已在浏览器打开支付宝收银台,完成支付后本卡片会自动确认到账。',
|
||||
child: Text(
|
||||
isQr
|
||||
? '打开支付宝扫码支付,完成后本卡片自动确认到账。'
|
||||
: '已在浏览器打开支付宝收银台,完成支付后本卡片会自动确认到账。',
|
||||
style: TextStyle(fontSize: AppDims.fsSm, color: t.text)),
|
||||
),
|
||||
]),
|
||||
if (isQr) ...[
|
||||
const SizedBox(height: 12),
|
||||
Center(
|
||||
child: Container(
|
||||
width: 180,
|
||||
height: 180,
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: t.bg,
|
||||
border: Border.all(color: t.border),
|
||||
borderRadius: BorderRadius.circular(AppDims.rMd),
|
||||
),
|
||||
child: (_order?.qrContent ?? '').isEmpty
|
||||
? Icon(LucideIcons.qrCode, size: 48, color: t.faint)
|
||||
: QrImageView(data: _order!.qrContent, size: 160),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 14),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
@@ -357,7 +405,7 @@ class _PurchaseCardState extends ConsumerState<PurchaseCard> {
|
||||
fontFamily: AppFonts.mono,
|
||||
fontFamilyFallback: AppFonts.monoFallback)),
|
||||
const SizedBox(height: 8),
|
||||
Text('金额 ¥${_order?.amount ?? '—'}',
|
||||
Text('金额 $amountText',
|
||||
style: TextStyle(fontSize: AppDims.fsSm, color: t.text)),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -669,6 +669,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
qr_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: qr_flutter
|
||||
sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
record_use:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -32,6 +32,8 @@ dependencies:
|
||||
# GBK 编码(TSPL TEXT 内置中文字体需要),支持 macOS/Windows/Linux
|
||||
charset_converter: ^2.1.0
|
||||
lucide_icons_flutter: ^3.1.14+2
|
||||
# 购买卡 render_type=qr 内嵌二维码(纯 Dart,全平台安全,无原生依赖)
|
||||
qr_flutter: ^4.1.0
|
||||
# Web 端去 URL # 前缀(usePathUrlStrategy),Flutter SDK 内置包
|
||||
flutter_web_plugins:
|
||||
sdk: flutter
|
||||
|
||||
Reference in New Issue
Block a user