fix(client/pay): CNY 渠道 method 对齐生产哪吒(nezha)+ 下单失败不再静默
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (pull_request) Successful in 24s
ci-pangolin / Cleartext Scan — Android 禁明文 (pull_request) Successful in 19s
ci-pangolin / OpenAPI Sync Check (pull_request) Successful in 45s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (pull_request) Successful in 20s
ci-pangolin / Flutter — analyze + test (pull_request) Successful in 37s
ci-pangolin / Codegen Drift — token 生成物未漂移 (pull_request) Successful in 3s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (pull_request) Successful in 4s
ci-pangolin / Go — build + test (pull_request) Failing after 15s
ci-pangolin / E2E Smoke — L4 进程级端到端 (pull_request) Failing after 10s
ci-pangolin / Go — integration (mysql/redis testcontainers) (pull_request) Successful in 4m41s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (pull_request) Failing after 20s
ci-pangolin / Lint — shellcheck (pull_request) Failing after 12m35s
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (pull_request) Successful in 24s
ci-pangolin / Cleartext Scan — Android 禁明文 (pull_request) Successful in 19s
ci-pangolin / OpenAPI Sync Check (pull_request) Successful in 45s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (pull_request) Successful in 20s
ci-pangolin / Flutter — analyze + test (pull_request) Successful in 37s
ci-pangolin / Codegen Drift — token 生成物未漂移 (pull_request) Successful in 3s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (pull_request) Successful in 4s
ci-pangolin / Go — build + test (pull_request) Failing after 15s
ci-pangolin / E2E Smoke — L4 进程级端到端 (pull_request) Failing after 10s
ci-pangolin / Go — integration (mysql/redis testcontainers) (pull_request) Successful in 4m41s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (pull_request) Failing after 20s
ci-pangolin / Lint — shellcheck (pull_request) Failing after 12m35s
根因(购买按钮全端「点了没反应」): - 生产 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) <noreply@anthropic.com>
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -244,7 +244,7 @@ class _PaymentScreenState extends ConsumerState<PaymentScreen> {
|
||||
}
|
||||
|
||||
Future<void> _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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<PurchaseScreen> {
|
||||
Future<void> _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'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<PaymentFlowState> {
|
||||
return Platform.isAndroid || Platform.isIOS;
|
||||
}
|
||||
|
||||
// 法币渠道(nezha)带 is_mobile 提示;crypto 不需要。经 pangolin 代理白名单
|
||||
// (is_mobile/render)透传到 pay。
|
||||
Map<String, String> _metadata(String method) =>
|
||||
method == 'alipay' ? {'is_mobile': _isMobile ? '1' : '0'} : const {};
|
||||
method == 'nezha' ? {'is_mobile': _isMobile ? '1' : '0'} : const {};
|
||||
|
||||
Future<void> start(PayCatalogItem item, String method) async {
|
||||
_stopPolling();
|
||||
@@ -98,6 +100,15 @@ class PaymentFlowController extends StateNotifier<PaymentFlowState> {
|
||||
} 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',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user