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',