This commit is contained in:
2026-09-16 11:06:33 +08:00
parent a4c659f4c6
commit a098c4377c
46 changed files with 356 additions and 109 deletions
@@ -23,7 +23,7 @@ public interface WarehouseSupplierClient {
@RequestParam String carrierCode);
record Snapshot(long supplierId, String name, String category, String status,
List<String> carrierCodes) {}
List<String> carrierCodes, String truckingScope) {}
@GetMapping("/{supplierId}/trucking-contact")
ApiResponse<TruckingContact> truckingContact(@RequestHeader("X-Internal-Service-Token") String token,
@@ -92,12 +92,17 @@ public final class SmartLoadingDtos { private SmartLoadingDtos(){}
Integer inboundFragmentCount,Integer fullPackageCount,List<Long> memberInboundIds) {
public PlanCoLoadGroup { memberInboundIds=memberInboundIds==null?List.of():List.copyOf(memberInboundIds); }
}
public record PlanPalletMembers(Long palletId,List<GroupMemberCandidate> members) {
public PlanPalletMembers { members=members==null?List.of():List.copyOf(members); }
}
public record Plan(Long id,String planNo,String status,Long bookingId,String containerNo,String voyageNo,
Integer version,Boolean canAssign,Boolean canEdit,String blockedReason,String finalCompletionStatus,
Long pdaTaskId,String pdaTaskStatus,Integer pdaTaskVersion,List<PlanCoLoadGroup> coLoadGroups,
List<PlanPalletMembers> palletMembers,
List<LoadingPlanItemEntity> items) {
public Plan {
coLoadGroups=coLoadGroups==null?List.of():List.copyOf(coLoadGroups);
palletMembers=palletMembers==null?List.of():List.copyOf(palletMembers);
items=items==null?List.of():List.copyOf(items);
}
}
@@ -9,6 +9,7 @@ import org.apache.ibatis.annotations.*;
import java.time.LocalDateTime;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.List;
/** 一件代发逐箱清点、待绑托与封托事实。 */
@@ -282,6 +283,21 @@ public interface DropshipPalletizationMapper {
ORDER BY o.inbound_code,o.id
""") List<GroupMemberCandidate> palletInboundMembers(@Param("palletId") long palletId);
@Select("""
<script>
SELECT c.pallet_id palletId,o.tenant_id accountId,o.id inboundId,o.order_id orderId,o.order_no orderNo,
o.inbound_code inboundCode,o.customer_name customerName,COUNT(DISTINCT c.id) availablePackageCount,
o.process_status orderStatusSnapshot,o.expected_arrival_at expectedArrivalAt
FROM fl_dropship_pallet_carton c
JOIN fl_inbound_order o ON o.id=c.inbound_id AND o.tenant_id=c.tenant_id
WHERE c.pallet_id IN
<foreach collection="palletIds" item="palletId" open="(" separator="," close=")">#{palletId}</foreach>
GROUP BY c.pallet_id,o.tenant_id,o.id,o.order_id,o.order_no,o.inbound_code,o.customer_name,
o.process_status,o.expected_arrival_at
ORDER BY c.pallet_id,o.inbound_code,o.id
</script>
""") List<PlanPalletMemberRow> palletInboundMembersByPalletIds(@Param("palletIds") Collection<Long> palletIds);
@Select("""
SELECT p.id pallet_id,p.pallet_no,'SEALED_PALLET' unit_type,TRUE indivisible,COUNT(DISTINCT c.id) carton_count,
(SELECT COUNT(DISTINCT sku.sku_code) FROM fl_dropship_pallet_carton pc
@@ -318,6 +334,7 @@ public interface DropshipPalletizationMapper {
@Getter @Setter class TaskRow { private Long id,tenantId,inboundId,warehouseId,assigneeId; private String inboundCode,customerName,warehouseName,taskStatus,assigneeName,processStatus,fulfillmentType; private Integer totalCartons,countedCartons,allocatedCartons,discrepancyCartons,expectedPalletCount,version; }
@Getter @Setter class PalletRow { private Long id,taskId,tenantId,inboundId,warehouseId; private String palletNo,palletStatus,sealOperationId; private Integer version; private LocalDateTime completedAt; }
@Getter @Setter class PlanPalletMemberRow { private Long palletId,accountId,inboundId,orderId; private String orderNo,inboundCode,customerName,orderStatusSnapshot; private Integer availablePackageCount; private LocalDateTime expectedArrivalAt; }
@Getter @Setter class LabelRow { private Long cartonLabelId,inboundDetailId; private String skuCode,cargoName,inboundCode,boxCode; private Integer unitsPerCarton; }
@Getter @Setter class LabelSkuRow { private Long inboundDetailId; private String skuCode,cargoName; private Integer expectedQuantity,lineNo; }
@Getter @Setter class CartonRow { private Long id,taskId,palletId,inboundId,inboundDetailId; private String palletNo,inboundCode,skuCode,cargoName,boxCode,discrepancyStatus,discrepancyRemark; private Integer expectedSkuQuantity,actualSkuQuantity; }
@@ -149,7 +149,7 @@ public class ContainerService {
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());
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());
@@ -1663,7 +1663,22 @@ public class SmartLoadingService {
: "COMPLETED".equals(task.taskStatus()) ? "COMPLETED" : "IN_PROGRESS";
return new Plan(p.getId(),p.getPlanNo(),p.getPlanStatus(),p.getContainerBookingId(),
b.getContainerNo(),b.getVoyageNo(),p.getVersion(),mutable,mutable,blockedReason,finalStatus,
task.taskId(),task.taskStatus(),task.taskVersion(),planCoLoadGroups(p.getId()),collapsePalletUnits(items));
task.taskId(),task.taskStatus(),task.taskVersion(),planCoLoadGroups(p.getId()),planPalletMembers(items),
collapsePalletUnits(items));
}
private List<PlanPalletMembers> planPalletMembers(List<LoadingPlanItemEntity> items) {
List<Long> palletIds=items.stream().map(LoadingPlanItemEntity::getPalletId).filter(Objects::nonNull)
.distinct().toList();
if(palletIds.isEmpty()) return List.of();
return dropshipPallets.palletInboundMembersByPalletIds(palletIds).stream()
.collect(java.util.stream.Collectors.groupingBy(
com.baiyue.firstleg.mapper.DropshipPalletizationMapper.PlanPalletMemberRow::getPalletId,
LinkedHashMap::new,java.util.stream.Collectors.toList()))
.entrySet().stream().map(entry->new PlanPalletMembers(entry.getKey(),entry.getValue().stream()
.map(row->new GroupMemberCandidate(row.getAccountId(),row.getInboundId(),row.getOrderId(),
row.getOrderNo(),row.getInboundCode(),row.getCustomerName(),row.getAvailablePackageCount(),
row.getOrderStatusSnapshot(),row.getExpectedArrivalAt()))
.toList())).toList();
}
private List<LoadingPlanItemEntity> collapsePalletUnits(List<LoadingPlanItemEntity> items) {
List<LoadingPlanItemEntity> result = new ArrayList<>();
@@ -34,7 +34,7 @@ public class TruckAppointmentFeeService {
if (item == null || !"ACTIVE".equals(item.getStatus()) || !Boolean.TRUE.equals(item.getPickupPayable())) {
throw conflict("国内拖车费项目不存在、已停用或未启用应付");
}
suppliers.trucking(appointment.getCarrierId(), null);
suppliers.domesticTrucking(appointment.getCarrierId(), null);
List<Share> shares = allocate(total, rows);
Map<Long, List<Share>> plans = new LinkedHashMap<>();
shares.forEach(share -> plans.computeIfAbsent(share.owner().planId(), ignored -> new ArrayList<>()).add(share));
@@ -47,8 +47,8 @@ public class TruckAppointmentService {
@Transactional public TruckAppointmentAttachmentEntity upload(long id,String type,MultipartFile file,long uid){String attachmentType=attachmentType(type);required(id);try{var response=files.upload(internalToken,uid,0L,"TRUCK_APPOINTMENT_ATTACHMENT",Long.toString(id),file);if(response==null||!response.successful()||response.data()==null)throw bad("附件上传失败");var m=response.data();TruckAppointmentAttachmentEntity a=new TruckAppointmentAttachmentEntity();a.setId(IdWorker.getId());a.setAppointmentId(id);a.setFileId(m.fileId());a.setAttachmentType(attachmentType);a.setFileName(m.fileName());a.setFileHash(m.sha256());a.setVisibility("PLATFORM");a.setUploadedBy(uid);a.setUploadedAt(LocalDateTime.now());repo.attachment(a);return a;}catch(FeignException ex){throw new BizException(ResultCode.SERVICE_UNAVAILABLE,"文件服务暂不可用");}}
@Transactional public TruckAppointmentAttachmentEntity attach(long id,AttachmentRequest r,long uid){String attachmentType=attachmentType(r.attachmentType());required(id);TruckAppointmentFileClient.Metadata metadata;try{var response=files.metadata(internalToken,0L,"TRUCK_APPOINTMENT_ATTACHMENT",Long.toString(id),r.fileId());if(response==null||!response.successful()||response.data()==null)throw bad("附件不存在或归属不匹配");metadata=response.data();}catch(FeignException ex){throw new BizException(ResultCode.SERVICE_UNAVAILABLE,"文件服务暂不可用");} TruckAppointmentAttachmentEntity a=new TruckAppointmentAttachmentEntity();a.setId(IdWorker.getId());a.setAppointmentId(id);a.setFileId(metadata.fileId());a.setAttachmentType(attachmentType);a.setFileName(metadata.fileName());a.setFileHash(metadata.sha256());a.setVisibility("PLATFORM");a.setUploadedBy(uid);a.setUploadedAt(LocalDateTime.now());repo.attachment(a);return a;}
@Transactional public void deleteAttachment(long id,long attachmentId){required(id);if(repo.deleteAttachment(id,attachmentId)!=1)throw new BizException(ResultCode.NOT_FOUND,"附件引用不存在");}
@Transactional public TruckAppointmentEntity create(long uid,String name,SaveRequest r){if(r.expectedVersion()==null||r.expectedVersion()!=0)throw bad("新增 expectedVersion 必须为0");validateRequired(r);completion.lockBooking(r.oceanBookingId());TruckAppointmentEntity active=repo.firstActiveForUpdate(r.oceanBookingId());if(active!=null)throw existingAppointmentConflict(active);AssociationContext links=validateAssociations(r);var carrier=suppliers.trucking(r.carrierId(),r.carrierName());TruckAppointmentEntity e=new TruckAppointmentEntity();e.setId(IdWorker.getId());e.setAppointmentNo(documentNumbers.next("TA"));e.setStatus(TruckAppointmentStatus.DRAFT);e.setRevision(0);e.setVersion(0);e.setCreatedAt(LocalDateTime.now());e.setUpdatedAt(e.getCreatedAt());copy(e,r,links,carrier.name());try{repo.insert(e);}catch(DuplicateKeyException exception){if(exception.getMessage()!=null&&exception.getMessage().contains("uk_truck_active_ocean_booking"))throw existingAppointmentConflict(repo.firstActiveForUpdate(r.oceanBookingId()));throw exception;}record(e,null,e.getStatus().name(),"CREATE",null,uid,name);completion.completeForBooking(e.getOceanBookingId());return enrich(required(e.getId()));}
@Transactional public TruckAppointmentEntity edit(long id,long uid,String name,SaveRequest r){validateRequired(r);TruckAppointmentEntity e=required(id);completion.lockBooking(Math.min(e.getOceanBookingId(),r.oceanBookingId()));if(!Objects.equals(e.getOceanBookingId(),r.oceanBookingId()))completion.lockBooking(Math.max(e.getOceanBookingId(),r.oceanBookingId()));checkVersion(e,r.expectedVersion());if(!(e.getStatus()==TruckAppointmentStatus.DRAFT||e.getStatus()==TruckAppointmentStatus.PENDING_DISPATCH||e.getStatus()==TruckAppointmentStatus.RESCHEDULED))throw conflict("当前状态不可编辑");TruckAppointmentEntity active=repo.firstActiveForUpdate(r.oceanBookingId());if(active!=null&&!Objects.equals(active.getId(),e.getId()))throw existingAppointmentConflict(active);AssociationContext links=validateAssociations(r);var carrier=suppliers.trucking(r.carrierId(),r.carrierName());copy(e,r,links,carrier.name());try{if(repo.updateEdit(e,r.expectedVersion())!=1)throw conflict("版本已变化,请刷新后重试");}catch(DuplicateKeyException exception){if(exception.getMessage()!=null&&exception.getMessage().contains("uk_truck_active_ocean_booking"))throw existingAppointmentConflict(repo.firstActiveForUpdate(r.oceanBookingId()));throw exception;}record(e,e.getStatus().name(),e.getStatus().name(),"EDIT",null,uid,name);completion.completeForBooking(e.getOceanBookingId());return enrich(required(id));}
@Transactional public TruckAppointmentEntity create(long uid,String name,SaveRequest r){if(r.expectedVersion()==null||r.expectedVersion()!=0)throw bad("新增 expectedVersion 必须为0");validateRequired(r);completion.lockBooking(r.oceanBookingId());TruckAppointmentEntity active=repo.firstActiveForUpdate(r.oceanBookingId());if(active!=null)throw existingAppointmentConflict(active);AssociationContext links=validateAssociations(r);var carrier=suppliers.domesticTrucking(r.carrierId(),r.carrierName());TruckAppointmentEntity e=new TruckAppointmentEntity();e.setId(IdWorker.getId());e.setAppointmentNo(documentNumbers.next("TA"));e.setStatus(TruckAppointmentStatus.DRAFT);e.setRevision(0);e.setVersion(0);e.setCreatedAt(LocalDateTime.now());e.setUpdatedAt(e.getCreatedAt());copy(e,r,links,carrier.name());try{repo.insert(e);}catch(DuplicateKeyException exception){if(exception.getMessage()!=null&&exception.getMessage().contains("uk_truck_active_ocean_booking"))throw existingAppointmentConflict(repo.firstActiveForUpdate(r.oceanBookingId()));throw exception;}record(e,null,e.getStatus().name(),"CREATE",null,uid,name);completion.completeForBooking(e.getOceanBookingId());return enrich(required(e.getId()));}
@Transactional public TruckAppointmentEntity edit(long id,long uid,String name,SaveRequest r){validateRequired(r);TruckAppointmentEntity e=required(id);completion.lockBooking(Math.min(e.getOceanBookingId(),r.oceanBookingId()));if(!Objects.equals(e.getOceanBookingId(),r.oceanBookingId()))completion.lockBooking(Math.max(e.getOceanBookingId(),r.oceanBookingId()));checkVersion(e,r.expectedVersion());if(!(e.getStatus()==TruckAppointmentStatus.DRAFT||e.getStatus()==TruckAppointmentStatus.PENDING_DISPATCH||e.getStatus()==TruckAppointmentStatus.RESCHEDULED))throw conflict("当前状态不可编辑");TruckAppointmentEntity active=repo.firstActiveForUpdate(r.oceanBookingId());if(active!=null&&!Objects.equals(active.getId(),e.getId()))throw existingAppointmentConflict(active);AssociationContext links=validateAssociations(r);var carrier=suppliers.domesticTrucking(r.carrierId(),r.carrierName());copy(e,r,links,carrier.name());try{if(repo.updateEdit(e,r.expectedVersion())!=1)throw conflict("版本已变化,请刷新后重试");}catch(DuplicateKeyException exception){if(exception.getMessage()!=null&&exception.getMessage().contains("uk_truck_active_ocean_booking"))throw existingAppointmentConflict(repo.firstActiveForUpdate(r.oceanBookingId()));throw exception;}record(e,e.getStatus().name(),e.getStatus().name(),"EDIT",null,uid,name);completion.completeForBooking(e.getOceanBookingId());return enrich(required(id));}
@Transactional
public TruckAppointmentEntity action(long id,String action,ActionRequest r,String idem,String clientHash,long uid,String name){
String requestHash=hash(action,id,r);verifyClientHash(clientHash,requestHash);
@@ -31,8 +31,21 @@ public class WarehouseSupplierSnapshotService {
}
}
public WarehouseSupplierClient.Snapshot trucking(Long supplierId, String suppliedName) {
return resolve(supplierId, suppliedName, "TRUCKING");
public WarehouseSupplierClient.Snapshot domesticTrucking(Long supplierId, String suppliedName) {
return trucking(supplierId, suppliedName, "DOMESTIC");
}
public WarehouseSupplierClient.Snapshot overseasTrucking(Long supplierId, String suppliedName) {
return trucking(supplierId, suppliedName, "OVERSEAS");
}
private WarehouseSupplierClient.Snapshot trucking(Long supplierId, String suppliedName, String scope) {
var snapshot = resolve(supplierId, suppliedName, "TRUCKING");
if (snapshot != null && !scope.equals(snapshot.truckingScope())) {
throw new BizException(ResultCode.CONFLICT, "DOMESTIC".equals(scope)
? "国内拖车只能使用国内拖车供应商" : "清关提柜只能使用海外车队");
}
return snapshot;
}
public WarehouseSupplierClient.Snapshot customs(Long supplierId, String suppliedName) {
@@ -96,7 +96,7 @@ public class TerminalDrayageService {
TerminalDrayageEntity value = mapper.lock(containerBookingId);
requireVersion(value, request.expectedVersion());
if (actualPickupAt(context, value) != null) throw conflict("实际提柜后不可更换码头车队");
var fleet = suppliers.trucking(request.fleetSupplierId(), request.fleetSupplierName());
var fleet = suppliers.overseasTrucking(request.fleetSupplierId(), request.fleetSupplierName());
if (value != null && Objects.equals(value.getFleetSupplierId(), fleet.supplierId())) return view(context, value);
if (value != null && value.getFleetSupplierId() != null) {
mails.replaceFleet(containerBookingId, context.base().containerNo(), value.getFleetSupplierId(), fleet.supplierId(), operatorId);