diff --git a/backend/src/main/java/com/unis/crm/service/OmsClient.java b/backend/src/main/java/com/unis/crm/service/OmsClient.java index 3ed771f8..ebc70a26 100644 --- a/backend/src/main/java/com/unis/crm/service/OmsClient.java +++ b/backend/src/main/java/com/unis/crm/service/OmsClient.java @@ -20,12 +20,16 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.springframework.stereotype.Service; @Service public class OmsClient { private static final Set SUCCESS_CODES = Set.of("0", "200", "success", "SUCCESS"); + private static final Pattern HTML_TITLE_PATTERN = Pattern.compile("]*>(.*?)", Pattern.CASE_INSENSITIVE | Pattern.DOTALL); + private static final Pattern HTML_HEADING_PATTERN = Pattern.compile("]*>(.*?)", Pattern.CASE_INSENSITIVE | Pattern.DOTALL); private static final List PROJECT_CODE_FIELDS = List.of( "project_code", "projectCode", @@ -207,16 +211,22 @@ public class OmsClient { private JsonNode sendRequest(HttpRequest request) { try { HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + String responseBody = response.body(); + String contentType = response.headers().firstValue("Content-Type").orElse(null); if (response.statusCode() < 200 || response.statusCode() >= 300) { - throw new BusinessException("OMS接口调用失败,HTTP状态码: " + response.statusCode() + ",响应: " + trimBody(response.body())); + throw new BusinessException(resolveOmsResponseMessage(response.statusCode(), responseBody, contentType)); } - JsonNode root = objectMapper.readTree(response.body()); + if (!looksLikeJson(contentType, responseBody)) { + throw new BusinessException(resolveOmsResponseMessage(response.statusCode(), responseBody, contentType)); + } + + JsonNode root = objectMapper.readTree(responseBody); String code = normalizeText(root.path("code").asText(null)); if (!isSuccessCode(code)) { String message = firstNonBlank( normalizeText(root.path("msg").asText(null)), normalizeText(root.path("message").asText(null)), - trimBody(response.body()), + trimBody(responseBody), "OMS接口调用失败"); throw new BusinessException(message); } @@ -229,6 +239,74 @@ public class OmsClient { } } + private boolean looksLikeJson(String contentType, String body) { + String normalizedContentType = normalizeText(contentType); + if (normalizedContentType != null) { + String lowerCaseContentType = normalizedContentType.toLowerCase(Locale.ROOT); + if (lowerCaseContentType.contains("application/json") || lowerCaseContentType.contains("+json")) { + return true; + } + } + + String normalizedBody = normalizeText(body); + return normalizedBody != null && (normalizedBody.startsWith("{") || normalizedBody.startsWith("[")); + } + + private String resolveOmsResponseMessage(int statusCode, String body, String contentType) { + String visibleMessage = firstNonBlank( + extractHtmlErrorMessage(body), + trimBody(body)); + if (!isBlank(visibleMessage)) { + return visibleMessage; + } + + String normalizedContentType = normalizeText(contentType); + if (!isBlank(normalizedContentType)) { + return "OMS接口调用失败,HTTP状态码: " + statusCode + ",响应类型: " + normalizedContentType; + } + return "OMS接口调用失败,HTTP状态码: " + statusCode; + } + + private String extractHtmlErrorMessage(String body) { + String normalizedBody = normalizeText(body); + if (normalizedBody == null || !normalizedBody.startsWith("<")) { + return null; + } + + String heading = extractHtmlFragment(normalizedBody, HTML_HEADING_PATTERN); + String title = extractHtmlFragment(normalizedBody, HTML_TITLE_PATTERN); + String textOnly = normalizeText( + normalizedBody + .replaceAll("(?is)]*>.*?", " ") + .replaceAll("(?is)]*>.*?", " ") + .replaceAll("(?is)<[^>]+>", " ") + .replace(" ", " ") + .replaceAll("\\s+", " ")); + + if (!isBlank(heading)) { + return heading; + } + if (!isBlank(textOnly)) { + if (!isBlank(title) && !textOnly.startsWith(title)) { + return (title + " " + textOnly).trim(); + } + return textOnly; + } + return title; + } + + private String extractHtmlFragment(String body, Pattern pattern) { + Matcher matcher = pattern.matcher(body); + if (!matcher.find()) { + return null; + } + String content = matcher.group(1); + if (content == null) { + return null; + } + return normalizeText(content.replaceAll("(?is)<[^>]+>", " ").replaceAll("\\s+", " ")); + } + private URI buildUri(String path, Map queryParams) { validateConfigured(); String baseUrl = normalizeBaseUrl(omsProperties.getBaseUrl()); diff --git a/frontend/src/lib/auth.ts b/frontend/src/lib/auth.ts index ae33bb3f..a5fa38e4 100644 --- a/frontend/src/lib/auth.ts +++ b/frontend/src/lib/auth.ts @@ -650,6 +650,64 @@ function handleUnauthorizedResponse() { window.location.href = `${LOGIN_PATH}?timeout=1`; } +function tryParseApiBody(rawText: string, contentType?: string | null): (ApiEnvelope & ApiErrorBody) | null { + const normalizedText = rawText.trim(); + const normalizedContentType = (contentType || "").toLowerCase(); + const looksLikeJson = normalizedContentType.includes("application/json") + || normalizedContentType.includes("+json") + || normalizedText.startsWith("{") + || normalizedText.startsWith("["); + + if (!normalizedText || !looksLikeJson) { + return null; + } + + try { + return JSON.parse(normalizedText) as ApiEnvelope & ApiErrorBody; + } catch { + return null; + } +} + +function extractHtmlErrorMessage(rawText: string) { + const titleMatch = rawText.match(/]*>([^<]+)<\/title>/i); + if (titleMatch?.[1]?.trim()) { + return titleMatch[1].trim(); + } + + const headingMatch = rawText.match(/]*>([^<]+)<\/h[1-6]>/i); + if (headingMatch?.[1]?.trim()) { + return headingMatch[1].trim(); + } + + const textOnly = rawText + .replace(//gi, " ") + .replace(//gi, " ") + .replace(/<[^>]+>/g, " ") + .replace(/\s+/g, " ") + .trim(); + + return textOnly || null; +} + +function buildApiErrorMessage(rawText: string, status: number, body?: ApiErrorBody | null) { + const directMessage = body?.msg?.trim() || body?.message?.trim(); + if (directMessage) { + return directMessage; + } + + const normalizedText = rawText.trim(); + if (!normalizedText) { + return `请求失败(${status})`; + } + + if (normalizedText.startsWith("<")) { + return extractHtmlErrorMessage(normalizedText) || `请求失败(${status})`; + } + + return normalizedText.length > 300 ? normalizedText.slice(0, 300) : normalizedText; +} + async function request(input: string, init?: RequestInit, withAuth = false): Promise { const headers = new Headers(init?.headers); if (!headers.has("Content-Type") && init?.body && !(init.body instanceof FormData)) { @@ -670,25 +728,19 @@ async function request(input: string, init?: RequestInit, withAuth = false): throw new Error("登录已失效,请重新登录"); } - let body: (ApiEnvelope & ApiErrorBody) | null = null; - try { - body = (await response.json()) as ApiEnvelope & ApiErrorBody; - } catch { - if (!response.ok) { - throw new Error(`请求失败(${response.status})`); - } - } + const rawText = await response.text(); + const body = tryParseApiBody(rawText, response.headers.get("content-type")); if (!response.ok) { - throw new Error(body?.msg || body?.message || `请求失败(${response.status})`); + throw new Error(buildApiErrorMessage(rawText, response.status, body)); } if (!body) { - throw new Error("接口返回为空"); + throw new Error(rawText.trim() ? buildApiErrorMessage(rawText, response.status, null) : "接口返回为空"); } if (body.code !== "0") { - throw new Error(body.msg || "请求失败"); + throw new Error(buildApiErrorMessage(rawText, response.status, body)); } return body.data;