437 lines
23 KiB
Java
437 lines
23 KiB
Java
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.entity.InboundMeasurementEntity;
|
|
import com.baiyue.firstleg.entity.InboundMeasurementItemEntity;
|
|
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
|
import com.baiyue.firstleg.entity.InboundWeightResultEntity;
|
|
import com.baiyue.firstleg.repository.InboundMeasurementRepository;
|
|
import feign.FeignException;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
import org.springframework.beans.factory.ObjectProvider;
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.transaction.support.TransactionTemplate;
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
import org.springframework.util.StringUtils;
|
|
import org.springframework.web.multipart.MultipartFile;
|
|
|
|
import java.math.BigDecimal;
|
|
import java.math.RoundingMode;
|
|
import java.time.LocalDateTime;
|
|
import java.util.List;
|
|
import java.util.Set;
|
|
|
|
/** 单箱当前测量新增/覆盖与人工确认箱数编排。 */
|
|
@Service
|
|
public class CartonMeasurementService {
|
|
private static final Logger log = LoggerFactory.getLogger(CartonMeasurementService.class);
|
|
private static final Set<String> DEVICE_WRITABLE_STATUSES = Set.of(
|
|
"PREPARING", "WAIT_MEASURE", "MEASURING", "MEASURE_REJECTED");
|
|
private static final Set<String> MANUAL_COUNT_EDITABLE_STATUSES = Set.of(
|
|
"PREPARING", "WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED");
|
|
private static final BigDecimal VOLUME_WEIGHT_FACTOR = new BigDecimal("167");
|
|
private static final long MAX_IMAGE_BYTES = 5L * 1024 * 1024;
|
|
|
|
private final InboundMeasurementRepository repository;
|
|
private final ObjectProvider<CartonLabelBindingResolver> resolverProvider;
|
|
private final MeasurementImageClient imageClient;
|
|
private final TransactionTemplate transactionTemplate;
|
|
private final MeasurementBatchService measurementBatchService;
|
|
|
|
@Value("${baiyue.security.internal-service-token}")
|
|
private String internalToken;
|
|
|
|
/** 设备及手工单箱录入复用测量批次的计算规则。 */
|
|
public CartonMeasurementService(InboundMeasurementRepository repository,
|
|
ObjectProvider<CartonLabelBindingResolver> resolverProvider,
|
|
MeasurementImageClient imageClient,
|
|
TransactionTemplate transactionTemplate,
|
|
MeasurementBatchService measurementBatchService) {
|
|
this.repository = repository;
|
|
this.resolverProvider = resolverProvider;
|
|
this.imageClient = imageClient;
|
|
this.transactionTemplate = transactionTemplate;
|
|
this.measurementBatchService = measurementBatchService;
|
|
}
|
|
|
|
public CartonMeasurementResult upsert(long tenantId, long operatorId, CartonMeasurementCommand command) {
|
|
return upsert(tenantId, operatorId, null, command,
|
|
MeasurementBatchService.SOURCE_THIRD_PARTY, "DEVICE");
|
|
}
|
|
|
|
public CartonMeasurementResult upsertForInbound(long tenantId, long operatorId, long inboundId,
|
|
CartonMeasurementCommand command) {
|
|
if (inboundId <= 0) {
|
|
throw new BizException(ResultCode.BAD_REQUEST, "入库单ID必须大于0");
|
|
}
|
|
return upsert(tenantId, operatorId, inboundId, command,
|
|
MeasurementBatchService.SOURCE_THIRD_PARTY, "DEVICE");
|
|
}
|
|
|
|
public CartonMeasurementResult upsertManualForInbound(long tenantId, long operatorId, long inboundId,
|
|
CartonMeasurementCommand command) {
|
|
if (inboundId <= 0) {
|
|
throw new BizException(ResultCode.BAD_REQUEST, "入库单ID必须大于0");
|
|
}
|
|
return upsert(tenantId, operatorId, inboundId, command,
|
|
MeasurementBatchService.SOURCE_MANUAL, "PLATFORM");
|
|
}
|
|
|
|
private CartonMeasurementResult upsert(long tenantId, long operatorId, Long expectedInboundId,
|
|
CartonMeasurementCommand command,
|
|
String sourceType, String sourceCode) {
|
|
validateCommand(command);
|
|
String boxCode = command.boxCode().trim();
|
|
CartonLabelBindingResolver.Binding binding = requireBinding(tenantId, boxCode);
|
|
if (expectedInboundId != null && binding.inboundId() != expectedInboundId) {
|
|
throw new BizException(ResultCode.NOT_FOUND, "箱号不属于当前入库单");
|
|
}
|
|
InboundOrderEntity inbound = requireDeviceWritableInbound(tenantId, binding.inboundId());
|
|
MeasurementImageClient.UploadResult uploaded = uploadImage(
|
|
tenantId, operatorId, inbound.getId(), command.image());
|
|
UpsertOutcome outcome;
|
|
try {
|
|
outcome = transactionTemplate.execute(status -> persist(
|
|
tenantId, operatorId, binding, boxCode, command, uploaded.fileId(), sourceType, sourceCode));
|
|
} catch (RuntimeException exception) {
|
|
cleanupImage(tenantId, operatorId, inbound.getId(), uploaded.fileId(), "回传落库失败补偿");
|
|
throw exception;
|
|
}
|
|
if (outcome == null) {
|
|
cleanupImage(tenantId, operatorId, inbound.getId(), uploaded.fileId(), "事务未返回结果补偿");
|
|
throw new BizException(ResultCode.INTERNAL_ERROR, "单箱测量保存失败");
|
|
}
|
|
if (outcome.oldImageFileId() != null && !outcome.oldImageFileId().equals(uploaded.fileId())) {
|
|
cleanupImage(tenantId, operatorId, inbound.getId(), outcome.oldImageFileId(), "同箱复测旧图清理");
|
|
}
|
|
return outcome.result();
|
|
}
|
|
|
|
@Transactional
|
|
public CartonMeasurementResult updateConfirmedPackageCount(
|
|
long tenantId, long inboundId, long itemId, int expectedMeasurementVersion,
|
|
int confirmedPackageCount) {
|
|
if (confirmedPackageCount < 1 || confirmedPackageCount > 1_000_000) {
|
|
throw new BizException(ResultCode.VALIDATION_FAILED, "确认箱数必须是1至1000000的整数");
|
|
}
|
|
InboundOrderEntity inbound = requireManualCountEditableInbound(tenantId, inboundId);
|
|
InboundMeasurementItemEntity item = repository.findItem(tenantId, itemId);
|
|
if (item == null) {
|
|
throw new BizException(ResultCode.NOT_FOUND, "单箱测量记录不存在");
|
|
}
|
|
InboundMeasurementEntity measurement = repository.findMeasurement(
|
|
tenantId, inboundId, item.getMeasurementId());
|
|
if (measurement == null) {
|
|
throw new BizException(ResultCode.NOT_FOUND, "单箱测量记录不属于当前入库单");
|
|
}
|
|
requireVersion(expectedMeasurementVersion, measurement.getVersion());
|
|
item.setPackageCount(confirmedPackageCount);
|
|
applyCalculatedValuesFromSnapshot(item);
|
|
item.setUpdatedAt(LocalDateTime.now());
|
|
if (!repository.updateItem(item)) {
|
|
throw concurrentModification();
|
|
}
|
|
List<InboundMeasurementItemEntity> items = repository.listItems(tenantId, measurement.getId());
|
|
recalculateMeasurement(measurement, items);
|
|
if (!repository.updateMeasurement(measurement)) {
|
|
throw concurrentModification();
|
|
}
|
|
// 提交审核后仍允许平台人工修正代表箱数,但订单原始总箱数不可被测量汇总覆盖。
|
|
if ("MEASURE_PENDING".equals(inbound.getProcessStatus())) {
|
|
List<InboundWeightResultEntity> confirmedWeights = repository.listWeights(tenantId, inboundId)
|
|
.stream()
|
|
.filter(weight -> "CONFIRMED".equals(weight.getRecognitionStatus()))
|
|
.toList();
|
|
if (!confirmedWeights.isEmpty()) {
|
|
if (confirmedWeights.stream().anyMatch(weight -> !positive(weight.getGrossWeightKg())
|
|
|| !positive(weight.getNetWeightKg()))) {
|
|
throw new BizException(ResultCode.VALIDATION_FAILED,
|
|
"已确认称重结果缺少有效毛重或净重,请重新确认后再修改箱数");
|
|
}
|
|
BigDecimal confirmedGrossWeight = confirmedWeights.stream()
|
|
.map(InboundWeightResultEntity::getGrossWeightKg)
|
|
.reduce(BigDecimal.ZERO, BigDecimal::add)
|
|
.setScale(3, RoundingMode.HALF_UP);
|
|
BigDecimal confirmedNetWeight = confirmedWeights.stream()
|
|
.map(InboundWeightResultEntity::getNetWeightKg)
|
|
.reduce(BigDecimal.ZERO, BigDecimal::add)
|
|
.setScale(3, RoundingMode.HALF_UP);
|
|
inbound.setActualGrossWeightKg(confirmedGrossWeight);
|
|
inbound.setActualNetWeightKg(confirmedNetWeight);
|
|
} else {
|
|
inbound.setActualGrossWeightKg(null);
|
|
inbound.setActualNetWeightKg(null);
|
|
}
|
|
inbound.setActualVolumeCbm(measurement.getTotalVolumeCbm());
|
|
inbound.setVolumeWeightKg(measurement.getVolumeWeightKg());
|
|
if (!repository.updateInbound(inbound)) {
|
|
throw concurrentModification();
|
|
}
|
|
}
|
|
return result(measurement, item, false);
|
|
}
|
|
|
|
/** 保存设备原始尺寸与证据,按客户默认算法或已保存的行算法生成计费快照。 */
|
|
private UpsertOutcome persist(long tenantId, long operatorId,
|
|
CartonLabelBindingResolver.Binding binding,
|
|
String boxCode, CartonMeasurementCommand command, long imageFileId,
|
|
String sourceType, String sourceCode) {
|
|
InboundOrderEntity inbound = repository.lockInbound(tenantId, binding.inboundId());
|
|
if (inbound == null) {
|
|
throw new BizException(ResultCode.NOT_FOUND, "入库单不存在");
|
|
}
|
|
if (!deviceWritable(inbound, tenantId)) {
|
|
throw new BizException(ResultCode.CONFLICT, "测量已提交,设备不能新增或覆盖数据");
|
|
}
|
|
InboundMeasurementItemEntity item = repository.findItemByBoxCode(tenantId, boxCode);
|
|
boolean created = item == null;
|
|
InboundMeasurementEntity measurement;
|
|
Long oldImageFileId = null;
|
|
if (created) {
|
|
measurement = repository.findLatestEditableMeasurement(tenantId, inbound.getId());
|
|
if (measurement == null) {
|
|
measurement = newMeasurement(tenantId, operatorId, inbound.getId(), sourceType, sourceCode);
|
|
repository.insertMeasurement(measurement);
|
|
}
|
|
item = new InboundMeasurementItemEntity();
|
|
item.setId(IdWorker.getId());
|
|
item.setTenantId(tenantId);
|
|
item.setMeasurementId(measurement.getId());
|
|
item.setPackageCount(1);
|
|
item.setCreatedAt(LocalDateTime.now());
|
|
} else {
|
|
measurement = repository.findMeasurement(tenantId, inbound.getId(), item.getMeasurementId());
|
|
if (measurement == null) {
|
|
throw new BizException(ResultCode.CONFLICT, "箱号已绑定到其他入库单");
|
|
}
|
|
oldImageFileId = item.getSourceImageFileId();
|
|
}
|
|
|
|
item.setInboundDetailId(binding.inboundDetailId());
|
|
item.setExternalDetailNo(boxCode);
|
|
item.setBoxCode(boxCode);
|
|
item.setScannedShipmentId(StringUtils.hasText(command.scannedShipmentId())
|
|
? command.scannedShipmentId().trim() : null);
|
|
item.setItemNo("B" + binding.cartonIndex());
|
|
item.setBoxSpecification(StringUtils.hasText(binding.boxSpecification())
|
|
? binding.boxSpecification().trim() : boxCode);
|
|
item.setLengthCm(command.lengthCm());
|
|
item.setWidthCm(command.widthCm());
|
|
item.setHeightCm(command.heightCm());
|
|
item.setSourceImageFileId(imageFileId);
|
|
item.setMachineNo(command.machineNo().trim());
|
|
item.setDeviceMeasuredAt(command.measuredAt());
|
|
item.setUpdatedAt(LocalDateTime.now());
|
|
measurementBatchService.calculateCartonDimensions(item);
|
|
|
|
if (created) {
|
|
repository.insertItem(item);
|
|
} else if (!repository.updateItem(item)) {
|
|
throw concurrentModification();
|
|
}
|
|
|
|
measurement.setStatus(MeasurementBatchService.STATUS_RECEIVED);
|
|
measurement.setSourceType(sourceType);
|
|
measurement.setSourceCode(sourceCode);
|
|
measurement.setDeviceCode(command.machineNo().trim());
|
|
measurement.setMeasuredAt(command.measuredAt());
|
|
measurement.setUpdatedAt(LocalDateTime.now());
|
|
recalculateMeasurement(measurement, repository.listItems(tenantId, measurement.getId()));
|
|
if (!repository.updateMeasurement(measurement)) {
|
|
throw concurrentModification();
|
|
}
|
|
return new UpsertOutcome(result(measurement, item, created), oldImageFileId);
|
|
}
|
|
|
|
private InboundMeasurementEntity newMeasurement(long tenantId, long operatorId, long inboundId,
|
|
String sourceType, String sourceCode) {
|
|
InboundMeasurementEntity measurement = new InboundMeasurementEntity();
|
|
measurement.setId(IdWorker.getId());
|
|
measurement.setTenantId(tenantId);
|
|
measurement.setInboundId(inboundId);
|
|
measurement.setSourceType(sourceType);
|
|
measurement.setSourceCode(sourceCode);
|
|
measurement.setStatus(MeasurementBatchService.STATUS_RECEIVED);
|
|
measurement.setCreatedBy(operatorId);
|
|
measurement.setCreatedAt(LocalDateTime.now());
|
|
measurement.setUpdatedAt(LocalDateTime.now());
|
|
measurement.setVersion(0);
|
|
return measurement;
|
|
}
|
|
|
|
/** 仅修改确认箱数时复用已保存的单箱快照,不重新套用当前客户规则。 */
|
|
private void applyCalculatedValuesFromSnapshot(InboundMeasurementItemEntity item) {
|
|
if (!positive(item.getOuterLengthCm()) || !positive(item.getOuterWidthCm())
|
|
|| !positive(item.getOuterHeightCm()) || !positive(item.getUnitVolumeCbm())
|
|
|| !StringUtils.hasText(item.getCalculationRuleVersion())) {
|
|
measurementBatchService.calculateCartonDimensions(item);
|
|
return;
|
|
}
|
|
BigDecimal count = BigDecimal.valueOf(item.getPackageCount());
|
|
BigDecimal totalRaw = item.getOuterLengthCm().multiply(item.getOuterWidthCm())
|
|
.multiply(item.getOuterHeightCm()).divide(new BigDecimal("1000000"), 6, RoundingMode.HALF_UP)
|
|
.multiply(count).setScale(6, RoundingMode.HALF_UP);
|
|
BigDecimal totalVolume = item.getUnitVolumeCbm().multiply(count)
|
|
.setScale(4, RoundingMode.UNNECESSARY);
|
|
item.setVolumeRawCbm(totalRaw);
|
|
item.setVolumeCbm(totalVolume);
|
|
item.setTotalVolumeCbm(totalVolume);
|
|
item.setVolumeWeightKg(totalVolume.multiply(VOLUME_WEIGHT_FACTOR).setScale(3, RoundingMode.CEILING));
|
|
}
|
|
|
|
private void recalculateMeasurement(InboundMeasurementEntity measurement,
|
|
List<InboundMeasurementItemEntity> items) {
|
|
long packages = items.stream().mapToLong(InboundMeasurementItemEntity::getPackageCount).sum();
|
|
if (packages > Integer.MAX_VALUE) {
|
|
throw new BizException(ResultCode.VALIDATION_FAILED, "测量确认总箱数超过系统上限");
|
|
}
|
|
BigDecimal rawVolume = items.stream().map(InboundMeasurementItemEntity::getVolumeRawCbm)
|
|
.reduce(BigDecimal.ZERO, BigDecimal::add).setScale(6, RoundingMode.HALF_UP);
|
|
BigDecimal totalVolume = items.stream().map(InboundMeasurementItemEntity::getTotalVolumeCbm)
|
|
.reduce(BigDecimal.ZERO, BigDecimal::add).setScale(4, RoundingMode.CEILING);
|
|
measurement.setTotalPackageCount((int) packages);
|
|
measurement.setTotalVolumeRawCbm(rawVolume);
|
|
measurement.setTotalVolumeCbm(totalVolume);
|
|
measurement.setVolumeWeightKg(totalVolume.multiply(VOLUME_WEIGHT_FACTOR)
|
|
.setScale(3, RoundingMode.CEILING));
|
|
}
|
|
|
|
private CartonLabelBindingResolver.Binding requireBinding(long tenantId, String boxCode) {
|
|
CartonLabelBindingResolver resolver = resolverProvider.getIfAvailable();
|
|
if (resolver == null) {
|
|
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "箱号映射服务暂不可用");
|
|
}
|
|
CartonLabelBindingResolver.Binding binding = resolver.resolveByBoxCode(tenantId, boxCode);
|
|
if (binding == null) {
|
|
throw new BizException(ResultCode.NOT_FOUND, "箱号不存在或不属于当前客户");
|
|
}
|
|
return binding;
|
|
}
|
|
|
|
private InboundOrderEntity requireDeviceWritableInbound(long tenantId, long inboundId) {
|
|
InboundOrderEntity inbound = repository.findInbound(tenantId, inboundId);
|
|
if (inbound == null) {
|
|
throw new BizException(ResultCode.NOT_FOUND, "入库单不存在");
|
|
}
|
|
if (!deviceWritable(inbound, tenantId)) {
|
|
throw new BizException(ResultCode.CONFLICT, "测量已提交,设备不能新增或覆盖数据");
|
|
}
|
|
return inbound;
|
|
}
|
|
|
|
/**
|
|
* 设备回传门禁以入库流程状态为主,并用测量批次状态兜底校验提交语义。
|
|
* RECEIVED/DRAFT 只是设备资料,不能把处于 WAIT_MEASURE 的入库单误判为已提交;
|
|
* SUBMITTED/APPROVED 则必须继续保留设备覆盖保护,即使旧数据的主状态快照滞后。
|
|
*/
|
|
private boolean deviceWritable(InboundOrderEntity inbound, long tenantId) {
|
|
if (!DEVICE_WRITABLE_STATUSES.contains(inbound.getProcessStatus())) {
|
|
return false;
|
|
}
|
|
InboundMeasurementEntity latest = repository.findLatestMeasurement(tenantId, inbound.getId());
|
|
return latest == null || !Set.of("SUBMITTED", "APPROVED").contains(latest.getStatus());
|
|
}
|
|
|
|
private InboundOrderEntity requireManualCountEditableInbound(long tenantId, long inboundId) {
|
|
InboundOrderEntity inbound = repository.findInbound(tenantId, inboundId);
|
|
if (inbound == null) {
|
|
throw new BizException(ResultCode.NOT_FOUND, "入库单不存在");
|
|
}
|
|
if (!MANUAL_COUNT_EDITABLE_STATUSES.contains(inbound.getProcessStatus())) {
|
|
throw new BizException(ResultCode.CONFLICT, "当前入库单状态不允许修改确认箱数");
|
|
}
|
|
return inbound;
|
|
}
|
|
|
|
private MeasurementImageClient.UploadResult uploadImage(long tenantId, long operatorId,
|
|
long inboundId, MultipartFile image) {
|
|
try {
|
|
ApiResponse<MeasurementImageClient.UploadResult> response = imageClient.upload(
|
|
internalToken, operatorId, tenantId, inboundId, image);
|
|
if (response == null || !response.successful() || response.data() == null
|
|
|| response.data().fileId() == null) {
|
|
throw downstream(response, "测量图片保存失败");
|
|
}
|
|
return response.data();
|
|
} catch (FeignException exception) {
|
|
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "文件服务暂不可用,测量数据未保存");
|
|
}
|
|
}
|
|
|
|
private void cleanupImage(long tenantId, long operatorId, long inboundId, long fileId, String action) {
|
|
try {
|
|
imageClient.delete(internalToken, operatorId, tenantId, inboundId, fileId);
|
|
} catch (RuntimeException exception) {
|
|
log.warn("{}失败 tenantId={}, inboundId={}, fileId={}", action, tenantId, inboundId, fileId, exception);
|
|
}
|
|
}
|
|
|
|
private BizException downstream(ApiResponse<?> response, String fallback) {
|
|
if (response == null) {
|
|
return new BizException(ResultCode.SERVICE_UNAVAILABLE, fallback);
|
|
}
|
|
String message = StringUtils.hasText(response.message()) ? response.message() : fallback;
|
|
return new BizException(ResultCode.of(response.code()), message);
|
|
}
|
|
|
|
private void validateCommand(CartonMeasurementCommand command) {
|
|
if (command == null || !StringUtils.hasText(command.boxCode())
|
|
|| command.boxCode().trim().length() > 128) {
|
|
throw new BizException(ResultCode.VALIDATION_FAILED, "箱号不能为空且不能超过128个字符");
|
|
}
|
|
if (!positive(command.lengthCm()) || !positive(command.widthCm()) || !positive(command.heightCm())) {
|
|
throw new BizException(ResultCode.VALIDATION_FAILED, "长、宽、高必须大于0");
|
|
}
|
|
if (!StringUtils.hasText(command.machineNo()) || command.machineNo().trim().length() > 64) {
|
|
throw new BizException(ResultCode.VALIDATION_FAILED, "设备编号不能为空且不能超过64个字符");
|
|
}
|
|
if (StringUtils.hasText(command.scannedShipmentId())
|
|
&& command.scannedShipmentId().trim().length() > 64) {
|
|
throw new BizException(ResultCode.VALIDATION_FAILED, "实扫Shipment ID不能超过64个字符");
|
|
}
|
|
if (command.measuredAt() == null) {
|
|
throw new BizException(ResultCode.VALIDATION_FAILED, "设备测量时间不能为空");
|
|
}
|
|
if (command.image() == null || command.image().isEmpty()) {
|
|
throw new BizException(ResultCode.VALIDATION_FAILED, "测量图片不能为空");
|
|
}
|
|
if (command.image().getSize() > MAX_IMAGE_BYTES) {
|
|
throw new BizException(ResultCode.VALIDATION_FAILED, "测量图片不能超过5MB");
|
|
}
|
|
}
|
|
|
|
private boolean positive(BigDecimal value) {
|
|
return value != null && value.compareTo(BigDecimal.ZERO) > 0;
|
|
}
|
|
|
|
private void requireVersion(int expected, Integer actual) {
|
|
if (actual == null || expected != actual) {
|
|
throw concurrentModification();
|
|
}
|
|
}
|
|
|
|
private CartonMeasurementResult result(InboundMeasurementEntity measurement,
|
|
InboundMeasurementItemEntity item, boolean created) {
|
|
return new CartonMeasurementResult(measurement.getId(), measurement.getVersion(), item.getId(),
|
|
item.getBoxCode(), item.getScannedShipmentId(), item.getPackageCount(),
|
|
item.getLengthCm(), item.getWidthCm(),
|
|
item.getHeightCm(), item.getSourceImageFileId(), item.getMachineNo(),
|
|
item.getDeviceMeasuredAt(), created);
|
|
}
|
|
|
|
private BizException concurrentModification() {
|
|
return new BizException(ResultCode.CONFLICT, "测量数据已被其他人修改,请刷新后重试");
|
|
}
|
|
|
|
private record UpsertOutcome(CartonMeasurementResult result, Long oldImageFileId) {
|
|
}
|
|
}
|