252 lines
17 KiB
Java
252 lines
17 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.WarehouseMasterDataClient;
|
|
import com.baiyue.firstleg.domain.SmartLoadingStatus;
|
|
import com.baiyue.common.core.container.ContainerSpecificationCatalog;
|
|
import com.baiyue.firstleg.domain.SmartLoadingChannelGroupCatalog;
|
|
import com.baiyue.firstleg.dto.ContainerDtos.FullUpdateRequest;
|
|
import com.baiyue.firstleg.dto.ContainerDtos.SaveRequest;
|
|
import com.baiyue.firstleg.dto.ContainerDtos.SimulationCreateRequest;
|
|
import com.baiyue.firstleg.dto.ContainerDtos.OceanBookingLinkRequest;
|
|
import com.baiyue.firstleg.domain.Iso6346ContainerNumber;
|
|
import com.baiyue.firstleg.entity.ContainerBookingEntity;
|
|
import com.baiyue.firstleg.repository.ContainerRepository;
|
|
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.ChannelRow;
|
|
import com.baiyue.firstleg.pricebookquery.application.port.in.PriceBookQueryUseCase;
|
|
import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
|
|
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingEntity;
|
|
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerRequirementEntity;
|
|
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerAllocationEntity;
|
|
import jakarta.annotation.Resource;
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
import org.springframework.dao.DuplicateKeyException;
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
|
import java.time.LocalDateTime;
|
|
|
|
@Service
|
|
public class ContainerService {
|
|
@Resource private ContainerRepository repository;
|
|
@Resource private WarehouseMasterDataClient warehouses;
|
|
@Resource private PriceBookQueryUseCase priceBooks;
|
|
@Resource private OceanBookingRepository oceanBookings;
|
|
@Resource private WarehouseSupplierSnapshotService suppliers;
|
|
@Resource private FirstlegDocumentNumberGenerator documentNumbers;
|
|
@Resource private SmartLoadingService smartLoadingService;
|
|
@Value("${baiyue.security.internal-service-token:}") private String internalToken;
|
|
@Value("${baiyue.warehouse.public-tenant-id:0}") private long publicWarehouseTenantId;
|
|
public void requireOverseas(long id) {
|
|
required(id);
|
|
if (!repository.departed(id)) throw new BizException(ResultCode.NOT_FOUND, "国外工作台货柜不存在");
|
|
}
|
|
|
|
/** 目的港清关资料允许已离仓或已实际开船货柜维护,不改写国内出仓事实。 */
|
|
public void requireClearanceAccess(long id) {
|
|
required(id);
|
|
if (!repository.departed(id) && !repository.sailed(id))
|
|
throw new BizException(ResultCode.NOT_FOUND, "货柜尚未离仓或实际开船,不能维护目的港清关资料");
|
|
}
|
|
|
|
public void requireDomestic(long id) {
|
|
required(id);
|
|
if (repository.departed(id)) throw new BizException(ResultCode.NOT_FOUND, "国内工作台货柜不存在");
|
|
}
|
|
|
|
@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 价格版本");
|
|
SmartLoadingChannelGroupCatalog.Definition channelGroup;
|
|
try { channelGroup = SmartLoadingChannelGroupCatalog.require(request.channelGroupCode()); }
|
|
catch (IllegalArgumentException exception) { throw conflict(exception.getMessage()); }
|
|
boolean activeGroup = priceBooks.channels(versionId, warehouse.id()).stream()
|
|
.map(ChannelRow::getChannelCode).anyMatch(channelGroup.channelCodes()::contains);
|
|
if (!activeGroup) throw conflict("所选物流渠道分组在当前仓库已失效,请重新选择");
|
|
ContainerBookingEntity value = new ContainerBookingEntity(); value.setId(IdWorker.getId());
|
|
value.setCabinetNo(documentNumbers.nextCabinetNo());
|
|
value.setSourceType("SIMULATION"); value.setRecordStatus("ACTIVE");
|
|
value.setChannelGroupCode(channelGroup.code()); value.setChannelCode(null); value.setChannelName(null);
|
|
value.setWarehouseId(warehouse.id()); value.setWarehouseName(warehouse.warehouseName());
|
|
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);
|
|
var plan = smartLoadingService.createForContainer(value.getId(), operatorId, operatorName);
|
|
value.setPlanId(plan.id()); value.setPlanNo(plan.planNo()); value.setPlanStatus(plan.status());
|
|
return value;
|
|
}
|
|
|
|
@Transactional
|
|
public ContainerBookingEntity linkOceanBooking(long id, long operatorId, String operatorName, OceanBookingLinkRequest request) {
|
|
ContainerBookingEntity container = required(id); requireVersion(container, request.expectedVersion()); requireFirstLinkMutable(container);
|
|
if (container.getOceanBookingId() != null) throw conflict("模拟柜已关联真实订舱柜");
|
|
String existingContainerNo = trim(container.getContainerNo());
|
|
String requestedContainerNo = trim(request.containerNo());
|
|
String containerNo = requestedContainerNo == null
|
|
? existingContainerNo
|
|
: Iso6346ContainerNumber.require(requestedContainerNo);
|
|
if (existingContainerNo != null && !existingContainerNo.equals(containerNo)) {
|
|
throw conflict("输入的真实集装箱号与当前柜次已录入柜号不一致,请先在预装运输信息中纠正");
|
|
}
|
|
OceanBookingEntity booking = oceanBookings.find(request.oceanBookingId());
|
|
if (booking == null || !"RELEASED".equals(booking.getStatus())) throw conflict("只能关联已发布订舱");
|
|
OceanBookingContainerRequirementEntity requirement = oceanBookings.requirement(request.oceanBookingId(), request.requirementId());
|
|
if (requirement == null) throw new BizException(ResultCode.NOT_FOUND, "订舱箱型需求不存在");
|
|
long allocated = oceanBookings.activeLinkedCount(request.oceanBookingId(), request.requirementId());
|
|
if (allocated >= requirement.getQuantity()) throw conflict("该订舱箱型需求已达到数量上限");
|
|
if (containerNo != null && repository.containerNoExists(request.oceanBookingId(), containerNo, id)) {
|
|
throw conflict("当前提单下集装箱号已存在");
|
|
}
|
|
if (!java.util.Objects.equals(container.getWarehouseId(), booking.getWarehouseId())
|
|
|| !java.util.Objects.equals(container.getChannelGroupCode(), booking.getChannelCode())
|
|
|| !java.util.Objects.equals(container.getContainerType(), requirement.getContainerType())) {
|
|
throw conflict("模拟柜与真实订舱柜仓库、渠道分组或柜型不一致");
|
|
}
|
|
try {
|
|
if (repository.linkReleasedBooking(id, request.oceanBookingId(), request.requirementId(), containerNo,
|
|
booking, request.expectedVersion(), operatorId, operatorName) != 1) {
|
|
throw conflict("真实订舱柜已被消费或货柜版本发生变化,请刷新后重试");
|
|
}
|
|
} catch (DuplicateKeyException exception) {
|
|
throw conflict("当前提单下集装箱号已存在");
|
|
}
|
|
OceanBookingContainerAllocationEntity allocation = new OceanBookingContainerAllocationEntity(); allocation.setId(IdWorker.getId()); allocation.setOceanBookingId(request.oceanBookingId()); allocation.setRequirementId(request.requirementId()); allocation.setContainerBookingId(id); allocation.setAllocatedBy(operatorId); allocation.setAllocatedByName(operatorName); allocation.setAllocatedAt(LocalDateTime.now()); oceanBookings.insert(allocation);
|
|
return required(id);
|
|
}
|
|
|
|
@Transactional
|
|
public ContainerBookingEntity assignRealContainerNo(long id, String requestedContainerNo, int expectedVersion,
|
|
long operatorId, String operatorName) {
|
|
ContainerBookingEntity container = required(id);
|
|
requireVersion(container, expectedVersion);
|
|
if (container.getOceanBookingId() == null || container.getOceanBookingRequirementId() == null) {
|
|
throw conflict("模拟柜尚未关联已发布订舱需求");
|
|
}
|
|
if (trim(container.getContainerNo()) != null) throw conflict("真实集装箱号已补录");
|
|
SmartLoadingStatus value = status(container);
|
|
if ((value != SmartLoadingStatus.WAIT_LOADING && value != SmartLoadingStatus.EDITING)
|
|
|| repository.hasPickTask(id) || repository.hasEvidence(id) || repository.hasDocuments(id)) {
|
|
throw conflict("已进入PDA、证据、单证或不可逆装柜阶段,不能补录真实柜号");
|
|
}
|
|
String containerNo = Iso6346ContainerNumber.require(requestedContainerNo);
|
|
if (repository.containerNoExists(container.getOceanBookingId(), containerNo, id)) {
|
|
throw conflict("当前提单下集装箱号已存在");
|
|
}
|
|
try {
|
|
if (repository.assignRealContainerNo(id, containerNo, expectedVersion, operatorId, operatorName) != 1) {
|
|
throw conflict("真实柜号已被补录或货柜版本发生变化,请刷新后重试");
|
|
}
|
|
} catch (DuplicateKeyException exception) {
|
|
throw conflict("当前提单下集装箱号已存在");
|
|
}
|
|
return required(id);
|
|
}
|
|
|
|
private void requireFirstLinkMutable(ContainerBookingEntity container) {
|
|
SmartLoadingStatus value = status(container);
|
|
if ((value != SmartLoadingStatus.WAIT_LOADING && value != SmartLoadingStatus.EDITING)
|
|
|| repository.hasPickTask(container.getId()) || repository.hasEvidence(container.getId())
|
|
|| repository.hasDocuments(container.getId())) throw conflict("已进入PDA、证据、单证或不可逆装柜阶段,不能关联订舱");
|
|
}
|
|
@Transactional
|
|
public ContainerBookingEntity updateDraft(long id, long operatorId, String operatorName,
|
|
FullUpdateRequest request) {
|
|
ContainerBookingEntity current = required(id);
|
|
requireVersion(current, request.version());
|
|
if (status(current) != SmartLoadingStatus.WAIT_LOADING && status(current) != SmartLoadingStatus.EDITING) {
|
|
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.overseasTrucking(request.data().terminalTruckingCompanyId(), request.data().terminalTruckingCompany());
|
|
copy(current, request.data());
|
|
current.setWarehouseId(warehouse.id()); current.setWarehouseName(warehouse.warehouseName());
|
|
current.setTerminalTruckingCompanyId(trucking == null ? null : trucking.supplierId());
|
|
current.setTerminalTruckingCompany(trucking == null ? null : trucking.name());
|
|
try {
|
|
if (repository.updateDraft(current, request.version(), operatorId, operatorName) != 1) {
|
|
throw conflict("货柜状态或版本已变化,请刷新后重试");
|
|
}
|
|
} catch (DuplicateKeyException ex) { throw duplicate(); }
|
|
return required(id);
|
|
}
|
|
|
|
private ContainerBookingEntity required(long id) {
|
|
ContainerBookingEntity value = repository.find(id);
|
|
if (value == null) throw new BizException(ResultCode.NOT_FOUND, "货柜不存在");
|
|
return value;
|
|
}
|
|
private WarehouseMasterDataClient.Warehouse requireDomesticTransitWarehouse(long warehouseId) {
|
|
try {
|
|
ApiResponse<WarehouseMasterDataClient.Detail> response = warehouses.active(
|
|
internalToken, publicWarehouseTenantId, warehouseId);
|
|
if (response == null || !response.successful() || response.data() == null
|
|
|| response.data().warehouse() == null) {
|
|
throw new BizException(ResultCode.NOT_FOUND, "国内中转仓不存在或已停用");
|
|
}
|
|
WarehouseMasterDataClient.Warehouse warehouse = response.data().warehouse();
|
|
if (!Long.valueOf(warehouseId).equals(warehouse.id())
|
|
|| !Long.valueOf(publicWarehouseTenantId).equals(warehouse.tenantId())
|
|
|| !"ACTIVE".equals(warehouse.status())) {
|
|
throw new BizException(ResultCode.NOT_FOUND, "国内中转仓不存在或已停用");
|
|
}
|
|
if (!"DOMESTIC".equals(warehouse.warehouseType()) || warehouse.warehouseName() == null
|
|
|| warehouse.warehouseName().isBlank()) {
|
|
throw new BizException(ResultCode.VALIDATION_FAILED, "虚拟柜只能选择启用中的国内中转仓");
|
|
}
|
|
return warehouse;
|
|
} catch (BizException exception) {
|
|
throw exception;
|
|
} catch (RuntimeException exception) {
|
|
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "国内中转仓服务暂时不可用");
|
|
}
|
|
}
|
|
private static void requireVersion(ContainerBookingEntity value, Integer version) {
|
|
if (!version.equals(value.getVersion())) throw conflict("货柜已被其他人修改,请刷新后重试");
|
|
}
|
|
private static SmartLoadingStatus status(ContainerBookingEntity value) {
|
|
try { return SmartLoadingStatus.valueOf(value.getPlanStatus()); }
|
|
catch (RuntimeException ex) { throw conflict("装柜计划状态无效,不能修改货柜"); }
|
|
}
|
|
private static void copy(ContainerBookingEntity v, SaveRequest r) {
|
|
var specification = ContainerSpecificationCatalog.requireMatch(r.containerType(), r.capacityCbm(), r.maxWeightKg());
|
|
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(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(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());
|
|
v.setEstimatedWarehouseArrivalAt(r.estimatedWarehouseArrivalAt());
|
|
v.setActualWarehouseArrivalAt(r.actualWarehouseArrivalAt());
|
|
}
|
|
private static void validateSchedule(SaveRequest r) {
|
|
validateRange(r.estimatedSailingAt(), r.estimatedPortArrivalAt(), "预计船期和预计到港时间");
|
|
validateRange(r.estimatedPortArrivalAt(), r.estimatedWarehouseArrivalAt(), "预计到港和预计到库时间");
|
|
validateRange(r.actualSailingAt(), r.actualPortArrivalAt(), "实际船期和实际到港时间");
|
|
validateRange(r.actualPortArrivalAt(), r.actualWarehouseArrivalAt(), "实际到港和实际到库时间");
|
|
}
|
|
private static void validateRange(LocalDateTime from, LocalDateTime to, String label) {
|
|
if (from != null && to != null && to.isBefore(from))
|
|
throw new BizException(ResultCode.VALIDATION_FAILED, label + "先后顺序不正确");
|
|
}
|
|
private static String trim(String value) {
|
|
if (value == null) return null;
|
|
String result = value.trim(); return result.isEmpty() ? null : result;
|
|
}
|
|
private static BizException duplicate() { return conflict("集装箱号或内部柜号已存在"); }
|
|
private static BizException conflict(String message) { return new BizException(ResultCode.CONFLICT, message); }
|
|
}
|