fix(firstleg): enable manual nodes for consolidated containers

This commit is contained in:
2026-09-10 11:55:09 +08:00
parent 2821c5e9c7
commit c1a36c6f40
11 changed files with 108 additions and 16 deletions
@@ -97,7 +97,7 @@ public class OutboundDispatchService {
OutboundDispatchReadMapper.OceanBinding ocean = repository.ocean(containerBookingId);
if (carrierSchedules != null) {
if (ocean == null || ocean.id() == null || ocean.customerId() == null) {
if (ocean == null || ocean.id() == null) {
throw conflict("当前货柜未关联有效订舱,无法创建船司查询任务");
}
carrierSchedules.ensureFromOutboundConfirm(ocean.customerId(), ocean.id(), containerBookingId,
@@ -129,7 +129,7 @@ public class OutboundDispatchService {
if (!Objects.equals(visible.version(), request.expectedVersion())) throw concurrent();
OutboundDispatchReadMapper.OceanBinding ocean = repository.ocean(containerBookingId);
if (carrierSchedules != null) {
if (ocean == null || ocean.id() == null || ocean.customerId() == null) {
if (ocean == null || ocean.id() == null) {
throw conflict("无法核验货柜后续船节点,禁止撤销国内出仓");
}
if (carrierSchedules.hasOutboundDepartureFact(ocean.customerId(), ocean.id(), containerBookingId)) {
@@ -14,6 +14,9 @@ import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.Query;
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.QueryView;
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.History;
import com.baiyue.firstleg.portschedule.service.CarrierScheduleQueryService;
import com.baiyue.firstleg.outbound.dispatch.service.OutboundDispatchService;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.service.FirstlegScopeGuard;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -41,6 +44,7 @@ import java.util.List;
@RequestMapping("/api/platform/v1/carrier-schedule-queries")
public class PlatformCarrierScheduleQueryController {
@Resource private CarrierScheduleQueryService service;
@Resource private OutboundDispatchService outboundDispatchService;
@Operation(summary = "查询六家船公司合规适配策略")
@GetMapping("/carriers")
@@ -97,6 +101,23 @@ public class PlatformCarrierScheduleQueryController {
return ApiResponse.success(service.create(user.userId(), user.username(), key, request));
}
@PostMapping("/containers/{containerBookingId}/ensure")
@PreAuthorize("hasAuthority('platform:firstleg:port-schedule-query:manual')")
@Operation(summary = "为已确认离仓货柜补建船节点任务后进入人工补录")
@BusinessAudit(event = "CARRIER_SCHEDULE_QUERY_ENSURE", resourceType = "CONTAINER_BOOKING",
resourceId = "#containerBookingId", description = "补建货柜船节点任务")
public ApiResponse<Detail> ensureForManualEntry(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable @Positive long containerBookingId,
@RequestHeader("Idempotency-Key") String key) {
FirstlegScopeGuard.platform(user);
var outbound = outboundDispatchService.detail(containerBookingId).aggregate();
if (!"WAITING_FOR_LOADING".equals(outbound.dispatchStatus()) || outbound.oceanBookingId() == null) {
throw new BizException(ResultCode.CONFLICT, "货柜尚未确认国内出仓,不能人工补录船节点");
}
return ApiResponse.success(service.ensureForManualEntry(outbound.oceanBookingId(), containerBookingId,
user.userId(), user.username(), key));
}
@PostMapping("/{queryId}/manual-results")
@PreAuthorize("hasAuthority('platform:firstleg:port-schedule-query:manual')")
@BusinessAudit(event = "CARRIER_SCHEDULE_QUERY_MANUAL_RESULT", resourceType = "CARRIER_SCHEDULE_QUERY",
@@ -13,7 +13,7 @@ import java.util.List;
public final class CarrierScheduleQueryDtos {
private CarrierScheduleQueryDtos() {}
public record Query(@Positive Long customerId, @Positive Long oceanBookingId,
public record Query(@Min(0) Long customerId, @Positive Long oceanBookingId,
@Positive Long containerBookingId, @Size(max = 16) String carrierCode,
@Size(max = 24) String status, Boolean expired,
@Size(max = 128) String keyword, @Min(1) Long page,
@@ -22,7 +22,7 @@ public final class CarrierScheduleQueryDtos {
}
public record CreateRequest(@NotNull @Min(0) @Max(0) Integer expectedVersion,
@NotNull @Positive Long customerId,
@NotNull @Min(0) Long customerId,
@NotNull @Positive Long oceanBookingId,
@NotNull @Positive Long containerBookingId,
@NotBlank @Size(max = 16) String carrierCode,
@@ -101,15 +101,17 @@ public class CarrierScheduleQueryService {
/** Idempotently starts the canonical tracking task after domestic outbound confirmation. */
@Transactional
public CarrierScheduleQueryEntity ensureFromOutboundConfirm(long customerId, long oceanBookingId,
public CarrierScheduleQueryEntity ensureFromOutboundConfirm(Long customerId, long oceanBookingId,
long containerBookingId,
long operatorId, String operatorName) {
CarrierScheduleQueryEntity existing = repository.findByBusinessScope(customerId, oceanBookingId,
containerBookingId);
OceanBookingEntity booking = portSchedules.oceanBooking(oceanBookingId);
ContainerBookingEntity container = portSchedules.containerBooking(containerBookingId);
if (booking == null || container == null) throw new BizException(ResultCode.NOT_FOUND, "出仓订舱或柜次不存在");
validateLinks(customerId, oceanBookingId, containerBookingId);
long scopeCustomerId = booking.getCustomerId() == null ? 0L : booking.getCustomerId();
if (customerId != null && customerId != scopeCustomerId) throw badRequest("customerId与订舱客户不一致");
CarrierScheduleQueryEntity existing = repository.findByBusinessScope(scopeCustomerId, oceanBookingId,
containerBookingId);
validateLinks(scopeCustomerId, oceanBookingId, containerBookingId);
String trackingType = "CONTAINER_NO";
String trackingNo = normalizedContainerNo(container);
if (existing != null) {
@@ -120,7 +122,7 @@ public class CarrierScheduleQueryService {
LocalDateTime now = now();
CarrierScheduleQueryEntity value = new CarrierScheduleQueryEntity();
value.setId(IdWorker.getId()); value.setQueryNo("CSQ" + value.getId());
value.setCustomerId(customerId); value.setOceanBookingId(oceanBookingId);
value.setCustomerId(scopeCustomerId); value.setOceanBookingId(oceanBookingId);
value.setContainerBookingId(containerBookingId); value.setCarrierCode(strategy.carrierCode());
value.setTrackingType(trackingType); value.setTrackingNo(trackingNo);
value.setOfficialUrl(strategy.officialUrl()); value.setAccessMode(strategy.accessMode());
@@ -136,7 +138,7 @@ public class CarrierScheduleQueryService {
try {
try { repository.insert(value); }
catch (DuplicateKeyException exception) {
CarrierScheduleQueryEntity concurrent = repository.findByBusinessScope(customerId, oceanBookingId,
CarrierScheduleQueryEntity concurrent = repository.findByBusinessScope(scopeCustomerId, oceanBookingId,
containerBookingId);
if (concurrent != null) {
requireFrozenTrackingIdentity(concurrent, trackingNo);
@@ -162,6 +164,13 @@ public class CarrierScheduleQueryService {
return value;
}
@Transactional
public Detail ensureForManualEntry(long oceanBookingId, long containerBookingId,
long operatorId, String operatorName, String key) {
validateKey(key);
return detail(ensureFromOutboundConfirm(null, oceanBookingId, containerBookingId, operatorId, operatorName));
}
private static String outboundManualReason(PreparedAttempt strategy, OceanBookingEntity booking) {
if (!"MANUAL_REQUIRED".equals(strategy.status())) return null;
String reason = strategy.manualReason();
@@ -424,7 +433,8 @@ public class CarrierScheduleQueryService {
private void validateLinks(long customerId, long oceanBookingId, Long containerBookingId) {
OceanBookingEntity booking = portSchedules.oceanBooking(oceanBookingId);
if (booking == null) throw new BizException(ResultCode.NOT_FOUND, "关联订舱不存在");
if (!Objects.equals(booking.getCustomerId(), customerId)) throw badRequest("customerId与订舱客户不一致");
long expectedCustomerId = booking.getCustomerId() == null ? 0L : booking.getCustomerId();
if (expectedCustomerId != customerId) throw badRequest("customerId与订舱客户不一致");
if (containerBookingId != null) {
if (portSchedules.containerBooking(containerBookingId) == null) {
throw new BizException(ResultCode.NOT_FOUND, "关联柜次不存在");