package com.ruoyi.lsfx.util; import com.fasterxml.jackson.databind.ObjectMapper; import com.ruoyi.lsfx.domain.CallerContext; import com.ruoyi.lsfx.exception.LsfxApiException; import com.ruoyi.system.domain.SysApiLog; import com.ruoyi.system.service.ISysApiLogService; import jakarta.annotation.Resource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.io.FileSystemResource; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.http.MediaTypeFactory; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestClientResponseException; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; import java.io.File; import java.io.PrintWriter; import java.io.StringWriter; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * 业务外部HTTP请求工具,统一保存完整调用日志。 */ @Component public class HttpUtil { private static final Logger log = LoggerFactory.getLogger(HttpUtil.class); private static final String CALL_SUCCESS = "0"; private static final String CALL_FAILED = "1"; @Resource private RestTemplate restTemplate; @Resource private ObjectMapper objectMapper; @Resource private ISysApiLogService apiLogService; public static org.springframework.core.io.Resource namedFileResource(File file, String filename) { return new NamedFileSystemResource(file, filename); } private static class NamedFileSystemResource extends FileSystemResource { private final String filename; NamedFileSystemResource(File file, String filename) { super(file); this.filename = StringUtils.hasText(filename) ? filename : file.getName(); } @Override public String getFilename() { return filename; } } public T get(CallerContext caller, String url, Map params, Map headers, Class responseType) { UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url); if (params != null) { params.forEach((key, value) -> { if (value != null) { builder.queryParam(key, value); } }); } String fullUrl = builder.toUriString(); HttpHeaders httpHeaders = createHeaders(headers); return execute(caller, fullUrl, HttpMethod.GET, httpHeaders, params, new HttpEntity<>(httpHeaders), responseType, "API返回数据为空", "GET请求异常"); } public T get(CallerContext caller, String url, Map headers, Class responseType) { HttpHeaders httpHeaders = createHeaders(headers); return execute(caller, url, HttpMethod.GET, httpHeaders, null, new HttpEntity<>(httpHeaders), responseType, "API返回数据为空", "网络请求失败"); } public T postJson(CallerContext caller, String url, Object request, Map headers, Class responseType) { HttpHeaders httpHeaders = createHeaders(headers); httpHeaders.setContentType(MediaType.APPLICATION_JSON); return execute(caller, url, HttpMethod.POST, httpHeaders, request, new HttpEntity<>(request, httpHeaders), responseType, "API返回数据为空", "网络请求失败"); } public T postFormData(CallerContext caller, String url, Map params, Map headers, Class responseType) { HttpHeaders httpHeaders = createHeaders(headers); httpHeaders.setContentType(MediaType.MULTIPART_FORM_DATA); MultiValueMap body = new LinkedMultiValueMap<>(); if (params != null) { params.forEach(body::add); } return execute(caller, url, HttpMethod.POST, httpHeaders, params, new HttpEntity<>(body, httpHeaders), responseType, "API返回数据为空", "网络请求失败"); } public T postUrlEncodedForm(CallerContext caller, String url, Map params, Map headers, Class responseType) { HttpHeaders httpHeaders = createHeaders(headers); httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED); MultiValueMap body = toUrlEncodedBody(params); return execute(caller, url, HttpMethod.POST, httpHeaders, params, new HttpEntity<>(body, httpHeaders), responseType, "API返回数据为空", "网络请求失败"); } public String postUrlEncodedFormForString(CallerContext caller, String url, Map params, Map headers) { HttpHeaders httpHeaders = createHeaders(headers); httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED); MultiValueMap body = toUrlEncodedBody(params); return execute(caller, url, HttpMethod.POST, httpHeaders, params, new HttpEntity<>(body, httpHeaders), String.class, "API返回数据为空", "网络请求失败"); } public T uploadFile(CallerContext caller, String url, Map params, Map headers, Class responseType) { HttpHeaders httpHeaders = createHeaders(headers); httpHeaders.setContentType(MediaType.MULTIPART_FORM_DATA); MultiValueMap body = new LinkedMultiValueMap<>(); if (params != null) { params.forEach((key, value) -> { if (value instanceof File file) { body.add(key, new FileSystemResource(file)); } else { body.add(key, value); } }); } return execute(caller, url, HttpMethod.POST, httpHeaders, params, new HttpEntity<>(body, httpHeaders), responseType, "文件上传返回数据为空", "文件上传请求失败"); } private T execute(CallerContext caller, String url, HttpMethod method, HttpHeaders requestHeaders, Object requestParams, HttpEntity requestEntity, Class responseType, String emptyResponseMessage, String requestFailureMessage) { if (caller == null) { throw new IllegalArgumentException("接口调用用户不能为空"); } long startTime = System.currentTimeMillis(); SysApiLog apiLog = createBaseApiLog(caller, url, method, startTime); try { captureRequest(apiLog, requestHeaders, requestParams); } catch (Exception e) { log.error("接口日志请求快照构建失败: method={}, url={}", method, url, e); } try { ResponseEntity response = restTemplate.exchange(url, method, requestEntity, String.class); captureResponse(apiLog, response); if (!response.getStatusCode().is2xxSuccessful()) { throw new LsfxApiException("API调用失败,HTTP状态码: " + response.getStatusCode().value()); } if (!StringUtils.hasText(response.getBody())) { throw new LsfxApiException(emptyResponseMessage); } T result = parseResponse(response.getBody(), responseType); apiLog.setCallStatus(CALL_SUCCESS); return result; } catch (RestClientResponseException e) { apiLog.setResponseStatus(e.getStatusCode().value()); apiLog.setResponseHeaders(safeSerialize(e.getResponseHeaders())); apiLog.setResponseBody(e.getResponseBodyAsString()); apiLog.setErrorMsg(formatException(e)); throw new LsfxApiException(requestFailureMessage + ": " + e.getMessage(), e); } catch (LsfxApiException e) { apiLog.setErrorMsg(formatException(e)); throw e; } catch (RestClientException e) { apiLog.setErrorMsg(formatException(e)); throw new LsfxApiException(requestFailureMessage + ": " + e.getMessage(), e); } catch (Exception e) { apiLog.setErrorMsg(formatException(e)); throw new LsfxApiException("响应解析失败: " + e.getMessage(), e); } finally { apiLog.setCostTime(System.currentTimeMillis() - startTime); persistApiLog(apiLog); } } private SysApiLog createBaseApiLog(CallerContext caller, String url, HttpMethod method, long startTime) { SysApiLog apiLog = new SysApiLog(); apiLog.setCallerUserId(caller.userId()); apiLog.setCallerUsername(caller.username()); apiLog.setApiUrl(url); apiLog.setHttpMethod(method.name()); apiLog.setCallStatus(CALL_FAILED); apiLog.setCallTime(new Date(startTime)); return apiLog; } private void captureRequest(SysApiLog apiLog, HttpHeaders headers, Object params) { apiLog.setContentType(headers.getContentType() == null ? null : headers.getContentType().toString()); apiLog.setRequestHeaders(safeSerialize(headers)); apiLog.setRequestParams(safeSerialize(normalizeValue(params))); } private void captureResponse(SysApiLog apiLog, ResponseEntity response) { apiLog.setResponseStatus(response.getStatusCode().value()); apiLog.setResponseHeaders(safeSerialize(response.getHeaders())); apiLog.setResponseBody(response.getBody()); } @SuppressWarnings("unchecked") private T parseResponse(String responseBody, Class responseType) throws Exception { if (String.class.equals(responseType)) { return (T) responseBody; } return objectMapper.readValue(responseBody, responseType); } private MultiValueMap toUrlEncodedBody(Map params) { MultiValueMap body = new LinkedMultiValueMap<>(); if (params != null) { params.forEach((key, value) -> { if (value != null) { body.add(key, value.toString()); } }); } return body; } private HttpHeaders createHeaders(Map headers) { HttpHeaders httpHeaders = new HttpHeaders(); if (headers != null) { headers.forEach(httpHeaders::set); } return httpHeaders; } private Object normalizeValue(Object value) { if (value == null) { return null; } if (value instanceof File file) { return fileMetadata(file.getName(), file.length()); } if (value instanceof FileSystemResource resource) { return fileMetadata(resource.getFilename(), resource.getFile().length()); } if (value instanceof org.springframework.core.io.Resource resource) { return fileMetadata(resource.getFilename(), null); } if (value instanceof Map map) { Map normalized = new LinkedHashMap<>(); map.forEach((key, item) -> normalized.put(String.valueOf(key), normalizeValue(item))); return normalized; } if (value instanceof Iterable iterable) { List normalized = new ArrayList<>(); iterable.forEach(item -> normalized.add(normalizeValue(item))); return normalized; } if (value.getClass().isArray()) { List normalized = new ArrayList<>(); for (int i = 0; i < Array.getLength(value); i++) { normalized.add(normalizeValue(Array.get(value, i))); } return normalized; } return value; } private Map fileMetadata(String filename, Long size) { Map metadata = new LinkedHashMap<>(); metadata.put("filename", filename); metadata.put("size", size); metadata.put("contentType", MediaTypeFactory.getMediaType(filename == null ? "" : filename) .map(MediaType::toString).orElse(null)); return metadata; } private String safeSerialize(Object value) { try { return objectMapper.writeValueAsString(value); } catch (Exception e) { log.error("接口日志序列化失败", e); return String.valueOf(value); } } private String formatException(Exception exception) { StringWriter writer = new StringWriter(); exception.printStackTrace(new PrintWriter(writer)); return writer.toString(); } private void persistApiLog(SysApiLog apiLog) { try { apiLogService.recordApiLog(apiLog); } catch (Exception e) { log.error("接口日志入库失败: method={}, url={}", apiLog.getHttpMethod(), apiLog.getApiUrl(), e); } } }