暂存提交
This commit is contained in:
+41
@@ -0,0 +1,41 @@
|
||||
package com.baiyue.integration.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.integration.dto.ocr.WeightOcrBatchRequest;
|
||||
import com.baiyue.integration.dto.ocr.WeightOcrBatchResult;
|
||||
import com.baiyue.integration.security.InternalServiceTokenGuard;
|
||||
import com.baiyue.integration.service.ocr.WeightOcrBatchService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 供平台内部服务调用的称重图片 OCR 接口。
|
||||
*/
|
||||
@Tag(name = "重量 OCR", description = "受内部服务令牌保护的称重图片批量识别能力")
|
||||
@RestController
|
||||
@RequestMapping("/internal/integrations/weight-ocr")
|
||||
public class WeightOcrController {
|
||||
@Resource
|
||||
private WeightOcrBatchService batchService;
|
||||
|
||||
@Resource
|
||||
private InternalServiceTokenGuard tokenGuard;
|
||||
|
||||
@Operation(summary = "批量识别称重图片")
|
||||
@PostMapping("/batches")
|
||||
public ApiResponse<WeightOcrBatchResult> recognize(
|
||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
@Valid @RequestBody WeightOcrBatchRequest request) throws IOException {
|
||||
tokenGuard.verify(token);
|
||||
return ApiResponse.success(batchService.recognize(request));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.baiyue.integration.dto.ocr;
|
||||
|
||||
/**
|
||||
* 百度 OCR 返回的文字候选及其识别框。
|
||||
*/
|
||||
public record OcrCandidate(
|
||||
String text,
|
||||
Double confidence,
|
||||
Integer left,
|
||||
Integer top,
|
||||
Integer width,
|
||||
Integer height
|
||||
) {
|
||||
public long area() {
|
||||
if (width == null || height == null || width <= 0 || height <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return (long) width * height;
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.baiyue.integration.dto.ocr;
|
||||
|
||||
import jakarta.validation.constraints.DecimalMax;
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
* 重量 OCR 批次请求。目录必须位于服务配置的允许根目录内。
|
||||
*/
|
||||
public record WeightOcrBatchRequest(
|
||||
@NotBlank(message = "输入目录不能为空") String inputDirectory,
|
||||
String outputDirectory,
|
||||
@DecimalMin(value = "0.0", message = "纵向裁剪起始比例不能小于 0")
|
||||
@DecimalMax(value = "0.95", message = "纵向裁剪起始比例不能大于 0.95")
|
||||
Double cropStartRatio,
|
||||
@DecimalMin(value = "0.0", message = "横向裁剪比例不能小于 0")
|
||||
@DecimalMax(value = "0.49", message = "横向裁剪比例不能大于 0.49")
|
||||
Double horizontalTrimRatio,
|
||||
Boolean recursive
|
||||
) {
|
||||
public double effectiveCropStartRatio() {
|
||||
return cropStartRatio == null ? 0.5 : cropStartRatio;
|
||||
}
|
||||
|
||||
public double effectiveHorizontalTrimRatio() {
|
||||
return horizontalTrimRatio == null ? 0.25 : horizontalTrimRatio;
|
||||
}
|
||||
|
||||
public boolean effectiveRecursive() {
|
||||
return Boolean.TRUE.equals(recursive);
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.baiyue.integration.dto.ocr;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 重量 OCR 批次汇总结果。
|
||||
*/
|
||||
public record WeightOcrBatchResult(
|
||||
String batchId,
|
||||
Instant startedAt,
|
||||
Instant finishedAt,
|
||||
String inputDirectory,
|
||||
String outputDirectory,
|
||||
int total,
|
||||
int recognized,
|
||||
int noWeightFound,
|
||||
int failed,
|
||||
List<WeightOcrImageResult> results
|
||||
) {
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.baiyue.integration.dto.ocr;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 单张称重图片识别结果。路径均为相对于受控根目录的相对路径。
|
||||
*/
|
||||
public record WeightOcrImageResult(
|
||||
String sourceImage,
|
||||
String croppedImage,
|
||||
String rawResponseFile,
|
||||
Status status,
|
||||
BigDecimal weight,
|
||||
Double confidence,
|
||||
List<OcrCandidate> candidates,
|
||||
String error
|
||||
) {
|
||||
public enum Status {
|
||||
RECOGNIZED,
|
||||
NO_WEIGHT_FOUND,
|
||||
FAILED
|
||||
}
|
||||
}
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
package com.baiyue.integration.provider.ocr;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.integration.config.IntegrationProperties;
|
||||
import com.baiyue.integration.dto.ocr.OcrCandidate;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 百度仪器仪表盘读数识别客户端,负责令牌缓存、限速和受控重试。
|
||||
*/
|
||||
@Component
|
||||
public class BaiduMeterClient {
|
||||
private static final int MAX_FORM_BYTES = 4 * 1024 * 1024;
|
||||
private static final int QPS_LIMIT_ERROR_CODE = 18;
|
||||
private static final long INITIAL_RETRY_DELAY_MILLIS = 1_000;
|
||||
|
||||
private final IntegrationProperties.BaiduOcr config;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final HttpClient httpClient;
|
||||
private final Object requestRateLock = new Object();
|
||||
|
||||
private volatile CachedToken cachedToken;
|
||||
private long nextRequestAtMillis;
|
||||
|
||||
public BaiduMeterClient(IntegrationProperties properties, ObjectMapper objectMapper) {
|
||||
this.config = properties.baiduOcr();
|
||||
this.objectMapper = objectMapper;
|
||||
this.httpClient = HttpClient.newBuilder()
|
||||
.connectTimeout(config == null ? java.time.Duration.ofSeconds(10)
|
||||
: config.effectiveConnectTimeout())
|
||||
.build();
|
||||
}
|
||||
|
||||
public void validateReady() {
|
||||
if (config == null || !config.enabled()) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "重量 OCR 能力未启用");
|
||||
}
|
||||
if (!config.configured()) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "百度 OCR 密钥或接口地址未配置");
|
||||
}
|
||||
}
|
||||
|
||||
public MeterResponse recognize(Path image) throws IOException, InterruptedException {
|
||||
validateReady();
|
||||
long imageBytes = Files.size(image);
|
||||
if (imageBytes <= 0 || imageBytes > config.effectiveMaxImageSize().toBytes()) {
|
||||
throw new IOException("裁剪图片超过允许大小: " + image.getFileName());
|
||||
}
|
||||
String base64 = Base64.getEncoder().encodeToString(Files.readAllBytes(image));
|
||||
String form = "image=" + encode(base64) + "&probability=true&poly_location=true";
|
||||
if (form.getBytes(StandardCharsets.UTF_8).length > MAX_FORM_BYTES) {
|
||||
throw new IOException("裁剪图片编码后超过百度接口 4MB 限制: " + image.getFileName());
|
||||
}
|
||||
|
||||
for (int retry = 0; ; retry++) {
|
||||
waitForRequestSlot();
|
||||
try {
|
||||
return sendRecognitionRequest(form);
|
||||
} catch (BaiduApiException exception) {
|
||||
if (exception.errorCode() != QPS_LIMIT_ERROR_CODE
|
||||
|| retry >= config.effectiveMaxQpsRetries()) {
|
||||
throw exception;
|
||||
}
|
||||
Thread.sleep(INITIAL_RETRY_DELAY_MILLIS << retry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private MeterResponse sendRecognitionRequest(String form) throws IOException, InterruptedException {
|
||||
String separator = config.meterUrl().toString().contains("?") ? "&" : "?";
|
||||
URI uri = URI.create(config.meterUrl() + separator + "access_token=" + encode(accessToken()));
|
||||
HttpRequest request = HttpRequest.newBuilder(uri)
|
||||
.timeout(config.effectiveRequestTimeout())
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.header("Accept", "application/json")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(form))
|
||||
.build();
|
||||
HttpResponse<String> response = httpClient.send(
|
||||
request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||
ensureSuccessfulHttpStatus(response.statusCode());
|
||||
|
||||
JsonNode root = objectMapper.readTree(response.body());
|
||||
ensureSuccessfulBaiduResponse(root);
|
||||
return new MeterResponse(response.body(), parseCandidates(root.path("words_result")));
|
||||
}
|
||||
|
||||
private void waitForRequestSlot() throws InterruptedException {
|
||||
synchronized (requestRateLock) {
|
||||
long waitMillis = nextRequestAtMillis - System.currentTimeMillis();
|
||||
if (waitMillis > 0) {
|
||||
Thread.sleep(waitMillis);
|
||||
}
|
||||
nextRequestAtMillis = System.currentTimeMillis()
|
||||
+ config.effectiveMinRequestInterval().toMillis();
|
||||
}
|
||||
}
|
||||
|
||||
private String accessToken() throws IOException, InterruptedException {
|
||||
CachedToken current = cachedToken;
|
||||
if (usable(current)) {
|
||||
return current.value();
|
||||
}
|
||||
synchronized (this) {
|
||||
current = cachedToken;
|
||||
if (usable(current)) {
|
||||
return current.value();
|
||||
}
|
||||
String form = "grant_type=client_credentials"
|
||||
+ "&client_id=" + encode(config.apiKey())
|
||||
+ "&client_secret=" + encode(config.secretKey());
|
||||
HttpRequest request = HttpRequest.newBuilder(config.tokenUrl())
|
||||
.timeout(config.effectiveRequestTimeout())
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(form))
|
||||
.build();
|
||||
HttpResponse<String> response = httpClient.send(
|
||||
request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||
ensureSuccessfulHttpStatus(response.statusCode());
|
||||
|
||||
JsonNode root = objectMapper.readTree(response.body());
|
||||
String token = root.path("access_token").asText();
|
||||
if (token.isBlank()) {
|
||||
throw new IOException("百度 OAuth 获取 access_token 失败: " + safeError(root));
|
||||
}
|
||||
long expiresIn = Math.max(120, root.path("expires_in").asLong(2_592_000));
|
||||
cachedToken = new CachedToken(token, Instant.now().plusSeconds(expiresIn));
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean usable(CachedToken token) {
|
||||
return token != null && token.expiresAt().isAfter(Instant.now().plusSeconds(60));
|
||||
}
|
||||
|
||||
private List<OcrCandidate> parseCandidates(JsonNode wordsResult) {
|
||||
List<OcrCandidate> candidates = new ArrayList<>();
|
||||
if (!wordsResult.isArray()) {
|
||||
return candidates;
|
||||
}
|
||||
for (JsonNode item : wordsResult) {
|
||||
JsonNode probability = item.path("probability");
|
||||
JsonNode location = item.path("location");
|
||||
candidates.add(new OcrCandidate(
|
||||
item.path("words").asText(),
|
||||
nullableDouble(probability.get("average")),
|
||||
nullableInt(location.get("left")),
|
||||
nullableInt(location.get("top")),
|
||||
nullableInt(location.get("width")),
|
||||
nullableInt(location.get("height"))
|
||||
));
|
||||
}
|
||||
return List.copyOf(candidates);
|
||||
}
|
||||
|
||||
private void ensureSuccessfulHttpStatus(int statusCode) throws IOException {
|
||||
if (statusCode < 200 || statusCode >= 300) {
|
||||
throw new IOException("百度接口 HTTP 状态异常: " + statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureSuccessfulBaiduResponse(JsonNode root) throws IOException {
|
||||
if (root.has("error_code")) {
|
||||
int errorCode = root.path("error_code").asInt(-1);
|
||||
throw new BaiduApiException(errorCode, "百度 OCR 调用失败: " + safeError(root));
|
||||
}
|
||||
}
|
||||
|
||||
private String safeError(JsonNode root) {
|
||||
return root.path("error_code").asText("unknown") + " - "
|
||||
+ root.path("error_msg").asText("unknown error");
|
||||
}
|
||||
|
||||
private static String encode(String value) {
|
||||
return URLEncoder.encode(value, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static Double nullableDouble(JsonNode node) {
|
||||
return node == null || !node.isNumber() ? null : node.doubleValue();
|
||||
}
|
||||
|
||||
private static Integer nullableInt(JsonNode node) {
|
||||
return node == null || !node.isNumber() ? null : node.intValue();
|
||||
}
|
||||
|
||||
public record MeterResponse(String rawJson, List<OcrCandidate> candidates) {
|
||||
}
|
||||
|
||||
private record CachedToken(String value, Instant expiresAt) {
|
||||
}
|
||||
|
||||
private static final class BaiduApiException extends IOException {
|
||||
private final int errorCode;
|
||||
|
||||
private BaiduApiException(int errorCode, String message) {
|
||||
super(message);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
private int errorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package com.baiyue.integration.service.ocr;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.imageio.IIOImage;
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.imageio.ImageWriteParam;
|
||||
import javax.imageio.ImageWriter;
|
||||
import javax.imageio.stream.ImageOutputStream;
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Iterator;
|
||||
|
||||
/**
|
||||
* 裁剪称重图片的下方中央区域,并统一输出为 JPEG。
|
||||
*/
|
||||
@Component
|
||||
public class ImageCropper {
|
||||
private static final float JPEG_QUALITY = 0.90f;
|
||||
|
||||
public void cropBottomCenter(Path source, Path target, double startRatio,
|
||||
double horizontalTrimRatio, long maxImagePixels) throws IOException {
|
||||
validateRatios(startRatio, horizontalTrimRatio);
|
||||
BufferedImage sourceImage = ImageIO.read(source.toFile());
|
||||
if (sourceImage == null) {
|
||||
throw new IOException("无法解析图片: " + source.getFileName());
|
||||
}
|
||||
long pixels = (long) sourceImage.getWidth() * sourceImage.getHeight();
|
||||
if (pixels <= 0 || pixels > maxImagePixels) {
|
||||
throw new IOException("图片像素超过允许上限: " + source.getFileName());
|
||||
}
|
||||
|
||||
int startY = (int) Math.floor(sourceImage.getHeight() * startRatio);
|
||||
int startX = (int) Math.floor(sourceImage.getWidth() * horizontalTrimRatio);
|
||||
int croppedWidth = sourceImage.getWidth() - startX * 2;
|
||||
int croppedHeight = sourceImage.getHeight() - startY;
|
||||
if (croppedWidth <= 0 || croppedHeight <= 0) {
|
||||
throw new IOException("图片裁剪区域无效: " + source.getFileName());
|
||||
}
|
||||
|
||||
BufferedImage cropped = new BufferedImage(croppedWidth, croppedHeight, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D graphics = cropped.createGraphics();
|
||||
try {
|
||||
graphics.setColor(Color.WHITE);
|
||||
graphics.fillRect(0, 0, croppedWidth, croppedHeight);
|
||||
graphics.drawImage(sourceImage, 0, 0, croppedWidth, croppedHeight,
|
||||
startX, startY, startX + croppedWidth, startY + croppedHeight, null);
|
||||
} finally {
|
||||
graphics.dispose();
|
||||
}
|
||||
|
||||
Files.createDirectories(target.getParent());
|
||||
Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpeg");
|
||||
if (!writers.hasNext()) {
|
||||
throw new IOException("当前 Java 环境不支持 JPEG 编码");
|
||||
}
|
||||
ImageWriter writer = writers.next();
|
||||
try (ImageOutputStream output = ImageIO.createImageOutputStream(target.toFile())) {
|
||||
writer.setOutput(output);
|
||||
ImageWriteParam parameters = writer.getDefaultWriteParam();
|
||||
parameters.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
|
||||
parameters.setCompressionQuality(JPEG_QUALITY);
|
||||
writer.write(null, new IIOImage(cropped, null, null), parameters);
|
||||
} finally {
|
||||
writer.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void validateRatios(double startRatio, double horizontalTrimRatio) {
|
||||
if (startRatio < 0 || startRatio >= 1) {
|
||||
throw new IllegalArgumentException("cropStartRatio 必须在 [0, 1) 范围内");
|
||||
}
|
||||
if (horizontalTrimRatio < 0 || horizontalTrimRatio >= 0.5) {
|
||||
throw new IllegalArgumentException("horizontalTrimRatio 必须在 [0, 0.5) 范围内");
|
||||
}
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package com.baiyue.integration.service.ocr;
|
||||
|
||||
import com.baiyue.integration.dto.ocr.OcrCandidate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 从 OCR 候选内容中选择最可能的重量数值。
|
||||
*/
|
||||
@Component
|
||||
public class WeightCandidateSelector {
|
||||
private static final Pattern WEIGHT_PATTERN = Pattern.compile("^[+-]?\\d{1,8}(?:\\.\\d{1,4})?$");
|
||||
|
||||
public Optional<SelectedWeight> select(List<OcrCandidate> candidates) {
|
||||
if (candidates == null || candidates.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return candidates.stream()
|
||||
.map(this::toSelectedWeight)
|
||||
.flatMap(Optional::stream)
|
||||
.max(Comparator
|
||||
.comparingLong((SelectedWeight selected) -> selected.candidate().area())
|
||||
.thenComparing(selected -> selected.confidence() == null ? -1 : selected.confidence()));
|
||||
}
|
||||
|
||||
private Optional<SelectedWeight> toSelectedWeight(OcrCandidate candidate) {
|
||||
if (candidate == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
String normalized = normalize(candidate.text());
|
||||
if (!WEIGHT_PATTERN.matcher(normalized).matches()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
try {
|
||||
return Optional.of(new SelectedWeight(
|
||||
new BigDecimal(normalized), candidate.confidence(), candidate));
|
||||
} catch (NumberFormatException ignored) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private String normalize(String value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
return value.strip()
|
||||
.replace(" ", "")
|
||||
.replace(',', '.')
|
||||
.replace('+', '+')
|
||||
.replace('-', '-');
|
||||
}
|
||||
|
||||
public record SelectedWeight(BigDecimal weight, Double confidence, OcrCandidate candidate) {
|
||||
}
|
||||
}
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
package com.baiyue.integration.service.ocr;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.integration.config.IntegrationProperties;
|
||||
import com.baiyue.integration.dto.ocr.WeightOcrBatchRequest;
|
||||
import com.baiyue.integration.dto.ocr.WeightOcrBatchResult;
|
||||
import com.baiyue.integration.dto.ocr.WeightOcrImageResult;
|
||||
import com.baiyue.integration.dto.ocr.WeightOcrImageResult.Status;
|
||||
import com.baiyue.integration.provider.ocr.BaiduMeterClient;
|
||||
import com.baiyue.integration.service.ocr.WeightOcrStoragePolicy.BatchPaths;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 编排称重图片批量裁剪、百度 OCR 调用和结果落盘。
|
||||
*/
|
||||
@Service
|
||||
public class WeightOcrBatchService {
|
||||
private static final Set<String> SUPPORTED_EXTENSIONS = Set.of("jpg", "jpeg", "png", "bmp");
|
||||
|
||||
private final IntegrationProperties properties;
|
||||
private final WeightOcrStoragePolicy storagePolicy;
|
||||
private final ImageCropper imageCropper;
|
||||
private final BaiduMeterClient baiduMeterClient;
|
||||
private final WeightCandidateSelector candidateSelector;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final Semaphore batchSlot = new Semaphore(1);
|
||||
|
||||
public WeightOcrBatchService(IntegrationProperties properties,
|
||||
WeightOcrStoragePolicy storagePolicy,
|
||||
ImageCropper imageCropper,
|
||||
BaiduMeterClient baiduMeterClient,
|
||||
WeightCandidateSelector candidateSelector,
|
||||
ObjectMapper objectMapper) {
|
||||
this.properties = properties;
|
||||
this.storagePolicy = storagePolicy;
|
||||
this.imageCropper = imageCropper;
|
||||
this.baiduMeterClient = baiduMeterClient;
|
||||
this.candidateSelector = candidateSelector;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
public WeightOcrBatchResult recognize(WeightOcrBatchRequest request) throws IOException {
|
||||
baiduMeterClient.validateReady();
|
||||
if (!batchSlot.tryAcquire()) {
|
||||
throw new BizException(ResultCode.TOO_MANY_REQUESTS, "已有重量 OCR 批次正在执行");
|
||||
}
|
||||
try {
|
||||
return doRecognize(request);
|
||||
} finally {
|
||||
batchSlot.release();
|
||||
}
|
||||
}
|
||||
|
||||
private WeightOcrBatchResult doRecognize(WeightOcrBatchRequest request) throws IOException {
|
||||
String batchId = UUID.randomUUID().toString();
|
||||
Instant startedAt = Instant.now();
|
||||
BatchPaths paths = storagePolicy.resolve(
|
||||
request.inputDirectory(), request.outputDirectory(), batchId);
|
||||
List<Path> images = findImages(paths.inputDirectory(), paths.outputDirectory(),
|
||||
request.effectiveRecursive(), config().effectiveMaxBatchSize());
|
||||
if (images.isEmpty()) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "输入目录中没有支持的图片");
|
||||
}
|
||||
|
||||
List<WeightOcrImageResult> results = new ArrayList<>(images.size());
|
||||
for (Path image : images) {
|
||||
results.add(recognizeOne(paths, image, request));
|
||||
}
|
||||
WeightOcrBatchResult result = buildResult(batchId, startedAt, paths, results);
|
||||
objectMapper.writerWithDefaultPrettyPrinter()
|
||||
.writeValue(paths.outputDirectory().resolve("batch-results.json").toFile(), result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private WeightOcrImageResult recognizeOne(BatchPaths paths, Path source,
|
||||
WeightOcrBatchRequest request) {
|
||||
Path relative = paths.inputDirectory().relativize(source);
|
||||
Path cropped = paths.outputDirectory().resolve("cropped")
|
||||
.resolve(replaceExtension(relative, ".jpg"));
|
||||
Path rawResponse = paths.outputDirectory().resolve("raw")
|
||||
.resolve(replaceExtension(relative, ".json"));
|
||||
try {
|
||||
long sourceBytes = Files.size(source);
|
||||
if (sourceBytes <= 0 || sourceBytes > config().effectiveMaxImageSize().toBytes()) {
|
||||
throw new IOException("原始图片超过允许大小");
|
||||
}
|
||||
imageCropper.cropBottomCenter(source, cropped,
|
||||
request.effectiveCropStartRatio(), request.effectiveHorizontalTrimRatio(),
|
||||
config().effectiveMaxImagePixels());
|
||||
BaiduMeterClient.MeterResponse response = baiduMeterClient.recognize(cropped);
|
||||
Files.createDirectories(rawResponse.getParent());
|
||||
Files.writeString(rawResponse, response.rawJson(), StandardCharsets.UTF_8);
|
||||
|
||||
return candidateSelector.select(response.candidates())
|
||||
.map(selected -> success(relative, paths, cropped, rawResponse,
|
||||
response, selected))
|
||||
.orElseGet(() -> noWeight(relative, paths, cropped, rawResponse, response));
|
||||
} catch (InterruptedException exception) {
|
||||
Thread.currentThread().interrupt();
|
||||
return failed(relative, paths, cropped, rawResponse, "调用百度 OCR 时线程被中断");
|
||||
} catch (Exception exception) {
|
||||
return failed(relative, paths, cropped, rawResponse, safeMessage(exception));
|
||||
}
|
||||
}
|
||||
|
||||
private WeightOcrImageResult success(Path source, BatchPaths paths, Path cropped,
|
||||
Path rawResponse, BaiduMeterClient.MeterResponse response,
|
||||
WeightCandidateSelector.SelectedWeight selected) {
|
||||
return new WeightOcrImageResult(
|
||||
source.toString(), outputRelative(paths, cropped), outputRelative(paths, rawResponse),
|
||||
Status.RECOGNIZED, selected.weight(), selected.confidence(),
|
||||
response.candidates(), null);
|
||||
}
|
||||
|
||||
private WeightOcrImageResult noWeight(Path source, BatchPaths paths, Path cropped,
|
||||
Path rawResponse, BaiduMeterClient.MeterResponse response) {
|
||||
return new WeightOcrImageResult(
|
||||
source.toString(), outputRelative(paths, cropped), outputRelative(paths, rawResponse),
|
||||
Status.NO_WEIGHT_FOUND, null, null, response.candidates(),
|
||||
"百度响应中没有符合重量格式的候选值");
|
||||
}
|
||||
|
||||
private WeightOcrImageResult failed(Path source, BatchPaths paths, Path cropped,
|
||||
Path rawResponse, String error) {
|
||||
return new WeightOcrImageResult(
|
||||
source.toString(), Files.exists(cropped) ? outputRelative(paths, cropped) : null,
|
||||
Files.exists(rawResponse) ? outputRelative(paths, rawResponse) : null,
|
||||
Status.FAILED, null, null, List.of(), error);
|
||||
}
|
||||
|
||||
private WeightOcrBatchResult buildResult(String batchId, Instant startedAt, BatchPaths paths,
|
||||
List<WeightOcrImageResult> results) {
|
||||
return new WeightOcrBatchResult(
|
||||
batchId, startedAt, Instant.now(), paths.inputDisplayPath(), paths.outputDisplayPath(),
|
||||
results.size(), count(results, Status.RECOGNIZED),
|
||||
count(results, Status.NO_WEIGHT_FOUND), count(results, Status.FAILED),
|
||||
List.copyOf(results));
|
||||
}
|
||||
|
||||
private int count(List<WeightOcrImageResult> results, Status status) {
|
||||
return (int) results.stream().filter(result -> result.status() == status).count();
|
||||
}
|
||||
|
||||
private List<Path> findImages(Path input, Path output, boolean recursive, int maxBatchSize)
|
||||
throws IOException {
|
||||
int maxDepth = recursive ? Integer.MAX_VALUE : 1;
|
||||
List<Path> images;
|
||||
try (Stream<Path> paths = Files.walk(input, maxDepth)) {
|
||||
images = paths.filter(Files::isRegularFile)
|
||||
.filter(path -> !path.startsWith(output))
|
||||
.filter(this::isSupportedImage)
|
||||
.sorted(Comparator.comparing(Path::toString))
|
||||
.limit((long) maxBatchSize + 1)
|
||||
.toList();
|
||||
}
|
||||
if (images.size() > maxBatchSize) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST,
|
||||
"单批图片数量不能超过 " + maxBatchSize + " 张");
|
||||
}
|
||||
return images;
|
||||
}
|
||||
|
||||
private boolean isSupportedImage(Path path) {
|
||||
String name = path.getFileName().toString();
|
||||
int dot = name.lastIndexOf('.');
|
||||
return dot >= 0 && dot < name.length() - 1
|
||||
&& SUPPORTED_EXTENSIONS.contains(name.substring(dot + 1).toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
private Path replaceExtension(Path relative, String extension) {
|
||||
String name = relative.getFileName().toString();
|
||||
int dot = name.lastIndexOf('.');
|
||||
String baseName = dot > 0 ? name.substring(0, dot) : name;
|
||||
return relative.getParent() == null
|
||||
? Path.of(baseName + extension)
|
||||
: relative.getParent().resolve(baseName + extension);
|
||||
}
|
||||
|
||||
private String outputRelative(BatchPaths paths, Path path) {
|
||||
return paths.outputDirectory().relativize(path).toString();
|
||||
}
|
||||
|
||||
private String safeMessage(Exception exception) {
|
||||
String message = exception.getMessage();
|
||||
return message == null || message.isBlank() ? "OCR 识别失败" : message;
|
||||
}
|
||||
|
||||
private IntegrationProperties.BaiduOcr config() {
|
||||
return properties.baiduOcr();
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package com.baiyue.integration.service.ocr;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.integration.config.IntegrationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* 将 OCR 文件访问限制在配置的输入、输出根目录内。
|
||||
*/
|
||||
@Component
|
||||
public class WeightOcrStoragePolicy {
|
||||
private final IntegrationProperties properties;
|
||||
|
||||
public WeightOcrStoragePolicy(IntegrationProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
public BatchPaths resolve(String inputValue, String outputValue, String batchId) throws IOException {
|
||||
IntegrationProperties.BaiduOcr config = requireConfig();
|
||||
Path inputRoot = prepareRoot(config.inputRoot(), "OCR 输入根目录");
|
||||
Path outputRoot = prepareRoot(config.outputRoot(), "OCR 输出根目录");
|
||||
|
||||
Path input = resolveRequested(inputRoot, inputValue).toRealPath();
|
||||
requireInside(inputRoot, input, "输入目录超出允许范围");
|
||||
if (!Files.isDirectory(input) || !Files.isReadable(input)) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "输入目录不存在或不可读");
|
||||
}
|
||||
|
||||
String outputName = outputValue == null || outputValue.isBlank() ? batchId : outputValue;
|
||||
Path output = resolveRequested(outputRoot, outputName);
|
||||
requireInside(outputRoot, output, "输出目录超出允许范围");
|
||||
Files.createDirectories(output);
|
||||
output = output.toRealPath();
|
||||
requireInside(outputRoot, output, "输出目录超出允许范围");
|
||||
return new BatchPaths(inputRoot, outputRoot, input, output);
|
||||
}
|
||||
|
||||
private IntegrationProperties.BaiduOcr requireConfig() {
|
||||
IntegrationProperties.BaiduOcr config = properties.baiduOcr();
|
||||
if (config == null || config.inputRoot() == null || config.outputRoot() == null) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "重量 OCR 存储目录未配置");
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
private Path prepareRoot(Path configured, String name) throws IOException {
|
||||
Path root = configured.toAbsolutePath().normalize();
|
||||
Files.createDirectories(root);
|
||||
if (!Files.isDirectory(root) || !Files.isReadable(root) || !Files.isWritable(root)) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, name + "不可用");
|
||||
}
|
||||
return root.toRealPath();
|
||||
}
|
||||
|
||||
private Path resolveRequested(Path root, String value) {
|
||||
try {
|
||||
Path requested = Path.of(value);
|
||||
return (requested.isAbsolute() ? requested : root.resolve(requested)).normalize();
|
||||
} catch (RuntimeException exception) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "目录格式无效");
|
||||
}
|
||||
}
|
||||
|
||||
private void requireInside(Path root, Path path, String message) {
|
||||
if (!path.startsWith(root)) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, message);
|
||||
}
|
||||
}
|
||||
|
||||
public record BatchPaths(Path inputRoot, Path outputRoot, Path inputDirectory, Path outputDirectory) {
|
||||
public String inputDisplayPath() {
|
||||
return display(inputRoot, inputDirectory);
|
||||
}
|
||||
|
||||
public String outputDisplayPath() {
|
||||
return display(outputRoot, outputDirectory);
|
||||
}
|
||||
|
||||
private String display(Path root, Path path) {
|
||||
String relative = root.relativize(path).toString();
|
||||
return relative.isBlank() ? "." : relative;
|
||||
}
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package com.baiyue.integration.service.ocr;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
class ImageCropperTest {
|
||||
@TempDir
|
||||
Path tempDirectory;
|
||||
|
||||
@Test
|
||||
void cropsConfiguredBottomCenterRegion() throws Exception {
|
||||
Path source = createImage();
|
||||
Path target = tempDirectory.resolve("cropped/result.jpg");
|
||||
|
||||
new ImageCropper().cropBottomCenter(source, target, 0.5, 0.25, 10_000);
|
||||
|
||||
BufferedImage cropped = ImageIO.read(target.toFile());
|
||||
assertThat(cropped.getWidth()).isEqualTo(50);
|
||||
assertThat(cropped.getHeight()).isEqualTo(40);
|
||||
assertThat(new Color(cropped.getRGB(10, 10))).isEqualTo(Color.BLACK);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsImageOverPixelLimit() throws Exception {
|
||||
Path source = createImage();
|
||||
assertThatThrownBy(() -> new ImageCropper()
|
||||
.cropBottomCenter(source, tempDirectory.resolve("result.jpg"), 0.5, 0.25, 100))
|
||||
.hasMessageContaining("像素超过");
|
||||
}
|
||||
|
||||
private Path createImage() throws Exception {
|
||||
BufferedImage image = new BufferedImage(100, 80, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D graphics = image.createGraphics();
|
||||
graphics.setColor(Color.WHITE);
|
||||
graphics.fillRect(0, 0, 100, 40);
|
||||
graphics.setColor(Color.BLACK);
|
||||
graphics.fillRect(0, 40, 100, 40);
|
||||
graphics.dispose();
|
||||
Path source = tempDirectory.resolve("source.png");
|
||||
ImageIO.write(image, "png", source.toFile());
|
||||
return source;
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package com.baiyue.integration.service.ocr;
|
||||
|
||||
import com.baiyue.integration.dto.ocr.OcrCandidate;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class WeightCandidateSelectorTest {
|
||||
private final WeightCandidateSelector selector = new WeightCandidateSelector();
|
||||
|
||||
@Test
|
||||
void selectsLargestNumericCandidateAndNormalizesDecimalSeparator() {
|
||||
List<OcrCandidate> candidates = List.of(
|
||||
new OcrCandidate("10:38", 0.99, 10, 10, 40, 10),
|
||||
new OcrCandidate(" 12,35 ", 0.97, 20, 30, 120, 60),
|
||||
new OcrCandidate("29", 0.99, 160, 10, 20, 10),
|
||||
new OcrCandidate("kg", 0.99, 150, 70, 30, 15));
|
||||
|
||||
WeightCandidateSelector.SelectedWeight selected = selector.select(candidates).orElseThrow();
|
||||
|
||||
assertThat(selected.weight()).isEqualByComparingTo("12.35");
|
||||
assertThat(selected.confidence()).isEqualTo(0.97);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsNonNumericCandidates() {
|
||||
assertThat(selector.select(List.of(
|
||||
new OcrCandidate("kg", 0.99, 0, 0, 20, 10),
|
||||
new OcrCandidate("10:38", 0.99, 0, 0, 20, 10)))).isEmpty();
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package com.baiyue.integration.service.ocr;
|
||||
|
||||
import com.baiyue.integration.config.IntegrationProperties;
|
||||
import com.baiyue.integration.dto.ocr.OcrCandidate;
|
||||
import com.baiyue.integration.dto.ocr.WeightOcrBatchRequest;
|
||||
import com.baiyue.integration.dto.ocr.WeightOcrImageResult.Status;
|
||||
import com.baiyue.integration.provider.ocr.BaiduMeterClient;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.springframework.util.unit.DataSize;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.Color;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class WeightOcrBatchServiceTest {
|
||||
@TempDir
|
||||
Path tempDirectory;
|
||||
|
||||
@Test
|
||||
void completesBatchAndWritesArtifactsWithoutCallingRealProvider() throws Exception {
|
||||
Path inputRoot = Files.createDirectories(tempDirectory.resolve("input"));
|
||||
Path outputRoot = tempDirectory.resolve("output");
|
||||
Path batchInput = Files.createDirectories(inputRoot.resolve("batch-a"));
|
||||
BufferedImage image = new BufferedImage(100, 80, BufferedImage.TYPE_INT_RGB);
|
||||
for (int y = 0; y < image.getHeight(); y++) {
|
||||
for (int x = 0; x < image.getWidth(); x++) {
|
||||
image.setRGB(x, y, Color.BLACK.getRGB());
|
||||
}
|
||||
}
|
||||
ImageIO.write(image, "png", batchInput.resolve("weight.png").toFile());
|
||||
|
||||
IntegrationProperties properties = properties(inputRoot, outputRoot);
|
||||
BaiduMeterClient client = mock(BaiduMeterClient.class);
|
||||
when(client.recognize(any())).thenReturn(new BaiduMeterClient.MeterResponse(
|
||||
"{\"words_result\":[]}",
|
||||
List.of(new OcrCandidate("12.35", 0.98, 10, 10, 80, 30))));
|
||||
ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules();
|
||||
WeightOcrBatchService service = new WeightOcrBatchService(
|
||||
properties, new WeightOcrStoragePolicy(properties), new ImageCropper(), client,
|
||||
new WeightCandidateSelector(), objectMapper);
|
||||
|
||||
var result = service.recognize(new WeightOcrBatchRequest("batch-a", null, null, null, false));
|
||||
|
||||
assertThat(result.total()).isEqualTo(1);
|
||||
assertThat(result.recognized()).isEqualTo(1);
|
||||
assertThat(result.results().get(0).status()).isEqualTo(Status.RECOGNIZED);
|
||||
Path batchOutput = outputRoot.resolve(result.outputDirectory());
|
||||
assertThat(batchOutput.resolve("cropped/weight.jpg")).isRegularFile();
|
||||
assertThat(batchOutput.resolve("raw/weight.json")).isRegularFile();
|
||||
assertThat(batchOutput.resolve("batch-results.json")).isRegularFile();
|
||||
}
|
||||
|
||||
private IntegrationProperties properties(Path inputRoot, Path outputRoot) {
|
||||
IntegrationProperties.BaiduOcr baidu = new IntegrationProperties.BaiduOcr(
|
||||
true, "key", "secret", URI.create("https://example.com/token"),
|
||||
URI.create("https://example.com/meter"), Duration.ofSeconds(1), Duration.ofSeconds(1),
|
||||
inputRoot, outputRoot, DataSize.ofMegabytes(3), 40_000_000L,
|
||||
50, Duration.ZERO, 0);
|
||||
return new IntegrationProperties(baidu, null);
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package com.baiyue.integration.service.ocr;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.integration.config.IntegrationProperties;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.springframework.util.unit.DataSize;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
class WeightOcrStoragePolicyTest {
|
||||
@TempDir
|
||||
Path tempDirectory;
|
||||
|
||||
@Test
|
||||
void resolvesDirectoriesInsideConfiguredRoots() throws Exception {
|
||||
Path input = Files.createDirectories(tempDirectory.resolve("input/batch-a"));
|
||||
Path outputRoot = tempDirectory.resolve("output");
|
||||
WeightOcrStoragePolicy policy = new WeightOcrStoragePolicy(properties(input.getParent(), outputRoot));
|
||||
|
||||
WeightOcrStoragePolicy.BatchPaths paths = policy.resolve("batch-a", null, "batch-id");
|
||||
|
||||
assertThat(paths.inputDirectory()).isEqualTo(input.toRealPath());
|
||||
assertThat(paths.outputDirectory()).isEqualTo(outputRoot.resolve("batch-id").toRealPath());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsInputAndOutputOutsideConfiguredRoots() throws Exception {
|
||||
Path inputRoot = Files.createDirectories(tempDirectory.resolve("input"));
|
||||
Path outputRoot = tempDirectory.resolve("output");
|
||||
Path outside = Files.createDirectories(tempDirectory.resolve("outside"));
|
||||
WeightOcrStoragePolicy policy = new WeightOcrStoragePolicy(properties(inputRoot, outputRoot));
|
||||
|
||||
assertThatThrownBy(() -> policy.resolve(outside.toString(), null, "batch-id"))
|
||||
.isInstanceOfSatisfying(BizException.class,
|
||||
exception -> assertThat(exception.getResultCode()).isEqualTo(ResultCode.FORBIDDEN));
|
||||
assertThatThrownBy(() -> policy.resolve(".", "../outside", "batch-id"))
|
||||
.isInstanceOfSatisfying(BizException.class,
|
||||
exception -> assertThat(exception.getResultCode()).isEqualTo(ResultCode.FORBIDDEN));
|
||||
}
|
||||
|
||||
private IntegrationProperties properties(Path inputRoot, Path outputRoot) {
|
||||
IntegrationProperties.BaiduOcr baidu = new IntegrationProperties.BaiduOcr(
|
||||
true, "key", "secret", URI.create("https://example.com/token"),
|
||||
URI.create("https://example.com/meter"), Duration.ofSeconds(1), Duration.ofSeconds(1),
|
||||
inputRoot, outputRoot, DataSize.ofMegabytes(3), 40_000_000L,
|
||||
50, Duration.ZERO, 0);
|
||||
return new IntegrationProperties(baidu, null);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user