From e29ed0a2c80bfec8cb31ec48bd4ff157ce70bd12 Mon Sep 17 00:00:00 2001 From: jingyun <> Date: Tue, 8 Sep 2026 14:27:32 +0800 Subject: [PATCH] =?UTF-8?q?fix=EF=BC=9A=E8=B4=AD=E4=B9=B0=E5=95=86?= =?UTF-8?q?=E5=93=81=E9=A5=B0=E5=93=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/mall_entry_controller.dart | 2 + .../mall/application/mall_tab_config.dart | 41 +++++++++++++++++-- .../market/presentation/market_page.dart | 24 +++++++++++ .../mall/mall_bridge_arguments_test.dart | 7 +++- test/features/mall/mall_tab_config_test.dart | 18 ++++++++ 5 files changed, 88 insertions(+), 4 deletions(-) diff --git a/lib/features/mall/application/mall_entry_controller.dart b/lib/features/mall/application/mall_entry_controller.dart index ecb1d59..9c38381 100644 --- a/lib/features/mall/application/mall_entry_controller.dart +++ b/lib/features/mall/application/mall_entry_controller.dart @@ -14,6 +14,7 @@ class MallEntryController { Future open( BuildContext context, { + MallEntryRequest request = MallEntryRequest.home, MallTabConfig? initialConfig, }) async { if (_opening) return MallEntryOpenResult.alreadyOpening; @@ -42,6 +43,7 @@ class MallEntryController { final uri = config.attributedUri( clickId: clickId, isDark: Theme.of(context).brightness == Brightness.dark, + entry: request, ); if (uri == null) return MallEntryOpenResult.unavailable; // The bottom mall entry is always resident. Server open-mode metadata is diff --git a/lib/features/mall/application/mall_tab_config.dart b/lib/features/mall/application/mall_tab_config.dart index e3f6601..d23e299 100644 --- a/lib/features/mall/application/mall_tab_config.dart +++ b/lib/features/mall/application/mall_tab_config.dart @@ -11,6 +11,33 @@ import '../../../common/network/dio_provider.dart'; /// 金值商城广告位的公共契约;商城是否展示完全由服务端配置决定。 const mallSourceApp = 'jinzhi'; +/// The native entry that opened the resident mall. H5 uses this context to +/// choose the appropriate landing flow while sharing the same WebView host. +class MallEntryRequest { + const MallEntryRequest({ + required this.sourceEntry, + required this.hookType, + this.entryScene, + this.entryMetal, + }); + + static const home = MallEntryRequest( + sourceEntry: 'mall_tab', + hookType: 'mall_tab', + ); + static const quoteRetailGold = MallEntryRequest( + sourceEntry: 'market_quote', + hookType: 'quote_cta', + entryScene: 'retail', + entryMetal: 'au', + ); + + final String sourceEntry; + final String hookType; + final String? entryScene; + final String? entryMetal; +} + class MallTabConfig { const MallTabConfig({ required this.enabled, @@ -49,7 +76,11 @@ class MallTabConfig { Uri? get initialUri => attributedUri(clickId: ''); - Uri? attributedUri({required String clickId, bool isDark = false}) { + Uri? attributedUri({ + required String clickId, + bool isDark = false, + MallEntryRequest entry = MallEntryRequest.home, + }) { final uri = Uri.tryParse(url.trim()); if (!_isAllowed(uri)) return null; return uri!.replace( @@ -59,9 +90,13 @@ class MallTabConfig { ? uri.queryParameters['from']! : mallSourceApp, 'source_app': mallSourceApp, - 'source_entry': 'mall_tab', + 'source_entry': entry.sourceEntry, 'placement': 'mall_tab', - 'hook_type': 'mall_tab', + 'hook_type': entry.hookType, + if (entry.entryScene?.isNotEmpty == true) + 'entry_scene': entry.entryScene!, + if (entry.entryMetal?.isNotEmpty == true) + 'entry_metal': entry.entryMetal!, 'target_app': 'mall', if (adId.isNotEmpty) 'ad_id': adId, if (trackingId.isNotEmpty) 'tracking_id': trackingId, diff --git a/lib/features/market/presentation/market_page.dart b/lib/features/market/presentation/market_page.dart index c0e8c3c..37fc8bc 100644 --- a/lib/features/market/presentation/market_page.dart +++ b/lib/features/market/presentation/market_page.dart @@ -5,6 +5,8 @@ import 'package:shadcn_ui/shadcn_ui.dart'; import '../../../common/domain/metal_type.dart'; import '../../../common/domain/money.dart'; import '../../../common/widgets/adaptive.dart'; +import '../../mall/application/mall_entry_controller.dart'; +import '../../mall/application/mall_tab_config.dart'; import '../application/exchange_calculator_controller.dart'; import '../application/market_controller.dart'; import '../data/market_models.dart'; @@ -93,6 +95,28 @@ class MarketPage extends ConsumerWidget { ), ), const SizedBox(height: 12), + FilledButton.icon( + key: const ValueKey('market-mall-retail-cta'), + onPressed: () async { + final result = await ref + .read(mallEntryControllerProvider) + .open( + context, + request: MallEntryRequest.quoteRetailGold, + ); + if (!context.mounted || + (result != MallEntryOpenResult.unavailable && + result != MallEntryOpenResult.failed)) { + return; + } + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('商城暂时无法打开,请稍后重试')), + ); + }, + icon: const Icon(Icons.shopping_bag_outlined), + label: const Text('买商城饰品(全款)'), + ), + const SizedBox(height: 12), _Card( child: Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/test/features/mall/mall_bridge_arguments_test.dart b/test/features/mall/mall_bridge_arguments_test.dart index 7860c2c..a137cfe 100644 --- a/test/features/mall/mall_bridge_arguments_test.dart +++ b/test/features/mall/mall_bridge_arguments_test.dart @@ -6,7 +6,8 @@ void main() { final arguments = buildEnterWebMallBridgeArguments( currentUri: Uri.parse( 'https://mall.example.com/?from=jinzhi&source_app=jinzhi' - '&source_entry=mall_tab&placement=mall_tab&hook_type=mall_tab' + '&source_entry=market_quote&placement=mall_tab&hook_type=quote_cta' + '&entry_scene=retail&entry_metal=au' '&tracking_id=tracking&click_id=click-1&ts=123&isNight=0', ), logged: true, @@ -23,6 +24,10 @@ void main() { expect(arguments['from'], 'jinzhi'); expect(arguments['source_app'], 'jinzhi'); + expect(arguments['source_entry'], 'market_quote'); + expect(arguments['hook_type'], 'quote_cta'); + expect(arguments['entry_scene'], 'retail'); + expect(arguments['entry_metal'], 'au'); expect(arguments['ad_id'], 'latest-ad'); expect(arguments['tracking_id'], 'latest-tracking'); expect(arguments['click_id'], 'latest-click'); diff --git a/test/features/mall/mall_tab_config_test.dart b/test/features/mall/mall_tab_config_test.dart index 7d356eb..622b764 100644 --- a/test/features/mall/mall_tab_config_test.dart +++ b/test/features/mall/mall_tab_config_test.dart @@ -44,6 +44,24 @@ void main() { expect(external.opensInApp, isFalse); }); + test('adds retail quote context to the mall URL for H5', () { + const config = MallTabConfig( + enabled: true, + url: 'https://mall.example.com/home', + ); + + final uri = config.attributedUri( + clickId: 'quote-click', + entry: MallEntryRequest.quoteRetailGold, + )!; + + expect(uri.queryParameters['source_entry'], 'market_quote'); + expect(uri.queryParameters['hook_type'], 'quote_cta'); + expect(uri.queryParameters['entry_scene'], 'retail'); + expect(uri.queryParameters['entry_metal'], 'au'); + expect(uri.queryParameters['placement'], 'mall_tab'); + }); + test('shows the first-install promo only for a configured mall entry', () { const configured = MallTabConfig( enabled: true,