This commit is contained in:
2026-08-17 12:31:24 +08:00
parent 8566c6ea9e
commit ac6cf9c904
23 changed files with 396 additions and 58 deletions
@@ -0,0 +1,43 @@
package com.baiyue.firstleg.domain;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import java.math.BigDecimal;
import java.util.Locale;
import java.util.Map;
/** Platform-controlled container specifications shared by booking and simulation flows. */
public final class ContainerSpecificationCatalog {
private static final Map<String, Specification> VALUES = Map.of(
"20GP", new Specification("20GP", new BigDecimal("33"), new BigDecimal("28000")),
"40GP", new Specification("40GP", new BigDecimal("67"), new BigDecimal("28000")),
"40HC", new Specification("40HC", new BigDecimal("76"), new BigDecimal("28000")),
"40NR", new Specification("40NR", new BigDecimal("67"), new BigDecimal("28000")),
"45HC", new Specification("45HC", new BigDecimal("86"), new BigDecimal("28000"))
);
private ContainerSpecificationCatalog() {}
public static Specification require(String containerType) {
String normalized = containerType == null ? "" : containerType.trim().toUpperCase(Locale.ROOT);
Specification specification = VALUES.get(normalized);
if (specification == null) {
throw new BizException(ResultCode.VALIDATION_FAILED, "柜型必须使用平台固定规格");
}
return specification;
}
public static Specification requireMatch(String containerType, BigDecimal capacityCbm, BigDecimal maxWeightKg) {
Specification specification = require(containerType);
if (capacityCbm == null || maxWeightKg == null
|| specification.capacityCbm().compareTo(capacityCbm) != 0
|| specification.maxWeightKg().compareTo(maxWeightKg) != 0) {
throw new BizException(ResultCode.VALIDATION_FAILED,
"柜型容量和限重必须使用平台固定规格");
}
return specification;
}
public record Specification(String containerType, BigDecimal capacityCbm, BigDecimal maxWeightKg) {}
}
@@ -6,9 +6,9 @@ import java.util.List;
@Mapper public interface ContainerOverviewMapper {
@Select("SELECT * FROM fl_container_document WHERE container_booking_id=#{id} AND document_type IN ('DELIVERY_ORDER','PICKUP_ORDER') ORDER BY document_type") List<ContainerDocumentEntity> documents(@Param("id") long id);
@Select("SELECT * FROM fl_container_transport_history WHERE container_booking_id=#{id} ORDER BY created_at,id") List<ContainerTransportHistoryEntity> transport(@Param("id") long id);
@Select("SELECT id,action,reason,operator_id,operator_name,occurred_at FROM fl_container_exception_history WHERE container_booking_id=#{id} ORDER BY occurred_at,id") List<ExceptionRow> exceptions(@Param("id") long id);
@Select("SELECT id,action,reason,operator_id operatorId,operator_name operatorName,occurred_at occurredAt,request_hash requestHash FROM fl_container_exception_history WHERE container_booking_id=#{id} ORDER BY occurred_at,id") List<ExceptionRow> exceptions(@Param("id") long id);
@Insert("INSERT INTO fl_container_exception_history(id,container_booking_id,action,reason,operator_id,operator_name,operation_id,request_hash,occurred_at) VALUES(#{id},#{bookingId},#{action},#{reason},#{operatorId},#{operatorName},#{operationId},#{requestHash},CURRENT_TIMESTAMP(3))") int insertException(@Param("id") long id,@Param("bookingId") long bookingId,@Param("action") String action,@Param("reason") String reason,@Param("operatorId") long operatorId,@Param("operatorName") String operatorName,@Param("operationId") String operationId,@Param("requestHash") String requestHash);
@Select("SELECT action,reason,operator_id operatorId,operator_name operatorName,occurred_at occurredAt FROM fl_container_exception_history WHERE container_booking_id=#{id} ORDER BY id DESC LIMIT 1") ExceptionRow latestException(@Param("id") long id);
@Select("SELECT id,action,reason,operator_id operatorId,operator_name operatorName,occurred_at occurredAt,request_hash requestHash FROM fl_container_exception_history WHERE container_booking_id=#{id} ORDER BY id DESC LIMIT 1") ExceptionRow latestException(@Param("id") long id);
@Select("SELECT id,action,reason,operator_id operatorId,operator_name operatorName,occurred_at occurredAt,request_hash requestHash FROM fl_container_exception_history WHERE container_booking_id=#{bookingId} AND operator_id=#{operatorId} AND operation_id=#{operationId} LIMIT 1") ExceptionRow exceptionOperation(@Param("bookingId") long bookingId,@Param("operatorId") long operatorId,@Param("operationId") String operationId);
@Select("SELECT id,loading_plan_id AS loadingPlanId,container_booking_id AS containerBookingId,"
+ "task_status AS taskStatus,version FROM fl_loading_pick_task "
@@ -5,6 +5,7 @@ import com.baiyue.common.core.api.PageResult;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.oceanbooking.domain.OceanBookingStatus;
import com.baiyue.firstleg.domain.ContainerSpecificationCatalog;
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.*;
import com.baiyue.firstleg.oceanbooking.entity.*;
import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
@@ -267,7 +268,7 @@ public class OceanBookingService {
}
private static List<OceanBookingPartyEntity> parties(long id, List<PartyRequest> requests) { return requests.stream().map(r -> { OceanBookingPartyEntity v = new OceanBookingPartyEntity(); v.setId(IdWorker.getId()); v.setOceanBookingId(id); v.setPartyType(r.partyType().trim().toUpperCase()); v.setPartyName(r.partyName().trim()); v.setAddress(trim(r.address())); v.setContactName(trim(r.contactName())); v.setContactPhone(trim(r.contactPhone())); return v; }).toList(); }
private static List<OceanBookingContainerRequirementEntity> requirements(long id, List<ContainerRequirementRequest> requests) { int[] sort = {0}; return requests.stream().map(r -> { OceanBookingContainerRequirementEntity v = new OceanBookingContainerRequirementEntity(); v.setId(IdWorker.getId()); v.setOceanBookingId(id); v.setContainerType(r.containerType().trim().toUpperCase()); v.setContainerKind(r.containerKind().trim().toUpperCase()); v.setCapacityCbm(r.capacityCbm()); v.setMaxWeightKg(r.maxWeightKg()); v.setQuantity(r.quantity()); v.setSortNo(sort[0]++); return v; }).toList(); }
private static List<OceanBookingContainerRequirementEntity> requirements(long id, List<ContainerRequirementRequest> requests) { int[] sort = {0}; return requests.stream().map(r -> { var specification = ContainerSpecificationCatalog.requireMatch(r.containerType(), r.capacityCbm(), r.maxWeightKg()); OceanBookingContainerRequirementEntity v = new OceanBookingContainerRequirementEntity(); v.setId(IdWorker.getId()); v.setOceanBookingId(id); v.setContainerType(specification.containerType()); v.setContainerKind(r.containerKind().trim().toUpperCase()); v.setCapacityCbm(specification.capacityCbm()); v.setMaxWeightKg(specification.maxWeightKg()); v.setQuantity(r.quantity()); v.setSortNo(sort[0]++); return v; }).toList(); }
private String hash(String action, Long id, Object request) { try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest((action + '|' + id + '|' + objectMapper.writeValueAsString(request)).getBytes(StandardCharsets.UTF_8))); } catch (Exception ex) { throw new IllegalStateException("无法计算订舱请求摘要", ex); } }
private String json(Object value) { if (value == null) return null; try { return objectMapper.writeValueAsString(value); } catch (JsonProcessingException ex) { throw new IllegalStateException("无法保存订舱历史快照", ex); } }
private static OceanBookingStatus parseStatus(String status) { try { return OceanBookingStatus.valueOf(status); } catch (RuntimeException ex) { throw badRequest("订舱状态无效"); } }
@@ -7,6 +7,7 @@ import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.client.WarehouseMasterDataClient;
import com.baiyue.firstleg.domain.SmartLoadingStatus;
import com.baiyue.firstleg.domain.ContainerSpecificationCatalog;
import com.baiyue.firstleg.dto.ContainerDtos.FullUpdateRequest;
import com.baiyue.firstleg.dto.ContainerDtos.Query;
import com.baiyue.firstleg.dto.ContainerDtos.SaveRequest;
@@ -56,6 +57,7 @@ public class ContainerService {
@Transactional
public ContainerBookingEntity createSimulation(long operatorId, String operatorName, SimulationCreateRequest request) {
var specification = ContainerSpecificationCatalog.requireMatch(request.containerType(), request.capacityCbm(), request.maxWeightKg());
WarehouseMasterDataClient.Warehouse warehouse = requireDomesticTransitWarehouse(request.warehouseId());
Long versionId = priceBooks.activeVersionId();
if (versionId == null) throw conflict("当前没有 ACTIVE 价格版本");
@@ -66,8 +68,8 @@ public class ContainerService {
value.setBookingNo("CB" + value.getId()); value.setCabinetNo("CAB-" + value.getId());
value.setChannelCode(channel.getChannelCode()); value.setChannelName(channel.getChannelName());
value.setWarehouseId(warehouse.id()); value.setWarehouseName(warehouse.warehouseName());
value.setContainerType(request.containerType().trim()); value.setCapacityCbm(request.capacityCbm());
value.setMaxWeightKg(request.maxWeightKg()); value.setCreatedBy(operatorId); value.setCreatedByName(operatorName);
value.setContainerType(specification.containerType()); value.setCapacityCbm(specification.capacityCbm());
value.setMaxWeightKg(specification.maxWeightKg()); value.setCreatedBy(operatorId); value.setCreatedByName(operatorName);
value.setUpdatedBy(operatorId); value.setUpdatedByName(operatorName); value.setVersion(0); repository.insert(value);
value.setPlanStatus(SmartLoadingStatus.WAIT_LOADING.name()); return value;
}
@@ -130,6 +132,7 @@ public class ContainerService {
throw conflict("货柜已进入预装流程,只能修改集装箱号和航次");
}
validateSchedule(request.data());
ContainerSpecificationCatalog.requireMatch(request.data().containerType(), request.data().capacityCbm(), request.data().maxWeightKg());
WarehouseMasterDataClient.Warehouse warehouse = requireDomesticTransitWarehouse(request.data().warehouseId());
var trucking = suppliers.trucking(request.data().terminalTruckingCompanyId(), request.data().terminalTruckingCompany());
ensureUnique(trim(request.data().containerNo()), request.data().cabinetNo().trim(), id);
@@ -214,16 +217,17 @@ public class ContainerService {
catch (RuntimeException ex) { throw conflict("装柜计划状态无效,不能修改货柜"); }
}
private static void copy(ContainerBookingEntity v, SaveRequest r) {
var specification = ContainerSpecificationCatalog.requireMatch(r.containerType(), r.capacityCbm(), r.maxWeightKg());
v.setContainerNo(trim(r.containerNo())); v.setCabinetNo(r.cabinetNo().trim());
v.setWarehouseId(r.warehouseId()); v.setWarehouseName(trim(r.warehouseName()));
v.setBillOfLadingNo(trim(r.billOfLadingNo())); v.setVesselName(trim(r.vesselName()));
v.setVoyageNo(trim(r.voyageNo())); v.setContainerType(r.containerType().trim());
v.setVoyageNo(trim(r.voyageNo())); v.setContainerType(specification.containerType());
v.setShippingCompanyId(r.shippingCompanyId()); v.setShippingCompanyName(trim(r.shippingCompanyName()));
v.setDeparturePortCode(trim(r.departurePortCode())); v.setDeparturePortName(trim(r.departurePortName()));
v.setDestinationPortCode(trim(r.destinationPortCode())); v.setDestinationPortName(trim(r.destinationPortName()));
v.setDestinationWarehouseCode(trim(r.destinationWarehouseCode()));
v.setDestinationWarehouseName(trim(r.destinationWarehouseName()));
v.setCapacityCbm(r.capacityCbm()); v.setMaxWeightKg(r.maxWeightKg());
v.setCapacityCbm(specification.capacityCbm()); v.setMaxWeightKg(specification.maxWeightKg());
v.setTerminalTruckingCompany(trim(r.terminalTruckingCompany())); v.setLoadingAt(r.loadingAt());
v.setEstimatedSailingAt(r.estimatedSailingAt()); v.setActualSailingAt(r.actualSailingAt());
v.setEstimatedPortArrivalAt(r.estimatedPortArrivalAt()); v.setActualPortArrivalAt(r.actualPortArrivalAt());
@@ -101,13 +101,25 @@ public class MeasurementLocationOccupationService {
private java.util.List<WarehousePalletLocationClient.PalletReference> confirmedPalletReferences(
long tenantId, long inboundId) {
java.util.List<WarehousePalletLocationClient.PalletReference> references = measurements
.listWeights(tenantId, inboundId).stream()
.filter(weight -> "CONFIRMED".equals(weight.getRecognitionStatus()))
.filter(weight -> weight.getConfirmedAt() != null && weight.getNetWeightKg() != null)
.filter(weight -> weight.getPalletId() != null && weight.getPalletId() > 0)
.map(weight -> new WarehousePalletLocationClient.PalletReference(weight.getId(), weight.getPalletId()))
.toList();
java.util.List<WarehousePalletLocationClient.PalletReference> references = new java.util.ArrayList<>();
for (InboundWeightResultEntity weight : measurements.listWeights(tenantId, inboundId)) {
if (!"CONFIRMED".equals(weight.getRecognitionStatus())) {
continue;
}
if (weight.getConfirmedAt() == null) {
throw new BizException(ResultCode.CONFLICT, "已确认称重图片缺少确认时间,无法分配库位");
}
if (weight.getNetWeightKg() == null || weight.getNetWeightKg().signum() <= 0) {
throw new BizException(ResultCode.CONFLICT, "已确认称重图片缺少有效净重,无法分配库位");
}
if (weight.getPalletId() == null || weight.getPalletId() <= 0) {
throw new BizException(ResultCode.CONFLICT, "已确认称重图片缺少有效托盘,无法分配库位");
}
if (weight.getId() == null || weight.getId() <= 0) {
throw new BizException(ResultCode.CONFLICT, "已确认称重图片缺少结果标识,无法分配库位");
}
references.add(new WarehousePalletLocationClient.PalletReference(weight.getId(), weight.getPalletId()));
}
if (references.isEmpty()) {
throw new BizException(ResultCode.CONFLICT, "至少需要一张已确认的称重图片才能提交测量");
}
@@ -78,13 +78,12 @@ public class MeasurementReviewService {
throw new BizException(ResultCode.VALIDATION_FAILED,
"人工确认箱数合计与入库单总箱数不一致,不能审核通过");
}
BigDecimal weight = items.stream().map(com.baiyue.firstleg.entity.InboundMeasurementItemEntity::getTotalWeightKg)
.filter(java.util.Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal weight = order.getActualGrossWeightKg();
BigDecimal volume = items.stream().map(com.baiyue.firstleg.entity.InboundMeasurementItemEntity::getTotalVolumeCbm)
.filter(java.util.Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
if (weight.signum() <= 0 || volume.signum() <= 0) {
if (weight == null || weight.signum() <= 0 || volume.signum() <= 0) {
throw new BizException(ResultCode.VALIDATION_FAILED,
"审核通过的测量快照必须包含大于零的实重和体积");
"审核通过的测量快照必须包含大于零的货物总重量和体积");
}
}