feat(firstleg): link simulated containers to real bookings
This commit is contained in:
+11
-3
@@ -6,8 +6,10 @@ import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.firstleg.dto.ContainerDtos.FullUpdateRequest;
|
||||
import com.baiyue.firstleg.dto.ContainerDtos.Query;
|
||||
import com.baiyue.firstleg.dto.ContainerDtos.SaveRequest;
|
||||
import com.baiyue.firstleg.dto.ContainerDtos.TransportUpdateRequest;
|
||||
import com.baiyue.firstleg.dto.ContainerDtos.SimulationCreateRequest;
|
||||
import com.baiyue.firstleg.dto.ContainerDtos.OceanBookingLinkRequest;
|
||||
import com.baiyue.firstleg.dto.ContainerDtos.OceanBookingUnlinkRequest;
|
||||
import com.baiyue.firstleg.entity.ContainerBookingEntity;
|
||||
import com.baiyue.firstleg.service.ContainerService;
|
||||
import com.baiyue.firstleg.service.FirstlegScopeGuard;
|
||||
@@ -88,9 +90,15 @@ public class PlatformContainerController {
|
||||
@PostMapping @PreAuthorize("hasAuthority('platform:firstleg:container:create')")
|
||||
@BusinessAudit(event="CONTAINER_CREATE", resourceType="CONTAINER", description="新增货柜基础资料", captureBefore=false)
|
||||
public ApiResponse<ContainerBookingEntity> create(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody SaveRequest request) {
|
||||
FirstlegScopeGuard.platform(user); return ApiResponse.success(service.create(user.userId(), user.username(), request));
|
||||
@Valid @RequestBody SimulationCreateRequest request) {
|
||||
FirstlegScopeGuard.platform(user); return ApiResponse.success(service.createSimulation(user.userId(), user.username(), request));
|
||||
}
|
||||
@PostMapping("/{id}/ocean-booking-link") @PreAuthorize("hasAuthority('platform:firstleg:container:edit')")
|
||||
@BusinessAudit(event="CONTAINER_OCEAN_BOOKING_LINK", resourceType="CONTAINER", resourceId="#id", description="关联真实订舱柜")
|
||||
public ApiResponse<ContainerBookingEntity> linkOceanBooking(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long id, @Valid @RequestBody OceanBookingLinkRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.linkOceanBooking(id, user.userId(), user.username(), request)); }
|
||||
@DeleteMapping("/{id}/ocean-booking-link") @PreAuthorize("hasAuthority('platform:firstleg:container:edit')")
|
||||
@BusinessAudit(event="CONTAINER_OCEAN_BOOKING_UNLINK", resourceType="CONTAINER", resourceId="#id", description="解除真实订舱柜关联")
|
||||
public ApiResponse<ContainerBookingEntity> unlinkOceanBooking(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long id, @Valid @RequestBody OceanBookingUnlinkRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.unlinkOceanBooking(id, user.userId(), user.username(), request)); }
|
||||
@PutMapping("/{id}") @PreAuthorize("hasAuthority('platform:firstleg:container:edit')")
|
||||
@BusinessAudit(event="CONTAINER_UPDATE", resourceType="CONTAINER", resourceId="#id", description="完整编辑货柜基础资料")
|
||||
@Operation(summary="完整编辑货柜", description="仅待装柜或预装柜中时允许")
|
||||
|
||||
@@ -92,4 +92,18 @@ public final class ContainerDtos {
|
||||
this(containerNo, voyageNo, version, "运输信息更新");
|
||||
}
|
||||
}
|
||||
|
||||
@Schema(name = "ContainerSimulationCreateRequest", description = "新增内部模拟柜;内部柜号由服务端生成")
|
||||
public record SimulationCreateRequest(@NotBlank @Size(max = 64) String channelCode,
|
||||
@NotNull @Positive Long warehouseId, @NotBlank @Size(max = 32) String containerType,
|
||||
@NotNull @DecimalMin(value = "0", inclusive = false) BigDecimal capacityCbm,
|
||||
@NotNull @DecimalMin(value = "0", inclusive = false) BigDecimal maxWeightKg) {}
|
||||
|
||||
@Schema(name = "ContainerOceanBookingLinkRequest", description = "将内部模拟柜关联到真实订舱箱型分配")
|
||||
public record OceanBookingLinkRequest(@NotNull @Positive Long oceanBookingId,
|
||||
@NotNull @Positive Long requirementId, @NotNull @PositiveOrZero Integer expectedVersion) {}
|
||||
|
||||
@Schema(name = "ContainerOceanBookingUnlinkRequest", description = "解除空模拟柜的真实订舱关联")
|
||||
public record OceanBookingUnlinkRequest(@NotNull @PositiveOrZero Integer expectedVersion,
|
||||
@NotBlank @Size(max = 500) String reason) {}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ public class ContainerBookingEntity {
|
||||
private String channelName;
|
||||
private Long oceanBookingId;
|
||||
private Long oceanBookingRequirementId;
|
||||
private Long customerId;
|
||||
private String customerName;
|
||||
private Long warehouseId;
|
||||
private String warehouseName;
|
||||
private String billOfLadingNo;
|
||||
|
||||
@@ -24,7 +24,7 @@ public interface ContainerBookingMapper extends BaseMapper<ContainerBookingEntit
|
||||
WHERE (#{query.containerNo} IS NULL OR b.container_no = #{query.containerNo})
|
||||
AND (#{query.cabinetNo} IS NULL OR b.cabinet_no = #{query.cabinetNo})
|
||||
AND (#{query.billOfLadingNo} IS NULL OR b.bill_of_lading_no = #{query.billOfLadingNo})
|
||||
AND (#{query.customerId} IS NULL OR EXISTS (
|
||||
AND (#{query.customerId} IS NULL OR b.customer_id = #{query.customerId} OR EXISTS (
|
||||
SELECT 1 FROM fl_ocean_booking_container_allocation customer_allocation
|
||||
JOIN fl_ocean_booking customer_booking ON customer_booking.id = customer_allocation.ocean_booking_id
|
||||
WHERE customer_allocation.container_booking_id = b.id
|
||||
@@ -98,6 +98,15 @@ public interface ContainerBookingMapper extends BaseMapper<ContainerBookingEntit
|
||||
""")
|
||||
int updateDraft(@Param("value") ContainerBookingEntity value, @Param("expectedVersion") int expectedVersion,
|
||||
@Param("operatorId") long operatorId, @Param("operatorName") String operatorName);
|
||||
@Update("UPDATE fl_container_booking SET ocean_booking_id=#{value.oceanBookingId},ocean_booking_requirement_id=#{value.oceanBookingRequirementId},customer_id=#{value.customerId},customer_name=#{value.customerName},updated_by=#{operatorId},updated_by_name=#{operatorName},updated_at=CURRENT_TIMESTAMP(3),version=version+1 WHERE id=#{value.id} AND version=#{expectedVersion} AND ocean_booking_id IS NULL")
|
||||
int updateLink(@Param("value") ContainerBookingEntity value, @Param("expectedVersion") int expectedVersion, @Param("operatorId") long operatorId, @Param("operatorName") String operatorName);
|
||||
@Update("UPDATE fl_container_booking SET ocean_booking_id=NULL,ocean_booking_requirement_id=NULL,customer_id=NULL,customer_name=NULL,updated_by=#{operatorId},updated_by_name=#{operatorName},updated_at=CURRENT_TIMESTAMP(3),version=version+1 WHERE id=#{id} AND version=#{expectedVersion}")
|
||||
int clearLink(@Param("id") long id, @Param("expectedVersion") int expectedVersion, @Param("operatorId") long operatorId, @Param("operatorName") String operatorName);
|
||||
@Select("SELECT COUNT(*) FROM fl_loading_plan_item i JOIN fl_loading_plan p ON p.id=i.loading_plan_id WHERE p.container_booking_id=#{id} AND i.deleted=0") int hasActiveLoadingItems(@Param("id") long id);
|
||||
@Select("SELECT COUNT(*) FROM fl_loading_pick_task t JOIN fl_loading_plan p ON p.id=t.loading_plan_id WHERE p.container_booking_id=#{id}") int hasPickTask(@Param("id") long id);
|
||||
@Select("SELECT COUNT(*) FROM fl_loading_evidence e JOIN fl_loading_plan_item i ON i.inbound_id=e.inbound_id AND i.account_id=e.tenant_id JOIN fl_loading_plan p ON p.id=i.loading_plan_id WHERE p.container_booking_id=#{id} AND i.deleted=0 AND e.status='ACTIVE'") int hasEvidence(@Param("id") long id);
|
||||
@Select("SELECT COUNT(*) FROM fl_container_document WHERE container_booking_id=#{id}") int hasDocuments(@Param("id") long id);
|
||||
@Select("SELECT COUNT(*) FROM fl_loading_plan_item i JOIN fl_loading_plan p ON p.id=i.loading_plan_id WHERE p.container_booking_id=#{id} AND i.deleted=0 AND i.account_id<>#{customerId}") int hasMismatchedCustomerItems(@Param("id") long id, @Param("customerId") long customerId);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_container_booking SET container_no=#{containerNo}, voyage_no=#{voyageNo},
|
||||
|
||||
-3
@@ -65,8 +65,5 @@ public class PlatformOceanBookingController {
|
||||
@PostMapping("/{oceanBookingId}/change-fail") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:change')")
|
||||
@BusinessAudit(event="OCEAN_BOOKING_CHANGE_FAIL", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="标记变更船期失败")
|
||||
public ApiResponse<Detail> failChange(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody ActionRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.failChange(oceanBookingId, user.userId(), user.username(), key, request)); }
|
||||
@PostMapping("/{oceanBookingId}/allocations") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:allocate')")
|
||||
@BusinessAudit(event="OCEAN_BOOKING_ALLOCATE_CONTAINER", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="分配订舱柜次")
|
||||
public ApiResponse<Detail> allocate(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody AllocationRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.allocate(oceanBookingId, user.userId(), user.username(), key, request)); }
|
||||
private static void validateOptionalCustomer(AuthenticatedUser user, Long customerId) { FirstlegScopeGuard.platform(user); if (customerId != null) FirstlegScopeGuard.platform(user, customerId); }
|
||||
}
|
||||
|
||||
@@ -83,9 +83,6 @@ public final class OceanBookingDtos {
|
||||
LocalDateTime estimatedSailingAt,
|
||||
LocalDateTime estimatedArrivalAt,
|
||||
@NotBlank @Size(max = 1000) String reason) {}
|
||||
public record AllocationRequest(@NotNull @Positive Long requirementId,
|
||||
@NotNull @Positive Long containerBookingId,
|
||||
@NotNull @Min(0) Integer expectedVersion) {}
|
||||
public record Detail(OceanBookingEntity oceanBooking,
|
||||
List<OceanBookingPartyEntity> parties,
|
||||
List<OceanBookingContainerRequirementEntity> containerRequirements,
|
||||
|
||||
+1
-9
@@ -17,6 +17,7 @@ public interface OceanBookingMapper extends BaseMapper<OceanBookingEntity> {
|
||||
<script>
|
||||
SELECT * FROM fl_ocean_booking
|
||||
WHERE (#{query.customerId} IS NULL OR customer_id=#{query.customerId})
|
||||
AND status IN ('CONFIRMED','CHANGE_PENDING','RELEASED','CANCELLED')
|
||||
AND (#{query.status} IS NULL OR status=#{query.status})
|
||||
AND (#{query.keyword} IS NULL OR booking_no LIKE CONCAT('%',#{query.keyword},'%')
|
||||
OR customer_name LIKE CONCAT('%',#{query.keyword},'%')
|
||||
@@ -95,13 +96,4 @@ public interface OceanBookingMapper extends BaseMapper<OceanBookingEntity> {
|
||||
int confirmChange(@Param("oceanBookingId") long oceanBookingId, @Param("expectedVersion") int expectedVersion,
|
||||
@Param("operatorId") long operatorId, @Param("operatorName") String operatorName);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_ocean_booking SET version=version+1,updated_by=#{operatorId},updated_by_name=#{operatorName},
|
||||
updated_at=CURRENT_TIMESTAMP(3)
|
||||
WHERE id=#{oceanBookingId} AND version=#{expectedVersion}
|
||||
AND status IN ('SUBMITTED','CONFIRMED','CHANGE_PENDING')
|
||||
""")
|
||||
int touchForAllocation(@Param("oceanBookingId") long oceanBookingId,
|
||||
@Param("expectedVersion") int expectedVersion,
|
||||
@Param("operatorId") long operatorId, @Param("operatorName") String operatorName);
|
||||
}
|
||||
|
||||
+1
-12
@@ -3,10 +3,6 @@ package com.baiyue.firstleg.oceanbooking.repository;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.firstleg.entity.ContainerBookingEntity;
|
||||
import com.baiyue.firstleg.mapper.ContainerBookingMapper;
|
||||
import com.baiyue.firstleg.mapper.LoadingPlanMapper;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanEntity;
|
||||
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.Query;
|
||||
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.ReleaseRequest;
|
||||
import com.baiyue.firstleg.oceanbooking.entity.*;
|
||||
@@ -24,8 +20,6 @@ public class OceanBookingRepository {
|
||||
@Resource private OceanBookingContainerAllocationMapper allocations;
|
||||
@Resource private OceanBookingStatusHistoryMapper histories;
|
||||
@Resource private OceanBookingOperationMapper operations;
|
||||
@Resource private ContainerBookingMapper containerBookings;
|
||||
@Resource private LoadingPlanMapper loadingPlans;
|
||||
|
||||
public PageResult<OceanBookingEntity> page(Query query) {
|
||||
return PageResult.from(bookings.selectPage(new Page<>(query.page(), query.size()), query));
|
||||
@@ -53,9 +47,6 @@ public class OceanBookingRepository {
|
||||
public int confirmChange(long id, int version, long operatorId, String operatorName) {
|
||||
return bookings.confirmChange(id, version, operatorId, operatorName);
|
||||
}
|
||||
public int touchForAllocation(long id, int version, long operatorId, String operatorName) {
|
||||
return bookings.touchForAllocation(id, version, operatorId, operatorName);
|
||||
}
|
||||
public List<OceanBookingPartyEntity> parties(long id) { return parties.selectList(new LambdaQueryWrapper<OceanBookingPartyEntity>().eq(OceanBookingPartyEntity::getOceanBookingId, id).orderByAsc(OceanBookingPartyEntity::getId)); }
|
||||
public List<OceanBookingContainerRequirementEntity> requirements(long id) { return requirements.selectList(new LambdaQueryWrapper<OceanBookingContainerRequirementEntity>().eq(OceanBookingContainerRequirementEntity::getOceanBookingId, id).orderByAsc(OceanBookingContainerRequirementEntity::getSortNo)); }
|
||||
public List<OceanBookingContainerAllocationEntity> allocations(long id) { return allocations.selectList(new LambdaQueryWrapper<OceanBookingContainerAllocationEntity>().eq(OceanBookingContainerAllocationEntity::getOceanBookingId, id).orderByAsc(OceanBookingContainerAllocationEntity::getAllocatedAt)); }
|
||||
@@ -67,14 +58,12 @@ public class OceanBookingRepository {
|
||||
public void replaceParties(long id, List<OceanBookingPartyEntity> values) { parties.delete(new LambdaQueryWrapper<OceanBookingPartyEntity>().eq(OceanBookingPartyEntity::getOceanBookingId, id)); values.forEach(parties::insert); }
|
||||
public void replaceRequirements(long id, List<OceanBookingContainerRequirementEntity> values) { requirements.delete(new LambdaQueryWrapper<OceanBookingContainerRequirementEntity>().eq(OceanBookingContainerRequirementEntity::getOceanBookingId, id)); values.forEach(requirements::insert); }
|
||||
public OceanBookingContainerRequirementEntity requirement(long id, long requirementId) { return requirements.selectOne(new LambdaQueryWrapper<OceanBookingContainerRequirementEntity>().eq(OceanBookingContainerRequirementEntity::getOceanBookingId, id).eq(OceanBookingContainerRequirementEntity::getId, requirementId)); }
|
||||
public ContainerBookingEntity containerBooking(long id) { return containerBookings.selectById(id); }
|
||||
public void insert(ContainerBookingEntity value) { containerBookings.insert(value); }
|
||||
public void insert(LoadingPlanEntity value) { loadingPlans.insert(value); }
|
||||
public int syncReleasedTransport(long id, ReleaseRequest request, long operatorId, String operatorName) {
|
||||
return containerBookings.syncReleasedTransport(id, request.billOfLadingNo().trim(), request.vesselName().trim(),
|
||||
request.voyageNo().trim(), request.estimatedSailingAt(), operatorId, operatorName);
|
||||
}
|
||||
public void insert(OceanBookingContainerAllocationEntity value) { allocations.insert(value); }
|
||||
public void deleteAllocation(long containerBookingId) { allocations.delete(new LambdaQueryWrapper<OceanBookingContainerAllocationEntity>().eq(OceanBookingContainerAllocationEntity::getContainerBookingId, containerBookingId)); }
|
||||
public void insert(OceanBookingStatusHistoryEntity value) { histories.insert(value); }
|
||||
public boolean claimOperation(Long id, long operatorId, String action, String key, String hash) { return operations.claim(com.baomidou.mybatisplus.core.toolkit.IdWorker.getId(), id, operatorId, action, key, hash) == 1; }
|
||||
public OceanBookingOperationEntity operation(long operatorId, String action, String key) { return operations.selectOne(new LambdaQueryWrapper<OceanBookingOperationEntity>().eq(OceanBookingOperationEntity::getOperatorId, operatorId).eq(OceanBookingOperationEntity::getOperation, action).eq(OceanBookingOperationEntity::getIdempotencyKey, key)); }
|
||||
|
||||
+6
-77
@@ -4,8 +4,6 @@ import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
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.entity.ContainerBookingEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanEntity;
|
||||
import com.baiyue.firstleg.oceanbooking.domain.OceanBookingStatus;
|
||||
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.*;
|
||||
import com.baiyue.firstleg.oceanbooking.entity.*;
|
||||
@@ -17,7 +15,6 @@ import com.baiyue.firstleg.service.LoadingPickService;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
@@ -34,6 +31,9 @@ import java.util.Set;
|
||||
@Service
|
||||
public class OceanBookingService {
|
||||
private static final Set<String> PARTY_TYPES = Set.of("SHIPPER", "CONSIGNEE", "NOTIFY_PARTY");
|
||||
private static final Set<OceanBookingStatus> OPERATIONAL_LIST_STATUSES = Set.of(
|
||||
OceanBookingStatus.CONFIRMED, OceanBookingStatus.CHANGE_PENDING,
|
||||
OceanBookingStatus.RELEASED, OceanBookingStatus.CANCELLED);
|
||||
@Resource private OceanBookingRepository repository;
|
||||
@Resource private PriceBookQueryRepository priceBooks;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
@@ -41,7 +41,9 @@ public class OceanBookingService {
|
||||
|
||||
public PageResult<OceanBookingEntity> page(Query query) {
|
||||
String status = trim(query.status());
|
||||
if (status != null) parseStatus(status);
|
||||
if (status != null && !OPERATIONAL_LIST_STATUSES.contains(parseStatus(status))) {
|
||||
throw badRequest("订舱列表仅支持已确认、变更中、已放舱或已取消状态");
|
||||
}
|
||||
return repository.page(new Query(query.customerId(), status, trim(query.keyword()), query.page(), query.size()));
|
||||
}
|
||||
|
||||
@@ -116,7 +118,6 @@ public class OceanBookingService {
|
||||
if (repository.transition(id, OceanBookingStatus.SUBMITTED.name(), OceanBookingStatus.CONFIRMED.name(),
|
||||
null, null, request.expectedVersion(), operatorId, name) != 1) throw concurrent();
|
||||
OceanBookingEntity after = required(id);
|
||||
generateContainers(after, repository.requirements(id), operatorId, name);
|
||||
addHistory(id, before.getStatus(), after.getStatus(), action, request.reason(), before, after,
|
||||
operatorId, name);
|
||||
return complete(id, operatorId, action, key, detail(after));
|
||||
@@ -210,33 +211,6 @@ public class OceanBookingService {
|
||||
return complete(id, operatorId, action, key, detail(after));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Detail allocate(long id, long operatorId, String name, String key, AllocationRequest request) {
|
||||
String action = "ALLOCATE_CONTAINER";
|
||||
Detail replay = claimOrReplay(id, operatorId, action, key, hash(action, id, request));
|
||||
if (replay != null) return replay;
|
||||
OceanBookingEntity booking = requiredVersion(id, request.expectedVersion());
|
||||
if (!Set.of(OceanBookingStatus.SUBMITTED.name(), OceanBookingStatus.CONFIRMED.name(),
|
||||
OceanBookingStatus.CHANGE_PENDING.name()).contains(booking.getStatus())) {
|
||||
throw conflict("当前订舱状态不可分配柜次");
|
||||
}
|
||||
OceanBookingContainerRequirementEntity requirement = repository.requirement(id, request.requirementId());
|
||||
if (requirement == null) throw new BizException(ResultCode.NOT_FOUND, "箱型需求不存在");
|
||||
ContainerBookingEntity container = repository.containerBooking(request.containerBookingId());
|
||||
if (container == null) throw new BizException(ResultCode.NOT_FOUND, "柜次不存在");
|
||||
if (!requirement.getContainerType().equals(container.getContainerType())) throw conflict("柜次箱型与订舱需求不一致");
|
||||
requireSameChannel(booking, container);
|
||||
long used = repository.allocations(id).stream().filter(v -> request.requirementId().equals(v.getRequirementId())).count();
|
||||
if (used >= requirement.getQuantity()) throw conflict("该箱型需求已分配完毕");
|
||||
if (repository.touchForAllocation(id, request.expectedVersion(), operatorId, name) != 1) throw concurrent();
|
||||
OceanBookingContainerAllocationEntity allocation = new OceanBookingContainerAllocationEntity();
|
||||
allocation.setId(IdWorker.getId()); allocation.setOceanBookingId(id);
|
||||
allocation.setRequirementId(request.requirementId()); allocation.setContainerBookingId(request.containerBookingId());
|
||||
allocation.setAllocatedBy(operatorId); allocation.setAllocatedByName(name); allocation.setAllocatedAt(LocalDateTime.now());
|
||||
try { repository.insert(allocation); } catch (DuplicateKeyException ex) { throw conflict("柜次已被其他订舱占用"); }
|
||||
return complete(id, operatorId, action, key, detail(id));
|
||||
}
|
||||
|
||||
private Detail transition(long id, long operatorId, String name, String key, String action,
|
||||
OceanBookingStatus from, OceanBookingStatus to, ActionRequest request,
|
||||
boolean failure) {
|
||||
@@ -348,44 +322,6 @@ public class OceanBookingService {
|
||||
v.setEstimatedSailingAt(null); v.setEstimatedArrivalAt(null); v.setRemark(trim(d.remark()));
|
||||
}
|
||||
|
||||
private void generateContainers(OceanBookingEntity booking,
|
||||
List<OceanBookingContainerRequirementEntity> requirements,
|
||||
long operatorId, String operatorName) {
|
||||
for (OceanBookingContainerRequirementEntity requirement : requirements) {
|
||||
for (int sequence = 1; sequence <= requirement.getQuantity(); sequence++) {
|
||||
ContainerBookingEntity container = new ContainerBookingEntity();
|
||||
container.setId(IdWorker.getId()); container.setBookingNo("CB" + container.getId());
|
||||
container.setCabinetNo("CAB-" + booking.getBookingNo() + '-' + requirement.getSortNo() + '-' + sequence);
|
||||
container.setContainerNo(null); container.setChannelCode(booking.getChannelCode());
|
||||
container.setChannelName(booking.getChannelName()); container.setOceanBookingId(booking.getId());
|
||||
container.setOceanBookingRequirementId(requirement.getId());
|
||||
container.setWarehouseId(booking.getWarehouseId()); container.setWarehouseName(booking.getWarehouseName());
|
||||
container.setContainerType(requirement.getContainerType());
|
||||
container.setCapacityCbm(requirement.getCapacityCbm());
|
||||
container.setMaxWeightKg(requirement.getMaxWeightKg());
|
||||
container.setShippingCompanyId(booking.getShippingCompanyId());
|
||||
container.setShippingCompanyName(booking.getShippingCompanyName());
|
||||
container.setDeparturePortCode(booking.getDeparturePortCode());
|
||||
container.setDeparturePortName(booking.getDeparturePortName());
|
||||
container.setDestinationPortCode(booking.getDestinationPortCode());
|
||||
container.setDestinationPortName(booking.getDestinationPortName());
|
||||
container.setCreatedBy(operatorId); container.setCreatedByName(operatorName);
|
||||
container.setUpdatedBy(operatorId); container.setUpdatedByName(operatorName); container.setVersion(0);
|
||||
repository.insert(container);
|
||||
LoadingPlanEntity plan = new LoadingPlanEntity();
|
||||
plan.setPlanNo("LP-" + container.getId()); plan.setContainerBookingId(container.getId());
|
||||
plan.setPlanStatus("EDITING"); plan.setCreatedBy(operatorId); plan.setCreatedByName(operatorName);
|
||||
plan.setUpdatedBy(operatorId); plan.setUpdatedByName(operatorName); plan.setVersion(0);
|
||||
repository.insert(plan);
|
||||
OceanBookingContainerAllocationEntity allocation = new OceanBookingContainerAllocationEntity();
|
||||
allocation.setId(IdWorker.getId()); allocation.setOceanBookingId(booking.getId());
|
||||
allocation.setRequirementId(requirement.getId()); allocation.setContainerBookingId(container.getId());
|
||||
allocation.setAllocatedBy(operatorId); allocation.setAllocatedByName(operatorName);
|
||||
allocation.setAllocatedAt(LocalDateTime.now()); repository.insert(allocation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateConfirmation(OceanBookingEntity booking,
|
||||
List<OceanBookingContainerRequirementEntity> requirements) {
|
||||
if (!StringUtils.hasText(booking.getChannelCode()) || !StringUtils.hasText(booking.getChannelName())
|
||||
@@ -398,13 +334,6 @@ public class OceanBookingService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static void requireSameChannel(OceanBookingEntity booking, ContainerBookingEntity container) {
|
||||
if (!StringUtils.hasText(booking.getChannelCode()) || !StringUtils.hasText(container.getChannelCode())) {
|
||||
throw conflict("订舱或柜次缺少渠道快照,请先完成存量数据回填");
|
||||
}
|
||||
if (!booking.getChannelCode().equals(container.getChannelCode())) throw conflict("柜次渠道与订舱渠道不一致");
|
||||
}
|
||||
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 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); } }
|
||||
|
||||
@@ -32,6 +32,13 @@ public class ContainerRepository {
|
||||
public int updateDraft(ContainerBookingEntity value, int version, long operatorId, String operatorName) {
|
||||
return mapper.updateDraft(value, version, operatorId, operatorName);
|
||||
}
|
||||
public int updateLink(ContainerBookingEntity value, int version, long operatorId, String operatorName) { return mapper.updateLink(value, version, operatorId, operatorName); }
|
||||
public int clearLink(long id, int version, long operatorId, String operatorName) { return mapper.clearLink(id, version, operatorId, operatorName); }
|
||||
public boolean hasActiveLoadingItems(long id) { return mapper.hasActiveLoadingItems(id) > 0; }
|
||||
public boolean hasPickTask(long id) { return mapper.hasPickTask(id) > 0; }
|
||||
public boolean hasEvidence(long id) { return mapper.hasEvidence(id) > 0; }
|
||||
public boolean hasDocuments(long id) { return mapper.hasDocuments(id) > 0; }
|
||||
public boolean hasMismatchedCustomerItems(long id, long customerId) { return mapper.hasMismatchedCustomerItems(id, customerId) > 0; }
|
||||
public int updateTransport(long id, String containerNo, String voyageNo, int version,
|
||||
long operatorId, String operatorName) {
|
||||
return mapper.updateTransport(id, containerNo, voyageNo, version, operatorId, operatorName);
|
||||
|
||||
@@ -11,6 +11,9 @@ import com.baiyue.firstleg.dto.ContainerDtos.FullUpdateRequest;
|
||||
import com.baiyue.firstleg.dto.ContainerDtos.Query;
|
||||
import com.baiyue.firstleg.dto.ContainerDtos.SaveRequest;
|
||||
import com.baiyue.firstleg.dto.ContainerDtos.TransportUpdateRequest;
|
||||
import com.baiyue.firstleg.dto.ContainerDtos.SimulationCreateRequest;
|
||||
import com.baiyue.firstleg.dto.ContainerDtos.OceanBookingLinkRequest;
|
||||
import com.baiyue.firstleg.dto.ContainerDtos.OceanBookingUnlinkRequest;
|
||||
import com.baiyue.firstleg.entity.ContainerBookingEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanEntity;
|
||||
import com.baiyue.firstleg.entity.ContainerTransportHistoryEntity;
|
||||
@@ -18,6 +21,11 @@ import com.baiyue.firstleg.mapper.ContainerTransportHistoryMapper;
|
||||
import com.baiyue.firstleg.repository.ContainerRepository;
|
||||
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
||||
import com.baiyue.firstleg.repository.SmartLoadingRepository;
|
||||
import com.baiyue.firstleg.repository.PriceBookQueryRepository;
|
||||
import com.baiyue.firstleg.repository.PriceBookQueryRows.ChannelRow;
|
||||
import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
|
||||
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingEntity;
|
||||
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerRequirementEntity;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@@ -34,6 +42,8 @@ public class ContainerService {
|
||||
@Resource private SmartLoadingRepository smartLoadingRepository;
|
||||
@Resource private LoadingPickRepository loadingPickRepository;
|
||||
@Resource private LoadingPickService loadingPickService;
|
||||
@Resource private PriceBookQueryRepository priceBooks;
|
||||
@Resource private OceanBookingRepository oceanBookings;
|
||||
@Autowired(required = false) private ContainerTransportHistoryMapper transportHistories;
|
||||
@Value("${baiyue.security.internal-service-token:}") private String internalToken;
|
||||
@Value("${baiyue.warehouse.public-tenant-id:0}") private long publicWarehouseTenantId;
|
||||
@@ -44,20 +54,70 @@ public class ContainerService {
|
||||
public ContainerBookingEntity detail(long id) { return required(id); }
|
||||
|
||||
@Transactional
|
||||
public ContainerBookingEntity create(long operatorId, String operatorName, SaveRequest request) {
|
||||
validateSchedule(request);
|
||||
public ContainerBookingEntity createSimulation(long operatorId, String operatorName, SimulationCreateRequest request) {
|
||||
WarehouseMasterDataClient.Warehouse warehouse = requireDomesticTransitWarehouse(request.warehouseId());
|
||||
ensureUnique(trim(request.containerNo()), request.cabinetNo().trim(), null);
|
||||
ContainerBookingEntity value = new ContainerBookingEntity();
|
||||
value.setId(IdWorker.getId());
|
||||
value.setBookingNo("CB" + value.getId());
|
||||
copy(value, request);
|
||||
Long versionId = priceBooks.activeVersionId();
|
||||
if (versionId == null) throw conflict("当前没有 ACTIVE 价格版本");
|
||||
ChannelRow channel = priceBooks.channels(versionId, warehouse.id()).stream()
|
||||
.filter(row -> request.channelCode().trim().equals(row.getChannelCode())).findFirst()
|
||||
.orElseThrow(() -> conflict("所选物流渠道已失效,请重新选择"));
|
||||
ContainerBookingEntity value = new ContainerBookingEntity(); value.setId(IdWorker.getId());
|
||||
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.setCreatedBy(operatorId); value.setCreatedByName(operatorName);
|
||||
value.setUpdatedBy(operatorId); value.setUpdatedByName(operatorName); value.setVersion(0);
|
||||
try { repository.insert(value); } catch (DuplicateKeyException ex) { throw duplicate(); }
|
||||
value.setPlanStatus(SmartLoadingStatus.WAIT_LOADING.name());
|
||||
return value;
|
||||
value.setContainerType(request.containerType().trim()); value.setCapacityCbm(request.capacityCbm());
|
||||
value.setMaxWeightKg(request.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;
|
||||
}
|
||||
|
||||
@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("模拟柜已关联真实订舱柜");
|
||||
OceanBookingEntity booking = oceanBookings.find(request.oceanBookingId());
|
||||
if (booking == null || !("CONFIRMED".equals(booking.getStatus()) || "RELEASED".equals(booking.getStatus()))) throw conflict("只能关联已确认的真实订舱柜");
|
||||
OceanBookingContainerRequirementEntity requirement = oceanBookings.requirement(booking.getId(), request.requirementId());
|
||||
if (requirement == null) throw new BizException(ResultCode.NOT_FOUND, "真实订舱柜箱型分配不存在");
|
||||
if (!java.util.Objects.equals(container.getWarehouseId(), booking.getWarehouseId())
|
||||
|| !java.util.Objects.equals(container.getChannelCode(), booking.getChannelCode())
|
||||
|| !java.util.Objects.equals(container.getContainerType(), requirement.getContainerType())
|
||||
|| container.getCapacityCbm() == null || requirement.getCapacityCbm() == null
|
||||
|| container.getMaxWeightKg() == null || requirement.getMaxWeightKg() == null
|
||||
|| container.getCapacityCbm().compareTo(requirement.getCapacityCbm()) != 0
|
||||
|| container.getMaxWeightKg().compareTo(requirement.getMaxWeightKg()) != 0) throw conflict("模拟柜与真实订舱柜仓库、渠道、箱型或容量不一致");
|
||||
long allocated = oceanBookings.allocations(booking.getId()).stream()
|
||||
.filter(a -> request.requirementId().equals(a.getRequirementId()) && a.getContainerBookingId() != null).count();
|
||||
if (requirement.getQuantity() == null || requirement.getQuantity() <= 0 || allocated >= requirement.getQuantity()) {
|
||||
throw conflict("真实订舱柜箱型分配已关联完毕");
|
||||
}
|
||||
if (booking.getCustomerId() != null && repository.hasMismatchedCustomerItems(id, booking.getCustomerId())) throw conflict("模拟柜已排货客户与真实订舱客户范围不一致");
|
||||
container.setOceanBookingId(booking.getId()); container.setOceanBookingRequirementId(requirement.getId());
|
||||
container.setCustomerId(booking.getCustomerId()); container.setCustomerName(booking.getCustomerName());
|
||||
if (repository.updateLink(container, request.expectedVersion(), operatorId, operatorName) != 1) throw conflict("货柜状态或版本已变化,请刷新后重试");
|
||||
com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerAllocationEntity allocation = new com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerAllocationEntity();
|
||||
allocation.setId(IdWorker.getId()); allocation.setOceanBookingId(booking.getId()); allocation.setRequirementId(requirement.getId()); allocation.setContainerBookingId(id); allocation.setAllocatedBy(operatorId); allocation.setAllocatedByName(operatorName); allocation.setAllocatedAt(LocalDateTime.now()); oceanBookings.insert(allocation);
|
||||
return required(id);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ContainerBookingEntity unlinkOceanBooking(long id, long operatorId, String operatorName, OceanBookingUnlinkRequest request) {
|
||||
ContainerBookingEntity container = required(id); requireVersion(container, request.expectedVersion()); requireUnlinkMutable(container);
|
||||
if (container.getOceanBookingId() == null) throw conflict("模拟柜尚未关联真实订舱柜");
|
||||
oceanBookings.deleteAllocation(id);
|
||||
if (repository.clearLink(id, request.expectedVersion(), operatorId, operatorName) != 1) 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、证据、单证或不可逆装柜阶段,不能关联订舱");
|
||||
}
|
||||
private void requireUnlinkMutable(ContainerBookingEntity container) {
|
||||
if (status(container) != SmartLoadingStatus.WAIT_LOADING || repository.hasActiveLoadingItems(container.getId())
|
||||
|| repository.hasPickTask(container.getId()) || repository.hasEvidence(container.getId())
|
||||
|| repository.hasDocuments(container.getId())) throw conflict("已有排货、PDA、证据或单证事实,不能解除订舱关联");
|
||||
}
|
||||
|
||||
@Transactional
|
||||
|
||||
@@ -47,7 +47,8 @@ public class SmartLoadingService {
|
||||
ContainerBookingEntity booking = requireBooking(bookingId);
|
||||
if (!Objects.equals(booking.getWarehouseId(), warehouseId)) throw validation("柜次仓库与候选仓库不一致");
|
||||
if (!StringUtils.hasText(booking.getChannelCode())) throw validation("柜次缺少渠道快照,请先完成存量数据回填");
|
||||
return candidates(warehouseId).stream().filter(candidate -> sameChannel(booking, candidate)).toList();
|
||||
return candidates(warehouseId).stream()
|
||||
.filter(candidate -> sameChannel(booking, candidate) && sameCustomerScope(booking, candidate)).toList();
|
||||
}
|
||||
|
||||
public Plan draftByBooking(long bookingId) {
|
||||
@@ -100,6 +101,7 @@ public class SmartLoadingService {
|
||||
Candidate candidate = repository.candidate(booking.getWarehouseId(), request.inboundId(), request.inboundDetailId());
|
||||
if (candidate == null) throw conflict("入库货物不存在或尚未完成入库");
|
||||
requireSameChannel(booking, candidate);
|
||||
requireSameCustomerScope(booking, candidate);
|
||||
OrderCargoClient.CargoBinding cargo = loadCargo(candidate);
|
||||
LoadingPlanItemEntity existing = repository.activeItem(planId, candidate.accountId(), request.inboundDetailId());
|
||||
int total = request.packageCount() + (existing == null ? 0 : existing.getPackageCount());
|
||||
@@ -143,6 +145,7 @@ public class SmartLoadingService {
|
||||
for (Candidate candidate : candidates(booking.getWarehouseId())) {
|
||||
if (!Objects.equals(candidate.warehouseId(), booking.getWarehouseId())) continue;
|
||||
if (!sameChannel(booking, candidate)) continue;
|
||||
if (!sameCustomerScope(booking, candidate)) continue;
|
||||
if (StringUtils.hasText(booking.getDestinationWarehouseCode())
|
||||
&& !Objects.equals(booking.getDestinationWarehouseCode(), candidate.destinationWarehouseCode())) continue;
|
||||
int count = fittingBoxes(booking, repository.items(planId), candidate);
|
||||
@@ -359,12 +362,18 @@ public class SmartLoadingService {
|
||||
return StringUtils.hasText(booking.getChannelCode()) && StringUtils.hasText(candidate.channelCode())
|
||||
&& booking.getChannelCode().equals(candidate.channelCode());
|
||||
}
|
||||
private static boolean sameCustomerScope(ContainerBookingEntity booking, Candidate candidate) {
|
||||
return booking.getCustomerId() == null || Objects.equals(booking.getCustomerId(), candidate.accountId());
|
||||
}
|
||||
private static void requireSameChannel(ContainerBookingEntity booking, Candidate candidate) {
|
||||
if (!StringUtils.hasText(booking.getChannelCode()) || !StringUtils.hasText(candidate.channelCode())) {
|
||||
throw validation("柜次或入库货物缺少渠道快照,请先完成存量数据回填");
|
||||
}
|
||||
if (!sameChannel(booking, candidate)) throw validation("禁止跨渠道混装");
|
||||
}
|
||||
private static void requireSameCustomerScope(ContainerBookingEntity booking, Candidate candidate) {
|
||||
if (!sameCustomerScope(booking, candidate)) throw forbidden();
|
||||
}
|
||||
private void touch(LoadingPlanEntity p,long op,String name){if(repository.touch(p.getId(),p.getVersion(),op,name)!=1)throw conflict("计划已被并发修改");}
|
||||
private Plan replay(String operation, long planId, String action, Long itemId, String requestHash) {
|
||||
LoadingPlanHistoryEntity history = repository.operation(operation);
|
||||
|
||||
Reference in New Issue
Block a user