现象:CmsSiteDirectoryImpl 两处 catch 只打 e.getMessage(),而平台 SDK 统一把 传输层失败包成 "OpenAPI request failed: ioError",底层 ConnectException / SocketTimeoutException / SSLException 全部丢失,日志分不清"对端没起"还是"读超时"。 改动: - 新增 rootCauseBrief():沿 cause 链取最内层原因追加为"(根因 X: msg)"; 无 cause 时不追加,避免与 e.getMessage() 重复。 - resolveSite / listSites 两处 catch 日志接入该方法。 契约不变:路径 /oapi/site/list、per-target basePath /cms、fail-closed 语义均未动。 测试:CmsSiteDirectoryImplTest 新增 1 例断言摘要形态;mvn test 82/82 绿。
This commit is contained in:
@@ -65,7 +65,7 @@ public class CmsSiteDirectoryImpl implements SiteDirectory {
|
|||||||
// 由 CmsSiteDirectoryImplTest 冻结)。仅用于 siteCode 反解,权限判定仍走 requireSchoolMember。
|
// 由 CmsSiteDirectoryImplTest 冻结)。仅用于 siteCode 反解,权限判定仍走 requireSchoolMember。
|
||||||
sites = fetchSites(null);
|
sites = fetchSites(null);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("[svc-hall] CMS 站点目录不可达:{}", e.getMessage());
|
log.warn("[svc-hall] CMS 站点目录不可达:{}{}", e.getMessage(), rootCauseBrief(e));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (sites != null) {
|
if (sites != null) {
|
||||||
@@ -93,7 +93,8 @@ public class CmsSiteDirectoryImpl implements SiteDirectory {
|
|||||||
try {
|
try {
|
||||||
return fetchSites(schId);
|
return fetchSites(schId);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("[svc-hall] CMS 站点列表获取失败(schId={}):{}", schId, e.getMessage());
|
log.warn("[svc-hall] CMS 站点列表获取失败(schId={}):{}{}",
|
||||||
|
schId, e.getMessage(), rootCauseBrief(e));
|
||||||
throw new BizException(ErrorCode.PARAM_ERROR, "站点服务不可达,请稍后重试");
|
throw new BizException(ErrorCode.PARAM_ERROR, "站点服务不可达,请稍后重试");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -124,6 +125,20 @@ public class CmsSiteDirectoryImpl implements SiteDirectory {
|
|||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最内层原因摘要。SDK 的 "OpenAPI request failed: ioError" 只说明请求没拿到响应,
|
||||||
|
* 真正的原因是 cause 链末端的 ConnectException/SocketTimeoutException/SSLException;
|
||||||
|
* 不补这一段,日志里永远分不清"对端没起"和"读超时"。
|
||||||
|
* 包可见:契约测试直接断言摘要形态。
|
||||||
|
*/
|
||||||
|
static String rootCauseBrief(Throwable e) {
|
||||||
|
Throwable cur = e;
|
||||||
|
while (cur.getCause() != null && cur.getCause() != cur) {
|
||||||
|
cur = cur.getCause();
|
||||||
|
}
|
||||||
|
return cur == e ? "" : "(根因 " + cur.getClass().getSimpleName() + ": " + cur.getMessage() + ")";
|
||||||
|
}
|
||||||
|
|
||||||
/** resolveSite 的全量缓存键(与真实 schId 不冲突的哨兵) */
|
/** resolveSite 的全量缓存键(与真实 schId 不冲突的哨兵) */
|
||||||
private static final String ALL_SITES_KEY = "-1";
|
private static final String ALL_SITES_KEY = "-1";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,4 +123,20 @@ class CmsSiteDirectoryImplTest {
|
|||||||
void listSites_rejectsNullSchId() {
|
void listSites_rejectsNullSchId() {
|
||||||
assertThrows(BizException.class, () -> directory.listSites(null));
|
assertThrows(BizException.class, () -> directory.listSites(null));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("rootCauseBrief:ioError 包装层要透出最内层原因(区分\"对端没起\"与\"读超时\")")
|
||||||
|
void rootCauseBrief_exposesInnermostCause() {
|
||||||
|
// 无 cause:不追加,避免与 e.getMessage() 重复
|
||||||
|
assertEquals("", CmsSiteDirectoryImpl.rootCauseBrief(
|
||||||
|
new RuntimeException("OpenAPI request failed: ioError")));
|
||||||
|
|
||||||
|
// 实际形态:OpenApiException(ioError) ← ResourceAccessException ← ConnectException
|
||||||
|
java.net.ConnectException refused = new java.net.ConnectException("Connection refused: connect");
|
||||||
|
Exception wrapped = new RuntimeException("OpenAPI request failed: ioError",
|
||||||
|
new RuntimeException("I/O error on GET request", refused));
|
||||||
|
String brief = CmsSiteDirectoryImpl.rootCauseBrief(wrapped);
|
||||||
|
assertTrue(brief.contains("ConnectException"), brief);
|
||||||
|
assertTrue(brief.contains("Connection refused: connect"), brief);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user