优化
This commit is contained in:
@@ -3203,6 +3203,21 @@ CREATE TABLE IF NOT EXISTS fl_price_origin_alias (
|
|||||||
KEY idx_price_origin_alias_name (alias_name, status)
|
KEY idx_price_origin_alias_name (alias_name, status)
|
||||||
) ENGINE=InnoDB COMMENT='价格表起运仓名称别名映射';
|
) ENGINE=InnoDB COMMENT='价格表起运仓名称别名映射';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS fl_price_warehouse_mapping (
|
||||||
|
id BIGINT UNSIGNED NOT NULL COMMENT '价格仓映射ID',
|
||||||
|
source_type VARCHAR(24) NOT NULL COMMENT '来源维度:ORIGIN_CODE或OVERSEAS_NAME',
|
||||||
|
source_value VARCHAR(128) NOT NULL COMMENT '解析后的来源仓编码或海外仓名称',
|
||||||
|
warehouse_id BIGINT UNSIGNED NOT NULL COMMENT '固定平台公共仓库ID',
|
||||||
|
warehouse_code VARCHAR(64) NOT NULL COMMENT '平台仓库稳定编码快照',
|
||||||
|
warehouse_type VARCHAR(16) NOT NULL COMMENT 'DOMESTIC或OVERSEAS',
|
||||||
|
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE',
|
||||||
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY uk_price_warehouse_mapping_source (source_type, source_value),
|
||||||
|
KEY idx_price_warehouse_mapping_target (warehouse_id, status)
|
||||||
|
) ENGINE=InnoDB COMMENT='价格表来源仓与固定平台公共仓映射';
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS fl_price_book_version (
|
CREATE TABLE IF NOT EXISTS fl_price_book_version (
|
||||||
id BIGINT UNSIGNED NOT NULL COMMENT '整表价格版本ID',
|
id BIGINT UNSIGNED NOT NULL COMMENT '整表价格版本ID',
|
||||||
tenant_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '平台公共价格作用域固定为0',
|
tenant_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '平台公共价格作用域固定为0',
|
||||||
@@ -3287,9 +3302,21 @@ VALUES
|
|||||||
(3, 'CN-YIWU-01', '义乌', 'ACTIVE'),
|
(3, 'CN-YIWU-01', '义乌', 'ACTIVE'),
|
||||||
(4, 'CN-DONGGUAN-01', '华南仓(深圳)', 'ACTIVE'),
|
(4, 'CN-DONGGUAN-01', '华南仓(深圳)', 'ACTIVE'),
|
||||||
(5, 'CN-DONGGUAN-01', '东莞仓', 'ACTIVE'),
|
(5, 'CN-DONGGUAN-01', '东莞仓', 'ACTIVE'),
|
||||||
(6, 'CN-DONGGUAN-01', '深圳仓', 'ACTIVE')
|
(6, 'CN-DONGGUAN-01', '深圳仓', 'ACTIVE'),
|
||||||
|
(7, 'CN-DONGGUAN-01', '华南仓(深圳/东莞)', 'ACTIVE')
|
||||||
ON DUPLICATE KEY UPDATE status = VALUES(status);
|
ON DUPLICATE KEY UPDATE status = VALUES(status);
|
||||||
|
|
||||||
|
INSERT INTO fl_price_warehouse_mapping
|
||||||
|
(id, source_type, source_value, warehouse_id, warehouse_code, warehouse_type, status)
|
||||||
|
VALUES
|
||||||
|
(1, 'ORIGIN_CODE', 'CN-YIWU-01', 193000000000200001, 'CN-YIWU-01', 'DOMESTIC', 'ACTIVE'),
|
||||||
|
(2, 'ORIGIN_CODE', 'CN-DONGGUAN-01', 193000000000200002, 'CN-DONGGUAN-01', 'DOMESTIC', 'ACTIVE'),
|
||||||
|
(3, 'OVERSEAS_NAME', '洛杉矶仓', 193000000000200003, 'US-LAX-01', 'OVERSEAS', 'ACTIVE'),
|
||||||
|
(4, 'OVERSEAS_NAME', '纽约仓', 193000000000200004, 'US-NYC-01', 'OVERSEAS', 'ACTIVE')
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
warehouse_id = VALUES(warehouse_id), warehouse_code = VALUES(warehouse_code),
|
||||||
|
warehouse_type = VALUES(warehouse_type), status = VALUES(status);
|
||||||
|
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
-- 来源:iam\src\main\resources\db\phase2-inbound-query-permissions.sql
|
-- 来源:iam\src\main\resources\db\phase2-inbound-query-permissions.sql
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
@@ -3676,6 +3703,7 @@ CALL unified_assert_column('baiyue_firstleg', 'fl_inbound_order', 'pricing_tax_m
|
|||||||
CALL unified_assert_column('baiyue_firstleg', 'fl_inbound_measurement_item', 'external_detail_no');
|
CALL unified_assert_column('baiyue_firstleg', 'fl_inbound_measurement_item', 'external_detail_no');
|
||||||
CALL unified_assert_table('baiyue_firstleg', 'fl_price_channel_catalog');
|
CALL unified_assert_table('baiyue_firstleg', 'fl_price_channel_catalog');
|
||||||
CALL unified_assert_table('baiyue_firstleg', 'fl_price_origin_alias');
|
CALL unified_assert_table('baiyue_firstleg', 'fl_price_origin_alias');
|
||||||
|
CALL unified_assert_table('baiyue_firstleg', 'fl_price_warehouse_mapping');
|
||||||
CALL unified_assert_table('baiyue_firstleg', 'fl_price_book_version');
|
CALL unified_assert_table('baiyue_firstleg', 'fl_price_book_version');
|
||||||
CALL unified_assert_column('baiyue_firstleg', 'fl_price_book_version', 'parse_issues_json');
|
CALL unified_assert_column('baiyue_firstleg', 'fl_price_book_version', 'parse_issues_json');
|
||||||
CALL unified_assert_table('baiyue_firstleg', 'fl_price_book_channel');
|
CALL unified_assert_table('baiyue_firstleg', 'fl_price_book_channel');
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ public interface WeightOcrClient {
|
|||||||
ApiResponse<Result> recognize(@RequestHeader("X-Internal-Service-Token") String token,
|
ApiResponse<Result> recognize(@RequestHeader("X-Internal-Service-Token") String token,
|
||||||
@RequestPart("file") MultipartFile file);
|
@RequestPart("file") MultipartFile file);
|
||||||
|
|
||||||
record Result(String sourceImage, String croppedImage, String rawResponseFile,
|
record Result(String status, BigDecimal weight, BigDecimal confidence,
|
||||||
String status, BigDecimal weight, Double confidence,
|
String ocrRawText, String error) {}
|
||||||
java.util.List<?> candidates, String error) {}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import java.io.ByteArrayInputStream;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -39,7 +40,7 @@ public class MeasurementImageService {
|
|||||||
FileObjectEntity object = storeObject(
|
FileObjectEntity object = storeObject(
|
||||||
tenantId, inboundId, operatorId, file, "INBOUND_MEASUREMENT_IMAGE");
|
tenantId, inboundId, operatorId, file, "INBOUND_MEASUREMENT_IMAGE");
|
||||||
return new UploadResult(object.getId(), object.getOriginalName(), object.getContentType(),
|
return new UploadResult(object.getId(), object.getOriginalName(), object.getContentType(),
|
||||||
object.getSizeBytes(), "NOT_REQUIRED", null, null, null, null);
|
object.getSizeBytes(), "NOT_REQUIRED", null, null, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private FileObjectEntity storeObject(long tenantId, long inboundId, long operatorId,
|
private FileObjectEntity storeObject(long tenantId, long inboundId, long operatorId,
|
||||||
@@ -116,7 +117,8 @@ public class MeasurementImageService {
|
|||||||
}
|
}
|
||||||
var result = response.data();
|
var result = response.data();
|
||||||
return new UploadResult(object.getId(), object.getOriginalName(), object.getContentType(),
|
return new UploadResult(object.getId(), object.getOriginalName(), object.getContentType(),
|
||||||
object.getSizeBytes(), result.status(), result.weight(), "KG", result.confidence(), result.error());
|
object.getSizeBytes(), result.status(), result.weight(), "KG",
|
||||||
|
normalizeConfidence(result.confidence()), result.ocrRawText(), result.error());
|
||||||
} catch (FeignException exception) {
|
} catch (FeignException exception) {
|
||||||
return failed(object, "OCR服务不可用,可稍后重试或人工录入");
|
return failed(object, "OCR服务不可用,可稍后重试或人工录入");
|
||||||
}
|
}
|
||||||
@@ -124,12 +126,19 @@ public class MeasurementImageService {
|
|||||||
|
|
||||||
private UploadResult failed(FileObjectEntity object, String message) {
|
private UploadResult failed(FileObjectEntity object, String message) {
|
||||||
return new UploadResult(object.getId(), object.getOriginalName(), object.getContentType(),
|
return new UploadResult(object.getId(), object.getOriginalName(), object.getContentType(),
|
||||||
object.getSizeBytes(), "FAILED", null, null, null, message);
|
object.getSizeBytes(), "FAILED", null, null, null, null, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal normalizeConfidence(BigDecimal value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
if (value.compareTo(BigDecimal.ZERO) < 0) value = BigDecimal.ZERO;
|
||||||
|
if (value.compareTo(BigDecimal.ONE) > 0) value = BigDecimal.ONE;
|
||||||
|
return value.setScale(5, RoundingMode.HALF_UP);
|
||||||
}
|
}
|
||||||
|
|
||||||
public record UploadResult(Long fileId, String fileName, String contentType, Long sizeBytes,
|
public record UploadResult(Long fileId, String fileName, String contentType, Long sizeBytes,
|
||||||
String recognitionStatus, BigDecimal weightValue, String weightUnit,
|
String recognitionStatus, BigDecimal weightValue, String weightUnit,
|
||||||
Double confidence, String error) {}
|
BigDecimal confidence, String ocrRawText, String error) {}
|
||||||
public record Download(String fileName, String contentType, byte[] content) {}
|
public record Download(String fileName, String contentType, byte[] content) {}
|
||||||
|
|
||||||
private record StoredImageMultipartFile(String originalFilename, String contentType, byte[] bytes)
|
private record StoredImageMultipartFile(String originalFilename, String contentType, byte[] bytes)
|
||||||
|
|||||||
+1
-1
@@ -29,7 +29,7 @@ class InternalMeasurementImageControllerTest {
|
|||||||
var file = new MockMultipartFile("file", "measure.png", MediaType.IMAGE_PNG_VALUE,
|
var file = new MockMultipartFile("file", "measure.png", MediaType.IMAGE_PNG_VALUE,
|
||||||
new byte[]{1, 2, 3});
|
new byte[]{1, 2, 3});
|
||||||
var result = new MeasurementImageService.UploadResult(41L, "measure.png",
|
var result = new MeasurementImageService.UploadResult(41L, "measure.png",
|
||||||
MediaType.IMAGE_PNG_VALUE, 3L, "NOT_REQUIRED", null, null, null, null);
|
MediaType.IMAGE_PNG_VALUE, 3L, "NOT_REQUIRED", null, null, null, null, null);
|
||||||
when(images.uploadMeasurementEvidence(11L, 21L, 31L, file)).thenReturn(result);
|
when(images.uploadMeasurementEvidence(11L, 21L, 31L, file)).thenReturn(result);
|
||||||
|
|
||||||
var response = controller.upload("internal-token", 31L, 11L, 21L, file);
|
var response = controller.upload("internal-token", 31L, 11L, 21L, file);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.baiyue.file.service;
|
package com.baiyue.file.service;
|
||||||
|
|
||||||
|
import com.baiyue.common.core.api.ApiResponse;
|
||||||
import com.baiyue.file.client.WeightOcrClient;
|
import com.baiyue.file.client.WeightOcrClient;
|
||||||
import com.baiyue.file.entity.FileObjectEntity;
|
import com.baiyue.file.entity.FileObjectEntity;
|
||||||
import com.baiyue.file.mapper.FileObjectMapper;
|
import com.baiyue.file.mapper.FileObjectMapper;
|
||||||
@@ -18,6 +19,7 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
|||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
@@ -91,4 +93,27 @@ class MeasurementImageServiceEvidenceTest {
|
|||||||
.isInstanceOf(com.baiyue.common.core.exception.BizException.class);
|
.isInstanceOf(com.baiyue.common.core.exception.BizException.class);
|
||||||
verifyNoInteractions(storage);
|
verifyNoInteractions(storage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void forwardsSafeOcrTextAndCanonicalConfidence() {
|
||||||
|
FileObjectEntity object = new FileObjectEntity();
|
||||||
|
object.setId(41L);
|
||||||
|
object.setTenantId(11L);
|
||||||
|
object.setBusinessType("INBOUND_WEIGHT_IMAGE");
|
||||||
|
object.setBusinessId("21");
|
||||||
|
object.setOriginalName("weight.jpg");
|
||||||
|
object.setContentType(MediaType.IMAGE_JPEG_VALUE);
|
||||||
|
object.setObjectKey("measurements/11/21/weight.jpg");
|
||||||
|
object.setSizeBytes(3L);
|
||||||
|
when(files.selectOne(any())).thenReturn(object);
|
||||||
|
when(storage.read(object.getObjectKey())).thenReturn(new byte[]{1, 2, 3});
|
||||||
|
when(ocr.recognize(org.mockito.ArgumentMatchers.eq("internal-token"), any()))
|
||||||
|
.thenReturn(ApiResponse.success(new WeightOcrClient.Result("RECOGNIZED",
|
||||||
|
new BigDecimal("285.5"), new BigDecimal("0.5097191334"), "285.5\nKG", null)));
|
||||||
|
|
||||||
|
var result = service.retryRecognition(11L, 21L, 41L);
|
||||||
|
|
||||||
|
assertThat(result.confidence()).isEqualByComparingTo("0.50972");
|
||||||
|
assertThat(result.ocrRawText()).isEqualTo("285.5\nKG");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.baiyue.firstleg.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@TableName("fl_price_warehouse_mapping")
|
||||||
|
public class PriceWarehouseMappingEntity {
|
||||||
|
@TableId(type = IdType.ASSIGN_ID)
|
||||||
|
private Long id;
|
||||||
|
private String sourceType;
|
||||||
|
private String sourceValue;
|
||||||
|
private Long warehouseId;
|
||||||
|
private String warehouseCode;
|
||||||
|
private String warehouseType;
|
||||||
|
private String status;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package com.baiyue.firstleg.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.baiyue.firstleg.entity.PriceWarehouseMappingEntity;
|
||||||
|
|
||||||
|
public interface PriceWarehouseMappingMapper extends BaseMapper<PriceWarehouseMappingEntity> {}
|
||||||
@@ -7,11 +7,13 @@ import com.baiyue.firstleg.entity.PriceBookChannelEntity;
|
|||||||
import com.baiyue.firstleg.entity.PriceBookVersionEntity;
|
import com.baiyue.firstleg.entity.PriceBookVersionEntity;
|
||||||
import com.baiyue.firstleg.entity.PriceChannelCatalogEntity;
|
import com.baiyue.firstleg.entity.PriceChannelCatalogEntity;
|
||||||
import com.baiyue.firstleg.entity.PriceOriginAliasEntity;
|
import com.baiyue.firstleg.entity.PriceOriginAliasEntity;
|
||||||
|
import com.baiyue.firstleg.entity.PriceWarehouseMappingEntity;
|
||||||
import com.baiyue.firstleg.mapper.CarrierFbaRateMapper;
|
import com.baiyue.firstleg.mapper.CarrierFbaRateMapper;
|
||||||
import com.baiyue.firstleg.mapper.PriceBookChannelMapper;
|
import com.baiyue.firstleg.mapper.PriceBookChannelMapper;
|
||||||
import com.baiyue.firstleg.mapper.PriceBookVersionMapper;
|
import com.baiyue.firstleg.mapper.PriceBookVersionMapper;
|
||||||
import com.baiyue.firstleg.mapper.PriceChannelCatalogMapper;
|
import com.baiyue.firstleg.mapper.PriceChannelCatalogMapper;
|
||||||
import com.baiyue.firstleg.mapper.PriceOriginAliasMapper;
|
import com.baiyue.firstleg.mapper.PriceOriginAliasMapper;
|
||||||
|
import com.baiyue.firstleg.mapper.PriceWarehouseMappingMapper;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
@@ -25,6 +27,7 @@ public class PriceBookRepository {
|
|||||||
@Resource private CarrierFbaRateMapper rates;
|
@Resource private CarrierFbaRateMapper rates;
|
||||||
@Resource private PriceChannelCatalogMapper catalog;
|
@Resource private PriceChannelCatalogMapper catalog;
|
||||||
@Resource private PriceOriginAliasMapper aliases;
|
@Resource private PriceOriginAliasMapper aliases;
|
||||||
|
@Resource private PriceWarehouseMappingMapper warehouseMappings;
|
||||||
|
|
||||||
public int insertVersion(PriceBookVersionEntity value) { return versions.insert(value); }
|
public int insertVersion(PriceBookVersionEntity value) { return versions.insert(value); }
|
||||||
public int updateVersion(PriceBookVersionEntity value) {
|
public int updateVersion(PriceBookVersionEntity value) {
|
||||||
@@ -109,6 +112,11 @@ public class PriceBookRepository {
|
|||||||
return aliases.selectList(new LambdaQueryWrapper<PriceOriginAliasEntity>()
|
return aliases.selectList(new LambdaQueryWrapper<PriceOriginAliasEntity>()
|
||||||
.eq(PriceOriginAliasEntity::getStatus, "ACTIVE"));
|
.eq(PriceOriginAliasEntity::getStatus, "ACTIVE"));
|
||||||
}
|
}
|
||||||
|
public List<PriceWarehouseMappingEntity> activeWarehouseMappings() {
|
||||||
|
return warehouseMappings.selectList(new LambdaQueryWrapper<PriceWarehouseMappingEntity>()
|
||||||
|
.eq(PriceWarehouseMappingEntity::getStatus, "ACTIVE")
|
||||||
|
.orderByAsc(PriceWarehouseMappingEntity::getId));
|
||||||
|
}
|
||||||
public void supersede(PriceBookVersionEntity old, LocalDateTime at, long operatorId) {
|
public void supersede(PriceBookVersionEntity old, LocalDateTime at, long operatorId) {
|
||||||
old.setStatus("SUPERSEDED"); old.setSupersededAt(at); old.setUpdatedBy(operatorId); old.setUpdatedAt(at);
|
old.setStatus("SUPERSEDED"); old.setSupersededAt(at); old.setUpdatedBy(operatorId); old.setUpdatedAt(at);
|
||||||
updateVersion(old);
|
updateVersion(old);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import com.baiyue.firstleg.entity.CarrierFbaRateEntity;
|
|||||||
import com.baiyue.firstleg.entity.PriceBookChannelEntity;
|
import com.baiyue.firstleg.entity.PriceBookChannelEntity;
|
||||||
import com.baiyue.firstleg.entity.PriceBookVersionEntity;
|
import com.baiyue.firstleg.entity.PriceBookVersionEntity;
|
||||||
import com.baiyue.firstleg.entity.PriceChannelCatalogEntity;
|
import com.baiyue.firstleg.entity.PriceChannelCatalogEntity;
|
||||||
|
import com.baiyue.firstleg.entity.PriceWarehouseMappingEntity;
|
||||||
import com.baiyue.firstleg.pricing.importer.PriceWorkbookParseResult;
|
import com.baiyue.firstleg.pricing.importer.PriceWorkbookParseResult;
|
||||||
import com.baiyue.firstleg.pricing.importer.PriceWorkbookParser;
|
import com.baiyue.firstleg.pricing.importer.PriceWorkbookParser;
|
||||||
import com.baiyue.firstleg.repository.PriceBookRepository;
|
import com.baiyue.firstleg.repository.PriceBookRepository;
|
||||||
@@ -262,7 +263,37 @@ public class PlatformPriceBookService {
|
|||||||
List<PriceBookDtos.ParseIssue> issues = parsed.issues().stream().map(value -> issue(
|
List<PriceBookDtos.ParseIssue> issues = parsed.issues().stream().map(value -> issue(
|
||||||
value.severity() == PriceWorkbookParseResult.Severity.ERROR ? "BLOCKING" : "WARNING",
|
value.severity() == PriceWorkbookParseResult.Severity.ERROR ? "BLOCKING" : "WARNING",
|
||||||
value.code(), value.message(), value.sheetName(), value.rowNumber(), null)).toList();
|
value.code(), value.message(), value.sheetName(), value.rowNumber(), null)).toList();
|
||||||
return new ParsedPriceBook(channels, rates, issues);
|
return new ParsedPriceBook(channels, applyInitializedWarehouseMappings(rates), issues);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<PriceBookDtos.ParsedRate> applyInitializedWarehouseMappings(List<PriceBookDtos.ParsedRate> rates) {
|
||||||
|
Map<String, PriceWarehouseMappingEntity> origins = new HashMap<>();
|
||||||
|
Map<String, PriceWarehouseMappingEntity> overseas = new HashMap<>();
|
||||||
|
for (PriceWarehouseMappingEntity mapping : repository.activeWarehouseMappings()) {
|
||||||
|
if (mapping == null || mapping.getWarehouseId() == null || mapping.getWarehouseId() <= 0) continue;
|
||||||
|
if ("ORIGIN_CODE".equals(mapping.getSourceType())
|
||||||
|
&& "DOMESTIC".equals(mapping.getWarehouseType())
|
||||||
|
&& mappingKey(mapping.getSourceValue()).equals(mappingKey(mapping.getWarehouseCode()))) {
|
||||||
|
origins.putIfAbsent(mappingKey(mapping.getSourceValue()), mapping);
|
||||||
|
} else if ("OVERSEAS_NAME".equals(mapping.getSourceType())
|
||||||
|
&& "OVERSEAS".equals(mapping.getWarehouseType())
|
||||||
|
&& StringUtils.hasText(mapping.getWarehouseCode())) {
|
||||||
|
overseas.putIfAbsent(mappingKey(mapping.getSourceValue()), mapping);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rates.stream().map(rate -> {
|
||||||
|
PriceWarehouseMappingEntity origin = origins.get(mappingKey(rate.originWarehouseCode()));
|
||||||
|
PriceWarehouseMappingEntity destination = rate.destinationType() == PricingDestinationType.OVERSEAS
|
||||||
|
? overseas.get(mappingKey(rate.destinationName())) : null;
|
||||||
|
return new PriceBookDtos.ParsedRate(rate.id(), rate.channelCode(), rate.priceModel(),
|
||||||
|
origin == null ? rate.originWarehouseId() : origin.getWarehouseId(),
|
||||||
|
rate.originWarehouseCode(), rate.originWarehouseName(),
|
||||||
|
destination == null ? rate.destinationCode() : mappingKey(destination.getWarehouseCode()), rate.destinationType(),
|
||||||
|
destination == null ? rate.destinationWarehouseId() : destination.getWarehouseId(),
|
||||||
|
rate.destinationName(), rate.regionCode(), rate.pricingTaxMode(), rate.chargeUnit(), rate.weightFromKg(),
|
||||||
|
rate.unitPrice(), rate.currency(), rate.sourceSheetName(), rate.sourceRowNumber(),
|
||||||
|
rate.sourceDestinationText(), rate.remark());
|
||||||
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private PriceBookDtos.ParsedRate parsedRate(String channelCode, PriceModel model,
|
private PriceBookDtos.ParsedRate parsedRate(String channelCode, PriceModel model,
|
||||||
|
|||||||
@@ -411,6 +411,21 @@ CREATE TABLE IF NOT EXISTS fl_price_origin_alias (
|
|||||||
KEY idx_price_origin_alias_name (alias_name, status)
|
KEY idx_price_origin_alias_name (alias_name, status)
|
||||||
) ENGINE=InnoDB COMMENT='价格表起运仓名称别名映射';
|
) ENGINE=InnoDB COMMENT='价格表起运仓名称别名映射';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS fl_price_warehouse_mapping (
|
||||||
|
id BIGINT UNSIGNED NOT NULL COMMENT '价格仓映射ID',
|
||||||
|
source_type VARCHAR(24) NOT NULL COMMENT '来源维度:ORIGIN_CODE或OVERSEAS_NAME',
|
||||||
|
source_value VARCHAR(128) NOT NULL COMMENT '解析后的来源仓编码或海外仓名称',
|
||||||
|
warehouse_id BIGINT UNSIGNED NOT NULL COMMENT '固定平台公共仓库ID',
|
||||||
|
warehouse_code VARCHAR(64) NOT NULL COMMENT '平台仓库稳定编码快照',
|
||||||
|
warehouse_type VARCHAR(16) NOT NULL COMMENT 'DOMESTIC或OVERSEAS',
|
||||||
|
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE',
|
||||||
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY uk_price_warehouse_mapping_source (source_type, source_value),
|
||||||
|
KEY idx_price_warehouse_mapping_target (warehouse_id, status)
|
||||||
|
) ENGINE=InnoDB COMMENT='价格表来源仓与固定平台公共仓映射';
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS fl_price_book_version (
|
CREATE TABLE IF NOT EXISTS fl_price_book_version (
|
||||||
id BIGINT UNSIGNED NOT NULL COMMENT '整表价格版本ID',
|
id BIGINT UNSIGNED NOT NULL COMMENT '整表价格版本ID',
|
||||||
tenant_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '平台公共价格作用域固定为0',
|
tenant_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '平台公共价格作用域固定为0',
|
||||||
@@ -494,9 +509,21 @@ VALUES
|
|||||||
(3, 'CN-YIWU-01', '义乌', 'ACTIVE'),
|
(3, 'CN-YIWU-01', '义乌', 'ACTIVE'),
|
||||||
(4, 'CN-DONGGUAN-01', '华南仓(深圳)', 'ACTIVE'),
|
(4, 'CN-DONGGUAN-01', '华南仓(深圳)', 'ACTIVE'),
|
||||||
(5, 'CN-DONGGUAN-01', '东莞仓', 'ACTIVE'),
|
(5, 'CN-DONGGUAN-01', '东莞仓', 'ACTIVE'),
|
||||||
(6, 'CN-DONGGUAN-01', '深圳仓', 'ACTIVE')
|
(6, 'CN-DONGGUAN-01', '深圳仓', 'ACTIVE'),
|
||||||
|
(7, 'CN-DONGGUAN-01', '华南仓(深圳/东莞)', 'ACTIVE')
|
||||||
ON DUPLICATE KEY UPDATE status = VALUES(status);
|
ON DUPLICATE KEY UPDATE status = VALUES(status);
|
||||||
|
|
||||||
|
INSERT INTO fl_price_warehouse_mapping
|
||||||
|
(id, source_type, source_value, warehouse_id, warehouse_code, warehouse_type, status)
|
||||||
|
VALUES
|
||||||
|
(1, 'ORIGIN_CODE', 'CN-YIWU-01', 193000000000200001, 'CN-YIWU-01', 'DOMESTIC', 'ACTIVE'),
|
||||||
|
(2, 'ORIGIN_CODE', 'CN-DONGGUAN-01', 193000000000200002, 'CN-DONGGUAN-01', 'DOMESTIC', 'ACTIVE'),
|
||||||
|
(3, 'OVERSEAS_NAME', '洛杉矶仓', 193000000000200003, 'US-LAX-01', 'OVERSEAS', 'ACTIVE'),
|
||||||
|
(4, 'OVERSEAS_NAME', '纽约仓', 193000000000200004, 'US-NYC-01', 'OVERSEAS', 'ACTIVE')
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
warehouse_id = VALUES(warehouse_id), warehouse_code = VALUES(warehouse_code),
|
||||||
|
warehouse_type = VALUES(warehouse_type), status = VALUES(status);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS fl_inbound_fee (
|
CREATE TABLE IF NOT EXISTS fl_inbound_fee (
|
||||||
id BIGINT UNSIGNED NOT NULL COMMENT '入库费用ID',
|
id BIGINT UNSIGNED NOT NULL COMMENT '入库费用ID',
|
||||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID',
|
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID',
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ class PriceBookSqlContractTest {
|
|||||||
assertThat(schema)
|
assertThat(schema)
|
||||||
.contains("CREATE TABLE IF NOT EXISTS fl_price_channel_catalog")
|
.contains("CREATE TABLE IF NOT EXISTS fl_price_channel_catalog")
|
||||||
.contains("CREATE TABLE IF NOT EXISTS fl_price_origin_alias")
|
.contains("CREATE TABLE IF NOT EXISTS fl_price_origin_alias")
|
||||||
|
.contains("CREATE TABLE IF NOT EXISTS fl_price_warehouse_mapping")
|
||||||
|
.contains("华南仓(深圳/东莞)")
|
||||||
.contains("CREATE TABLE IF NOT EXISTS fl_price_book_version")
|
.contains("CREATE TABLE IF NOT EXISTS fl_price_book_version")
|
||||||
.contains("CREATE TABLE IF NOT EXISTS fl_price_book_channel")
|
.contains("CREATE TABLE IF NOT EXISTS fl_price_book_channel")
|
||||||
.contains("price_book_version_id")
|
.contains("price_book_version_id")
|
||||||
@@ -40,6 +42,11 @@ class PriceBookSqlContractTest {
|
|||||||
.contains("destination_name VARCHAR(128)")
|
.contains("destination_name VARCHAR(128)")
|
||||||
.contains("parse_issues_json JSON NULL")
|
.contains("parse_issues_json JSON NULL")
|
||||||
.contains("DEFAULT 'PENDING_CONFIRMATION'");
|
.contains("DEFAULT 'PENDING_CONFIRMATION'");
|
||||||
|
assertThat(schema)
|
||||||
|
.contains("'ORIGIN_CODE', 'CN-YIWU-01', 193000000000200001")
|
||||||
|
.contains("'ORIGIN_CODE', 'CN-DONGGUAN-01', 193000000000200002")
|
||||||
|
.contains("'OVERSEAS_NAME', '洛杉矶仓', 193000000000200003")
|
||||||
|
.contains("'OVERSEAS_NAME', '纽约仓', 193000000000200004");
|
||||||
assertThat(channelTable)
|
assertThat(channelTable)
|
||||||
.contains("created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID'")
|
.contains("created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID'")
|
||||||
.contains("updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改人ID'")
|
.contains("updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改人ID'")
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import com.baiyue.firstleg.entity.CarrierFbaRateEntity;
|
|||||||
import com.baiyue.firstleg.entity.PriceBookChannelEntity;
|
import com.baiyue.firstleg.entity.PriceBookChannelEntity;
|
||||||
import com.baiyue.firstleg.entity.PriceBookVersionEntity;
|
import com.baiyue.firstleg.entity.PriceBookVersionEntity;
|
||||||
import com.baiyue.firstleg.entity.PriceChannelCatalogEntity;
|
import com.baiyue.firstleg.entity.PriceChannelCatalogEntity;
|
||||||
|
import com.baiyue.firstleg.entity.PriceWarehouseMappingEntity;
|
||||||
import com.baiyue.firstleg.repository.PriceBookRepository;
|
import com.baiyue.firstleg.repository.PriceBookRepository;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
@@ -230,6 +231,29 @@ class PlatformPriceBookServiceTest {
|
|||||||
.doesNotContain("DUPLICATE_RATE");
|
.doesNotContain("DUPLICATE_RATE");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void initializedWarehouseMappingsBindFixedDomesticAndOverseasWarehouses() {
|
||||||
|
CarrierFbaRateEntity overseasRate = rate(81L);
|
||||||
|
overseasRate.setDestinationType(PricingDestinationType.OVERSEAS.name());
|
||||||
|
overseasRate.setDestinationCode(null);
|
||||||
|
overseasRate.setDestinationName("洛杉矶仓");
|
||||||
|
PriceBookDtos.ParsedRate parsed = ReflectionTestUtils.invokeMethod(service, "rateDto", overseasRate);
|
||||||
|
when(repository.activeWarehouseMappings()).thenReturn(List.of(
|
||||||
|
warehouseMapping("ORIGIN_CODE", "CN-YIWU-01", 193000000000200001L, "CN-YIWU-01", "DOMESTIC"),
|
||||||
|
warehouseMapping("OVERSEAS_NAME", "洛杉矶仓", 193000000000200003L, "US-LAX-01", "OVERSEAS")
|
||||||
|
));
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<PriceBookDtos.ParsedRate> mapped = ReflectionTestUtils.invokeMethod(
|
||||||
|
service, "applyInitializedWarehouseMappings", List.of(parsed));
|
||||||
|
|
||||||
|
assertThat(mapped).singleElement().satisfies(rate -> {
|
||||||
|
assertThat(rate.originWarehouseId()).isEqualTo(193000000000200001L);
|
||||||
|
assertThat(rate.destinationWarehouseId()).isEqualTo(193000000000200003L);
|
||||||
|
assertThat(rate.destinationCode()).isEqualTo("US-LAX-01");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void duplicatePublishIsRejectedAfterPublishLock() {
|
void duplicatePublishIsRejectedAfterPublishLock() {
|
||||||
when(repository.findVersion(31L)).thenReturn(version(31L, PriceBookStatus.ACTIVE));
|
when(repository.findVersion(31L)).thenReturn(version(31L, PriceBookStatus.ACTIVE));
|
||||||
@@ -300,6 +324,18 @@ class PlatformPriceBookServiceTest {
|
|||||||
List.of(new PriceBookConfirmationRequest.OriginMapping(code, id)), List.of());
|
List.of(new PriceBookConfirmationRequest.OriginMapping(code, id)), List.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static PriceWarehouseMappingEntity warehouseMapping(String sourceType, String sourceValue, long warehouseId,
|
||||||
|
String warehouseCode, String warehouseType) {
|
||||||
|
PriceWarehouseMappingEntity value = new PriceWarehouseMappingEntity();
|
||||||
|
value.setSourceType(sourceType);
|
||||||
|
value.setSourceValue(sourceValue);
|
||||||
|
value.setWarehouseId(warehouseId);
|
||||||
|
value.setWarehouseCode(warehouseCode);
|
||||||
|
value.setWarehouseType(warehouseType);
|
||||||
|
value.setStatus("ACTIVE");
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
private void executeTransactionsImmediately() {
|
private void executeTransactionsImmediately() {
|
||||||
when(transactions.execute(any())).thenAnswer(invocation -> {
|
when(transactions.execute(any())).thenAnswer(invocation -> {
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
package com.baiyue.integration.dto.ocr;
|
package com.baiyue.integration.dto.ocr;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 百度 OCR 返回的文字候选及其识别框。
|
* 百度 OCR 返回的文字候选及其识别框。
|
||||||
*/
|
*/
|
||||||
public record OcrCandidate(
|
public record OcrCandidate(
|
||||||
String text,
|
String text,
|
||||||
Double confidence,
|
BigDecimal confidence,
|
||||||
Integer left,
|
Integer left,
|
||||||
Integer top,
|
Integer top,
|
||||||
Integer width,
|
Integer width,
|
||||||
Integer height
|
Integer height
|
||||||
) {
|
) {
|
||||||
|
public OcrCandidate {
|
||||||
|
confidence = OcrConfidence.normalize(confidence);
|
||||||
|
}
|
||||||
|
|
||||||
public long area() {
|
public long area() {
|
||||||
if (width == null || height == null || width <= 0 || height <= 0) {
|
if (width == null || height == null || width <= 0 || height <= 0) {
|
||||||
return 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,
|
String rawResponseFile,
|
||||||
Status status,
|
Status status,
|
||||||
BigDecimal weight,
|
BigDecimal weight,
|
||||||
Double confidence,
|
BigDecimal confidence,
|
||||||
List<OcrCandidate> candidates,
|
List<OcrCandidate> candidates,
|
||||||
|
String ocrRawText,
|
||||||
String error
|
String error
|
||||||
) {
|
) {
|
||||||
|
public WeightOcrImageResult {
|
||||||
|
confidence = OcrConfidence.normalize(confidence);
|
||||||
|
}
|
||||||
|
|
||||||
public enum Status {
|
public enum Status {
|
||||||
RECOGNIZED,
|
RECOGNIZED,
|
||||||
NO_WEIGHT_FOUND,
|
NO_WEIGHT_FOUND,
|
||||||
|
|||||||
+4
-3
@@ -12,6 +12,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
@@ -163,7 +164,7 @@ public class BaiduMeterClient {
|
|||||||
JsonNode location = item.path("location");
|
JsonNode location = item.path("location");
|
||||||
candidates.add(new OcrCandidate(
|
candidates.add(new OcrCandidate(
|
||||||
item.path("words").asText(),
|
item.path("words").asText(),
|
||||||
nullableDouble(probability.get("average")),
|
nullableDecimal(probability.get("average")),
|
||||||
nullableInt(location.get("left")),
|
nullableInt(location.get("left")),
|
||||||
nullableInt(location.get("top")),
|
nullableInt(location.get("top")),
|
||||||
nullableInt(location.get("width")),
|
nullableInt(location.get("width")),
|
||||||
@@ -195,8 +196,8 @@ public class BaiduMeterClient {
|
|||||||
return URLEncoder.encode(value, StandardCharsets.UTF_8);
|
return URLEncoder.encode(value, StandardCharsets.UTF_8);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Double nullableDouble(JsonNode node) {
|
private static BigDecimal nullableDecimal(JsonNode node) {
|
||||||
return node == null || !node.isNumber() ? null : node.doubleValue();
|
return node == null || !node.isNumber() ? null : node.decimalValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Integer nullableInt(JsonNode node) {
|
private static Integer nullableInt(JsonNode node) {
|
||||||
|
|||||||
+3
-2
@@ -25,7 +25,8 @@ public class WeightCandidateSelector {
|
|||||||
.flatMap(Optional::stream)
|
.flatMap(Optional::stream)
|
||||||
.max(Comparator
|
.max(Comparator
|
||||||
.comparingLong((SelectedWeight selected) -> selected.candidate().area())
|
.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) {
|
private Optional<SelectedWeight> toSelectedWeight(OcrCandidate candidate) {
|
||||||
@@ -55,6 +56,6 @@ public class WeightCandidateSelector {
|
|||||||
.replace('-', '-');
|
.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
|
@Service
|
||||||
public class WeightOcrBatchService {
|
public class WeightOcrBatchService {
|
||||||
private static final Set<String> SUPPORTED_EXTENSIONS = Set.of("jpg", "jpeg", "png", "bmp");
|
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 IntegrationProperties properties;
|
||||||
@Resource private WeightOcrStoragePolicy storagePolicy;
|
@Resource private WeightOcrStoragePolicy storagePolicy;
|
||||||
@@ -113,7 +114,7 @@ public class WeightOcrBatchService {
|
|||||||
return new WeightOcrImageResult(
|
return new WeightOcrImageResult(
|
||||||
source.toString(), outputRelative(paths, cropped), outputRelative(paths, rawResponse),
|
source.toString(), outputRelative(paths, cropped), outputRelative(paths, rawResponse),
|
||||||
Status.RECOGNIZED, selected.weight(), selected.confidence(),
|
Status.RECOGNIZED, selected.weight(), selected.confidence(),
|
||||||
response.candidates(), null);
|
response.candidates(), summarizeCandidates(response.candidates()), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private WeightOcrImageResult noWeight(Path source, BatchPaths paths, Path cropped,
|
private WeightOcrImageResult noWeight(Path source, BatchPaths paths, Path cropped,
|
||||||
@@ -121,6 +122,7 @@ public class WeightOcrBatchService {
|
|||||||
return new WeightOcrImageResult(
|
return new WeightOcrImageResult(
|
||||||
source.toString(), outputRelative(paths, cropped), outputRelative(paths, rawResponse),
|
source.toString(), outputRelative(paths, cropped), outputRelative(paths, rawResponse),
|
||||||
Status.NO_WEIGHT_FOUND, null, null, response.candidates(),
|
Status.NO_WEIGHT_FOUND, null, null, response.candidates(),
|
||||||
|
summarizeCandidates(response.candidates()),
|
||||||
"百度响应中没有符合重量格式的候选值");
|
"百度响应中没有符合重量格式的候选值");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +131,33 @@ public class WeightOcrBatchService {
|
|||||||
return new WeightOcrImageResult(
|
return new WeightOcrImageResult(
|
||||||
source.toString(), Files.exists(cropped) ? outputRelative(paths, cropped) : null,
|
source.toString(), Files.exists(cropped) ? outputRelative(paths, cropped) : null,
|
||||||
Files.exists(rawResponse) ? outputRelative(paths, rawResponse) : 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().strip().replaceAll("[\\p{Cc}\\p{Cf}]+", " ");
|
||||||
|
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,
|
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) {
|
if (batch.results() == null || batch.results().size() != 1) {
|
||||||
throw new BizException(ResultCode.INTERNAL_ERROR, "OCR单图识别结果数量异常");
|
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 {
|
} finally {
|
||||||
deleteRequestDirectory(requestDirectory);
|
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.DisplayName;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
@@ -15,21 +16,31 @@ class WeightCandidateSelectorTest {
|
|||||||
@Test
|
@Test
|
||||||
void selectsLargestNumericCandidateAndNormalizesDecimalSeparator() {
|
void selectsLargestNumericCandidateAndNormalizesDecimalSeparator() {
|
||||||
List<OcrCandidate> candidates = List.of(
|
List<OcrCandidate> candidates = List.of(
|
||||||
new OcrCandidate("10:38", 0.99, 10, 10, 40, 10),
|
new OcrCandidate("10:38", new BigDecimal("0.99"), 10, 10, 40, 10),
|
||||||
new OcrCandidate(" 12,35 ", 0.97, 20, 30, 120, 60),
|
new OcrCandidate(" 12,35 ", new BigDecimal("0.97"), 20, 30, 120, 60),
|
||||||
new OcrCandidate("29", 0.99, 160, 10, 20, 10),
|
new OcrCandidate("29", new BigDecimal("0.99"), 160, 10, 20, 10),
|
||||||
new OcrCandidate("kg", 0.99, 150, 70, 30, 15));
|
new OcrCandidate("kg", new BigDecimal("0.99"), 150, 70, 30, 15));
|
||||||
|
|
||||||
WeightCandidateSelector.SelectedWeight selected = selector.select(candidates).orElseThrow();
|
WeightCandidateSelector.SelectedWeight selected = selector.select(candidates).orElseThrow();
|
||||||
|
|
||||||
assertThat(selected.weight()).isEqualByComparingTo("12.35");
|
assertThat(selected.weight()).isEqualByComparingTo("12.35");
|
||||||
assertThat(selected.confidence()).isEqualTo(0.97);
|
assertThat(selected.confidence()).isEqualByComparingTo("0.97000");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void rejectsNonNumericCandidates() {
|
void rejectsNonNumericCandidates() {
|
||||||
assertThat(selector.select(List.of(
|
assertThat(selector.select(List.of(
|
||||||
new OcrCandidate("kg", 0.99, 0, 0, 20, 10),
|
new OcrCandidate("kg", new BigDecimal("0.99"), 0, 0, 20, 10),
|
||||||
new OcrCandidate("10:38", 0.99, 0, 0, 20, 10)))).isEmpty();
|
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 javax.imageio.ImageIO;
|
||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@@ -56,13 +57,17 @@ class WeightOcrBatchServiceTest {
|
|||||||
when(properties.baiduOcr()).thenReturn(properties(inputRoot, outputRoot).baiduOcr());
|
when(properties.baiduOcr()).thenReturn(properties(inputRoot, outputRoot).baiduOcr());
|
||||||
when(client.recognize(any())).thenReturn(new BaiduMeterClient.MeterResponse(
|
when(client.recognize(any())).thenReturn(new BaiduMeterClient.MeterResponse(
|
||||||
"{\"words_result\":[]}",
|
"{\"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));
|
var result = service.recognize(new WeightOcrBatchRequest("batch-a", null, null, null, false));
|
||||||
|
|
||||||
assertThat(result.total()).isEqualTo(1);
|
assertThat(result.total()).isEqualTo(1);
|
||||||
assertThat(result.recognized()).isEqualTo(1);
|
assertThat(result.recognized()).isEqualTo(1);
|
||||||
assertThat(result.results().get(0).status()).isEqualTo(Status.RECOGNIZED);
|
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());
|
Path batchOutput = outputRoot.resolve(result.outputDirectory());
|
||||||
assertThat(batchOutput.resolve("cropped/weight.jpg")).isRegularFile();
|
assertThat(batchOutput.resolve("cropped/weight.jpg")).isRegularFile();
|
||||||
assertThat(batchOutput.resolve("raw/weight.json")).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