This commit is contained in:
2026-08-14 16:06:27 +08:00
parent 87eb1aa72f
commit 87e919f3c4
54 changed files with 984 additions and 175 deletions
@@ -45,12 +45,11 @@ public class PlatformLoadingPdaController {
@GetMapping("/tasks")
@PreAuthorize("hasAuthority('platform:firstleg:pda:view')")
public ApiResponse<Task> task(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String containerNo,
@RequestParam(required = false) String cabinetNo,
@RequestParam(required = false) Long loadingPlanId,
@RequestParam @Positive long warehouseId) {
FirstlegScopeGuard.platform(user);
return ApiResponse.success(service.task(containerNo, cabinetNo, loadingPlanId, warehouseId));
return ApiResponse.success(service.task(cabinetNo, loadingPlanId, warehouseId));
}
@GetMapping("/tasks/pending")
@@ -60,17 +60,10 @@ public final class LoadingPickDtos {
@NotNull @Min(1) Long warehouseId,
@NotNull @Min(1) Long loadingPlanId,
@NotNull @Min(1) Long containerBookingId,
@Size(max = 64) String containerNo,
@NotNull @Min(0) Integer expectedVersion,
@NotBlank @Size(max = 64) String inventorySnapshotToken,
@NotEmpty @Size(max = 500) List<@Valid BatchItemRequest> items
) {
@Deprecated
public BatchRequest(Long warehouseId, String containerNo, Integer expectedVersion,
String inventorySnapshotToken, List<BatchItemRequest> items) {
this(warehouseId, null, null, containerNo, expectedVersion, inventorySnapshotToken, items);
}
}
) {}
public record BatchItemRequest(
@NotNull @Min(1) Long itemId,
@@ -59,6 +59,10 @@ public class OceanBookingRepository {
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)); }
public OceanBookingContainerAllocationEntity allocationByContainer(long containerBookingId) {
return allocations.selectOne(new LambdaQueryWrapper<OceanBookingContainerAllocationEntity>()
.eq(OceanBookingContainerAllocationEntity::getContainerBookingId, containerBookingId));
}
public List<OceanBookingStatusHistoryEntity> histories(long id) { return histories.selectList(new LambdaQueryWrapper<OceanBookingStatusHistoryEntity>().eq(OceanBookingStatusHistoryEntity::getOceanBookingId, id).orderByAsc(OceanBookingStatusHistoryEntity::getCreatedAt)); }
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); }
@@ -10,6 +10,7 @@ import com.baiyue.firstleg.oceanbooking.domain.OceanBookingStatus;
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.*;
import com.baiyue.firstleg.oceanbooking.entity.*;
import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
import com.baiyue.firstleg.service.LoadingPickService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
@@ -31,6 +32,7 @@ public class OceanBookingService {
private static final Set<String> PARTY_TYPES = Set.of("SHIPPER", "CONSIGNEE", "NOTIFY_PARTY");
@Resource private OceanBookingRepository repository;
@Resource private ObjectMapper objectMapper;
@Resource private LoadingPickService loadingPickService;
public PageResult<OceanBookingEntity> page(Query query) {
String status = trim(query.status());
@@ -106,6 +108,8 @@ public class OceanBookingService {
requireStatus(before, OceanBookingStatus.CONFIRMED);
if (repository.release(id, request, operatorId, name) != 1) throw concurrent();
repository.syncReleasedTransport(id, request, operatorId, name);
repository.allocations(id).forEach(allocation ->
loadingPickService.beginIfReady(allocation.getContainerBookingId(), operatorId, name));
OceanBookingEntity after = required(id);
addHistory(id, before.getStatus(), after.getStatus(), action, request.reason(), before, after,
operatorId, name);
@@ -104,23 +104,10 @@ public class ContainerService {
history.setOperatorId(operatorId); history.setOperatorName(operatorName);
if (transportHistories != null) transportHistories.insert(history);
ContainerBookingEntity updated = required(id);
beginPdaTaskAfterContainerBound(updated, containerNo, operatorId, operatorName);
loadingPickService.beginIfReady(updated.getId(), operatorId, operatorName);
return updated;
}
/** 预装清单先推送、后录入集装箱号时,补建对应的 PDA 拣货任务。 */
private void beginPdaTaskAfterContainerBound(ContainerBookingEntity booking, String containerNo,
long operatorId, String operatorName) {
if (containerNo == null) return;
LoadingPlanEntity plan = smartLoadingRepository.planByBooking(booking.getId());
if (plan == null || plan.getPreloadPushedAt() == null || !"EDITING".equals(plan.getPlanStatus())) return;
// The PDA task and its item versions are stable work identities. A later transport-number
// correction only changes the booking display value and must not resynchronize the task.
if (loadingPickRepository.taskByPlan(plan.getId()) != null) return;
loadingPickService.begin(plan.getId(), containerNo, operatorId, operatorName,
smartLoadingRepository.items(plan.getId()));
}
private ContainerBookingEntity required(long id) {
ContainerBookingEntity value = repository.find(id);
if (value == null) throw new BizException(ResultCode.NOT_FOUND, "货柜不存在");
@@ -174,8 +174,12 @@ public class InboundMeasurementService {
InboundOrderEntity inbound = requireInbound(tenantId, inboundId);
InboundWeightResultEntity weight = requireWeight(tenantId, inboundId, resultId);
requireVersion(request.version(), weight.getVersion());
boolean wasConfirmed = WEIGHT_CONFIRMED.equals(weight.getRecognitionStatus());
applyWeightCorrection(weight, request.version(), request.adjustedWeightValue(),
request.adjustedWeightUnit(), request.adjustmentReason(), operatorId, operatorName);
if (wasConfirmed) {
recalculateConfirmedWeight(inbound, operatorId);
}
return toWeight(weight);
}
@@ -33,6 +33,7 @@ import com.baiyue.firstleg.entity.LoadingPlanHistoryEntity;
import com.baiyue.firstleg.mapper.LoadingPickItemMapper.CartonLabelRow;
import com.baiyue.firstleg.repository.LoadingPickRepository;
import com.baiyue.firstleg.repository.SmartLoadingRepository;
import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
import jakarta.annotation.Resource;
import feign.FeignException;
import org.springframework.beans.factory.annotation.Value;
@@ -65,6 +66,8 @@ public class LoadingPickService {
@Resource
private SmartLoadingRepository smartLoadingRepository;
@Resource
private OceanBookingRepository oceanBookingRepository;
@Resource
private LoadingListService loadingListService;
@Resource
private PdaPickSettlementService settlementService;
@@ -73,6 +76,22 @@ public class LoadingPickService {
@Value("${baiyue.security.internal-service-token:test-internal-service-token}")
private String internalToken;
/** Opens the field task only after planning, transport identity and ocean release have converged. */
@Transactional
public void beginIfReady(long containerBookingId, long operatorId, String operatorName) {
var booking = smartLoadingRepository.booking(containerBookingId);
if (booking == null || !StringUtils.hasText(booking.getContainerNo())) return;
var plan = smartLoadingRepository.planByBooking(containerBookingId);
if (plan == null || plan.getPreloadPushedAt() == null || !"EDITING".equals(plan.getPlanStatus())) return;
var allocation = oceanBookingRepository.allocationByContainer(containerBookingId);
if (allocation == null) return;
var oceanBooking = oceanBookingRepository.find(allocation.getOceanBookingId());
if (oceanBooking == null || !"RELEASED".equals(oceanBooking.getStatus())) return;
if (repository.taskByPlan(plan.getId()) != null) return;
begin(plan.getId(), booking.getContainerNo(), operatorId, operatorName,
smartLoadingRepository.items(plan.getId()));
}
@Transactional
public void begin(long planId, String containerNo, long operatorId, String operatorName,
List<LoadingPlanItemEntity> planItems) {
@@ -111,29 +130,14 @@ public class LoadingPickService {
}
}
public Task task(String containerNo, long warehouseId) {
if (!StringUtils.hasText(containerNo)) {
throw new BizException(ResultCode.BAD_REQUEST, "集装箱号不能为空");
}
if (warehouseId <= 0) {
throw new BizException(ResultCode.BAD_REQUEST, "warehouseId必须大于0");
}
LoadingPickTaskEntity task = repository.taskByContainer(containerNo.trim(), warehouseId);
if (task == null) {
throw new BizException(ResultCode.NOT_FOUND, "未找到装柜中的PDA拣货任务");
}
return view(task, warehouseId);
}
public Task task(String containerNo, String cabinetNo, Long loadingPlanId, long warehouseId) {
int selectors = (StringUtils.hasText(containerNo) ? 1 : 0) + (StringUtils.hasText(cabinetNo) ? 1 : 0)
public Task task(String cabinetNo, Long loadingPlanId, long warehouseId) {
int selectors = (StringUtils.hasText(cabinetNo) ? 1 : 0)
+ (loadingPlanId != null ? 1 : 0);
if (selectors != 1) throw new BizException(ResultCode.BAD_REQUEST,
"containerNo、cabinetNo、loadingPlanId必须且只能提供一个");
"cabinetNo、loadingPlanId必须且只能提供一个");
LoadingPickTaskEntity task = StringUtils.hasText(cabinetNo)
? repository.taskByCabinet(cabinetNo.trim(), warehouseId)
: loadingPlanId != null ? repository.taskByPlan(loadingPlanId, warehouseId)
: repository.taskByContainer(containerNo.trim(), warehouseId);
: repository.taskByPlan(loadingPlanId, warehouseId);
if (task == null) throw new BizException(ResultCode.NOT_FOUND, "未找到装柜中的PDA拣货任务");
return view(task, warehouseId);
}
@@ -854,6 +858,7 @@ public class LoadingPickService {
}
StringBuilder canonical = new StringBuilder("BATCH|").append(taskId).append('|')
.append(request.warehouseId()).append('|').append(request.loadingPlanId()).append('|')
.append(request.containerBookingId()).append('|')
.append(request.expectedVersion()).append('|').append(request.inventorySnapshotToken().trim());
request.items().stream().sorted(Comparator.comparing(BatchItemRequest::itemId,
Comparator.nullsFirst(Long::compareTo)))
@@ -49,7 +49,7 @@ public class MerchantPriceBookService {
}
public Response requireAvailableChannel(long merchantTenantId, long originWarehouseId, String channelCode,
String destinationType) {
String destinationType) {
requireMerchant(merchantTenantId);
if (originWarehouseId <= 0) throw validation("请选择有效起运仓");
Long versionId = repository.activeVersionId();
@@ -59,7 +59,7 @@ public class MerchantPriceBookService {
}
var prefix = repository.channelPrefix(normalized);
if (prefix == null || !StringUtils.hasText(prefix.inboundCodePrefix())) {
throw new BizException(ResultCode.CONFLICT, "所选渠道未配置入仓码前缀");
throw new BizException(ResultCode.CONFLICT, "所选渠道未配置进仓编码");
}
String selected = "OVERSEAS".equals(destinationType) && StringUtils.hasText(prefix.overseasInboundCodePrefix())
? prefix.overseasInboundCodePrefix() : prefix.inboundCodePrefix();
@@ -225,21 +225,22 @@ public class SmartLoadingService {
String key = trim(idempotencyKey);
if (!StringUtils.hasText(key) || key.length() > 128) throw new BizException(ResultCode.BAD_REQUEST, "必须提供有效 Idempotency-Key");
LoadingPlanEntity plan = repository.plan(planId); if (plan == null) throw new BizException(ResultCode.NOT_FOUND, "装柜计划不存在");
if (key.equals(plan.getPushIdempotencyKey())) return view(plan, requireBooking(plan.getContainerBookingId()));
if (key.equals(plan.getPushIdempotencyKey())) {
loadingPickService.beginIfReady(plan.getContainerBookingId(), operatorId, operatorName);
return view(plan, requireBooking(plan.getContainerBookingId()));
}
if (StringUtils.hasText(plan.getPushIdempotencyKey())) throw conflict("预装清单已推送,不能使用新的幂等键重复推送");
if (!"EDITING".equals(plan.getPlanStatus()) || !Objects.equals(plan.getVersion(), request.expectedVersion())) throw conflict("仅编辑中的计划可开始装柜且版本必须匹配");
ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId());
List<LoadingPlanItemEntity> items = repository.items(planId);
if (items.isEmpty()) throw validation("装柜明细不能为空");
checkPlanCapacity(booking, items);
if (StringUtils.hasText(booking.getContainerNo())) {
loadingPickService.begin(planId, booking.getContainerNo(), operatorId, operatorName, items);
}
try {
if (repository.push(planId, key, request.expectedVersion(), operatorId, operatorName) != 1) throw conflict("装柜计划已被并发修改");
} catch (DuplicateKeyException e) { throw conflict("幂等键已用于其他推送"); }
history(planId, null, "PUSH_PRELOAD", "EDITING", "EDITING", key, operatorId, operatorName);
LoadingPlanEntity pushed = repository.plan(planId);
loadingPickService.beginIfReady(pushed.getContainerBookingId(), operatorId, operatorName);
return view(pushed, requireBooking(pushed.getContainerBookingId()));
}