Merge branch 'test' into test-02huogui
# Conflicts: # deploy/sql/头程数据库统一增量-MySQL8.sql # firstleg/src/main/java/com/baiyue/firstleg/service/FbaPricingService.java # firstleg/src/main/resources/application.yml # firstleg/src/main/resources/openapi/fba-pricing-v1.yaml # firstleg/src/test/java/com/baiyue/firstleg/service/FbaPricingServiceTest.java # order/src/main/java/com/baiyue/order/controller/PlatformFirstLegOrderController.java # order/src/test/java/com/baiyue/order/controller/OrderControllerPermissionTest.java
This commit is contained in:
@@ -1,16 +1,22 @@
|
||||
package com.baiyue.integration.dto.ocr;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 百度 OCR 返回的文字候选及其识别框。
|
||||
*/
|
||||
public record OcrCandidate(
|
||||
String text,
|
||||
Double confidence,
|
||||
BigDecimal confidence,
|
||||
Integer left,
|
||||
Integer top,
|
||||
Integer width,
|
||||
Integer height
|
||||
) {
|
||||
public OcrCandidate {
|
||||
confidence = OcrConfidence.normalize(confidence);
|
||||
}
|
||||
|
||||
public long area() {
|
||||
if (width == null || height == null || width <= 0 || height <= 0) {
|
||||
return 0;
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.baiyue.integration.dto.ocr;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
|
||||
/** Canonical confidence representation shared by OCR response models. */
|
||||
public final class OcrConfidence {
|
||||
private static final int SCALE = 5;
|
||||
private static final BigDecimal ZERO = BigDecimal.ZERO.setScale(SCALE);
|
||||
private static final BigDecimal ONE = BigDecimal.ONE.setScale(SCALE);
|
||||
|
||||
private OcrConfidence() {
|
||||
}
|
||||
|
||||
public static BigDecimal normalize(BigDecimal value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value.compareTo(BigDecimal.ZERO) < 0) {
|
||||
return ZERO;
|
||||
}
|
||||
if (value.compareTo(BigDecimal.ONE) > 0) {
|
||||
return ONE;
|
||||
}
|
||||
return value.setScale(SCALE, RoundingMode.HALF_UP);
|
||||
}
|
||||
}
|
||||
+6
-1
@@ -12,10 +12,15 @@ public record WeightOcrImageResult(
|
||||
String rawResponseFile,
|
||||
Status status,
|
||||
BigDecimal weight,
|
||||
Double confidence,
|
||||
BigDecimal confidence,
|
||||
List<OcrCandidate> candidates,
|
||||
String ocrRawText,
|
||||
String error
|
||||
) {
|
||||
public WeightOcrImageResult {
|
||||
confidence = OcrConfidence.normalize(confidence);
|
||||
}
|
||||
|
||||
public enum Status {
|
||||
/** 已成功识别到有效重量。 */
|
||||
RECOGNIZED,
|
||||
|
||||
+4
-3
@@ -12,6 +12,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.net.http.HttpClient;
|
||||
@@ -163,7 +164,7 @@ public class BaiduMeterClient {
|
||||
JsonNode location = item.path("location");
|
||||
candidates.add(new OcrCandidate(
|
||||
item.path("words").asText(),
|
||||
nullableDouble(probability.get("average")),
|
||||
nullableDecimal(probability.get("average")),
|
||||
nullableInt(location.get("left")),
|
||||
nullableInt(location.get("top")),
|
||||
nullableInt(location.get("width")),
|
||||
@@ -195,8 +196,8 @@ public class BaiduMeterClient {
|
||||
return URLEncoder.encode(value, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static Double nullableDouble(JsonNode node) {
|
||||
return node == null || !node.isNumber() ? null : node.doubleValue();
|
||||
private static BigDecimal nullableDecimal(JsonNode node) {
|
||||
return node == null || !node.isNumber() ? null : node.decimalValue();
|
||||
}
|
||||
|
||||
private static Integer nullableInt(JsonNode node) {
|
||||
|
||||
+3
-2
@@ -25,7 +25,8 @@ public class WeightCandidateSelector {
|
||||
.flatMap(Optional::stream)
|
||||
.max(Comparator
|
||||
.comparingLong((SelectedWeight selected) -> selected.candidate().area())
|
||||
.thenComparing(selected -> selected.confidence() == null ? -1 : selected.confidence()));
|
||||
.thenComparing(selected -> selected.confidence() == null
|
||||
? BigDecimal.valueOf(-1) : selected.confidence()));
|
||||
}
|
||||
|
||||
private Optional<SelectedWeight> toSelectedWeight(OcrCandidate candidate) {
|
||||
@@ -55,6 +56,6 @@ public class WeightCandidateSelector {
|
||||
.replace('-', '-');
|
||||
}
|
||||
|
||||
public record SelectedWeight(BigDecimal weight, Double confidence, OcrCandidate candidate) {
|
||||
public record SelectedWeight(BigDecimal weight, BigDecimal confidence, OcrCandidate candidate) {
|
||||
}
|
||||
}
|
||||
|
||||
+30
-2
@@ -34,6 +34,7 @@ import java.util.stream.Stream;
|
||||
@Service
|
||||
public class WeightOcrBatchService {
|
||||
private static final Set<String> SUPPORTED_EXTENSIONS = Set.of("jpg", "jpeg", "png", "bmp");
|
||||
private static final int RAW_TEXT_MAX_LENGTH = 1000;
|
||||
|
||||
@Resource private IntegrationProperties properties;
|
||||
@Resource private WeightOcrStoragePolicy storagePolicy;
|
||||
@@ -113,7 +114,7 @@ public class WeightOcrBatchService {
|
||||
return new WeightOcrImageResult(
|
||||
source.toString(), outputRelative(paths, cropped), outputRelative(paths, rawResponse),
|
||||
Status.RECOGNIZED, selected.weight(), selected.confidence(),
|
||||
response.candidates(), null);
|
||||
response.candidates(), summarizeCandidates(response.candidates()), null);
|
||||
}
|
||||
|
||||
private WeightOcrImageResult noWeight(Path source, BatchPaths paths, Path cropped,
|
||||
@@ -121,6 +122,7 @@ public class WeightOcrBatchService {
|
||||
return new WeightOcrImageResult(
|
||||
source.toString(), outputRelative(paths, cropped), outputRelative(paths, rawResponse),
|
||||
Status.NO_WEIGHT_FOUND, null, null, response.candidates(),
|
||||
summarizeCandidates(response.candidates()),
|
||||
"百度响应中没有符合重量格式的候选值");
|
||||
}
|
||||
|
||||
@@ -129,7 +131,33 @@ public class WeightOcrBatchService {
|
||||
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);
|
||||
Status.FAILED, null, null, List.of(), null, error);
|
||||
}
|
||||
|
||||
private String summarizeCandidates(List<com.baiyue.integration.dto.ocr.OcrCandidate> candidates) {
|
||||
if (candidates == null || candidates.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
StringBuilder summary = new StringBuilder();
|
||||
for (var candidate : candidates) {
|
||||
if (candidate == null || candidate.text() == null) {
|
||||
continue;
|
||||
}
|
||||
String text = candidate.text().replaceAll("[\\p{Cc}\\p{Cf}]+", " ").strip();
|
||||
if (text.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
int separatorLength = summary.isEmpty() ? 0 : 1;
|
||||
int remaining = RAW_TEXT_MAX_LENGTH - summary.length() - separatorLength;
|
||||
if (remaining <= 0) {
|
||||
break;
|
||||
}
|
||||
if (separatorLength > 0) {
|
||||
summary.append('\n');
|
||||
}
|
||||
summary.append(text, 0, Math.min(text.length(), remaining));
|
||||
}
|
||||
return summary.isEmpty() ? null : summary.toString();
|
||||
}
|
||||
|
||||
private WeightOcrBatchResult buildResult(String batchId, Instant startedAt, BatchPaths paths,
|
||||
|
||||
+3
-1
@@ -56,7 +56,9 @@ public class WeightOcrSingleImageService {
|
||||
if (batch.results() == null || batch.results().size() != 1) {
|
||||
throw new BizException(ResultCode.INTERNAL_ERROR, "OCR单图识别结果数量异常");
|
||||
}
|
||||
return batch.results().get(0);
|
||||
WeightOcrImageResult result = batch.results().get(0);
|
||||
return new WeightOcrImageResult(null, null, null, result.status(), result.weight(),
|
||||
result.confidence(), java.util.List.of(), result.ocrRawText(), result.error());
|
||||
} finally {
|
||||
deleteRequestDirectory(requestDirectory);
|
||||
}
|
||||
|
||||
+18
-7
@@ -4,6 +4,7 @@ import com.baiyue.integration.dto.ocr.OcrCandidate;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -15,21 +16,31 @@ class WeightCandidateSelectorTest {
|
||||
@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));
|
||||
new OcrCandidate("10:38", new BigDecimal("0.99"), 10, 10, 40, 10),
|
||||
new OcrCandidate(" 12,35 ", new BigDecimal("0.97"), 20, 30, 120, 60),
|
||||
new OcrCandidate("29", new BigDecimal("0.99"), 160, 10, 20, 10),
|
||||
new OcrCandidate("kg", new BigDecimal("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);
|
||||
assertThat(selected.confidence()).isEqualByComparingTo("0.97000");
|
||||
}
|
||||
|
||||
@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();
|
||||
new OcrCandidate("kg", new BigDecimal("0.99"), 0, 0, 20, 10),
|
||||
new OcrCandidate("10:38", new BigDecimal("0.99"), 0, 0, 20, 10)))).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void canonicalizesConfidenceToRangeAndFiveDecimalPlaces() {
|
||||
assertThat(new OcrCandidate("1", new BigDecimal("0.5097191334"), 0, 0, 1, 1)
|
||||
.confidence()).isEqualByComparingTo("0.50972");
|
||||
assertThat(new OcrCandidate("1", new BigDecimal("1.2"), 0, 0, 1, 1)
|
||||
.confidence()).isEqualByComparingTo("1.00000");
|
||||
assertThat(new OcrCandidate("1", new BigDecimal("-0.1"), 0, 0, 1, 1)
|
||||
.confidence()).isEqualByComparingTo("0.00000");
|
||||
}
|
||||
}
|
||||
|
||||
+6
-1
@@ -19,6 +19,7 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.Color;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.math.BigDecimal;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -56,13 +57,17 @@ class WeightOcrBatchServiceTest {
|
||||
when(properties.baiduOcr()).thenReturn(properties(inputRoot, outputRoot).baiduOcr());
|
||||
when(client.recognize(any())).thenReturn(new BaiduMeterClient.MeterResponse(
|
||||
"{\"words_result\":[]}",
|
||||
List.of(new OcrCandidate("12.35", 0.98, 10, 10, 80, 30))));
|
||||
List.of(
|
||||
new OcrCandidate("12.35", new BigDecimal("0.5097191334"), 10, 10, 80, 30),
|
||||
new OcrCandidate("KG\u0000", new BigDecimal("0.98"), 10, 50, 30, 10))));
|
||||
|
||||
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);
|
||||
assertThat(result.results().get(0).confidence()).isEqualByComparingTo("0.50972");
|
||||
assertThat(result.results().get(0).ocrRawText()).isEqualTo("12.35\nKG");
|
||||
Path batchOutput = outputRoot.resolve(result.outputDirectory());
|
||||
assertThat(batchOutput.resolve("cropped/weight.jpg")).isRegularFile();
|
||||
assertThat(batchOutput.resolve("raw/weight.json")).isRegularFile();
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
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.WeightOcrBatchResult;
|
||||
import com.baiyue.integration.dto.ocr.WeightOcrImageResult;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
import org.springframework.util.unit.DataSize;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@SpringJUnitConfig(WeightOcrSingleImageService.class)
|
||||
class WeightOcrSingleImageServiceTest {
|
||||
@TempDir Path tempDirectory;
|
||||
@MockBean IntegrationProperties properties;
|
||||
@MockBean WeightOcrBatchService batches;
|
||||
@Resource WeightOcrSingleImageService service;
|
||||
|
||||
@Test
|
||||
void returnsSafeContractWithoutTemporaryPathsOrCandidateDetails() throws Exception {
|
||||
var config = new IntegrationProperties.BaiduOcr(true, "key", "secret",
|
||||
URI.create("https://example.com/token"), URI.create("https://example.com/meter"),
|
||||
Duration.ofSeconds(1), Duration.ofSeconds(1), tempDirectory, tempDirectory.resolve("out"),
|
||||
DataSize.ofMegabytes(3), 40_000_000L, 50, Duration.ZERO, 0);
|
||||
when(properties.baiduOcr()).thenReturn(config);
|
||||
var imageResult = new WeightOcrImageResult("weight.jpg", "cropped/weight.jpg", "raw/weight.json",
|
||||
WeightOcrImageResult.Status.RECOGNIZED, new BigDecimal("285.5"),
|
||||
new BigDecimal("0.5097191334"),
|
||||
List.of(new OcrCandidate("285.5", new BigDecimal("0.5097191334"), 0, 0, 10, 10)),
|
||||
"285.5", null);
|
||||
when(batches.recognize(any())).thenReturn(new WeightOcrBatchResult("batch", Instant.now(),
|
||||
Instant.now(), "input", "output", 1, 1, 0, 0, List.of(imageResult)));
|
||||
|
||||
var result = service.recognize(new MockMultipartFile(
|
||||
"file", "weight.jpg", "image/jpeg", new byte[]{1, 2, 3}));
|
||||
|
||||
assertThat(result.confidence()).isEqualByComparingTo("0.50972");
|
||||
assertThat(result.ocrRawText()).isEqualTo("285.5");
|
||||
assertThat(result.sourceImage()).isNull();
|
||||
assertThat(result.croppedImage()).isNull();
|
||||
assertThat(result.rawResponseFile()).isNull();
|
||||
assertThat(result.candidates()).isEmpty();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user