1313import java .net .URL ;
1414import java .nio .charset .StandardCharsets ;
1515import java .util .Scanner ;
16+ import java .util .concurrent .CompletableFuture ;
1617
1718/**
1819 * 版本检查器 - 负责检查远程版本信息
1920 */
2021@ Slf4j
2122public class VersionChecker {
2223
24+ private static final String GITHUB_API_URL = "https://api.github.com/repos/lakernote/easy-postman/releases/latest" ;
2325 private static final String GITEE_API_URL = "https://gitee.com/api/v5/repos/lakernote/easy-postman/releases/latest" ;
2426
27+ private static final String SOURCE_GITHUB = "GitHub" ;
28+ private static final String SOURCE_GITEE = "Gitee" ;
29+ private static final String SOURCE_KEY = "_source" ;
30+ private static final String BROWSER_DOWNLOAD_URL = "browser_download_url" ;
31+ private static final String MACOS_ARM64_DMG = "-macos-arm64.dmg" ;
32+ private static final String PORTABLE_ZIP = "-portable.zip" ;
33+
34+ // 缓存最佳源,避免重复检测
35+ private static volatile String cachedBestSource = null ;
36+
2537 /**
2638 * 检查更新信息
2739 */
2840 public UpdateInfo checkForUpdate () {
2941 try {
3042 String currentVersion = SystemUtil .getCurrentVersion ();
31- JSONObject releaseInfo = fetchLatestReleaseInfo ();
43+
44+ // 并行检测最快的源
45+ String bestSource = detectBestSource ();
46+ JSONObject releaseInfo = fetchLatestReleaseInfo (bestSource );
47+
48+ if (releaseInfo == null ) {
49+ // 如果最佳源失败,尝试备用源
50+ String fallbackSource = bestSource .equals (GITHUB_API_URL ) ? GITEE_API_URL : GITHUB_API_URL ;
51+ log .info ("Primary source failed, trying fallback source" );
52+ releaseInfo = fetchLatestReleaseInfo (fallbackSource );
53+
54+ if (releaseInfo != null ) {
55+ cachedBestSource = fallbackSource ; // 更新缓存
56+ }
57+ }
3258
3359 if (releaseInfo == null ) {
3460 return UpdateInfo .noUpdateAvailable (I18nUtil .getMessage (MessageKeys .UPDATE_FETCH_RELEASE_FAILED ));
3561 }
3662
3763 String latestVersion = releaseInfo .getStr ("tag_name" );
38- log .info ("Current version: {}, Gitee latest version: {}" , currentVersion , latestVersion );
64+ String sourceName = releaseInfo .getStr (SOURCE_KEY , "unknown" );
65+ log .info ("Current version: {}, Latest version: {} (from {})" , currentVersion , latestVersion , sourceName );
66+
3967 if (latestVersion == null ) {
4068 return UpdateInfo .noUpdateAvailable (I18nUtil .getMessage (MessageKeys .UPDATE_NO_VERSION_INFO ));
4169 }
@@ -52,12 +80,93 @@ public UpdateInfo checkForUpdate() {
5280 }
5381 }
5482
83+ /**
84+ * 检测最佳源(GitHub 或 Gitee)
85+ * 策略:并行测试连接速度,选择响应最快的源
86+ */
87+ private static String detectBestSource () {
88+ // 如果已有缓存,直接使用
89+ if (cachedBestSource != null ) {
90+ log .info ("Using cached best source: {}" , cachedBestSource .contains ("github" ) ? SOURCE_GITHUB : SOURCE_GITEE );
91+ return cachedBestSource ;
92+ }
93+
94+ log .info ("Detecting best update source..." );
95+
96+ // 并行测试两个源的连接速度
97+ CompletableFuture <Long > githubTest = CompletableFuture .supplyAsync (() -> testSourceSpeed (GITHUB_API_URL , SOURCE_GITHUB ));
98+ CompletableFuture <Long > giteeTest = CompletableFuture .supplyAsync (() -> testSourceSpeed (GITEE_API_URL , SOURCE_GITEE ));
99+
100+ try {
101+ // 等待两个测试完成,最多等待 5 秒
102+ CompletableFuture .allOf (githubTest , giteeTest ).get (5 , java .util .concurrent .TimeUnit .SECONDS );
103+
104+ long githubTime = githubTest .getNow (Long .MAX_VALUE );
105+ long giteeTime = giteeTest .getNow (Long .MAX_VALUE );
106+
107+ // 选择响应时间更短的源
108+ if (githubTime < giteeTime && githubTime < 5000 ) {
109+ log .info ("GitHub is faster ({} ms vs {} ms), using GitHub" , githubTime , giteeTime );
110+ cachedBestSource = GITHUB_API_URL ;
111+ return GITHUB_API_URL ;
112+ } else if (giteeTime < Long .MAX_VALUE ) {
113+ log .info ("Gitee is faster or preferred ({} ms vs {} ms), using Gitee" , giteeTime , githubTime );
114+ cachedBestSource = GITEE_API_URL ;
115+ return GITEE_API_URL ;
116+ }
117+
118+ } catch (java .util .concurrent .TimeoutException e ) {
119+ log .debug ("Source detection timeout: {}" , e .getMessage ());
120+ Thread .currentThread ().interrupt ();
121+ } catch (java .util .concurrent .ExecutionException e ) {
122+ log .debug ("Source detection execution error: {}" , e .getMessage ());
123+ } catch (InterruptedException e ) {
124+ log .debug ("Source detection interrupted: {}" , e .getMessage ());
125+ Thread .currentThread ().interrupt ();
126+ }
127+
128+ // 默认使用 Gitee(国内用户居多)
129+ log .info ("Using default source: Gitee" );
130+ cachedBestSource = GITEE_API_URL ;
131+ return GITEE_API_URL ;
132+ }
133+
134+ /**
135+ * 测试源的连接速度(返回响应时间,单位:毫秒)
136+ */
137+ private static long testSourceSpeed (String apiUrl , String sourceName ) {
138+ long startTime = System .currentTimeMillis ();
139+ try {
140+ URL url = new URL (apiUrl );
141+ HttpURLConnection conn = (HttpURLConnection ) url .openConnection ();
142+ conn .setConnectTimeout (3000 );
143+ conn .setReadTimeout (3000 );
144+ conn .setRequestMethod ("HEAD" ); // 只测试连接,不下载内容
145+ conn .setRequestProperty ("User-Agent" , "Mozilla/5.0 (compatible; EasyPostman/" + SystemUtil .getCurrentVersion () + ")" );
146+
147+ int code = conn .getResponseCode ();
148+ long responseTime = System .currentTimeMillis () - startTime ;
149+
150+ if (code == 200 || code == 301 || code == 302 ) {
151+ log .debug ("{} connection test successful: {} ms" , sourceName , responseTime );
152+ return responseTime ;
153+ } else {
154+ log .debug ("{} connection test failed with code: {}" , sourceName , code );
155+ return Long .MAX_VALUE ;
156+ }
157+ } catch (Exception e ) {
158+ log .debug ("{} connection test failed: {}" , sourceName , e .getMessage ());
159+ return Long .MAX_VALUE ;
160+ }
161+ }
162+
55163 /**
56164 * 获取最新版本发布信息
57165 */
58- private JSONObject fetchLatestReleaseInfo () {
166+ private JSONObject fetchLatestReleaseInfo (String apiUrl ) {
167+ String sourceName = apiUrl .contains ("github" ) ? SOURCE_GITHUB : SOURCE_GITEE ;
59168 try {
60- URL url = new URL (GITEE_API_URL );
169+ URL url = new URL (apiUrl );
61170 HttpURLConnection conn = (HttpURLConnection ) url .openConnection ();
62171 conn .setConnectTimeout (10000 );
63172 conn .setReadTimeout (10000 );
@@ -75,7 +184,11 @@ private JSONObject fetchLatestReleaseInfo() {
75184 try (InputStream is = conn .getInputStream ();
76185 Scanner scanner = new Scanner (is , StandardCharsets .UTF_8 )) {
77186 String json = scanner .useDelimiter ("\\ A" ).next ();
78- return new JSONObject (json );
187+ JSONObject releaseInfo = new JSONObject (json );
188+ // 标记数据来源
189+ releaseInfo .set (SOURCE_KEY , sourceName );
190+ log .info ("Successfully fetched release info from {}" , sourceName );
191+ return releaseInfo ;
79192 }
80193 } else {
81194 // 尝试读取错误响应
@@ -90,12 +203,12 @@ private JSONObject fetchLatestReleaseInfo() {
90203 // 忽略读取错误响应的异常
91204 }
92205
93- log .warn ("Failed to fetch release info, HTTP code: {}, response: {}" , code , errorResponse );
206+ log .warn ("Failed to fetch release info from {} , HTTP code: {}, response: {}" , sourceName , code , errorResponse );
94207
95208 return null ;
96209 }
97210 } catch (Exception e ) {
98- log .debug ("Error fetching release info: {}" , e .getMessage ());
211+ log .debug ("Error fetching release info from {} : {}" , sourceName , e .getMessage ());
99212 }
100213 return null ;
101214 }
@@ -115,19 +228,27 @@ public String getDownloadUrl(JSONObject releaseInfo) {
115228 }
116229
117230 String osName = System .getProperty ("os.name" ).toLowerCase ();
231+ String source = releaseInfo .getStr (SOURCE_KEY , "unknown" );
118232
233+ String downloadUrl ;
119234 if (osName .contains ("win" )) {
120235 // Windows 特殊处理:区分便携版和安装版
121- return getWindowsDownloadUrl (assets );
236+ downloadUrl = getWindowsDownloadUrl (assets );
122237 } else if (osName .contains ("mac" )) {
123238 // macOS 特殊处理:支持新版本的架构特定 DMG 和旧版本的通用 DMG
124- return getMacDownloadUrl (assets );
239+ downloadUrl = getMacDownloadUrl (assets );
125240 } else if (osName .contains ("linux" )) {
126- return findAssetByExtension (assets , ".deb" );
241+ downloadUrl = findAssetByExtension (assets , ".deb" );
127242 } else {
128243 log .warn ("Unsupported OS: {}" , osName );
129244 return null ;
130245 }
246+
247+ if (downloadUrl != null ) {
248+ log .info ("Found download URL from {}: {}" , source , downloadUrl );
249+ }
250+
251+ return downloadUrl ;
131252 }
132253
133254 /**
@@ -139,8 +260,8 @@ private String getWindowsDownloadUrl(JSONArray assets) {
139260
140261 if (isPortable ) {
141262 // 便携版:优先下载便携版 ZIP
142- log .info ("Detected portable version, looking for - portable.zip" );
143- String portableUrl = findAssetByPattern (assets , "-portable.zip" );
263+ log .info ("Detected portable version, looking for portable.zip" );
264+ String portableUrl = findAssetByPattern (assets , PORTABLE_ZIP );
144265
145266 if (portableUrl != null ) {
146267 log .info ("Found portable ZIP for update" );
@@ -225,7 +346,7 @@ private String findAssetByPattern(JSONArray assets, String pattern) {
225346 JSONObject asset = assets .getJSONObject (i );
226347 String name = asset .getStr ("name" );
227348 if (name != null && name .contains (pattern )) {
228- String url = asset .getStr ("browser_download_url" );
349+ String url = asset .getStr (BROWSER_DOWNLOAD_URL );
229350 log .debug ("Found asset by pattern '{}': {} -> {}" , pattern , name , url );
230351 return url ;
231352 }
@@ -293,7 +414,7 @@ private String findAssetByExtension(JSONArray assets, String extension) {
293414 JSONObject asset = assets .getJSONObject (i );
294415 String name = asset .getStr ("name" );
295416 if (name != null && name .endsWith (extension )) {
296- String url = asset .getStr ("browser_download_url" );
417+ String url = asset .getStr (BROWSER_DOWNLOAD_URL );
297418 log .debug ("Found asset: {} -> {}" , name , url );
298419 return url ;
299420 }
@@ -310,18 +431,14 @@ private String findGenericDmg(JSONArray assets) {
310431 for (int i = 0 ; i < assets .size (); i ++) {
311432 JSONObject asset = assets .getJSONObject (i );
312433 String name = asset .getStr ("name" );
313- if (name != null && name .endsWith (".dmg" )) {
314- // 排除带有架构后缀的 DMG(支持新旧两种命名格式)
315- // 新格式: -macos-x86_64.dmg, -macos-arm64.dmg
316- // 旧格式: -intel.dmg, -arm64.dmg
317- if (!name .endsWith ("-intel.dmg" ) &&
434+ if (name != null && name .endsWith (".dmg" ) &&
435+ !name .endsWith ("-intel.dmg" ) &&
318436 !name .endsWith ("-arm64.dmg" ) &&
319437 !name .endsWith ("-macos-x86_64.dmg" ) &&
320- !name .endsWith ("-macos-arm64.dmg" )) {
321- String url = asset .getStr ("browser_download_url" );
322- log .debug ("Found generic DMG (without architecture suffix): {} -> {}" , name , url );
323- return url ;
324- }
438+ !name .endsWith (MACOS_ARM64_DMG )) {
439+ String url = asset .getStr (BROWSER_DOWNLOAD_URL );
440+ log .debug ("Found generic DMG (without architecture suffix): {} -> {}" , name , url );
441+ return url ;
325442 }
326443 }
327444 return null ;
@@ -341,7 +458,7 @@ private String getMacPackageSuffix() {
341458 // 检测 Apple Silicon (ARM64)
342459 if (arch .contains ("aarch64" ) || arch .equals ("arm64" )) {
343460 log .info ("Detected Apple Silicon (ARM64), using -macos-arm64.dmg" );
344- return "-macos-arm64.dmg" ;
461+ return MACOS_ARM64_DMG ;
345462 }
346463
347464 // 检测 Intel (x86_64)
@@ -356,7 +473,7 @@ private String getMacPackageSuffix() {
356473
357474 // 默认返回 ARM64 版本(因为新 Mac 都是 Apple Silicon)
358475 log .info ("Unable to detect architecture, defaulting to -macos-arm64.dmg" );
359- return "-macos-arm64.dmg" ;
476+ return MACOS_ARM64_DMG ;
360477 }
361478
362479
@@ -385,7 +502,7 @@ private int compareVersion(String v1, String v2) {
385502
386503 private int parseIntSafe (String s ) {
387504 try {
388- return Integer .parseInt (s .replaceAll ("[^0-9] " , "" ));
505+ return Integer .parseInt (s .replaceAll ("\\ D " , "" ));
389506 } catch (Exception e ) {
390507 return 0 ;
391508 }
0 commit comments