From 252779c6076e9a60a79bb21473751a275e519f5e Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Sun, 12 Jul 2026 22:09:31 +0800 Subject: [PATCH] =?UTF-8?q?fix(client/pay):=20CNY=20=E6=B8=A0=E9=81=93=20m?= =?UTF-8?q?ethod=20=E5=AF=B9=E9=BD=90=E7=94=9F=E4=BA=A7=E5=93=AA=E5=90=92(?= =?UTF-8?q?nezha)+=20=E4=B8=8B=E5=8D=95=E5=A4=B1=E8=B4=A5=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E9=9D=99=E9=BB=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因(购买按钮全端「点了没反应」): - 生产 pay-v2 的 CNY 渠道是哪吒(nezha)聚合(底层支付宝/微信),客户端却给 CNY 发 method=alipay → pay 不认 → POST /api/v2/orders 400。USDT/crypto 一直正常。 - 且 _buy 只在 awaitingPayment 时导航,下单失败(phase=failed)既不导航也不弹错, 非 Auth 异常还直接从 start() 抛出未捕获 → 用户看着「没反应」。 改: - PayChannelX.method:cny 'alipay' → 'nezha'(usdt 仍 crypto) - payment_provider._metadata 法币条件同步 nezha;start() 补 catch(_) 兜非 Auth 异常 落 failed(状态机不再卡 creating) - purchase_page._buy:phase==failed 时 showPangolinToast 显式提示 - payment_page 换支付方式的另一渠道 alipay → nezha - 测试钉:PayChannel.cny.method == 'nezha'(发错 method 会被 pay 400) nezha 返回 render_type=redirect(payload.url),客户端现成 redirect 分支直接接; orders_page 早有 'nezha'→payMethodNezha 标签。服务端无需改(method 透传、 SettlementCurrency 只区分 crypto vs 法币)。 验证:flutter analyze 干净;payment 相关 18 测试全过。 Co-Authored-By: Claude Opus 4.8 (1M context) --- client/lib/models/payment.dart | 3 ++- client/lib/screens/payment_page.dart | 2 +- client/lib/screens/purchase_page.dart | 12 +++++++++++- client/lib/state/payment_provider.dart | 15 +++++++++++++-- client/test/unit/payment_api_test.dart | 8 ++++++++ 5 files changed, 35 insertions(+), 5 deletions(-) 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');