From a63334f5a542d6a4d977beb9b89d8c787904cc80 Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Tue, 20 Oct 2026 18:01:12 +0800 Subject: [PATCH] =?UTF-8?q?fix(flutter):=20=E4=BF=AE=E5=A4=8D=20iOS=20?= =?UTF-8?q?=E5=90=AF=E5=8A=A8=E7=BD=91=E7=BB=9C=E5=8A=A0=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ios/Runner/Info.plist | 33 ++++++++++++++ lib/core/config/app_config.dart | 64 ++++++++++++++++++--------- test/config/server_endpoint_test.dart | 58 ++++++++++++++++++++++-- 3 files changed, 129 insertions(+), 26 deletions(-) diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index bbee514..2c7c7c4 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -28,8 +28,41 @@ NSAppTransportSecurity + NSAllowsArbitraryLoads + NSAllowsLocalNetworking + NSExceptionDomains + + 127.0.0.1 + + NSExceptionAllowsInsecureHTTPLoads + + NSIncludesSubdomains + + + localhost + + NSExceptionAllowsInsecureHTTPLoads + + NSIncludesSubdomains + + + 192.168.31.187 + + NSExceptionAllowsInsecureHTTPLoads + + NSIncludesSubdomains + + + 39.162.50.99 + + NSExceptionAllowsInsecureHTTPLoads + + NSIncludesSubdomains + + + NSLocalNetworkUsageDescription 开发阶段需要访问同一局域网内的短剧内容服务。 diff --git a/lib/core/config/app_config.dart b/lib/core/config/app_config.dart index 486d36c..041d608 100644 --- a/lib/core/config/app_config.dart +++ b/lib/core/config/app_config.dart @@ -1,3 +1,5 @@ +// Author: fruit.com.cn + import 'dart:io'; import 'package:flutter/foundation.dart'; @@ -22,12 +24,17 @@ class AppConfig { /// 公网地址(远程/移动网络访问时使用)。 static const String publicBaseUrl = 'http://39.162.50.99:8081'; - static const String _loopbackBaseUrl = 'http://127.0.0.1:8081'; - static const String _overrideBaseUrl = String.fromEnvironment('API_BASE_URL'); static String _runtimeBaseUrl = ''; + static const Duration _probeTimeout = Duration(seconds: 2); + static const List _probeCandidates = [ + lanBaseUrl, + devHostBaseUrl, + publicBaseUrl, + ]; + /// Build-time server URL supplied through `--dart-define=API_BASE_URL`. static String get compiledBaseUrl => _overrideBaseUrl; @@ -40,7 +47,7 @@ class AppConfig { : (compiledBaseUrl.isNotEmpty ? compiledBaseUrl : defaultBaseUrl); static final String _defaultBaseUrl = _runsOnDevMachine - ? _loopbackBaseUrl + ? lanBaseUrl : devHostBaseUrl; /// 模拟器、桌面端与开发机同机,环回地址可用;iOS 真机不满足。 @@ -66,29 +73,42 @@ class AppConfig { static const Duration requestTimeout = Duration(seconds: 8); - /// 探测内网地址是否可达,可达则切换为内网直连,否则回落到公网地址。 + /// 按环回、开发机、公网顺序探测,写入首个返回 200 的地址。 /// - /// Wi-Fi 下内网直连更快;移动数据下内网不可达,自动回落到公网端口映射。 - static Future probeAndApplyBaseUrl() async { - try { - final client = HttpClient() - ..connectionTimeout = const Duration(seconds: 2); - final request = await client.getUrl( - Uri.parse('$lanBaseUrl/app/categories'), - ); - final response = await request.close().timeout(const Duration(seconds: 2)); - await response.drain(); - client.close(force: true); - if (response.statusCode == 200) { - applyRuntimeBaseUrl(lanBaseUrl); + /// 全部不可达时使用平台默认地址,真机保留开发机地址,避免退回公网。 + static Future probeAndApplyBaseUrl({ + Future Function(String baseUrl)? probe, + String? fallbackBaseUrl, + }) async { + final candidateProbe = probe ?? _probeBaseUrl; + for (final candidate in _probeCandidates) { + if (await candidateProbe(candidate)) { + applyRuntimeBaseUrl(candidate); return; } - } catch (_) { - // 内网不可达,回落到公网。 } - applyRuntimeBaseUrl( - compiledBaseUrl.isNotEmpty ? compiledBaseUrl : publicBaseUrl, - ); + applyRuntimeBaseUrl(fallbackBaseUrl ?? defaultBaseUrl); + } + + static Future _probeBaseUrl(String baseUrl) async { + final client = HttpClient()..connectionTimeout = _probeTimeout; + try { + return await _requestCategories( + client, + Uri.parse('$baseUrl/app/categories'), + ).timeout(_probeTimeout); + } catch (_) { + return false; + } finally { + client.close(force: true); + } + } + + static Future _requestCategories(HttpClient client, Uri uri) async { + final request = await client.getUrl(uri); + final response = await request.close(); + await response.drain(); + return response.statusCode == 200; } /// 后端返回的是相对路径(/files/...),这里补上服务地址。 diff --git a/test/config/server_endpoint_test.dart b/test/config/server_endpoint_test.dart index 4093ba0..159e199 100644 --- a/test/config/server_endpoint_test.dart +++ b/test/config/server_endpoint_test.dart @@ -60,6 +60,59 @@ void main() { expect(AppConfig.devHostBaseUrl, 'http://192.168.31.187:8081'); }); + test('should_keep_loopback_priority_when_probe_succeeds', () async { + final attempts = []; + + await AppConfig.probeAndApplyBaseUrl( + fallbackBaseUrl: AppConfig.devHostBaseUrl, + probe: (baseUrl) async { + attempts.add(baseUrl); + return baseUrl == AppConfig.lanBaseUrl; + }, + ); + + expect(attempts, [AppConfig.lanBaseUrl]); + expect(AppConfig.apiBaseUrl, AppConfig.lanBaseUrl); + }); + + test('should_probe_loopback_dev_host_then_public_in_order', () async { + final attempts = []; + + await AppConfig.probeAndApplyBaseUrl( + fallbackBaseUrl: AppConfig.devHostBaseUrl, + probe: (baseUrl) async { + attempts.add(baseUrl); + return baseUrl == AppConfig.publicBaseUrl; + }, + ); + + expect(attempts, [ + AppConfig.lanBaseUrl, + AppConfig.devHostBaseUrl, + AppConfig.publicBaseUrl, + ]); + expect(AppConfig.apiBaseUrl, AppConfig.publicBaseUrl); + }); + + test('should_use_platform_default_when_all_probes_fail', () async { + final attempts = []; + + await AppConfig.probeAndApplyBaseUrl( + fallbackBaseUrl: AppConfig.devHostBaseUrl, + probe: (baseUrl) async { + attempts.add(baseUrl); + return false; + }, + ); + + expect(attempts, [ + AppConfig.lanBaseUrl, + AppConfig.devHostBaseUrl, + AppConfig.publicBaseUrl, + ]); + expect(AppConfig.apiBaseUrl, AppConfig.devHostBaseUrl); + }); + test('should_apply_stored_url_only_when_debug_is_enabled', () async { SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.withData({ @@ -81,10 +134,7 @@ void main() { isNull, ); - expect( - await ServerEndpoint.save(' http://192.168.31.187:8081/ '), - isTrue, - ); + expect(await ServerEndpoint.save(' http://192.168.31.187:8081/ '), isTrue); expect( await SharedPreferencesAsync().getString(ServerEndpoint.preferenceKey), 'http://192.168.31.187:8081',