refactor(config): 服务地址改为 dev/prod 配置文件与单个环境开关

启动时按环回、局域网、公网顺序探测地址,换网络后会静默切到别处,排查
困难。改为代码内一个布尔开关选择环境,地址分别放在 dev_config.dart 与
prod_config.dart,不再探测;调试条改地址后直接重载,不再重新探测。
release 构建若仍是开发开关会启动即报错,避免线上包连开发机。
This commit is contained in:
Zhou Weidong authored and trae-solo-agent committed 2026-10-29 10:44:50 +08:00
1 parent 57f84c45bd
commit a6d6b66d31
13 files changed
+101 -382

No files matched your search

+35 -150
View File
@@ -1,4 +1,4 @@
import 'dart:async';
// Author: fruit.com.cn
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -6,6 +6,8 @@ import 'package:shared_preferences_platform_interface/in_memory_shared_preferenc
import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart';
import 'package:fruitshorts/core/config/app_config.dart';
import 'package:fruitshorts/core/config/dev_config.dart';
import 'package:fruitshorts/core/config/prod_config.dart';
import 'package:fruitshorts/core/config/server_endpoint.dart';
void main() {
@@ -35,11 +37,10 @@ void main() {
expect(ServerEndpoint.normalize('http://'), isNull);
});
test('should_prioritize_runtime_over_dart_define_over_default', () {
test('should_prefer_valid_stored_url_over_build_default', () {
expect(
ServerEndpoint.resolve(
stored: ' http://runtime.test:8081/ ',
compiled: 'http://define.test:8081',
fallback: 'http://default.test:8081',
),
'http://runtime.test:8081',
@@ -47,173 +48,57 @@ void main() {
expect(
ServerEndpoint.resolve(
stored: 'ftp://invalid',
compiled: 'http://define.test:8081/',
fallback: 'http://default.test:8081',
fallback: 'http://default.test:8081/',
),
'http://define.test:8081',
'http://default.test:8081',
);
expect(
ServerEndpoint.resolve(
stored: null,
compiled: '',
fallback: 'http://default.test:8081/',
fallback: 'http://default.test:8081',
),
'http://default.test:8081',
);
});
test('should_use_the_configured_local_address_for_device_default', () {
final uri = Uri.parse(AppConfig.devHostBaseUrl);
expect(uri.scheme, 'http');
expect(uri.host, isNotEmpty);
expect(uri.port, 8081);
expect(uri.path, isEmpty);
test('should_pick_address_by_environment_switch', () {
// 仓库里的开关默认指向开发环境。
expect(AppConfig.useDevConfig, isTrue);
expect(AppConfig.baseUrl, DevConfig.baseUrl);
expect(AppConfig.apiBaseUrl, DevConfig.baseUrl);
expect(Uri.parse(AppConfig.baseUrl).port, 8081);
});
test('should_skip_probe_when_compiled_override_is_set', () async {
const compiled = 'http://compiled.test:8081';
var probeCalls = 0;
test('should_keep_dev_and_prod_addresses_in_separate_files', () {
final dev = Uri.parse(DevConfig.baseUrl);
final prod = Uri.parse(ProdConfig.baseUrl);
await AppConfig.probeAndApplyBaseUrl(
compiledOverride: compiled,
probe: (_) async {
probeCalls++;
return true;
},
);
expect(probeCalls, 0);
expect(AppConfig.apiBaseUrl, compiled);
expect(dev.scheme, 'http');
expect(dev.port, 8081);
expect(dev.path, isEmpty);
expect(prod.scheme, 'http');
expect(prod.port, 8081);
expect(prod.path, isEmpty);
expect(DevConfig.baseUrl, isNot(ProdConfig.baseUrl));
});
test('should_keep_runtime_endpoint_when_compiled_override_is_set', () async {
const runtime = 'http://runtime.test:8081';
const compiled = 'http://compiled.test:8081';
var probeCalls = 0;
AppConfig.applyRuntimeBaseUrl(runtime);
test('should_prefer_runtime_override_over_environment_switch', () {
AppConfig.applyRuntimeBaseUrl('http://runtime.test:8081');
await AppConfig.probeAndApplyBaseUrl(
compiledOverride: compiled,
probe: (_) async {
probeCalls++;
return true;
},
);
expect(probeCalls, 0);
expect(AppConfig.apiBaseUrl, runtime);
expect(AppConfig.apiBaseUrl, 'http://runtime.test:8081');
expect(AppConfig.baseUrl, DevConfig.baseUrl);
});
test('should_keep_probing_when_compiled_override_is_empty', () async {
final attempts = <String>[];
await AppConfig.probeAndApplyBaseUrl(
compiledOverride: '',
probe: (baseUrl) async {
attempts.add(baseUrl);
return baseUrl == AppConfig.publicBaseUrl;
},
test('should_append_host_to_relative_media_paths', () {
expect(AppConfig.mediaUrl(''), '');
expect(
AppConfig.mediaUrl('https://cdn.test/a.mp4'),
'https://cdn.test/a.mp4',
);
expect(attempts, [
AppConfig.lanBaseUrl,
AppConfig.devHostBaseUrl,
AppConfig.publicBaseUrl,
]);
expect(AppConfig.apiBaseUrl, AppConfig.publicBaseUrl);
});
test(
'should_probe_all_candidates_in_parallel_before_applying_priority',
() async {
final attempts = <String>[];
var active = 0;
var maxActive = 0;
final allStarted = Completer<void>();
final probing = AppConfig.probeAndApplyBaseUrl(
fallbackBaseUrl: AppConfig.devHostBaseUrl,
probe: (baseUrl) async {
attempts.add(baseUrl);
active++;
if (active > maxActive) maxActive = active;
if (active == 3) allStarted.complete();
await allStarted.future;
active--;
return baseUrl == AppConfig.lanBaseUrl ||
baseUrl == AppConfig.devHostBaseUrl;
},
);
await probing.timeout(const Duration(seconds: 1));
expect(attempts, [
AppConfig.lanBaseUrl,
AppConfig.devHostBaseUrl,
AppConfig.publicBaseUrl,
]);
expect(maxActive, 3);
expect(AppConfig.apiBaseUrl, AppConfig.lanBaseUrl);
},
);
test('should_probe_loopback_then_public_in_order', () async {
final attempts = <String>[];
await AppConfig.probeAndApplyBaseUrl(
fallbackBaseUrl: AppConfig.devHostBaseUrl,
probe: (baseUrl) async {
attempts.add(baseUrl);
return baseUrl == AppConfig.publicBaseUrl;
},
expect(
AppConfig.mediaUrl('/files/a.mp4'),
'${AppConfig.apiBaseUrl}/files/a.mp4',
);
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 = <String>[];
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_probe_explicit_runtime_endpoint_first', () async {
const custom = 'http://custom.test:8081';
final attempts = <String>[];
AppConfig.applyRuntimeBaseUrl(custom);
await AppConfig.probeAndApplyBaseUrl(
probe: (baseUrl) async {
attempts.add(baseUrl);
return baseUrl == custom;
},
);
expect(attempts, [
custom,
AppConfig.lanBaseUrl,
AppConfig.devHostBaseUrl,
AppConfig.publicBaseUrl,
]);
expect(AppConfig.apiBaseUrl, custom);
});
test('should_apply_stored_url_only_when_debug_is_enabled', () async {
+4 -4
View File
@@ -136,19 +136,19 @@ void main() {
});
test(
'should_use_probed_endpoint_when_client_was_created_before_probe',
'should_follow_runtime_endpoint_applied_after_client_creation',
() async {
const probedBaseUrl = 'http://probed.test:8081';
const runtimeBaseUrl = 'http://runtime.test:8081';
final adapter = FakeHttpAdapter(
(_) async => FakeHttpResponse(envelope(null)),
);
final client = ApiClient(adapter: adapter);
AppConfig.applyRuntimeBaseUrl(probedBaseUrl);
AppConfig.applyRuntimeBaseUrl(runtimeBaseUrl);
addTearDown(AppConfig.clearRuntimeBaseUrl);
await client.get('/app/home/feed');
expect(adapter.requests.single.uri.origin, probedBaseUrl);
expect(adapter.requests.single.uri.origin, runtimeBaseUrl);
},
);
}
+3 -27
View File
@@ -6,7 +6,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:fruitshorts/core/config/app_config.dart';
import 'package:fruitshorts/core/data/services/catalog_retry.dart';
import 'package:fruitshorts/core/theme/app_colors.dart';
import 'package:fruitshorts/core/widgets/catalog_endpoint_editor.dart';
import 'package:fruitshorts/l10n/app_localizations.dart';
@@ -55,20 +54,7 @@ void main() {
);
});
test('should_probe_before_reloading_catalog', () async {
final steps = <String>[];
await retryCatalogLoad(
probe: () async => steps.add('probe'),
reload: () async => steps.add('load'),
);
expect(steps, ['probe', 'load']);
});
testWidgets('should_save_probe_and_reload_after_valid_endpoint', (
tester,
) async {
testWidgets('should_save_and_reload_after_valid_endpoint', (tester) async {
AppConfig.applyRuntimeBaseUrl('http://127.0.0.1:8081');
final steps = <String>[];
await _pumpEndpointEditor(
@@ -77,7 +63,6 @@ void main() {
steps.add('save:$value');
return true;
},
probe: () async => steps.add('probe'),
reload: () async => steps.add('load'),
);
@@ -101,12 +86,11 @@ void main() {
await tester.tap(find.byKey(const Key('catalog-debug-endpoint-save')));
await tester.pumpAndSettle();
expect(steps, ['save:http://10.0.0.8:8081', 'probe', 'load']);
expect(steps, ['save:http://10.0.0.8:8081', 'load']);
});
testWidgets('should_reject_invalid_endpoint_without_saving', (tester) async {
var saves = 0;
var probes = 0;
var loads = 0;
await _pumpEndpointEditor(
tester,
@@ -114,7 +98,6 @@ void main() {
saves++;
return true;
},
probe: () async => probes++,
reload: () async => loads++,
);
@@ -134,7 +117,6 @@ void main() {
findsOneWidget,
);
expect(saves, 0);
expect(probes, 0);
expect(loads, 0);
});
}
@@ -142,7 +124,6 @@ void main() {
Future<void> _pumpEndpointEditor(
WidgetTester tester, {
required Future<bool> Function(String value) save,
required Future<void> Function() probe,
required Future<void> Function() reload,
}) async {
await tester.pumpWidget(
@@ -152,12 +133,7 @@ Future<void> _pumpEndpointEditor(
body: TextButton(
key: const Key('open-endpoint-editor'),
onPressed: () => unawaited(
showCatalogEndpointEditor(
context,
save: save,
probe: probe,
reload: reload,
),
showCatalogEndpointEditor(context, save: save, reload: reload),
),
child: const Text('open'),
),
+2 -3
View File
@@ -18,9 +18,8 @@ void main() {
);
expect(uri.isAbsolute, isTrue);
// Uri.parse().toString() 会把 host 规范成小写,而 devHostBaseUrl 里的
// Bonjour 名字带大写(jiangdeMacBook-Air.local)—— 比较时统一小写,
// 否则会依赖运行平台(flutter test 里 defaultTargetPlatform 默认是 android)
// Uri.parse().toString() 会把 host 规范成小写,而开发地址可能带大写
// (Bonjour 名字),比较时统一小写,否则会依赖运行平台。
expect(
uri.toString().toLowerCase(),
'${AppConfig.apiBaseUrl}/files/episode/202610/clip.mp4'.toLowerCase(),