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'; /// 办公室内网(Wi-Fi)直连地址,双网络自动切换时的首选。 static const String lanBaseUrl = 'http://192.168.31.250:8081'; /// 公网地址(远程/移动网络访问时使用)。 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 = ''; /// 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 ? _loopbackBaseUrl : 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); /// 探测内网地址是否可达,可达则切换为内网直连,否则回落到公网地址。 /// /// 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); return; } } catch (_) { // 内网不可达,回落到公网。 } applyRuntimeBaseUrl( compiledBaseUrl.isNotEmpty ? compiledBaseUrl : publicBaseUrl, ); } /// 后端返回的是相对路径(/files/...),这里补上服务地址。 static String mediaUrl(String path) { if (path.isEmpty) return ''; if (path.startsWith('http://') || path.startsWith('https://')) return path; return '$apiBaseUrl$path'; } }