178 lines
6.0 KiB
Dart
178 lines
6.0 KiB
Dart
// Author: fruit.com.cn
|
|
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
/// 客户端与后端约定的运行时配置。
|
|
///
|
|
/// 接口地址优先级:运行时设置 > `--dart-define=API_BASE_URL=...` > 平台默认值。
|
|
/// 开发机的局域网地址会随网络变化,换网络后改 [devHostBaseUrl] 即可。
|
|
class AppConfig {
|
|
const AppConfig._();
|
|
|
|
/// 真机默认使用的开发机局域网地址。
|
|
///
|
|
/// 这个地址跟着电脑所在的网络走,换网络后要用
|
|
/// `ipconfig getifaddr en0` 重新确认。默认值只改这一行。
|
|
static const String devHostBaseUrl = 'http://192.168.31.187:8081';
|
|
|
|
/// 本机后端地址:通过 `adb reverse tcp:8081 tcp:8081` 映射到开发机,
|
|
/// 探测可达时 App 直连本机服务;不可达时回落到公网地址。
|
|
static const String lanBaseUrl = 'http://127.0.0.1:8081';
|
|
|
|
/// 公网地址(远程/移动网络访问时使用)。
|
|
static const String publicBaseUrl = 'http://39.162.50.99:8081';
|
|
|
|
static const String _overrideBaseUrl = String.fromEnvironment('API_BASE_URL');
|
|
|
|
static String _runtimeBaseUrl = '';
|
|
|
|
static Future<void>? _ready;
|
|
|
|
/// 启动地址探测完成信号;默认已完成,避免影响既有测试与调用方。
|
|
static Future<void> get ready => _ready ?? Future<void>.value();
|
|
static set ready(Future<void> value) => _ready = value;
|
|
|
|
static const Duration _probeTimeout = Duration(seconds: 2);
|
|
static const List<String> _probeCandidates = [
|
|
lanBaseUrl,
|
|
devHostBaseUrl,
|
|
publicBaseUrl,
|
|
];
|
|
|
|
/// Build-time server URL supplied through `--dart-define=API_BASE_URL`.
|
|
static String get compiledBaseUrl => _overrideBaseUrl;
|
|
|
|
/// Existing platform-aware fallback used when no runtime or build override exists.
|
|
static String get defaultBaseUrl => _defaultBaseUrl;
|
|
|
|
/// Effective server URL, including the debug-only runtime override.
|
|
static String get apiBaseUrl => _runtimeBaseUrl.isNotEmpty
|
|
? _runtimeBaseUrl
|
|
: (compiledBaseUrl.isNotEmpty ? compiledBaseUrl : defaultBaseUrl);
|
|
|
|
static final String _defaultBaseUrl = _runsOnDevMachine
|
|
? lanBaseUrl
|
|
: devHostBaseUrl;
|
|
|
|
/// 模拟器、桌面端与开发机同机,环回地址可用;iOS 真机不满足。
|
|
/// Android 没有等价的探测方式,统一按真机处理(模拟器用
|
|
/// `--dart-define=API_BASE_URL=http://10.0.2.2:8081` 覆盖)。
|
|
static final bool _runsOnDevMachine =
|
|
kIsWeb ||
|
|
defaultTargetPlatform == TargetPlatform.macOS ||
|
|
defaultTargetPlatform == TargetPlatform.windows ||
|
|
defaultTargetPlatform == TargetPlatform.linux ||
|
|
(defaultTargetPlatform == TargetPlatform.iOS &&
|
|
Platform.environment.containsKey('SIMULATOR_DEVICE_NAME'));
|
|
|
|
/// Applies an already normalized runtime URL. Passing null clears it.
|
|
static void applyRuntimeBaseUrl(String? value) {
|
|
_runtimeBaseUrl = value ?? '';
|
|
}
|
|
|
|
/// Clears the in-memory runtime override, mainly for tests and restarts.
|
|
static void clearRuntimeBaseUrl() {
|
|
_runtimeBaseUrl = '';
|
|
}
|
|
|
|
static const Duration requestTimeout = Duration(seconds: 8);
|
|
|
|
/// 按环回、开发机、公网顺序探测,写入首个返回 200 的地址。
|
|
///
|
|
/// 全部不可达时使用平台默认地址,真机保留开发机地址,避免退回公网。
|
|
static Future<void> probeAndApplyBaseUrl({
|
|
Future<bool> Function(String baseUrl)? probe,
|
|
String? fallbackBaseUrl,
|
|
|
|
/// 编译期地址测试注入;未传时读取 `API_BASE_URL`。
|
|
String? compiledOverride,
|
|
}) async {
|
|
final compiled = compiledOverride ?? compiledBaseUrl;
|
|
if (compiled.isNotEmpty) {
|
|
if (_runtimeBaseUrl.isEmpty) applyRuntimeBaseUrl(compiled);
|
|
return;
|
|
}
|
|
final candidates = <String>[
|
|
if (_runtimeBaseUrl.isNotEmpty) _runtimeBaseUrl,
|
|
for (final candidate in _probeCandidates)
|
|
if (candidate != _runtimeBaseUrl) candidate,
|
|
];
|
|
final results = await Future.wait([
|
|
for (final candidate in candidates)
|
|
probe == null
|
|
? _probeBaseUrl(candidate)
|
|
: _runInjectedProbe(candidate, probe),
|
|
]);
|
|
for (var index = 0; index < candidates.length; index++) {
|
|
final candidate = candidates[index];
|
|
final result = results[index];
|
|
if (kDebugMode) {
|
|
final status = result.ok
|
|
? 'ok'
|
|
: 'fail(${result.reason ?? 'request failed'})';
|
|
debugPrint('[probe] $candidate -> $status');
|
|
}
|
|
if (result.ok) {
|
|
applyRuntimeBaseUrl(candidate);
|
|
return;
|
|
}
|
|
}
|
|
applyRuntimeBaseUrl(fallbackBaseUrl ?? defaultBaseUrl);
|
|
}
|
|
|
|
static Future<_ProbeResult> _runInjectedProbe(
|
|
String baseUrl,
|
|
Future<bool> Function(String baseUrl) probe,
|
|
) async {
|
|
try {
|
|
return _ProbeResult(ok: await probe(baseUrl));
|
|
} catch (error) {
|
|
return _ProbeResult(ok: false, reason: error.toString());
|
|
}
|
|
}
|
|
|
|
static Future<_ProbeResult> _probeBaseUrl(String baseUrl) async {
|
|
final client = HttpClient()..connectionTimeout = _probeTimeout;
|
|
try {
|
|
return await _requestCategories(
|
|
client,
|
|
Uri.parse('$baseUrl/app/categories'),
|
|
).timeout(_probeTimeout);
|
|
} catch (error) {
|
|
return _ProbeResult(ok: false, reason: error.toString());
|
|
} finally {
|
|
client.close(force: true);
|
|
}
|
|
}
|
|
|
|
static Future<_ProbeResult> _requestCategories(
|
|
HttpClient client,
|
|
Uri uri,
|
|
) async {
|
|
final request = await client.getUrl(uri);
|
|
final response = await request.close();
|
|
await response.drain<void>();
|
|
final ok = response.statusCode == 200;
|
|
return _ProbeResult(
|
|
ok: ok,
|
|
reason: ok ? null : 'HTTP ${response.statusCode}',
|
|
);
|
|
}
|
|
|
|
/// 后端返回的是相对路径(/files/...),这里补上服务地址。
|
|
static String mediaUrl(String path) {
|
|
if (path.isEmpty) return '';
|
|
if (path.startsWith('http://') || path.startsWith('https://')) return path;
|
|
return '$apiBaseUrl$path';
|
|
}
|
|
}
|
|
|
|
class _ProbeResult {
|
|
const _ProbeResult({required this.ok, this.reason});
|
|
|
|
final bool ok;
|
|
final String? reason;
|
|
}
|