diff --git a/client/lib/models/payment.dart b/client/lib/models/payment.dart index b753ad8..c2d8492 100644 --- a/client/lib/models/payment.dart +++ b/client/lib/models/payment.dart @@ -7,7 +7,8 @@ enum PayChannel { usdt, cny } extension PayChannelX on PayChannel { String get currency => this == PayChannel.usdt ? 'USDT' : 'CNY'; - String get method => this == PayChannel.usdt ? 'crypto' : 'alipay'; + // CNY 渠道生产走哪吒(nezha)聚合(底层支付宝/微信),非直连 alipay 商户。 + String get method => this == PayChannel.usdt ? 'crypto' : 'nezha'; } class PayCatalogItem { diff --git a/client/lib/screens/payment_page.dart b/client/lib/screens/payment_page.dart index 8e18669..b455770 100644 --- a/client/lib/screens/payment_page.dart +++ b/client/lib/screens/payment_page.dart @@ -244,7 +244,7 @@ class _PaymentScreenState extends ConsumerState { } Future _pickAndSwitch(BuildContext context, WidgetRef ref, PaymentFlowState s) async { - final other = s.method == 'crypto' ? 'alipay' : 'crypto'; + final other = s.method == 'crypto' ? 'nezha' : 'crypto'; await ref.read(paymentFlowProvider.notifier).switchMethod(other); } diff --git a/client/lib/screens/purchase_page.dart b/client/lib/screens/purchase_page.dart index f984310..7da2ff1 100644 --- a/client/lib/screens/purchase_page.dart +++ b/client/lib/screens/purchase_page.dart @@ -10,6 +10,7 @@ import '../models/payment.dart'; import '../pangolin_theme.dart'; import '../state/payment_provider.dart'; import '../widgets/pangolin_icons.dart'; +import '../widgets/pangolin_toast.dart'; import '../widgets/plan_card.dart'; import '../widgets/seg_switch.dart'; @@ -50,8 +51,17 @@ class _PurchaseScreenState extends ConsumerState { Future _buy(PayCatalogItem item) async { await ref.read(paymentFlowProvider.notifier).start(item, _channel.method); if (!mounted) return; - if (ref.read(paymentFlowProvider).phase == PaymentPhase.awaitingPayment) { + final st = ref.read(paymentFlowProvider); + if (st.phase == PaymentPhase.awaitingPayment) { widget.onOrderCreated?.call(); + } else if (st.phase == PaymentPhase.failed) { + // 下单失败给可见 toast,别让用户以为「点了没反应」。 + showPangolinToast( + context, + t.lang == AppLang.zh + ? (st.errorZh ?? '下单失败,请稍后重试') + : (st.errorEn ?? 'Failed to create order, please retry'), + ); } } diff --git a/client/lib/state/payment_provider.dart b/client/lib/state/payment_provider.dart index 9060697..36a26e5 100644 --- a/client/lib/state/payment_provider.dart +++ b/client/lib/state/payment_provider.dart @@ -36,7 +36,7 @@ class PaymentFlowState { final PaymentPhase phase; final PayCatalogItem? item; - final String method; // alipay | crypto + final String method; // nezha | crypto final PayOrder? order; final PayOrderStatus? status; final String? errorZh; @@ -82,8 +82,10 @@ class PaymentFlowController extends StateNotifier { return Platform.isAndroid || Platform.isIOS; } + // 法币渠道(nezha)带 is_mobile 提示;crypto 不需要。经 pangolin 代理白名单 + // (is_mobile/render)透传到 pay。 Map _metadata(String method) => - method == 'alipay' ? {'is_mobile': _isMobile ? '1' : '0'} : const {}; + method == 'nezha' ? {'is_mobile': _isMobile ? '1' : '0'} : const {}; Future start(PayCatalogItem item, String method) async { _stopPolling(); @@ -98,6 +100,15 @@ class PaymentFlowController extends StateNotifier { } on AuthApiException catch (e) { if (!mounted) return; state = state.copyWith(phase: PaymentPhase.failed, errorZh: e.messageZh, errorEn: e.messageEn); + } catch (_) { + // 网络/解析等非 Auth 异常也要落 failed —— 否则状态机卡在 creating, + // 购买页看着「点了没反应」。 + if (!mounted) return; + state = state.copyWith( + phase: PaymentPhase.failed, + errorZh: '下单失败,请检查网络后重试', + errorEn: 'Failed to create order, please check your network and retry', + ); } } diff --git a/client/test/unit/payment_api_test.dart b/client/test/unit/payment_api_test.dart index 2a0da99..2fa86f5 100644 --- a/client/test/unit/payment_api_test.dart +++ b/client/test/unit/payment_api_test.dart @@ -29,6 +29,14 @@ void main() { expect(items.first.priceLabel(PayChannel.cny), '¥29.99'); }); + test('PayChannel 渠道→method 映射:usdt→crypto / cny→nezha(生产渠道码)', () { + // 回归钉:CNY 生产走哪吒(nezha)聚合,不是直连 alipay——发错 method 会被 pay 回 400。 + expect(PayChannel.usdt.method, 'crypto'); + expect(PayChannel.cny.method, 'nezha'); + expect(PayChannel.usdt.currency, 'USDT'); + expect(PayChannel.cny.currency, 'CNY'); + }); + test('createOrder 只传 sku+method+metadata,解析 session', () async { final api = PaymentApi(_client(MockClient((req) async { expect(req.url.path, '/v1/pay/orders');