|
@@ -4,11 +4,16 @@ import com.fasterxml.jackson.databind.JsonNode;
|
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
import com.usky.issue.domain.IssueCloudConfig;
|
|
import com.usky.issue.domain.IssueCloudConfig;
|
|
|
import com.usky.issue.service.constant.CloudIntegrationConstants;
|
|
import com.usky.issue.service.constant.CloudIntegrationConstants;
|
|
|
|
|
+import com.usky.issue.service.support.CloudEntitySupport;
|
|
|
import com.usky.issue.service.util.AesGcmCipher;
|
|
import com.usky.issue.service.util.AesGcmCipher;
|
|
|
|
|
+import com.usky.issue.service.util.CredentialMaskUtil;
|
|
|
import com.usky.issue.service.util.HmacSignUtil;
|
|
import com.usky.issue.service.util.HmacSignUtil;
|
|
|
import com.usky.issue.service.vo.CloudConnectionTestResult;
|
|
import com.usky.issue.service.vo.CloudConnectionTestResult;
|
|
|
|
|
+import com.usky.issue.service.vo.CloudPollResult;
|
|
|
import com.usky.issue.service.vo.SyncPacket;
|
|
import com.usky.issue.service.vo.SyncPacket;
|
|
|
|
|
+import com.usky.issue.service.vo.SyncPollRequest;
|
|
|
import com.usky.issue.service.vo.SyncResponse;
|
|
import com.usky.issue.service.vo.SyncResponse;
|
|
|
|
|
+import com.usky.issue.service.util.ApiResultParser;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
import org.springframework.boot.web.client.RestTemplateBuilder;
|
|
import org.springframework.boot.web.client.RestTemplateBuilder;
|
|
|
import org.springframework.http.HttpEntity;
|
|
import org.springframework.http.HttpEntity;
|
|
@@ -20,6 +25,7 @@ import org.springframework.stereotype.Component;
|
|
|
import org.springframework.util.StringUtils;
|
|
import org.springframework.util.StringUtils;
|
|
|
import org.springframework.web.client.HttpClientErrorException;
|
|
import org.springframework.web.client.HttpClientErrorException;
|
|
|
import org.springframework.web.client.HttpServerErrorException;
|
|
import org.springframework.web.client.HttpServerErrorException;
|
|
|
|
|
+import org.springframework.web.client.HttpStatusCodeException;
|
|
|
import org.springframework.web.client.ResourceAccessException;
|
|
import org.springframework.web.client.ResourceAccessException;
|
|
|
import org.springframework.web.client.RestTemplate;
|
|
import org.springframework.web.client.RestTemplate;
|
|
|
import org.springframework.web.util.UriComponentsBuilder;
|
|
import org.springframework.web.util.UriComponentsBuilder;
|
|
@@ -64,11 +70,14 @@ public class CloudPlatformClient {
|
|
|
|
|
|
|
|
String testUrl = buildTestConnectionUrl(cloudAddress);
|
|
String testUrl = buildTestConnectionUrl(cloudAddress);
|
|
|
long start = System.currentTimeMillis();
|
|
long start = System.currentTimeMillis();
|
|
|
|
|
+ log.info("[连接测试-HTTP] 准备请求 url={}, tenantId={}, credentialKey={}, hasToken={}",
|
|
|
|
|
+ testUrl, tenantId, CredentialMaskUtil.mask(credentialKey), StringUtils.hasText(token));
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
HttpHeaders headers = buildAuthHeaders(tenantId, credentialKey, token);
|
|
HttpHeaders headers = buildAuthHeaders(tenantId, credentialKey, token);
|
|
|
ResponseEntity<String> response = restTemplate.exchange(
|
|
ResponseEntity<String> response = restTemplate.exchange(
|
|
|
testUrl, HttpMethod.POST, new HttpEntity<>(headers), String.class);
|
|
testUrl, HttpMethod.POST, new HttpEntity<>(headers), String.class);
|
|
|
|
|
+ log.info("[连接测试-HTTP] 响应 status={}, body={}", response.getStatusCodeValue(), response.getBody());
|
|
|
return parseConnectionTestResult(response.getBody(), System.currentTimeMillis() - start);
|
|
return parseConnectionTestResult(response.getBody(), System.currentTimeMillis() - start);
|
|
|
} catch (ResourceAccessException ex) {
|
|
} catch (ResourceAccessException ex) {
|
|
|
log.error("云平台网络不可达: url={}, error={}", testUrl, ex.getMessage());
|
|
log.error("云平台网络不可达: url={}, error={}", testUrl, ex.getMessage());
|
|
@@ -97,7 +106,8 @@ public class CloudPlatformClient {
|
|
|
}
|
|
}
|
|
|
try {
|
|
try {
|
|
|
String credential = aesGcmCipher.decrypt(config.getCredentialKey());
|
|
String credential = aesGcmCipher.decrypt(config.getCredentialKey());
|
|
|
- return testConnection(config.getCloudAddress(), config.getTenantId(), credential, config.getToken());
|
|
|
|
|
|
|
+ String token = CloudEntitySupport.resolveAccessToken(config);
|
|
|
|
|
+ return testConnection(config.getCloudAddress(), config.getTenantId(), credential, token);
|
|
|
} catch (Exception ex) {
|
|
} catch (Exception ex) {
|
|
|
log.error("凭证解密失败 tenantId={}", config.getTenantId(), ex);
|
|
log.error("凭证解密失败 tenantId={}", config.getTenantId(), ex);
|
|
|
return CloudConnectionTestResult.failure(false, "凭证解密失败");
|
|
return CloudConnectionTestResult.failure(false, "凭证解密失败");
|
|
@@ -124,31 +134,64 @@ public class CloudPlatformClient {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- public SyncPacket pollCloud(Integer tenantId, String tableName, Long lastVersion, IssueCloudConfig config) {
|
|
|
|
|
|
|
+ public CloudPollResult pollCloud(Integer tenantId, String tableName, Long lastVersion,
|
|
|
|
|
+ IssueCloudConfig config, boolean forceBackfill) {
|
|
|
try {
|
|
try {
|
|
|
String credential = aesGcmCipher.decrypt(config.getCredentialKey());
|
|
String credential = aesGcmCipher.decrypt(config.getCredentialKey());
|
|
|
HttpHeaders headers = buildAuthHeaders(config, credential);
|
|
HttpHeaders headers = buildAuthHeaders(config, credential);
|
|
|
- String url = UriComponentsBuilder
|
|
|
|
|
- .fromHttpUrl(resolveIntegrationBaseUrl(config) + CloudIntegrationConstants.PATH_SYNC_POLL)
|
|
|
|
|
- .queryParam("tenantId", tenantId)
|
|
|
|
|
- .queryParam("tableName", tableName)
|
|
|
|
|
- .queryParam("lastVersion", lastVersion == null ? 0L : lastVersion)
|
|
|
|
|
- .toUriString();
|
|
|
|
|
|
|
+ headers.setContentType(MediaType.APPLICATION_JSON);
|
|
|
|
|
+
|
|
|
|
|
+ SyncPollRequest pollRequest = new SyncPollRequest();
|
|
|
|
|
+ pollRequest.setTenantId(String.valueOf(tenantId));
|
|
|
|
|
+ pollRequest.setTableName(tableName);
|
|
|
|
|
+ pollRequest.setLastVersion(lastVersion == null ? 0L : lastVersion);
|
|
|
|
|
+ pollRequest.setForceBackfill(forceBackfill);
|
|
|
|
|
+ String requestBody = OBJECT_MAPPER.writeValueAsString(pollRequest);
|
|
|
|
|
+
|
|
|
|
|
+ String url = resolveIntegrationBaseUrl(config) + CloudIntegrationConstants.PATH_SYNC_POLL;
|
|
|
|
|
+ log.info("[pollCloud] POST url={}, tenantId={}, tableName={}, lastVersion={}, forceBackfill={}, hasToken={}",
|
|
|
|
|
+ url, tenantId, tableName, lastVersion, forceBackfill,
|
|
|
|
|
+ StringUtils.hasText(CloudEntitySupport.resolveAccessToken(config)));
|
|
|
|
|
|
|
|
ResponseEntity<String> response = restTemplate.exchange(
|
|
ResponseEntity<String> response = restTemplate.exchange(
|
|
|
- url, HttpMethod.GET, new HttpEntity<>(headers), String.class);
|
|
|
|
|
- if (!StringUtils.hasText(response.getBody())) {
|
|
|
|
|
- return null;
|
|
|
|
|
|
|
+ url, HttpMethod.POST, new HttpEntity<>(requestBody, headers), String.class);
|
|
|
|
|
+ return parsePollResponse(response.getBody());
|
|
|
|
|
+ } catch (HttpStatusCodeException ex) {
|
|
|
|
|
+ log.warn("[pollCloud] HTTP {} tenantId={}, body={}",
|
|
|
|
|
+ ex.getStatusCode(), tenantId, ex.getResponseBodyAsString());
|
|
|
|
|
+ return parsePollResponse(ex.getResponseBodyAsString());
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("[pollCloud] 轮询云端失败 tenantId={}, tableName={}", tenantId, tableName, e);
|
|
|
|
|
+ return CloudPollResult.builder().requestFailed(true).errorMessage(e.getMessage()).build();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private CloudPollResult parsePollResponse(String body) {
|
|
|
|
|
+ if (!StringUtils.hasText(body)) {
|
|
|
|
|
+ return CloudPollResult.builder().requestFailed(true).errorMessage("云端返回空响应").build();
|
|
|
|
|
+ }
|
|
|
|
|
+ log.info("[pollCloud] 响应 body={}", body);
|
|
|
|
|
+ try {
|
|
|
|
|
+ JsonNode json = OBJECT_MAPPER.readTree(body);
|
|
|
|
|
+ if (!ApiResultParser.isSuccess(json)) {
|
|
|
|
|
+ String msg = ApiResultParser.resolveMessage(json);
|
|
|
|
|
+ log.warn("[pollCloud] 业务失败 msg={}", msg);
|
|
|
|
|
+ boolean authFailed = msg != null && (msg.contains("认证") || msg.contains("令牌"));
|
|
|
|
|
+ return CloudPollResult.builder()
|
|
|
|
|
+ .authFailed(authFailed)
|
|
|
|
|
+ .requestFailed(true)
|
|
|
|
|
+ .errorMessage(StringUtils.hasText(msg) ? msg : "poll 调用失败")
|
|
|
|
|
+ .build();
|
|
|
}
|
|
}
|
|
|
- JsonNode json = OBJECT_MAPPER.readTree(response.getBody());
|
|
|
|
|
- JsonNode dataNode = json.has("data") ? json.get("data") : json;
|
|
|
|
|
|
|
+ JsonNode dataNode = json.has("data") ? json.get("data") : null;
|
|
|
if (dataNode == null || dataNode.isNull()) {
|
|
if (dataNode == null || dataNode.isNull()) {
|
|
|
- return null;
|
|
|
|
|
|
|
+ return CloudPollResult.builder().packet(null).build();
|
|
|
}
|
|
}
|
|
|
- return OBJECT_MAPPER.treeToValue(dataNode, SyncPacket.class);
|
|
|
|
|
|
|
+ SyncPacket packet = OBJECT_MAPPER.treeToValue(dataNode, SyncPacket.class);
|
|
|
|
|
+ return CloudPollResult.builder().packet(packet).build();
|
|
|
} catch (Exception e) {
|
|
} catch (Exception e) {
|
|
|
- log.error("轮询云端失败", e);
|
|
|
|
|
- return null;
|
|
|
|
|
|
|
+ log.error("[pollCloud] 响应解析失败 body={}", body, e);
|
|
|
|
|
+ return CloudPollResult.builder().requestFailed(true).errorMessage("响应解析失败: " + e.getMessage()).build();
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -172,7 +215,7 @@ public class CloudPlatformClient {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private HttpHeaders buildAuthHeaders(IssueCloudConfig config, String credential) {
|
|
private HttpHeaders buildAuthHeaders(IssueCloudConfig config, String credential) {
|
|
|
- return buildAuthHeaders(config.getTenantId(), credential, config.getToken());
|
|
|
|
|
|
|
+ return buildAuthHeaders(config.getTenantId(), credential, CloudEntitySupport.resolveAccessToken(config));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private HttpHeaders buildAuthHeaders(Integer tenantId, String credential, String token) {
|
|
private HttpHeaders buildAuthHeaders(Integer tenantId, String credential, String token) {
|
|
@@ -187,17 +230,19 @@ public class CloudPlatformClient {
|
|
|
|
|
|
|
|
private CloudConnectionTestResult parseConnectionTestResult(String body, long costTime) {
|
|
private CloudConnectionTestResult parseConnectionTestResult(String body, long costTime) {
|
|
|
if (!StringUtils.hasText(body)) {
|
|
if (!StringUtils.hasText(body)) {
|
|
|
|
|
+ log.warn("[连接测试-HTTP] 云端返回空响应");
|
|
|
return CloudConnectionTestResult.failure(true, "云端返回空响应");
|
|
return CloudConnectionTestResult.failure(true, "云端返回空响应");
|
|
|
}
|
|
}
|
|
|
try {
|
|
try {
|
|
|
JsonNode json = OBJECT_MAPPER.readTree(body);
|
|
JsonNode json = OBJECT_MAPPER.readTree(body);
|
|
|
JsonNode dataNode = json.has("data") ? json.get("data") : json;
|
|
JsonNode dataNode = json.has("data") ? json.get("data") : json;
|
|
|
if (dataNode == null || dataNode.isNull()) {
|
|
if (dataNode == null || dataNode.isNull()) {
|
|
|
|
|
+ log.warn("[连接测试-HTTP] 云端返回空 data, rawBody={}", body);
|
|
|
return CloudConnectionTestResult.failure(true, "云端返回空数据");
|
|
return CloudConnectionTestResult.failure(true, "云端返回空数据");
|
|
|
}
|
|
}
|
|
|
CloudConnectionTestResult result = new CloudConnectionTestResult();
|
|
CloudConnectionTestResult result = new CloudConnectionTestResult();
|
|
|
result.setSuccess(dataNode.path("success").asBoolean(false));
|
|
result.setSuccess(dataNode.path("success").asBoolean(false));
|
|
|
- result.setMessage(dataNode.path("message").asText(null));
|
|
|
|
|
|
|
+ result.setMessage(resolveResponseMessage(json, dataNode));
|
|
|
result.setNetworkReachable(dataNode.has("networkReachable")
|
|
result.setNetworkReachable(dataNode.has("networkReachable")
|
|
|
? dataNode.get("networkReachable").asBoolean() : true);
|
|
? dataNode.get("networkReachable").asBoolean() : true);
|
|
|
result.setAuthValid(dataNode.has("authValid")
|
|
result.setAuthValid(dataNode.has("authValid")
|
|
@@ -209,14 +254,30 @@ public class CloudPlatformClient {
|
|
|
result.setMessage(result.isSuccess()
|
|
result.setMessage(result.isSuccess()
|
|
|
? CloudIntegrationConstants.MSG_CONNECTION_SUCCESS
|
|
? CloudIntegrationConstants.MSG_CONNECTION_SUCCESS
|
|
|
: "连接测试未通过");
|
|
: "连接测试未通过");
|
|
|
|
|
+ log.warn("[连接测试-HTTP] 响应缺少 message,已使用默认文案, parsedSuccess={}, rawBody={}",
|
|
|
|
|
+ result.isSuccess(), body);
|
|
|
}
|
|
}
|
|
|
|
|
+ log.info("[连接测试-HTTP] 解析结果 success={}, authValid={}, tenantExists={}, message={}",
|
|
|
|
|
+ result.isSuccess(), result.isAuthValid(), result.isTenantExists(), result.getMessage());
|
|
|
return result;
|
|
return result;
|
|
|
} catch (Exception e) {
|
|
} catch (Exception e) {
|
|
|
- log.error("连接测试响应解析失败: {}", body, e);
|
|
|
|
|
|
|
+ log.error("[连接测试-HTTP] 响应解析失败, rawBody={}", body, e);
|
|
|
return CloudConnectionTestResult.failure(true, "响应解析失败");
|
|
return CloudConnectionTestResult.failure(true, "响应解析失败");
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private String resolveResponseMessage(JsonNode root, JsonNode dataNode) {
|
|
|
|
|
+ String message = dataNode.path("message").asText(null);
|
|
|
|
|
+ if (StringUtils.hasText(message)) {
|
|
|
|
|
+ return message;
|
|
|
|
|
+ }
|
|
|
|
|
+ message = root.path("msg").asText(null);
|
|
|
|
|
+ if (StringUtils.hasText(message)) {
|
|
|
|
|
+ return message;
|
|
|
|
|
+ }
|
|
|
|
|
+ return dataNode.path("errorMessage").asText(null);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private String buildTestConnectionUrl(String cloudAddress) {
|
|
private String buildTestConnectionUrl(String cloudAddress) {
|
|
|
return normalizeCloudBaseUrl(cloudAddress) + URL_END;
|
|
return normalizeCloudBaseUrl(cloudAddress) + URL_END;
|
|
|
}
|
|
}
|