优化
This commit is contained in:
@@ -22,6 +22,13 @@ public interface MeasurementImageClient {
|
||||
@PathVariable long inboundId,
|
||||
@RequestPart("file") MultipartFile file);
|
||||
|
||||
@PostMapping(value = "/receipts/{tenantId}/{receiptId}", consumes = "multipart/form-data")
|
||||
ApiResponse<UploadResult> uploadReceipt(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestHeader("X-Operator-Id") long operatorId,
|
||||
@PathVariable long tenantId,
|
||||
@PathVariable long receiptId,
|
||||
@RequestPart("file") MultipartFile file);
|
||||
|
||||
@DeleteMapping("/{tenantId}/{inboundId}/{fileId}")
|
||||
ApiResponse<Void> delete(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestHeader("X-Operator-Id") long operatorId,
|
||||
@@ -29,6 +36,13 @@ public interface MeasurementImageClient {
|
||||
@PathVariable long inboundId,
|
||||
@PathVariable long fileId);
|
||||
|
||||
@DeleteMapping("/receipts/{tenantId}/{receiptId}/{fileId}")
|
||||
ApiResponse<Void> deleteReceipt(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestHeader("X-Operator-Id") long operatorId,
|
||||
@PathVariable long tenantId,
|
||||
@PathVariable long receiptId,
|
||||
@PathVariable long fileId);
|
||||
|
||||
@GetMapping("/{tenantId}/{inboundId}/{fileId}")
|
||||
org.springframework.http.ResponseEntity<byte[]> download(
|
||||
@RequestHeader("X-Internal-Service-Token") String token,
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ import jakarta.annotation.Resource;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.firstleg.dto.OpenApiMeasurementDtos.EventResponse;
|
||||
import com.baiyue.firstleg.dto.CartonMeasurementResult;
|
||||
import com.baiyue.firstleg.dto.MojiaMeasurementDtos.UploadResponse;
|
||||
import com.baiyue.firstleg.security.MeasurementRequestHeaders;
|
||||
import com.baiyue.firstleg.service.InboundMeasurementOpenApiService;
|
||||
import com.baiyue.firstleg.service.MojiaMeasurementOpenApiService;
|
||||
@@ -45,7 +45,7 @@ public class OpenApiInboundMeasurementController {
|
||||
@Operation(summary = "墨家设备按箱码上传单箱测量结果",
|
||||
description = "首轮联调使用固定 X-App-Key;JSON 中 picture 为 JPEG Data URL,boxCode 原样取自柏悦箱唛二维码")
|
||||
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ApiResponse<CartonMeasurementResult> receiveMojia(
|
||||
public ApiResponse<UploadResponse> receiveMojia(
|
||||
@RequestHeader("X-App-Key") String appKey,
|
||||
@RequestHeader(value = "X-Timestamp", required = false) String timestamp,
|
||||
@RequestHeader(value = "X-Nonce", required = false) String nonce,
|
||||
|
||||
@@ -25,4 +25,16 @@ public final class MojiaMeasurementDtos {
|
||||
@NotBlank @Size(max = 7_100_000) String picture
|
||||
) {
|
||||
}
|
||||
|
||||
public record UploadResponse(
|
||||
boolean accepted,
|
||||
boolean matched,
|
||||
Long receiptId,
|
||||
Long measurementId,
|
||||
Long itemId,
|
||||
String boxCode,
|
||||
Long imageFileId,
|
||||
boolean created
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.baiyue.firstleg.dto;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
@@ -13,6 +14,8 @@ public record UnifiedInboundQuery(
|
||||
Long tenantId,
|
||||
@Size(max = 64) String keyword,
|
||||
@Size(max = 64) String orderNo,
|
||||
@Schema(allowableValues = {"PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED", "WAIT_SPLIT",
|
||||
"LOADING", "LOADED", "DELIVERED"})
|
||||
@Size(max = 32) String mainStatus,
|
||||
Long customerId,
|
||||
Long receivingWarehouseId,
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
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;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** 墨家回传未匹配正式入库单时的联调收件记录。 */
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("fl_mojia_measurement_receipt")
|
||||
public class MojiaMeasurementReceiptEntity {
|
||||
@TableId(type = IdType.INPUT)
|
||||
private Long id;
|
||||
private Long tenantId;
|
||||
private Long subjectId;
|
||||
private String boxCode;
|
||||
private BigDecimal lengthCm;
|
||||
private BigDecimal widthCm;
|
||||
private BigDecimal heightCm;
|
||||
private BigDecimal weightKg;
|
||||
private String machineNo;
|
||||
private LocalDateTime measuredAt;
|
||||
private Long imageFileId;
|
||||
private String status;
|
||||
private String matchFailureCode;
|
||||
private String matchFailureReason;
|
||||
private String requestId;
|
||||
private String traceId;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.baiyue.firstleg.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.firstleg.entity.MojiaMeasurementReceiptEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface MojiaMeasurementReceiptMapper extends BaseMapper<MojiaMeasurementReceiptEntity> {
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package com.baiyue.firstleg.repository;
|
||||
|
||||
import com.baiyue.firstleg.entity.MojiaMeasurementReceiptEntity;
|
||||
import com.baiyue.firstleg.mapper.MojiaMeasurementReceiptMapper;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public class MojiaMeasurementReceiptRepository {
|
||||
private final MojiaMeasurementReceiptMapper mapper;
|
||||
|
||||
public MojiaMeasurementReceiptRepository(MojiaMeasurementReceiptMapper mapper) {
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
public void insert(MojiaMeasurementReceiptEntity receipt) {
|
||||
if (mapper.insert(receipt) != 1) {
|
||||
throw new IllegalStateException("墨家联调收件记录保存失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-9
@@ -18,8 +18,6 @@ import java.util.List;
|
||||
public class CustomerDimensionWeightingService {
|
||||
private static final BigDecimal TWO = new BigDecimal("2");
|
||||
private static final BigDecimal CUBIC_CENTIMETERS_PER_CBM = new BigDecimal("1000000");
|
||||
private static final String OVERSIZE_WARNING =
|
||||
"实测尺寸超出客户最大箱规,已按实测尺寸记录,需人工复核";
|
||||
|
||||
private final CustomerDimensionConfigRepository repository;
|
||||
|
||||
@@ -64,16 +62,16 @@ public class CustomerDimensionWeightingService {
|
||||
.filter(rule -> rule.contains(measured))
|
||||
.findFirst()
|
||||
.map(Rule::weighted)
|
||||
.orElseGet(() -> actualOversize(lengthCm, widthCm, heightCm));
|
||||
.orElseGet(() -> actualDimensions(lengthCm, widthCm, heightCm));
|
||||
}
|
||||
|
||||
private WeightedDimensions actualOversize(BigDecimal lengthCm, BigDecimal widthCm,
|
||||
BigDecimal heightCm) {
|
||||
private WeightedDimensions actualDimensions(BigDecimal lengthCm, BigDecimal widthCm,
|
||||
BigDecimal heightCm) {
|
||||
BigDecimal rawVolume = lengthCm.multiply(widthCm).multiply(heightCm)
|
||||
.divide(CUBIC_CENTIMETERS_PER_CBM, 6, RoundingMode.HALF_UP);
|
||||
return new WeightedDimensions(null, null, lengthCm, widthCm, heightCm,
|
||||
rawVolume, rawVolume.setScale(3, RoundingMode.HALF_UP),
|
||||
"ACTUAL_DIMENSION_OVERSIZE_V1", OVERSIZE_WARNING);
|
||||
"ACTUAL_DIMENSION_V1");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,8 +83,7 @@ public class CustomerDimensionWeightingService {
|
||||
BigDecimal outerHeightCm,
|
||||
BigDecimal rawVolumeCbm,
|
||||
BigDecimal unitVolumeCbm,
|
||||
String calculationRuleVersion,
|
||||
String warning
|
||||
String calculationRuleVersion
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -123,7 +120,7 @@ public class CustomerDimensionWeightingService {
|
||||
.divide(CUBIC_CENTIMETERS_PER_CBM, 6, RoundingMode.HALF_UP);
|
||||
BigDecimal unitVolume = rawVolume.setScale(3, RoundingMode.HALF_UP);
|
||||
return new WeightedDimensions(boxType, version, outerLength, outerWidth, outerHeight,
|
||||
rawVolume, unitVolume, "CUSTOMER_DIMENSION_" + boxType.name() + "_V" + version, null);
|
||||
rawVolume, unitVolume, "CUSTOMER_DIMENSION_" + boxType.name() + "_V" + version);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -163,8 +163,7 @@ public class MeasurementBatchService {
|
||||
item.setVolumeWeightKg(totalVolume.multiply(VOLUME_WEIGHT_FACTOR).setScale(3, RoundingMode.CEILING));
|
||||
item.setCalculationRuleVersion(weighted.calculationRuleVersion());
|
||||
item.setSourceImageFileId(source.intelligentImageFileId());
|
||||
item.setOverLimitWarning(weighted.warning() == null
|
||||
? source.overLimitWarning() : weighted.warning());
|
||||
item.setOverLimitWarning(source.overLimitWarning());
|
||||
item.setOuterLengthCm(weighted.outerLengthCm());
|
||||
item.setOuterWidthCm(weighted.outerWidthCm());
|
||||
item.setOuterHeightCm(weighted.outerHeightCm());
|
||||
|
||||
+96
-8
@@ -1,10 +1,16 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.client.MeasurementImageClient;
|
||||
import com.baiyue.firstleg.dto.CartonMeasurementCommand;
|
||||
import com.baiyue.firstleg.dto.CartonMeasurementResult;
|
||||
import com.baiyue.firstleg.dto.MojiaMeasurementDtos.UploadRequest;
|
||||
import com.baiyue.firstleg.dto.MojiaMeasurementDtos.UploadResponse;
|
||||
import com.baiyue.firstleg.entity.MojiaMeasurementReceiptEntity;
|
||||
import com.baiyue.firstleg.repository.MojiaMeasurementReceiptRepository;
|
||||
import com.baiyue.firstleg.security.AuthenticatedMeasurementApp;
|
||||
import com.baiyue.firstleg.security.MeasurementRequestAuthenticator;
|
||||
import com.baiyue.firstleg.security.MeasurementRequestHeaders;
|
||||
@@ -16,6 +22,7 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
@@ -39,36 +46,54 @@ public class MojiaMeasurementOpenApiService {
|
||||
private final CartonMeasurementService cartonMeasurements;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final Validator validator;
|
||||
private final MeasurementImageClient imageClient;
|
||||
private final MojiaMeasurementReceiptRepository receipts;
|
||||
|
||||
@Value("${baiyue.security.internal-service-token}")
|
||||
private String internalToken;
|
||||
|
||||
public MojiaMeasurementOpenApiService(MeasurementRequestAuthenticator authenticator,
|
||||
CartonMeasurementService cartonMeasurements,
|
||||
ObjectMapper objectMapper,
|
||||
Validator validator) {
|
||||
Validator validator,
|
||||
MeasurementImageClient imageClient,
|
||||
MojiaMeasurementReceiptRepository receipts) {
|
||||
this.authenticator = authenticator;
|
||||
this.cartonMeasurements = cartonMeasurements;
|
||||
this.objectMapper = objectMapper;
|
||||
this.validator = validator;
|
||||
this.imageClient = imageClient;
|
||||
this.receipts = receipts;
|
||||
}
|
||||
|
||||
public CartonMeasurementResult receive(MeasurementRequestHeaders headers, String rawJson) {
|
||||
public UploadResponse receive(MeasurementRequestHeaders headers, String rawJson) {
|
||||
UploadRequest request = null;
|
||||
try {
|
||||
AuthenticatedMeasurementApp app = authenticator.authenticate(
|
||||
"POST", CALLBACK_PATH, headers, rawJson, false);
|
||||
request = parseAndValidate(rawJson);
|
||||
MultipartFile image = decodeJpeg(request.picture());
|
||||
CartonMeasurementResult result = cartonMeasurements.upsert(
|
||||
app.tenantId(), app.subjectId(), new CartonMeasurementCommand(
|
||||
request.boxCode(), request.length(), request.width(), request.height(), request.weight(),
|
||||
request.machineNo(), request.measuredAt() == null ? LocalDateTime.now() : request.measuredAt(),
|
||||
image));
|
||||
CartonMeasurementResult result;
|
||||
try {
|
||||
result = cartonMeasurements.upsert(
|
||||
app.tenantId(), app.subjectId(), new CartonMeasurementCommand(
|
||||
request.boxCode(), request.length(), request.width(), request.height(), request.weight(),
|
||||
request.machineNo(), request.measuredAt() == null ? LocalDateTime.now() : request.measuredAt(),
|
||||
image));
|
||||
} catch (BizException exception) {
|
||||
if (exception.getResultCode() == ResultCode.NOT_FOUND) {
|
||||
return saveUnmatched(app, request, image, exception);
|
||||
}
|
||||
throw exception;
|
||||
}
|
||||
log.info("MOJIA_MEASUREMENT accepted requestId={} traceId={} boxCode={} machineNo={} "
|
||||
+ "lengthCm={} widthCm={} heightCm={} weightKg={} imageBytes={} measurementId={} "
|
||||
+ "itemId={} created={}",
|
||||
correlation("requestId"), correlation("traceId"), safe(request.boxCode()),
|
||||
safe(request.machineNo()), request.length(), request.width(), request.height(), request.weight(),
|
||||
image.getSize(), result.measurementId(), result.itemId(), result.created());
|
||||
return result;
|
||||
return new UploadResponse(true, true, null, result.measurementId(), result.itemId(),
|
||||
result.boxCode(), result.imageFileId(), result.created());
|
||||
} catch (RuntimeException exception) {
|
||||
String code = exception instanceof BizException biz ? biz.getResultCode().code() : "S0500";
|
||||
log.warn("MOJIA_MEASUREMENT rejected requestId={} traceId={} boxCode={} machineNo={} "
|
||||
@@ -81,6 +106,69 @@ public class MojiaMeasurementOpenApiService {
|
||||
}
|
||||
}
|
||||
|
||||
private UploadResponse saveUnmatched(AuthenticatedMeasurementApp app, UploadRequest request,
|
||||
MultipartFile image, BizException matchFailure) {
|
||||
long receiptId = IdWorker.getId();
|
||||
MeasurementImageClient.UploadResult uploaded = uploadReceiptImage(
|
||||
app.tenantId(), app.subjectId(), receiptId, image);
|
||||
try {
|
||||
MojiaMeasurementReceiptEntity receipt = new MojiaMeasurementReceiptEntity();
|
||||
receipt.setId(receiptId);
|
||||
receipt.setTenantId(app.tenantId());
|
||||
receipt.setSubjectId(app.subjectId());
|
||||
receipt.setBoxCode(request.boxCode().trim());
|
||||
receipt.setLengthCm(request.length());
|
||||
receipt.setWidthCm(request.width());
|
||||
receipt.setHeightCm(request.height());
|
||||
receipt.setWeightKg(request.weight());
|
||||
receipt.setMachineNo(request.machineNo().trim());
|
||||
receipt.setMeasuredAt(request.measuredAt() == null ? LocalDateTime.now() : request.measuredAt());
|
||||
receipt.setImageFileId(uploaded.fileId());
|
||||
receipt.setStatus("UNMATCHED");
|
||||
receipt.setMatchFailureCode(matchFailure.getResultCode().code());
|
||||
receipt.setMatchFailureReason(safe(matchFailure.getMessage()));
|
||||
receipt.setRequestId(correlation("requestId"));
|
||||
receipt.setTraceId(correlation("traceId"));
|
||||
receipt.setCreatedAt(LocalDateTime.now());
|
||||
receipt.setUpdatedAt(LocalDateTime.now());
|
||||
receipts.insert(receipt);
|
||||
} catch (RuntimeException exception) {
|
||||
cleanupReceiptImage(app.tenantId(), app.subjectId(), receiptId, uploaded.fileId());
|
||||
throw exception;
|
||||
}
|
||||
log.info("MOJIA_MEASUREMENT accepted requestId={} traceId={} boxCode={} machineNo={} "
|
||||
+ "matched=false receiptId={} imageBytes={} imageFileId={} matchCode={}",
|
||||
correlation("requestId"), correlation("traceId"), safe(request.boxCode()),
|
||||
safe(request.machineNo()), receiptId, image.getSize(), uploaded.fileId(),
|
||||
matchFailure.getResultCode().code());
|
||||
return new UploadResponse(true, false, receiptId, null, null, request.boxCode(),
|
||||
uploaded.fileId(), true);
|
||||
}
|
||||
|
||||
private MeasurementImageClient.UploadResult uploadReceiptImage(long tenantId, long operatorId,
|
||||
long receiptId, MultipartFile image) {
|
||||
try {
|
||||
ApiResponse<MeasurementImageClient.UploadResult> response = imageClient.uploadReceipt(
|
||||
internalToken, operatorId, tenantId, receiptId, image);
|
||||
if (response == null || !response.successful() || response.data() == null
|
||||
|| response.data().fileId() == null) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "墨家联调图片保存失败");
|
||||
}
|
||||
return response.data();
|
||||
} catch (feign.FeignException exception) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "文件服务暂不可用,墨家回传未保存");
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanupReceiptImage(long tenantId, long operatorId, long receiptId, long fileId) {
|
||||
try {
|
||||
imageClient.deleteReceipt(internalToken, operatorId, tenantId, receiptId, fileId);
|
||||
} catch (RuntimeException exception) {
|
||||
log.warn("墨家联调收件图片补偿删除失败 tenantId={} receiptId={} fileId={}",
|
||||
tenantId, receiptId, fileId, exception);
|
||||
}
|
||||
}
|
||||
|
||||
private UploadRequest parseAndValidate(String rawJson) {
|
||||
if (rawJson == null || rawJson.isBlank()) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "请求 JSON 不能为空");
|
||||
|
||||
@@ -28,7 +28,7 @@ import java.util.stream.Collectors;
|
||||
@Service
|
||||
public class UnifiedInboundQueryService {
|
||||
private static final Set<String> MAIN_STATUSES = Set.of(
|
||||
"PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED", "LOADING", "LOADED", "DELIVERED");
|
||||
"PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED");
|
||||
private static final Set<String> REVIEW_STATUSES = Set.of("UNRECORDED", "PENDING", "APPROVED", "REJECTED");
|
||||
|
||||
@Resource private InboundOrderRepository inbounds;
|
||||
@@ -71,10 +71,14 @@ public class UnifiedInboundQueryService {
|
||||
private List<UnifiedInboundSummary> summaries(List<InboundOrderEntity> records) {
|
||||
Map<Long, Long> owners = records.stream().collect(Collectors.toMap(
|
||||
InboundOrderEntity::getId, InboundOrderEntity::getTenantId, (left, right) -> left));
|
||||
var preloads = operations.findPreloads(owners);
|
||||
var picks = operations.findPicks(owners);
|
||||
var evidences = operations.findEvidence(owners);
|
||||
var deliveries = operations.findDeliveryHistory(owners);
|
||||
Map<Long, Long> phaseThreeOwners = records.stream()
|
||||
.filter(inbound -> requiresPhaseThreeSummary(mainStatus(inbound.getProcessStatus())))
|
||||
.collect(Collectors.toMap(
|
||||
InboundOrderEntity::getId, InboundOrderEntity::getTenantId, (left, right) -> left));
|
||||
var preloads = operations.findPreloads(phaseThreeOwners);
|
||||
var picks = operations.findPicks(phaseThreeOwners);
|
||||
var evidences = operations.findEvidence(phaseThreeOwners);
|
||||
var deliveries = operations.findDeliveryHistory(phaseThreeOwners);
|
||||
var transports = operations.findTransports(owners);
|
||||
return records.stream().map(inbound -> summary(inbound, null, preloads.get(inbound.getId()),
|
||||
picks.get(inbound.getId()), evidences.getOrDefault(inbound.getId(), List.of()),
|
||||
@@ -120,8 +124,8 @@ public class UnifiedInboundQueryService {
|
||||
if (status == null) return null;
|
||||
return switch (status) {
|
||||
case "WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED" -> "WAIT_MEASURE";
|
||||
// WAIT_SPLIT is retained only to normalize records created before status unification.
|
||||
case "WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING", "WAIT_SPLIT", "INBOUNDED" -> "INBOUNDED";
|
||||
case "WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING", "INBOUNDED" -> "INBOUNDED";
|
||||
case "WAIT_SPLIT" -> "WAIT_SPLIT";
|
||||
case "LOADING", "LOADED", "DELIVERED" -> status;
|
||||
default -> null;
|
||||
};
|
||||
@@ -132,9 +136,10 @@ public class UnifiedInboundQueryService {
|
||||
return switch (mainStatus) {
|
||||
case "PENDING_REVIEW" -> "APPROVE_ORDER";
|
||||
case "WAIT_MEASURE" -> "MEASURE_PENDING".equals(processStatus) ? "CONFIRM_MEASUREMENT" : null;
|
||||
case "INBOUNDED" -> !List.of("INBOUNDED", "WAIT_SPLIT").contains(processStatus) ? null
|
||||
case "INBOUNDED" -> !"INBOUNDED".equals(processStatus) ? null
|
||||
: "CONFIRMED".equals(preloadStatus) ? null
|
||||
: preloadStatus == null ? "SMART_SPLIT" : "CONFIRM_PRELOAD";
|
||||
case "WAIT_SPLIT" -> null;
|
||||
case "LOADING" -> "COMPLETE_LOADING";
|
||||
case "LOADED" -> "COMPLETE_DELIVERY";
|
||||
case "DELIVERED" -> null;
|
||||
@@ -142,11 +147,16 @@ public class UnifiedInboundQueryService {
|
||||
};
|
||||
}
|
||||
|
||||
static boolean requiresPhaseThreeSummary(String mainStatus) {
|
||||
return mainStatus != null && Set.of("LOADING", "LOADED", "DELIVERED").contains(mainStatus);
|
||||
}
|
||||
|
||||
private List<String> internalStatuses(String mainStatus) {
|
||||
if (!StringUtils.hasText(mainStatus)) return List.of();
|
||||
return switch (mainStatus) {
|
||||
case "WAIT_MEASURE" -> List.of("WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED");
|
||||
case "INBOUNDED" -> List.of("WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING", "WAIT_SPLIT", "INBOUNDED");
|
||||
case "INBOUNDED" -> List.of("WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING", "INBOUNDED");
|
||||
case "WAIT_SPLIT" -> List.of("WAIT_SPLIT");
|
||||
case "LOADING", "LOADED", "DELIVERED" -> List.of(mainStatus);
|
||||
default -> List.of("__NO_MATCH__");
|
||||
};
|
||||
|
||||
@@ -235,6 +235,30 @@ CREATE TABLE IF NOT EXISTS fl_inbound_measurement_item (
|
||||
CONSTRAINT fk_measure_item_batch FOREIGN KEY (measurement_id) REFERENCES fl_inbound_measurement (id)
|
||||
) ENGINE=InnoDB COMMENT='入库长宽高和箱数测量明细';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_mojia_measurement_receipt (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '墨家联调收件记录ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID',
|
||||
subject_id BIGINT UNSIGNED NOT NULL COMMENT '设备应用操作主体ID',
|
||||
box_code VARCHAR(128) NOT NULL COMMENT '设备扫描的原始箱码',
|
||||
length_cm DECIMAL(12,3) NOT NULL COMMENT '设备实测长度cm',
|
||||
width_cm DECIMAL(12,3) NOT NULL COMMENT '设备实测宽度cm',
|
||||
height_cm DECIMAL(12,3) NOT NULL COMMENT '设备实测高度cm',
|
||||
weight_kg DECIMAL(18,3) NULL COMMENT '设备实测重量kg',
|
||||
machine_no VARCHAR(64) NOT NULL COMMENT '设备编号',
|
||||
measured_at DATETIME(3) NOT NULL COMMENT '设备测量时间或服务接收时间',
|
||||
image_file_id BIGINT UNSIGNED NOT NULL COMMENT 'File服务中的墨家联调图片ID',
|
||||
status VARCHAR(24) NOT NULL DEFAULT 'UNMATCHED' COMMENT '收件状态',
|
||||
match_failure_code VARCHAR(16) NOT NULL COMMENT '正式业务匹配失败码',
|
||||
match_failure_reason VARCHAR(500) NOT NULL COMMENT '脱敏后的匹配失败原因',
|
||||
request_id VARCHAR(128) NULL COMMENT '请求链路ID',
|
||||
trace_id VARCHAR(128) NULL COMMENT '调用链ID',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_mojia_receipt_status_time (tenant_id, status, created_at),
|
||||
KEY idx_mojia_receipt_box_time (tenant_id, box_code, created_at)
|
||||
) ENGINE=InnoDB COMMENT='墨家模拟或未匹配入库单的原始测量收件箱';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_inbound_weight_result (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT 'OCR重量结果ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '数据所属租户ID',
|
||||
|
||||
@@ -546,9 +546,9 @@ BEGIN
|
||||
END IF;
|
||||
IF (SELECT COUNT(DISTINCT index_name) FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE()
|
||||
AND index_name IN ('uk_inbound_master_order', 'uk_inbound_parent_sequence',
|
||||
AND index_name IN ('uk_inbound_master_order', 'uk_inbound_one_child',
|
||||
'uk_inbound_split_operation', 'uk_inbound_split_idempotency',
|
||||
'uk_inbound_split_child', 'uk_inbound_split_history_operation')) <> 6 THEN
|
||||
'uk_inbound_split_history_operation')) <> 5 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '主子入仓单唯一约束未完整创建';
|
||||
END IF;
|
||||
IF (SELECT COUNT(*) FROM information_schema.table_constraints
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
-- 将已废弃的 WAIT_SPLIT 统一为 INBOUNDED。
|
||||
-- 执行前请确认所有服务已升级至本次状态契约;脚本可重复执行。
|
||||
-- WAIT_SPLIT 是二期当前有效终点。本脚本仅校验兼容结构,禁止改写主表或历史状态。
|
||||
USE baiyue_firstleg;
|
||||
|
||||
UPDATE fl_inbound_order
|
||||
SET process_status = 'INBOUNDED',
|
||||
updated_at = CURRENT_TIMESTAMP(3),
|
||||
version = version + 1
|
||||
WHERE process_status = 'WAIT_SPLIT';
|
||||
|
||||
UPDATE fl_inbound_status_history
|
||||
SET from_status = 'INBOUNDED'
|
||||
WHERE from_status = 'WAIT_SPLIT';
|
||||
|
||||
UPDATE fl_inbound_status_history
|
||||
SET to_status = 'INBOUNDED'
|
||||
WHERE to_status = 'WAIT_SPLIT';
|
||||
DROP PROCEDURE IF EXISTS assert_wait_split_compatibility;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE assert_wait_split_compatibility()
|
||||
BEGIN
|
||||
IF (SELECT COUNT(*) FROM information_schema.columns
|
||||
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_inbound_order'
|
||||
AND column_name = 'process_status') <> 1
|
||||
OR (SELECT COUNT(*) FROM information_schema.columns
|
||||
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_inbound_status_history'
|
||||
AND column_name IN ('from_status', 'to_status')) <> 2 THEN
|
||||
SIGNAL SQLSTATE '45000'
|
||||
SET MESSAGE_TEXT = 'WAIT_SPLIT 兼容校验失败:入库状态字段不完整';
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
CALL assert_wait_split_compatibility();
|
||||
DROP PROCEDURE assert_wait_split_compatibility;
|
||||
|
||||
@@ -235,27 +235,84 @@ DELIMITER $$
|
||||
CREATE PROCEDURE migrate_legacy_loading_pick_task()
|
||||
BEGIN
|
||||
DECLARE source_exists INT DEFAULT 0;
|
||||
DECLARE source_total_columns INT DEFAULT 0;
|
||||
DECLARE source_smart_columns INT DEFAULT 0;
|
||||
DECLARE source_inbound_columns INT DEFAULT 0;
|
||||
DECLARE target_exists INT DEFAULT 0;
|
||||
DECLARE target_total_columns INT DEFAULT 0;
|
||||
DECLARE target_inbound_columns INT DEFAULT 0;
|
||||
|
||||
SELECT COUNT(*) INTO source_exists
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_loading_pick_task';
|
||||
IF source_exists = 1 THEN
|
||||
SELECT COUNT(*) INTO source_total_columns
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_loading_pick_task';
|
||||
SELECT COUNT(*) INTO source_smart_columns
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_loading_pick_task'
|
||||
AND column_name IN ('loading_plan_id', 'container_no', 'task_status');
|
||||
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_loading_pick_task' AND (
|
||||
(column_name = 'id' AND LOWER(column_type) = 'bigint unsigned' AND is_nullable = 'NO')
|
||||
OR (column_name = 'loading_plan_id' AND LOWER(column_type) = 'bigint unsigned' AND is_nullable = 'NO')
|
||||
OR (column_name = 'container_no' AND LOWER(column_type) = 'varchar(64)' AND is_nullable = 'NO')
|
||||
OR (column_name = 'task_status' AND LOWER(column_type) = 'varchar(16)' AND is_nullable = 'NO' AND column_default = 'LOADING')
|
||||
OR (column_name = 'started_by' AND LOWER(column_type) = 'bigint unsigned' AND is_nullable = 'NO')
|
||||
OR (column_name = 'started_by_name' AND LOWER(column_type) = 'varchar(64)' AND is_nullable = 'YES')
|
||||
OR (column_name = 'started_at' AND data_type = 'datetime' AND datetime_precision = 3 AND is_nullable = 'NO' AND LOWER(column_default) LIKE 'current_timestamp%')
|
||||
OR (column_name = 'completed_by' AND LOWER(column_type) = 'bigint unsigned' AND is_nullable = 'YES')
|
||||
OR (column_name = 'completed_by_name' AND LOWER(column_type) = 'varchar(64)' AND is_nullable = 'YES')
|
||||
OR (column_name = 'completed_at' AND data_type = 'datetime' AND datetime_precision = 3 AND is_nullable = 'YES')
|
||||
OR (column_name = 'version' AND LOWER(column_type) = 'int unsigned' AND is_nullable = 'NO' AND CAST(column_default AS CHAR) = '0')
|
||||
);
|
||||
SELECT COUNT(*) INTO source_inbound_columns
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_loading_pick_task'
|
||||
AND column_name IN ('tenant_id', 'inbound_id', 'status', 'expected_count', 'picked_count');
|
||||
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_loading_pick_task' AND (
|
||||
(column_name = 'id' AND LOWER(column_type) = 'bigint unsigned' AND is_nullable = 'NO')
|
||||
OR (column_name = 'tenant_id' AND LOWER(column_type) = 'bigint unsigned' AND is_nullable = 'NO')
|
||||
OR (column_name = 'inbound_id' AND LOWER(column_type) = 'bigint unsigned' AND is_nullable = 'NO')
|
||||
OR (column_name = 'status' AND LOWER(column_type) = 'varchar(24)' AND is_nullable = 'NO' AND column_default = 'PENDING')
|
||||
OR (column_name = 'version' AND LOWER(column_type) = 'int' AND is_nullable = 'NO' AND CAST(column_default AS CHAR) = '0')
|
||||
OR (column_name = 'expected_count' AND LOWER(column_type) = 'int unsigned' AND is_nullable = 'YES')
|
||||
OR (column_name = 'picked_count' AND LOWER(column_type) = 'int unsigned' AND is_nullable = 'YES')
|
||||
OR (column_name = 'remark' AND LOWER(column_type) = 'varchar(1000)' AND is_nullable = 'YES')
|
||||
OR (column_name = 'updated_by' AND LOWER(column_type) = 'bigint unsigned' AND is_nullable = 'NO')
|
||||
OR (column_name = 'updated_by_name' AND LOWER(column_type) = 'varchar(64)' AND is_nullable = 'YES')
|
||||
OR (column_name = 'created_at' AND data_type = 'datetime' AND datetime_precision = 3 AND is_nullable = 'NO' AND LOWER(column_default) LIKE 'current_timestamp%')
|
||||
OR (column_name = 'updated_at' AND data_type = 'datetime' AND datetime_precision = 3 AND is_nullable = 'NO' AND LOWER(column_default) LIKE 'current_timestamp%' AND LOWER(extra) LIKE '%on update current_timestamp%')
|
||||
);
|
||||
|
||||
IF source_smart_columns = 3 AND source_inbound_columns = 0 THEN
|
||||
IF source_total_columns = 11 AND source_smart_columns = 11 AND source_inbound_columns < 12
|
||||
AND (SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_loading_pick_task'
|
||||
AND index_name = 'uk_loading_pick_task_plan' AND non_unique = 0
|
||||
AND seq_in_index = 1 AND column_name = 'loading_plan_id') = 1
|
||||
AND (SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_loading_pick_task'
|
||||
AND index_name = 'idx_loading_pick_container'
|
||||
AND ((seq_in_index = 1 AND column_name = 'container_no')
|
||||
OR (seq_in_index = 2 AND column_name = 'task_status')
|
||||
OR (seq_in_index = 3 AND column_name = 'started_at'))) = 3
|
||||
AND (SELECT COUNT(*) FROM information_schema.key_column_usage
|
||||
WHERE constraint_schema = 'baiyue_firstleg' AND table_name = 'fl_loading_pick_task'
|
||||
AND column_name = 'loading_plan_id' AND referenced_table_name = 'fl_loading_plan'
|
||||
AND referenced_column_name = 'id') = 1 THEN
|
||||
SET source_exists = 1;
|
||||
ELSEIF source_smart_columns = 0 AND source_inbound_columns = 5 THEN
|
||||
ELSEIF source_total_columns = 12 AND source_inbound_columns = 12 AND source_smart_columns < 11
|
||||
AND (SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_loading_pick_task'
|
||||
AND index_name = 'uk_loading_pick_tenant_inbound' AND non_unique = 0
|
||||
AND ((seq_in_index = 1 AND column_name = 'tenant_id')
|
||||
OR (seq_in_index = 2 AND column_name = 'inbound_id'))) = 2
|
||||
AND (SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_loading_pick_task'
|
||||
AND index_name = 'idx_loading_pick_status'
|
||||
AND ((seq_in_index = 1 AND column_name = 'tenant_id')
|
||||
OR (seq_in_index = 2 AND column_name = 'status')
|
||||
OR (seq_in_index = 3 AND column_name = 'updated_at'))) = 3
|
||||
AND (SELECT COUNT(*) FROM information_schema.key_column_usage
|
||||
WHERE constraint_schema = 'baiyue_firstleg' AND table_name = 'fl_loading_pick_task'
|
||||
AND column_name = 'inbound_id' AND referenced_table_name = 'fl_inbound_order'
|
||||
AND referenced_column_name = 'id') = 1 THEN
|
||||
SELECT COUNT(*) INTO target_exists
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'baiyue_firstleg'
|
||||
@@ -263,14 +320,44 @@ BEGIN
|
||||
IF target_exists = 0 THEN
|
||||
RENAME TABLE fl_loading_pick_task TO fl_inbound_loading_pick_task;
|
||||
ELSE
|
||||
SELECT COUNT(*) INTO target_total_columns
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_inbound_loading_pick_task';
|
||||
SELECT COUNT(*) INTO target_inbound_columns
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'baiyue_firstleg'
|
||||
AND table_name = 'fl_inbound_loading_pick_task'
|
||||
AND column_name IN ('tenant_id', 'inbound_id', 'status', 'expected_count', 'picked_count');
|
||||
IF target_inbound_columns <> 5 THEN
|
||||
AND table_name = 'fl_inbound_loading_pick_task' AND (
|
||||
(column_name = 'id' AND LOWER(column_type) = 'bigint unsigned' AND is_nullable = 'NO')
|
||||
OR (column_name = 'tenant_id' AND LOWER(column_type) = 'bigint unsigned' AND is_nullable = 'NO')
|
||||
OR (column_name = 'inbound_id' AND LOWER(column_type) = 'bigint unsigned' AND is_nullable = 'NO')
|
||||
OR (column_name = 'status' AND LOWER(column_type) = 'varchar(24)' AND is_nullable = 'NO' AND column_default = 'PENDING')
|
||||
OR (column_name = 'version' AND LOWER(column_type) = 'int' AND is_nullable = 'NO' AND CAST(column_default AS CHAR) = '0')
|
||||
OR (column_name = 'expected_count' AND LOWER(column_type) = 'int unsigned' AND is_nullable = 'YES')
|
||||
OR (column_name = 'picked_count' AND LOWER(column_type) = 'int unsigned' AND is_nullable = 'YES')
|
||||
OR (column_name = 'remark' AND LOWER(column_type) = 'varchar(1000)' AND is_nullable = 'YES')
|
||||
OR (column_name = 'updated_by' AND LOWER(column_type) = 'bigint unsigned' AND is_nullable = 'NO')
|
||||
OR (column_name = 'updated_by_name' AND LOWER(column_type) = 'varchar(64)' AND is_nullable = 'YES')
|
||||
OR (column_name = 'created_at' AND data_type = 'datetime' AND datetime_precision = 3 AND is_nullable = 'NO' AND LOWER(column_default) LIKE 'current_timestamp%')
|
||||
OR (column_name = 'updated_at' AND data_type = 'datetime' AND datetime_precision = 3 AND is_nullable = 'NO' AND LOWER(column_default) LIKE 'current_timestamp%' AND LOWER(extra) LIKE '%on update current_timestamp%')
|
||||
);
|
||||
IF target_total_columns <> 12 OR target_inbound_columns <> 12
|
||||
OR (SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_inbound_loading_pick_task'
|
||||
AND index_name = 'uk_loading_pick_tenant_inbound' AND non_unique = 0
|
||||
AND ((seq_in_index = 1 AND column_name = 'tenant_id')
|
||||
OR (seq_in_index = 2 AND column_name = 'inbound_id'))) <> 2
|
||||
OR (SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = 'baiyue_firstleg' AND table_name = 'fl_inbound_loading_pick_task'
|
||||
AND index_name = 'idx_loading_pick_status'
|
||||
AND ((seq_in_index = 1 AND column_name = 'tenant_id')
|
||||
OR (seq_in_index = 2 AND column_name = 'status')
|
||||
OR (seq_in_index = 3 AND column_name = 'updated_at'))) <> 3
|
||||
OR (SELECT COUNT(*) FROM information_schema.key_column_usage
|
||||
WHERE constraint_schema = 'baiyue_firstleg' AND table_name = 'fl_inbound_loading_pick_task'
|
||||
AND column_name = 'inbound_id' AND referenced_table_name = 'fl_inbound_order'
|
||||
AND referenced_column_name = 'id') <> 1 THEN
|
||||
SIGNAL SQLSTATE '45000'
|
||||
SET MESSAGE_TEXT = '拣货任务迁移失败:fl_inbound_loading_pick_task 不是入库汇总结构';
|
||||
SET MESSAGE_TEXT = '拣货任务迁移失败:fl_inbound_loading_pick_task 不是完整入库汇总结构';
|
||||
END IF;
|
||||
INSERT INTO fl_inbound_loading_pick_task
|
||||
(id, tenant_id, inbound_id, status, version, expected_count, picked_count,
|
||||
@@ -776,6 +863,95 @@ DELIMITER ;
|
||||
|
||||
-- 三期智能装柜、预装清单和装柜清单平台权限;与一期平台根菜单幂等合并。
|
||||
USE baiyue_iam;
|
||||
DROP PROCEDURE IF EXISTS relocate_legacy_inbound_loading_permissions;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE relocate_legacy_inbound_loading_permissions()
|
||||
BEGIN
|
||||
DECLARE legacy_count INT DEFAULT 0;
|
||||
SELECT COUNT(*) INTO legacy_count
|
||||
FROM iam_permission
|
||||
WHERE tenant_id = 0 AND app_code = 'PLATFORM'
|
||||
AND permission_code IN (
|
||||
'firstleg:inbound:split', 'firstleg:inbound:preload', 'firstleg:inbound:preload-view',
|
||||
'platform:firstleg:inbound:loading:edit', 'platform:firstleg:inbound:loading:view',
|
||||
'platform:firstleg:inbound:loading:complete', 'platform:firstleg:inbound:delivery:complete',
|
||||
'platform:firstleg:inbound:picking:edit', 'platform:firstleg:inbound:picking:view'
|
||||
);
|
||||
IF legacy_count > 0 THEN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM iam_permission
|
||||
WHERE id BETWEEN 5102121 AND 5102129
|
||||
AND permission_code <> CASE id
|
||||
WHEN 5102121 THEN 'firstleg:inbound:split'
|
||||
WHEN 5102122 THEN 'firstleg:inbound:preload'
|
||||
WHEN 5102123 THEN 'firstleg:inbound:preload-view'
|
||||
WHEN 5102124 THEN 'platform:firstleg:inbound:loading:edit'
|
||||
WHEN 5102125 THEN 'platform:firstleg:inbound:loading:view'
|
||||
WHEN 5102126 THEN 'platform:firstleg:inbound:loading:complete'
|
||||
WHEN 5102127 THEN 'platform:firstleg:inbound:delivery:complete'
|
||||
WHEN 5102128 THEN 'platform:firstleg:inbound:picking:edit'
|
||||
WHEN 5102129 THEN 'platform:firstleg:inbound:picking:view'
|
||||
END
|
||||
) THEN
|
||||
SIGNAL SQLSTATE '45000'
|
||||
SET MESSAGE_TEXT = '三期权限迁移失败:5102121-5102129 已被其他权限占用';
|
||||
END IF;
|
||||
DROP TEMPORARY TABLE IF EXISTS tmp_phase3_inbound_role_grant;
|
||||
CREATE TEMPORARY TABLE tmp_phase3_inbound_role_grant (
|
||||
role_id BIGINT UNSIGNED NOT NULL,
|
||||
tenant_id BIGINT UNSIGNED NOT NULL,
|
||||
permission_code VARCHAR(128) NOT NULL,
|
||||
PRIMARY KEY (role_id, tenant_id, permission_code)
|
||||
) ENGINE=MEMORY;
|
||||
INSERT IGNORE INTO tmp_phase3_inbound_role_grant (role_id, tenant_id, permission_code)
|
||||
SELECT role_permission.role_id, role_permission.tenant_id, permission.permission_code
|
||||
FROM iam_role_permission role_permission
|
||||
JOIN iam_permission permission ON permission.id = role_permission.permission_id
|
||||
WHERE permission.permission_code IN (
|
||||
'firstleg:inbound:split', 'firstleg:inbound:preload', 'firstleg:inbound:preload-view',
|
||||
'platform:firstleg:inbound:loading:edit', 'platform:firstleg:inbound:loading:view',
|
||||
'platform:firstleg:inbound:loading:complete', 'platform:firstleg:inbound:delivery:complete',
|
||||
'platform:firstleg:inbound:picking:edit', 'platform:firstleg:inbound:picking:view'
|
||||
);
|
||||
DELETE role_permission FROM iam_role_permission role_permission
|
||||
JOIN iam_permission permission ON permission.id = role_permission.permission_id
|
||||
WHERE permission.permission_code IN (
|
||||
'firstleg:inbound:split', 'firstleg:inbound:preload', 'firstleg:inbound:preload-view',
|
||||
'platform:firstleg:inbound:loading:edit', 'platform:firstleg:inbound:loading:view',
|
||||
'platform:firstleg:inbound:loading:complete', 'platform:firstleg:inbound:delivery:complete',
|
||||
'platform:firstleg:inbound:picking:edit', 'platform:firstleg:inbound:picking:view'
|
||||
);
|
||||
DELETE FROM iam_permission WHERE permission_code IN (
|
||||
'firstleg:inbound:split', 'firstleg:inbound:preload', 'firstleg:inbound:preload-view',
|
||||
'platform:firstleg:inbound:loading:edit', 'platform:firstleg:inbound:loading:view',
|
||||
'platform:firstleg:inbound:loading:complete', 'platform:firstleg:inbound:delivery:complete',
|
||||
'platform:firstleg:inbound:picking:edit', 'platform:firstleg:inbound:picking:view'
|
||||
);
|
||||
INSERT INTO iam_permission
|
||||
(id, tenant_id, app_code, permission_type, parent_id, permission_code,
|
||||
permission_name, route_path, http_method, sort_no, status, built_in, deleted)
|
||||
VALUES
|
||||
(5102121, 0, 'PLATFORM', 'BUTTON', 5102030, 'firstleg:inbound:split', '智能分柜', NULL, NULL, 140, 'ACTIVE', 1, 0),
|
||||
(5102122, 0, 'PLATFORM', 'BUTTON', 5102030, 'firstleg:inbound:preload', '编辑并确认预装清单', NULL, NULL, 150, 'ACTIVE', 1, 0),
|
||||
(5102123, 0, 'PLATFORM', 'BUTTON', 5102030, 'firstleg:inbound:preload-view', '查看预装清单', NULL, NULL, 160, 'ACTIVE', 1, 0),
|
||||
(5102124, 0, 'PLATFORM', 'BUTTON', 5102030, 'platform:firstleg:inbound:loading:edit', '维护装柜证据', NULL, NULL, 170, 'ACTIVE', 1, 0),
|
||||
(5102125, 0, 'PLATFORM', 'BUTTON', 5102030, 'platform:firstleg:inbound:loading:view', '查看装柜证据', NULL, NULL, 180, 'ACTIVE', 1, 0),
|
||||
(5102126, 0, 'PLATFORM', 'BUTTON', 5102030, 'platform:firstleg:inbound:loading:complete', '完成装柜', NULL, NULL, 190, 'ACTIVE', 1, 0),
|
||||
(5102127, 0, 'PLATFORM', 'BUTTON', 5102030, 'platform:firstleg:inbound:delivery:complete', '完成派送', NULL, NULL, 200, 'ACTIVE', 1, 0),
|
||||
(5102128, 0, 'PLATFORM', 'BUTTON', 5102030, 'platform:firstleg:inbound:picking:edit', '执行拣货', NULL, NULL, 210, 'ACTIVE', 1, 0),
|
||||
(5102129, 0, 'PLATFORM', 'BUTTON', 5102030, 'platform:firstleg:inbound:picking:view', '查看拣货任务', NULL, NULL, 220, 'ACTIVE', 1, 0);
|
||||
INSERT IGNORE INTO iam_role_permission (role_id, permission_id, tenant_id)
|
||||
SELECT grant_row.role_id, permission.id, grant_row.tenant_id
|
||||
FROM tmp_phase3_inbound_role_grant grant_row
|
||||
JOIN iam_permission permission ON permission.permission_code = grant_row.permission_code
|
||||
AND permission.tenant_id = 0 AND permission.app_code = 'PLATFORM';
|
||||
DROP TEMPORARY TABLE tmp_phase3_inbound_role_grant;
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
CALL relocate_legacy_inbound_loading_permissions();
|
||||
DROP PROCEDURE relocate_legacy_inbound_loading_permissions;
|
||||
|
||||
INSERT INTO iam_permission
|
||||
(id, tenant_id, app_code, permission_type, parent_id, permission_code,
|
||||
permission_name, route_path, http_method, sort_no, status, built_in, deleted)
|
||||
@@ -857,9 +1033,9 @@ BEGIN
|
||||
IF (SELECT COUNT(*) FROM information_schema.check_constraints
|
||||
WHERE constraint_schema = 'baiyue_firstleg'
|
||||
AND constraint_name = 'chk_loading_plan_status'
|
||||
AND check_clause LIKE '%''EDITING''%'
|
||||
AND check_clause LIKE '%''LOADING''%'
|
||||
AND check_clause LIKE '%''COMPLETED''%') <> 1 THEN
|
||||
AND UPPER(check_clause) LIKE '%EDITING%'
|
||||
AND UPPER(check_clause) LIKE '%LOADING%'
|
||||
AND UPPER(check_clause) LIKE '%COMPLETED%') <> 1 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '智能装柜迁移失败:装柜状态约束不是最终三状态';
|
||||
END IF;
|
||||
IF (SELECT COUNT(*) FROM information_schema.columns
|
||||
|
||||
@@ -262,7 +262,11 @@ paths:
|
||||
format: binary
|
||||
description: 当前测量明细唯一对应图片,JPG/PNG/BMP且不超过5MB
|
||||
responses:
|
||||
'200': {description: 成功或业务失败,按响应体字符串 code 判断;非法签名返回 A0401}
|
||||
'200':
|
||||
description: JSON 墨家协议在正式箱码匹配或未匹配联调收件成功时均返回 code=200;data.matched 区分结果。其他业务失败按 code 判断。
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/MojiaMeasurementUploadEnvelope'}
|
||||
'413': {description: 请求体超过网关限制}
|
||||
'415': {description: Content-Type 不支持}
|
||||
/openapi/v1/inbound-measurements/events/{eventNo}:
|
||||
@@ -523,6 +527,24 @@ components:
|
||||
required: true
|
||||
schema: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
schemas:
|
||||
MojiaMeasurementUploadEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
code: {type: string, example: '200'}
|
||||
message: {type: string, example: success}
|
||||
data: {$ref: '#/components/schemas/MojiaMeasurementUploadResponse'}
|
||||
MojiaMeasurementUploadResponse:
|
||||
type: object
|
||||
required: [accepted, matched, boxCode, created]
|
||||
properties:
|
||||
accepted: {type: boolean, example: true}
|
||||
matched: {type: boolean, description: true 表示已写入正式箱级测量;false 表示已写入联调收件箱等待排查}
|
||||
receiptId: {type: string, nullable: true, description: matched=false 时返回的联调收件记录ID}
|
||||
measurementId: {type: string, nullable: true}
|
||||
itemId: {type: string, nullable: true}
|
||||
boxCode: {type: string}
|
||||
imageFileId: {type: string, nullable: true}
|
||||
created: {type: boolean}
|
||||
CustomerDimensionConfigUpdateRequest:
|
||||
type: object
|
||||
required: [rules]
|
||||
|
||||
@@ -46,21 +46,6 @@ paths:
|
||||
schema: {$ref: '#/components/schemas/TransportUpdateRequest'}
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/PreloadDetailResponse'}
|
||||
/api/platform/v1/first-leg/loading-plans/{planId}/complete:
|
||||
post:
|
||||
summary: 完成预装
|
||||
description: 仅允许将 LOADING 单向推进为 COMPLETED;完成前必须完成全部 PDA 拣货。
|
||||
x-permission: platform:firstleg:preload:complete
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/PlanId'}
|
||||
- {$ref: '#/components/parameters/IdempotencyKey'}
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/AdvanceRequest'}
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/PreloadDetailResponse'}
|
||||
/api/platform/v1/first-leg/loading-lists:
|
||||
get:
|
||||
summary: 分页查询装柜清单
|
||||
@@ -326,10 +311,3 @@ components:
|
||||
voyageNo: {type: string, maxLength: 64, nullable: true, description: 航次}
|
||||
expectedVersion: {type: integer, format: int32, minimum: 0, description: 当前乐观锁版本}
|
||||
operationReason: {type: string, maxLength: 500, nullable: true, description: 操作原因}
|
||||
AdvanceRequest:
|
||||
type: object
|
||||
required: [expectedVersion]
|
||||
additionalProperties: false
|
||||
properties:
|
||||
expectedVersion: {type: integer, format: int32, minimum: 0, description: 当前乐观锁版本}
|
||||
operationReason: {type: string, maxLength: 500, nullable: true, description: 操作原因}
|
||||
|
||||
+10
-2
@@ -76,13 +76,21 @@ class LoadingListControllerContractTest {
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
Map<String, Object> document = stringMap(new Yaml().load(yaml));
|
||||
assertThat(document.get("openapi")).isEqualTo("3.0.3");
|
||||
assertThat((Map<?, ?>) document.get("paths")).hasSize(6);
|
||||
Map<String, Object> paths = stringMap(document.get("paths"));
|
||||
assertThat(paths).containsOnlyKeys(
|
||||
"/api/platform/v1/first-leg/loading-plans",
|
||||
"/api/platform/v1/first-leg/loading-plans/{planId}",
|
||||
"/api/platform/v1/first-leg/loading-plans/{planId}/transport",
|
||||
"/api/platform/v1/first-leg/loading-lists",
|
||||
"/api/platform/v1/first-leg/loading-lists/{planId}");
|
||||
Map<String, Object> components = stringMap(document.get("components"));
|
||||
Map<String, Object> schemas = stringMap(components.get("schemas"));
|
||||
assertThat(schemas).containsKeys(
|
||||
"LongId", "PreloadPage", "ConfirmedPage", "Customer", "PreloadSummary",
|
||||
"ConfirmedSummary", "LoadingItem", "OperationHistory", "PreloadDetail",
|
||||
"ConfirmedDetail", "TransportUpdateRequest", "AdvanceRequest");
|
||||
"ConfirmedDetail", "TransportUpdateRequest");
|
||||
assertThat(paths)
|
||||
.doesNotContainKey("/api/platform/v1/first-leg/loading-plans/{planId}/complete");
|
||||
assertThat(stringMap(schemas.get("LongId"))).containsEntry("type", "string");
|
||||
assertThat(properties(schemas, "PreloadPage")).containsKeys("records", "page", "size", "total", "pages");
|
||||
assertThat(properties(schemas, "PreloadSummary")).containsKeys("planId", "customers", "skuCount");
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package com.baiyue.firstleg.loading.controller;
|
||||
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.firstleg.loading.dto.LoadingRequests.CompleteDeliveryRequest;
|
||||
import com.baiyue.firstleg.loading.dto.LoadingRequests.CompleteLoadingRequest;
|
||||
import com.baiyue.firstleg.loading.dto.LoadingRequests.PickingRequest;
|
||||
import com.baiyue.firstleg.loading.dto.LoadingRequests.UpsertEvidenceRequest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class PlatformLoadingDeliveryControllerContractTest {
|
||||
@Test
|
||||
void keepsInboundScopedPathsAndPermissions() throws Exception {
|
||||
assertThat(PlatformLoadingDeliveryController.class.getAnnotation(RequestMapping.class).value())
|
||||
.containsExactly("/api/platform/v1/inbounds");
|
||||
|
||||
post("evidence", "/{inboundId}/loading/evidence",
|
||||
"platform:firstleg:inbound:loading:edit", UpsertEvidenceRequest.class);
|
||||
get("evidence", "/{inboundId}/loading/evidence",
|
||||
"platform:firstleg:inbound:loading:view");
|
||||
post("completeLoading", "/{inboundId}/loading/complete",
|
||||
"platform:firstleg:inbound:loading:complete", CompleteLoadingRequest.class);
|
||||
post("completeDelivery", "/{inboundId}/delivery/complete",
|
||||
"platform:firstleg:inbound:delivery:complete", CompleteDeliveryRequest.class);
|
||||
post("startPicking", "/{inboundId}/loading/picking/start",
|
||||
"platform:firstleg:inbound:picking:edit", PickingRequest.class);
|
||||
post("completePicking", "/{inboundId}/loading/picking/complete",
|
||||
"platform:firstleg:inbound:picking:edit", PickingRequest.class);
|
||||
get("picking", "/{inboundId}/loading/picking",
|
||||
"platform:firstleg:inbound:picking:view");
|
||||
}
|
||||
|
||||
private static void post(String name, String path, String authority, Class<?> requestType) throws Exception {
|
||||
Method method = PlatformLoadingDeliveryController.class.getDeclaredMethod(name,
|
||||
AuthenticatedUser.class, long.class, long.class, String.class, requestType);
|
||||
assertThat(method.getAnnotation(PostMapping.class).value()).containsExactly(path);
|
||||
assertAuthority(method, authority);
|
||||
assertThat(method.getParameters()[1].getAnnotation(RequestParam.class)).isNotNull();
|
||||
assertThat(method.getParameters()[3].getAnnotation(RequestHeader.class).value())
|
||||
.isEqualTo("Idempotency-Key");
|
||||
}
|
||||
|
||||
private static void get(String name, String path, String authority) throws Exception {
|
||||
Method method = PlatformLoadingDeliveryController.class.getDeclaredMethod(name,
|
||||
AuthenticatedUser.class, long.class, long.class);
|
||||
assertThat(method.getAnnotation(GetMapping.class).value()).containsExactly(path);
|
||||
assertAuthority(method, authority);
|
||||
assertThat(method.getParameters()[1].getAnnotation(RequestParam.class)).isNotNull();
|
||||
}
|
||||
|
||||
private static void assertAuthority(Method method, String authority) {
|
||||
assertThat(method.getAnnotation(PreAuthorize.class).value())
|
||||
.isEqualTo("hasAuthority('" + authority + "')");
|
||||
}
|
||||
}
|
||||
+10
-3
@@ -11,14 +11,21 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class InboundStatusUnificationSqlContractTest {
|
||||
@Test
|
||||
void syncsStatusUnificationMigrationToUnifiedDeploymentScript() throws Exception {
|
||||
void preservesWaitSplitWithoutDataRewrite() throws Exception {
|
||||
String module = normalize(new ClassPathResource("db/phase3-inbounded-status-unification.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8)).trim();
|
||||
String unified = normalize(Files.readString(unifiedPath(), StandardCharsets.UTF_8));
|
||||
|
||||
assertThat(module)
|
||||
.contains("CREATE PROCEDURE assert_wait_split_compatibility()")
|
||||
.contains("WAIT_SPLIT 是二期当前有效终点")
|
||||
.doesNotContain("UPDATE fl_inbound_order")
|
||||
.doesNotContain("UPDATE fl_inbound_status_history");
|
||||
assertThat(unified)
|
||||
.contains("-- 来源:firstleg/src/main/resources/db/phase3-inbounded-status-unification.sql")
|
||||
.contains(module);
|
||||
.contains("WAIT_SPLIT 是二期当前有效终点;统一迁移不得改写主表或历史状态")
|
||||
.doesNotContain("SET process_status = 'INBOUNDED'")
|
||||
.doesNotContain("SET from_status = 'INBOUNDED'")
|
||||
.doesNotContain("SET to_status = 'INBOUNDED'");
|
||||
}
|
||||
|
||||
private static Path unifiedPath() {
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package com.baiyue.firstleg.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class MojiaMeasurementReceiptSqlContractTest {
|
||||
@Test
|
||||
void schemaAndUnifiedMigrationKeepUnmatchedMeasurementsWithImageReferences() throws Exception {
|
||||
String schema = new ClassPathResource("db/phase2-inbound-schema.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
String unified = Files.readString(
|
||||
Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"),
|
||||
StandardCharsets.UTF_8);
|
||||
|
||||
for (String sql : new String[]{schema, unified}) {
|
||||
assertThat(sql)
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_mojia_measurement_receipt")
|
||||
.contains("box_code VARCHAR(128) NOT NULL")
|
||||
.contains("image_file_id BIGINT UNSIGNED NOT NULL")
|
||||
.contains("status VARCHAR(24) NOT NULL DEFAULT 'UNMATCHED'")
|
||||
.contains("idx_mojia_receipt_status_time")
|
||||
.doesNotContain("picture_base64");
|
||||
}
|
||||
assertThat(unified)
|
||||
.contains("墨家未匹配测量收件箱字段未完整创建")
|
||||
.contains("测量计算规则版本字段长度不是64");
|
||||
}
|
||||
}
|
||||
+43
-1
@@ -91,7 +91,11 @@ class SmartLoadingSqlContractTest {
|
||||
void shouldAssertCompletePermissionGrants() throws Exception {
|
||||
assertPermissionContract(migration());
|
||||
assertPermissionContract(unifiedMigration());
|
||||
assertThat(unifiedMigration()).contains(migration().trim());
|
||||
assertThat(unifiedMigration())
|
||||
.contains("CALL migrate_legacy_loading_pick_task()")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_inbound_loading_pick_task")
|
||||
.contains("5102121, 0, 'PLATFORM'")
|
||||
.contains("5102129, 0, 'PLATFORM'");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -126,6 +130,8 @@ class SmartLoadingSqlContractTest {
|
||||
void shouldProvidePdaContract() throws Exception {
|
||||
assertThat(migration())
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_task")
|
||||
.contains("CALL migrate_legacy_loading_pick_task()")
|
||||
.contains("RENAME TABLE fl_loading_pick_task TO fl_inbound_loading_pick_task")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_item")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_history")
|
||||
.contains("UNIQUE KEY uk_loading_pick_task_plan (loading_plan_id)")
|
||||
@@ -142,6 +148,38 @@ class SmartLoadingSqlContractTest {
|
||||
.contains(")) <> 16 THEN");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("统一脚本只定义并调用一次智能装柜断言")
|
||||
void shouldDefineSmartLoadingAssertionOnce() throws Exception {
|
||||
String unified = unifiedMigration();
|
||||
assertThat(count(unified, "CREATE PROCEDURE assert_phase3_smart_loading_schema()"))
|
||||
.isEqualTo(1);
|
||||
assertThat(count(unified, "CALL assert_phase3_smart_loading_schema();"))
|
||||
.isEqualTo(1);
|
||||
assertThat(count(unified, "SELECT 'PASS' AS migration_status"))
|
||||
.isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("详细拣货和入库汇总拣货使用独立完整表型")
|
||||
void shouldKeepDetailedAndInboundPickTaskShapesSeparate() throws Exception {
|
||||
String unified = unifiedMigration();
|
||||
assertThat(unified)
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_task (")
|
||||
.contains("loading_plan_id BIGINT UNSIGNED NOT NULL")
|
||||
.contains("container_no VARCHAR(64) NOT NULL")
|
||||
.contains("task_status VARCHAR(16) NOT NULL DEFAULT 'LOADING'")
|
||||
.contains("UNIQUE KEY uk_loading_pick_task_plan (loading_plan_id)")
|
||||
.contains("KEY idx_loading_pick_container (container_no, task_status, started_at)")
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_inbound_loading_pick_task (")
|
||||
.contains("tenant_id BIGINT UNSIGNED NOT NULL")
|
||||
.contains("inbound_id BIGINT UNSIGNED NOT NULL")
|
||||
.contains("expected_count INT UNSIGNED NULL")
|
||||
.contains("picked_count INT UNSIGNED NULL")
|
||||
.contains("UNIQUE KEY uk_loading_pick_tenant_inbound (tenant_id, inbound_id)")
|
||||
.contains("KEY idx_loading_pick_status (tenant_id, status, updated_at)");
|
||||
}
|
||||
|
||||
private static String migration() throws Exception {
|
||||
return new ClassPathResource("db/phase3-smart-loading-schema.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
@@ -174,4 +212,8 @@ class SmartLoadingSqlContractTest {
|
||||
Matcher matcher = PERMISSION_PATTERN.matcher(sql);
|
||||
return matcher.results().map(result -> result.group(1)).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private static int count(String value, String token) {
|
||||
return (value.length() - value.replace(token, "").length()) / token.length();
|
||||
}
|
||||
}
|
||||
|
||||
+2
-3
@@ -56,7 +56,7 @@ class CustomerDimensionWeightingServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void dimensionsBeyondTheLargestRuleAreRecordedUsingActualDimensions() {
|
||||
void dimensionsOutsideConfiguredWeightingRulesUseActualDimensions() {
|
||||
var weighted = service.load(10001L).resolve(
|
||||
new BigDecimal("60.001"), new BigDecimal("50"), new BigDecimal("50"));
|
||||
|
||||
@@ -64,8 +64,7 @@ class CustomerDimensionWeightingServiceTest {
|
||||
assertThat(weighted.outerLengthCm()).isEqualByComparingTo("60.001");
|
||||
assertThat(weighted.outerWidthCm()).isEqualByComparingTo("50");
|
||||
assertThat(weighted.outerHeightCm()).isEqualByComparingTo("50");
|
||||
assertThat(weighted.calculationRuleVersion()).isEqualTo("ACTUAL_DIMENSION_OVERSIZE_V1");
|
||||
assertThat(weighted.warning()).contains("已按实测尺寸记录").contains("人工复核");
|
||||
assertThat(weighted.calculationRuleVersion()).isEqualTo("ACTUAL_DIMENSION_V1");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+48
-2
@@ -1,7 +1,14 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.client.MeasurementImageClient;
|
||||
import com.baiyue.firstleg.dto.CartonMeasurementCommand;
|
||||
import com.baiyue.firstleg.dto.CartonMeasurementResult;
|
||||
import com.baiyue.firstleg.dto.MojiaMeasurementDtos.UploadResponse;
|
||||
import com.baiyue.firstleg.entity.MojiaMeasurementReceiptEntity;
|
||||
import com.baiyue.firstleg.repository.MojiaMeasurementReceiptRepository;
|
||||
import com.baiyue.firstleg.security.AuthenticatedMeasurementApp;
|
||||
import com.baiyue.firstleg.security.MeasurementRequestAuthenticator;
|
||||
import com.baiyue.firstleg.security.MeasurementRequestHeaders;
|
||||
@@ -9,6 +16,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.validation.Validation;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
@@ -26,8 +34,11 @@ class MojiaMeasurementOpenApiServiceTest {
|
||||
void fixedAppKeyJsonIsConvertedToTheExistingCartonMeasurementFlow() throws Exception {
|
||||
MeasurementRequestAuthenticator authenticator = mock(MeasurementRequestAuthenticator.class);
|
||||
CartonMeasurementService cartons = mock(CartonMeasurementService.class);
|
||||
MeasurementImageClient images = mock(MeasurementImageClient.class);
|
||||
MojiaMeasurementReceiptRepository receipts = mock(MojiaMeasurementReceiptRepository.class);
|
||||
var service = new MojiaMeasurementOpenApiService(authenticator, cartons,
|
||||
new ObjectMapper().findAndRegisterModules(), Validation.buildDefaultValidatorFactory().getValidator());
|
||||
new ObjectMapper().findAndRegisterModules(), Validation.buildDefaultValidatorFactory().getValidator(),
|
||||
images, receipts);
|
||||
MeasurementRequestHeaders headers = new MeasurementRequestHeaders("mojia", null, null, null, null);
|
||||
when(authenticator.authenticate("POST", MojiaMeasurementOpenApiService.CALLBACK_PATH,
|
||||
headers, json(), false)).thenReturn(new AuthenticatedMeasurementApp("mojia", 10001L, 90001L, Map.of()));
|
||||
@@ -35,8 +46,9 @@ class MojiaMeasurementOpenApiServiceTest {
|
||||
1L, 1, 2L, "RK-001#4-1", 1, new BigDecimal("60.1"), new BigDecimal("40.2"),
|
||||
new BigDecimal("30.3"), new BigDecimal("12.5"), 3L, "MJ-01", LocalDateTime.now(), true));
|
||||
|
||||
CartonMeasurementResult result = service.receive(headers, json());
|
||||
UploadResponse result = service.receive(headers, json());
|
||||
|
||||
assertThat(result.matched()).isTrue();
|
||||
assertThat(result.boxCode()).isEqualTo("RK-001#4-1");
|
||||
ArgumentCaptor<CartonMeasurementCommand> command = ArgumentCaptor.forClass(CartonMeasurementCommand.class);
|
||||
verify(cartons).upsert(eq(10001L), eq(90001L), command.capture());
|
||||
@@ -46,6 +58,40 @@ class MojiaMeasurementOpenApiServiceTest {
|
||||
assertThat(command.getValue().image().getBytes()).containsExactly(0xff, 0xd8, 0xff, 0xd9);
|
||||
}
|
||||
|
||||
@Test
|
||||
void unmatchedSimulationIsAcceptedWithFieldsAndImageSavedForInvestigation() {
|
||||
MeasurementRequestAuthenticator authenticator = mock(MeasurementRequestAuthenticator.class);
|
||||
CartonMeasurementService cartons = mock(CartonMeasurementService.class);
|
||||
MeasurementImageClient images = mock(MeasurementImageClient.class);
|
||||
MojiaMeasurementReceiptRepository receipts = mock(MojiaMeasurementReceiptRepository.class);
|
||||
var service = new MojiaMeasurementOpenApiService(authenticator, cartons,
|
||||
new ObjectMapper().findAndRegisterModules(), Validation.buildDefaultValidatorFactory().getValidator(),
|
||||
images, receipts);
|
||||
ReflectionTestUtils.setField(service, "internalToken", "internal-token");
|
||||
MeasurementRequestHeaders headers = new MeasurementRequestHeaders("mojia", null, null, null, null);
|
||||
when(authenticator.authenticate("POST", MojiaMeasurementOpenApiService.CALLBACK_PATH,
|
||||
headers, json(), false)).thenReturn(new AuthenticatedMeasurementApp("mojia", 10001L, 90001L, Map.of()));
|
||||
when(cartons.upsert(eq(10001L), eq(90001L), any()))
|
||||
.thenThrow(new BizException(ResultCode.NOT_FOUND, "箱号不存在或不属于当前客户"));
|
||||
when(images.uploadReceipt(eq("internal-token"), eq(90001L), eq(10001L), any(Long.TYPE), any()))
|
||||
.thenReturn(ApiResponse.success(new MeasurementImageClient.UploadResult(
|
||||
3L, "mojia.jpg", "image/jpeg", 4L, "NOT_REQUIRED", null, null, null, null)));
|
||||
|
||||
UploadResponse result = service.receive(headers, json());
|
||||
|
||||
assertThat(result.accepted()).isTrue();
|
||||
assertThat(result.matched()).isFalse();
|
||||
assertThat(result.receiptId()).isNotNull();
|
||||
assertThat(result.imageFileId()).isEqualTo(3L);
|
||||
ArgumentCaptor<MojiaMeasurementReceiptEntity> receipt =
|
||||
ArgumentCaptor.forClass(MojiaMeasurementReceiptEntity.class);
|
||||
verify(receipts).insert(receipt.capture());
|
||||
assertThat(receipt.getValue().getBoxCode()).isEqualTo("RK-001#4-1");
|
||||
assertThat(receipt.getValue().getLengthCm()).isEqualByComparingTo("60.1");
|
||||
assertThat(receipt.getValue().getImageFileId()).isEqualTo(3L);
|
||||
assertThat(receipt.getValue().getStatus()).isEqualTo("UNMATCHED");
|
||||
}
|
||||
|
||||
private String json() {
|
||||
return """
|
||||
{"boxCode":"RK-001#4-1","length":60.1,"width":40.2,"height":30.3,
|
||||
|
||||
+24
-2
@@ -17,7 +17,7 @@ class UnifiedInboundQueryServiceTest {
|
||||
assertThat(UnifiedInboundQueryService.mainStatus("WAIT_PRICING")).isEqualTo("INBOUNDED");
|
||||
assertThat(UnifiedInboundQueryService.mainStatus("FEE_CONFIRMING")).isEqualTo("INBOUNDED");
|
||||
assertThat(UnifiedInboundQueryService.mainStatus("INBOUNDING")).isEqualTo("INBOUNDED");
|
||||
assertThat(UnifiedInboundQueryService.mainStatus("WAIT_SPLIT")).isEqualTo("INBOUNDED");
|
||||
assertThat(UnifiedInboundQueryService.mainStatus("WAIT_SPLIT")).isEqualTo("WAIT_SPLIT");
|
||||
assertThat(UnifiedInboundQueryService.mainStatus("INBOUNDED")).isEqualTo("INBOUNDED");
|
||||
assertThat(UnifiedInboundQueryService.mainStatus("LOADING")).isEqualTo("LOADING");
|
||||
assertThat(UnifiedInboundQueryService.mainStatus("LOADED")).isEqualTo("LOADED");
|
||||
@@ -37,12 +37,34 @@ class UnifiedInboundQueryServiceTest {
|
||||
assertThat(UnifiedInboundQueryService.nextAction("INBOUNDED", "INBOUNDING", null)).isNull();
|
||||
assertThat(UnifiedInboundQueryService.nextAction("INBOUNDED", "INBOUNDED", "CONFIRMED")).isNull();
|
||||
assertThat(UnifiedInboundQueryService.nextAction("INBOUNDED", "WAIT_SPLIT", null))
|
||||
.isEqualTo("SMART_SPLIT");
|
||||
.isNull();
|
||||
assertThat(UnifiedInboundQueryService.nextAction("WAIT_SPLIT", "WAIT_SPLIT", null)).isNull();
|
||||
assertThat(UnifiedInboundQueryService.nextAction("LOADED", "LOADED", null))
|
||||
.isEqualTo("COMPLETE_DELIVERY");
|
||||
assertThat(UnifiedInboundQueryService.nextAction("DELIVERED", "DELIVERED", null)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void waitSplitFilterRemainsSeparateFromInbounded() throws Exception {
|
||||
var method = UnifiedInboundQueryService.class.getDeclaredMethod("internalStatuses", String.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
assertThat(method.invoke(new UnifiedInboundQueryService(), "WAIT_SPLIT"))
|
||||
.isEqualTo(java.util.List.of("WAIT_SPLIT"));
|
||||
assertThat(method.invoke(new UnifiedInboundQueryService(), "INBOUNDED"))
|
||||
.isEqualTo(java.util.List.of("WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING", "INBOUNDED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void phaseTwoMeasurementStatusesDoNotRequireUndeployedPhaseThreeTables() {
|
||||
assertThat(UnifiedInboundQueryService.requiresPhaseThreeSummary("WAIT_MEASURE")).isFalse();
|
||||
assertThat(UnifiedInboundQueryService.requiresPhaseThreeSummary("INBOUNDED")).isFalse();
|
||||
assertThat(UnifiedInboundQueryService.requiresPhaseThreeSummary("WAIT_SPLIT")).isFalse();
|
||||
assertThat(UnifiedInboundQueryService.requiresPhaseThreeSummary("LOADING")).isTrue();
|
||||
assertThat(UnifiedInboundQueryService.requiresPhaseThreeSummary("LOADED")).isTrue();
|
||||
assertThat(UnifiedInboundQueryService.requiresPhaseThreeSummary("DELIVERED")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsUnknownReviewStatusBeforeRepositoryQuery() {
|
||||
UnifiedInboundQuery query = new UnifiedInboundQuery(
|
||||
|
||||
Reference in New Issue
Block a user