package com.example.demo.barcode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.io.ByteArrayResource; import org.springframework.http.*; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.*; import org.springframework.web.client.RestTemplate; import org.springframework.web.client.HttpStatusCodeException; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.time.Duration; @RestController @RequestMapping("/api/barcode") public class BarcodeProxyController { private final PythonBarcodeProperties properties; private final RestTemplate restTemplate; private static final Logger log = LoggerFactory.getLogger(BarcodeProxyController.class); public BarcodeProxyController(PythonBarcodeProperties properties) { this.properties = properties; SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); factory.setConnectTimeout((int) Duration.ofSeconds(2).toMillis()); factory.setReadTimeout((int) Duration.ofSeconds(8).toMillis()); this.restTemplate = new RestTemplate(factory); } @PostMapping(value = "/scan", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity scan(@RequestPart("file") MultipartFile file) throws IOException { if (file == null || file.isEmpty()) { return ResponseEntity.badRequest().body("{\"success\":false,\"message\":\"文件为空\"}"); } long maxBytes = (long) properties.getMaxUploadMb() * 1024L * 1024L; if (file.getSize() > maxBytes) { return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE) .body(String.format("{\"success\":false,\"message\":\"文件过大(> %dMB)\"}", properties.getMaxUploadMb())); } String url = String.format("http://%s:%d/api/barcode/scan", properties.getHost(), properties.getPort()); if (log.isDebugEnabled()) { log.debug("转发条码识别请求: url={} filename={} size={}B", url, file.getOriginalFilename(), file.getSize()); } // 构建 multipart/form-data 请求转发 MultiValueMap body = new LinkedMultiValueMap<>(); HttpHeaders fileHeaders = new HttpHeaders(); String contentType = file.getContentType(); MediaType mediaType = MediaType.APPLICATION_OCTET_STREAM; if (contentType != null && !contentType.isBlank()) { try { mediaType = MediaType.parseMediaType(contentType); } catch (Exception ignored) { mediaType = MediaType.APPLICATION_OCTET_STREAM; } } fileHeaders.setContentType(mediaType); String originalName = file.getOriginalFilename(); if (originalName == null || originalName.isBlank()) { originalName = file.getName(); } if (originalName == null || originalName.isBlank()) { originalName = "upload.bin"; } fileHeaders.setContentDisposition(ContentDisposition.builder("form-data") .name("file") .filename(originalName) .build()); final String finalFilename = originalName; ByteArrayResource resource = new ByteArrayResource(file.getBytes()) { @Override public String getFilename() { return finalFilename; } }; HttpEntity fileEntity = new HttpEntity<>(resource, fileHeaders); body.add("file", fileEntity); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.MULTIPART_FORM_DATA); HttpEntity> req = new HttpEntity<>(body, headers); try { long t0 = System.currentTimeMillis(); ResponseEntity resp = restTemplate.postForEntity(url, req, String.class); long cost = System.currentTimeMillis() - t0; if (log.isDebugEnabled()) { String bodyStr = resp.getBody(); if (bodyStr != null && bodyStr.length() > 500) { bodyStr = bodyStr.substring(0, 500) + "..."; } log.debug("转发完成: status={} cost={}ms resp={}", resp.getStatusCodeValue(), cost, bodyStr); } return ResponseEntity.status(resp.getStatusCode()) .contentType(MediaType.APPLICATION_JSON) .body(resp.getBody()); } catch (HttpStatusCodeException ex) { String bodyStr = ex.getResponseBodyAsString(); if (bodyStr != null && bodyStr.length() > 500) { bodyStr = bodyStr.substring(0, 500) + "..."; } log.warn("Python 服务返回非 2xx: status={} body={}", ex.getStatusCode(), bodyStr); MediaType respType = ex.getResponseHeaders() != null ? ex.getResponseHeaders().getContentType() : MediaType.APPLICATION_JSON; if (respType == null) { respType = MediaType.APPLICATION_JSON; } return ResponseEntity.status(ex.getStatusCode()) .contentType(respType) .body(ex.getResponseBodyAsString()); } catch (Exception ex) { // Python 服务不可用或超时等异常 log.warn("转发到 Python 服务失败: {}:{} path=/api/barcode/scan, err={}", properties.getHost(), properties.getPort(), ex.toString()); return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE) .contentType(MediaType.APPLICATION_JSON) .body("{\"success\":false,\"message\":\"识别服务不可用,请稍后重试\"}"); } } }