This commit is contained in:
2026-08-12 17:29:30 +08:00
parent 4562e3f1d1
commit b5649cf8ae
42 changed files with 1205 additions and 78 deletions
@@ -60,6 +60,15 @@ public class PlatformLoadingPdaController {
return ApiResponse.success(service.pendingTasks(user.userId(), keyword));
}
@GetMapping("/tasks/{taskId}")
@PreAuthorize("hasAuthority('platform:firstleg:pda:view')")
public ApiResponse<Task> taskById(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable @Positive long taskId,
@RequestParam @Positive long warehouseId) {
FirstlegScopeGuard.platform(user);
return ApiResponse.success(service.task(taskId, warehouseId));
}
@GetMapping("/tasks/{taskId}/inventory-snapshot")
@PreAuthorize("hasAuthority('platform:firstleg:pda:view')")
public ApiResponse<InventorySnapshot> inventorySnapshot(@AuthenticationPrincipal AuthenticatedUser user,
@@ -84,6 +84,7 @@ public final class ContainerDtos {
public record TransportUpdateRequest(
@Size(max = 64) String containerNo,
@Size(max = 64) String voyageNo,
@NotNull @PositiveOrZero Integer version
@NotNull @PositiveOrZero Integer version,
@NotBlank @Size(max = 500) String reason
) {}
}
@@ -62,6 +62,7 @@ public final class LoadingListDtos {
public record PreloadSummary(
Long planId,
Long containerBookingId,
String preloadNo,
String containerNo,
String internalContainerNo,
@@ -86,6 +87,7 @@ public final class LoadingListDtos {
public record ConfirmedSummary(
Long planId,
Long containerBookingId,
String loadingListNo,
Long warehouseId,
String warehouseName,
@@ -58,7 +58,9 @@ public final class LoadingPickDtos {
/** PDA 在线草稿只在员工提交时发送一次,服务端原子处理整批明细。 */
public record BatchRequest(
@NotNull @Min(1) Long warehouseId,
@NotBlank @Size(max = 64) String containerNo,
@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
@@ -125,13 +127,15 @@ public final class LoadingPickDtos {
Long operatorId, String operatorName, LocalDateTime createdAt) {
}
public record Task(Long id, Long loadingPlanId, String containerNo, String taskStatus,
public record Task(Long id, Long loadingPlanId, Long containerBookingId, String cabinetNo,
String currentContainerNo, String taskStatus,
String workflowStatus, Integer version, Assignment assignment,
Review review, List<PickItem> items) {
}
/** PDA 工作台只展示当前作业员可领取或已领取的进行中任务。 */
public record PendingTask(Long taskId, Long loadingPlanId, String containerNo,
public record PendingTask(Long taskId, Long loadingPlanId, Long containerBookingId,
String cabinetNo, String currentContainerNo,
Long warehouseId, String warehouseName, String taskStatus,
String workflowStatus, Integer version, Integer assignmentVersion,
Integer itemCount, Integer pendingItemCount) {
@@ -146,7 +150,8 @@ public final class LoadingPickDtos {
List<InventoryResource> resources) {
}
public record InventorySnapshot(Long taskId, Integer taskVersion, String containerNo,
public record InventorySnapshot(Long taskId, Long loadingPlanId, Long containerBookingId,
String cabinetNo, Integer taskVersion, String currentContainerNo,
Long warehouseId, LocalDateTime capturedAt, String snapshotToken,
List<InboundInventory> inbounds) {
}
@@ -2,12 +2,12 @@ package com.baiyue.firstleg.dto;
import com.baiyue.firstleg.entity.LoadingPlanItemEntity; import jakarta.validation.constraints.*; import java.math.BigDecimal; import java.util.List;
public final class SmartLoadingDtos { private SmartLoadingDtos(){}
public record Candidate(Long accountId, Long inboundId, Long inboundDetailId, Long orderId, String orderNo, String inboundCode,
Long warehouseId, Long destinationWarehouseId, String destinationWarehouseCode, String destinationWarehouseName, String customerName, String goodsName, Integer availablePackageCount,
Long warehouseId, String channelCode, String channelName, Long destinationWarehouseId, String destinationWarehouseCode, String destinationWarehouseName, String customerName, String goodsName, Integer availablePackageCount,
BigDecimal unitWeightKg, BigDecimal unitVolumeCbm) {
public Candidate(Long accountId, Long inboundId, Long inboundDetailId, Long orderId, String orderNo, String inboundCode,
Long warehouseId, String destinationWarehouseCode, String customerName, String goodsName, Integer availablePackageCount,
BigDecimal unitWeightKg, BigDecimal unitVolumeCbm) {
this(accountId,inboundId,inboundDetailId,orderId,orderNo,inboundCode,warehouseId,null,destinationWarehouseCode,null,customerName,goodsName,availablePackageCount,unitWeightKg,unitVolumeCbm);
this(accountId,inboundId,inboundDetailId,orderId,orderNo,inboundCode,warehouseId,null,null,null,destinationWarehouseCode,null,customerName,goodsName,availablePackageCount,unitWeightKg,unitVolumeCbm);
}
}
public record CreateRequest(@NotNull @Positive Long bookingId, @Size(max=500) String remark) {}
@@ -20,6 +20,10 @@ public class ContainerBookingEntity {
private String bookingNo;
private String containerNo;
private String cabinetNo;
private String channelCode;
private String channelName;
private Long oceanBookingId;
private Long oceanBookingRequirementId;
private Long warehouseId;
private String warehouseName;
private String billOfLadingNo;
@@ -0,0 +1,21 @@
package com.baiyue.firstleg.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Getter;
import lombok.Setter;
@Getter @Setter
@TableName("fl_container_transport_history")
public class ContainerTransportHistoryEntity {
@TableId(type = IdType.ASSIGN_ID) private Long id;
private Long containerBookingId;
private String beforeContainerNo;
private String afterContainerNo;
private String beforeVoyageNo;
private String afterVoyageNo;
private String reason;
private Long operatorId;
private String operatorName;
}
@@ -17,6 +17,8 @@ public class LoadingPickTaskEntity {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long loadingPlanId;
private Long containerBookingId;
private String cabinetNo;
private String containerNo;
private String taskStatus;
private Long startedBy;
@@ -96,4 +96,15 @@ public interface ContainerBookingMapper extends BaseMapper<ContainerBookingEntit
int updateTransport(@Param("id") long id, @Param("containerNo") String containerNo,
@Param("voyageNo") String voyageNo, @Param("expectedVersion") int expectedVersion,
@Param("operatorId") long operatorId, @Param("operatorName") String operatorName);
@Update("""
UPDATE fl_container_booking SET bill_of_lading_no=#{billOfLadingNo},vessel_name=#{vesselName},
voyage_no=#{voyageNo},estimated_sailing_at=#{estimatedSailingAt},updated_by=#{operatorId},
updated_by_name=#{operatorName},updated_at=CURRENT_TIMESTAMP(3),version=version+1
WHERE ocean_booking_id=#{oceanBookingId}
""")
int syncReleasedTransport(@Param("oceanBookingId") long oceanBookingId,
@Param("billOfLadingNo") String billOfLadingNo, @Param("vesselName") String vesselName,
@Param("voyageNo") String voyageNo, @Param("estimatedSailingAt") java.time.LocalDateTime estimatedSailingAt,
@Param("operatorId") long operatorId, @Param("operatorName") String operatorName);
}
@@ -0,0 +1,8 @@
package com.baiyue.firstleg.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baiyue.firstleg.entity.ContainerTransportHistoryEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ContainerTransportHistoryMapper extends BaseMapper<ContainerTransportHistoryEntity> {}
@@ -24,7 +24,7 @@ public interface LoadingPickTaskMapper extends BaseMapper<LoadingPickTaskEntity>
JOIN fl_loading_pick_item pick_item ON pick_item.pick_task_id = task.id
JOIN fl_loading_plan plan ON plan.id = task.loading_plan_id
JOIN fl_container_booking booking ON booking.id = plan.container_booking_id
WHERE task.container_no = #{containerNo} AND task.task_status = 'LOADING'
WHERE booking.container_no = #{containerNo} AND task.task_status = 'LOADING'
AND booking.warehouse_id = #{warehouseId}
LIMIT 1
""")
@@ -34,7 +34,8 @@ public interface LoadingPickTaskMapper extends BaseMapper<LoadingPickTaskEntity>
@Select("""
<script>
SELECT task.id AS taskId, task.loading_plan_id AS loadingPlanId,
task.container_no AS containerNo, booking.warehouse_id AS warehouseId,
task.container_booking_id AS containerBookingId, task.cabinet_no AS cabinetNo,
booking.container_no AS containerNo, booking.warehouse_id AS warehouseId,
booking.warehouse_name AS warehouseName, task.task_status AS taskStatus,
CASE WHEN assignment.pick_task_id IS NULL THEN 'UNASSIGNED' ELSE 'IN_PROGRESS' END AS workflowStatus,
task.version AS version, COALESCE(assignment.version, 0) AS assignmentVersion,
@@ -51,7 +52,8 @@ public interface LoadingPickTaskMapper extends BaseMapper<LoadingPickTaskEntity>
AND (task.container_no LIKE CONCAT('%', #{keyword}, '%')
OR booking.warehouse_name LIKE CONCAT('%', #{keyword}, '%'))
</if>
GROUP BY task.id, task.loading_plan_id, task.container_no, booking.warehouse_id,
GROUP BY task.id, task.loading_plan_id, task.container_booking_id, task.cabinet_no,
booking.container_no, booking.warehouse_id,
booking.warehouse_name, task.task_status, task.version,
assignment.pick_task_id, assignment.version
ORDER BY task.id DESC
@@ -65,6 +67,8 @@ public interface LoadingPickTaskMapper extends BaseMapper<LoadingPickTaskEntity>
private Long taskId;
private Long loadingPlanId;
private String containerNo;
private Long containerBookingId;
private String cabinetNo;
private Long warehouseId;
private String warehouseName;
private String taskStatus;
@@ -80,6 +84,10 @@ public interface LoadingPickTaskMapper extends BaseMapper<LoadingPickTaskEntity>
public void setLoadingPlanId(Long loadingPlanId) { this.loadingPlanId = loadingPlanId; }
public String getContainerNo() { return containerNo; }
public void setContainerNo(String containerNo) { this.containerNo = containerNo; }
public Long getContainerBookingId() { return containerBookingId; }
public void setContainerBookingId(Long value) { this.containerBookingId = value; }
public String getCabinetNo() { return cabinetNo; }
public void setCabinetNo(String value) { this.cabinetNo = value; }
public Long getWarehouseId() { return warehouseId; }
public void setWarehouseId(Long warehouseId) { this.warehouseId = warehouseId; }
public String getWarehouseName() { return warehouseName; }
@@ -4,7 +4,7 @@ import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate; import org.apache.iba
@Select("""
SELECT o.tenant_id accountId,o.id inboundId,i.inbound_detail_id inboundDetailId,
o.order_id orderId,o.order_no orderNo,o.inbound_code inboundCode,
o.warehouse_id warehouseId,o.destination_warehouse_id destinationWarehouseId,
o.warehouse_id warehouseId,o.channel_code channelCode,o.channel_name channelName,o.destination_warehouse_id destinationWarehouseId,
o.destination_code destinationWarehouseCode,o.destination_name destinationWarehouseName,
o.customer_name customerName,COALESCE(m.actual_goods_name,'') goodsName,
i.package_count-COALESCE((SELECT SUM(li.package_count) FROM fl_loading_plan_item li
@@ -42,7 +42,7 @@ import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate; import org.apache.iba
@Select("""
SELECT o.tenant_id accountId,o.id inboundId,i.inbound_detail_id inboundDetailId,
o.order_id orderId,o.order_no orderNo,o.inbound_code inboundCode,
o.warehouse_id warehouseId,o.destination_warehouse_id destinationWarehouseId,
o.warehouse_id warehouseId,o.channel_code channelCode,o.channel_name channelName,o.destination_warehouse_id destinationWarehouseId,
o.destination_code destinationWarehouseCode,o.destination_name destinationWarehouseName,
o.customer_name customerName,COALESCE(m.actual_goods_name,'') goodsName,
i.package_count-COALESCE((SELECT SUM(li.package_count) FROM fl_loading_plan_item li
@@ -43,7 +43,7 @@ public class PlatformOceanBookingController {
public ApiResponse<Detail> confirm(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody ActionRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.confirm(oceanBookingId, user.userId(), user.username(), key, request)); }
@PostMapping("/{oceanBookingId}/release") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:release')")
@BusinessAudit(event="OCEAN_BOOKING_RELEASE", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="确认放舱")
public ApiResponse<Detail> release(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody ActionRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.release(oceanBookingId, user.userId(), user.username(), key, request)); }
public ApiResponse<Detail> release(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody ReleaseRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.release(oceanBookingId, user.userId(), user.username(), key, request)); }
@PostMapping("/{oceanBookingId}/fail") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:confirm')")
@BusinessAudit(event="OCEAN_BOOKING_FAIL", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="标记海运订舱失败")
public ApiResponse<Detail> fail(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody ActionRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.fail(oceanBookingId, user.userId(), user.username(), key, request)); }
@@ -15,6 +15,7 @@ import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;
import java.time.LocalDateTime;
import java.math.BigDecimal;
import java.util.List;
public final class OceanBookingDtos {
@@ -34,10 +35,16 @@ public final class OceanBookingDtos {
public record ContainerRequirementRequest(@NotBlank @Size(max = 32) String containerType,
@NotBlank @Size(max = 32) String containerKind,
@NotNull @jakarta.validation.constraints.DecimalMin(value = "0", inclusive = false) BigDecimal capacityCbm,
@NotNull @jakarta.validation.constraints.DecimalMin(value = "0", inclusive = false) BigDecimal maxWeightKg,
@NotNull @Positive Integer quantity) {}
public record DraftData(@NotNull @Positive Long customerId,
@NotBlank @Size(max = 128) String customerName,
@NotBlank @Size(max = 64) String channelCode,
@NotBlank @Size(max = 128) String channelName,
@NotNull @Positive Long warehouseId,
@NotBlank @Size(max = 128) String warehouseName,
@Size(max = 128) String billOfLadingNo,
@Positive Long shippingCompanyId,
@Size(max = 128) String shippingCompanyName,
@@ -64,6 +71,13 @@ public final class OceanBookingDtos {
@Valid @NotNull DraftData data) {}
public record ActionRequest(@NotNull @Min(0) Integer expectedVersion,
@Size(max = 1000) String reason) {}
public record ReleaseRequest(@NotNull @Min(0) Integer expectedVersion,
@NotBlank @Size(max = 128) String billOfLadingNo,
@NotBlank @Size(max = 128) String vesselName,
@NotBlank @Size(max = 64) String voyageNo,
@NotNull LocalDateTime estimatedSailingAt,
@NotNull LocalDateTime estimatedArrivalAt,
@Size(max = 1000) String reason) {}
public record ChangeRequest(@NotNull @Min(0) Integer expectedVersion,
@NotBlank @Size(max = 128) String vesselName,
@NotBlank @Size(max = 64) String voyageNo,
@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Getter;
import lombok.Setter;
import java.math.BigDecimal;
@Getter
@Setter
@@ -14,6 +15,8 @@ public class OceanBookingContainerRequirementEntity {
private Long oceanBookingId;
private String containerType;
private String containerKind;
private BigDecimal capacityCbm;
private BigDecimal maxWeightKg;
private Integer quantity;
private Integer sortNo;
}
@@ -20,6 +20,10 @@ public class OceanBookingEntity {
private String status;
private Long customerId;
private String customerName;
private String channelCode;
private String channelName;
private Long warehouseId;
private String warehouseName;
private String billOfLadingNo;
private Long shippingCompanyId;
private String shippingCompanyName;
@@ -30,6 +30,7 @@ public interface OceanBookingMapper extends BaseMapper<OceanBookingEntity> {
@Update("""
UPDATE fl_ocean_booking SET customer_id=#{value.customerId},customer_name=#{value.customerName},
channel_code=#{value.channelCode},channel_name=#{value.channelName},warehouse_id=#{value.warehouseId},warehouse_name=#{value.warehouseName},
bill_of_lading_no=#{value.billOfLadingNo},shipping_company_id=#{value.shippingCompanyId},
shipping_company_name=#{value.shippingCompanyName},booking_agent_id=#{value.bookingAgentId},
booking_agent_name=#{value.bookingAgentName},vessel_name=#{value.vesselName},voyage_no=#{value.voyageNo},
@@ -56,6 +57,20 @@ public interface OceanBookingMapper extends BaseMapper<OceanBookingEntity> {
@Param("expectedVersion") int expectedVersion, @Param("operatorId") long operatorId,
@Param("operatorName") String operatorName);
@Update("""
UPDATE fl_ocean_booking SET status='RELEASED',bill_of_lading_no=#{billOfLadingNo},
vessel_name=#{vesselName},voyage_no=#{voyageNo},estimated_sailing_at=#{estimatedSailingAt},
estimated_arrival_at=#{estimatedArrivalAt},updated_by=#{operatorId},updated_by_name=#{operatorName},
updated_at=CURRENT_TIMESTAMP(3),version=version+1
WHERE id=#{oceanBookingId} AND status='CONFIRMED' AND version=#{expectedVersion}
""")
int release(@Param("oceanBookingId") long oceanBookingId, @Param("billOfLadingNo") String billOfLadingNo,
@Param("vesselName") String vesselName, @Param("voyageNo") String voyageNo,
@Param("estimatedSailingAt") LocalDateTime estimatedSailingAt,
@Param("estimatedArrivalAt") LocalDateTime estimatedArrivalAt,
@Param("expectedVersion") int expectedVersion, @Param("operatorId") long operatorId,
@Param("operatorName") String operatorName);
@Update("""
UPDATE fl_ocean_booking SET status='CHANGE_PENDING',pending_vessel_name=#{vesselName},
pending_voyage_no=#{voyageNo},pending_estimated_sailing_at=#{estimatedSailingAt},
@@ -5,7 +5,10 @@ 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.*;
import com.baiyue.firstleg.oceanbooking.mapper.*;
import jakarta.annotation.Resource;
@@ -22,6 +25,7 @@ public class OceanBookingRepository {
@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));
@@ -35,6 +39,11 @@ public class OceanBookingRepository {
int version, long operatorId, String operatorName) {
return bookings.transition(id, from, to, failureReason, cancellationReason, version, operatorId, operatorName);
}
public int release(long id, ReleaseRequest request, long operatorId, String operatorName) {
return bookings.release(id, request.billOfLadingNo().trim(), request.vesselName().trim(),
request.voyageNo().trim(), request.estimatedSailingAt(), request.estimatedArrivalAt(),
request.expectedVersion(), operatorId, operatorName);
}
public int requestChange(long id, OceanBookingEntity pending, String reason, int version,
long operatorId, String operatorName) {
return bookings.requestChange(id, pending.getPendingVesselName(), pending.getPendingVoyageNo(),
@@ -55,6 +64,12 @@ public class OceanBookingRepository {
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 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; }
@@ -5,6 +5,7 @@ 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.*;
@@ -82,12 +83,32 @@ public class OceanBookingService {
OceanBookingStatus.SUBMITTED, request, false);
}
@Transactional public Detail confirm(long id, long operatorId, String name, String key, ActionRequest request) {
return transition(id, operatorId, name, key, "CONFIRM", OceanBookingStatus.SUBMITTED,
OceanBookingStatus.CONFIRMED, request, false);
String action = "CONFIRM";
Detail replay = claimOrReplay(id, operatorId, action, key, hash(action, id, request));
if (replay != null) return replay;
OceanBookingEntity before = requiredVersion(id, request.expectedVersion());
requireStatus(before, OceanBookingStatus.SUBMITTED);
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));
}
@Transactional public Detail release(long id, long operatorId, String name, String key, ActionRequest request) {
return transition(id, operatorId, name, key, "RELEASE", OceanBookingStatus.CONFIRMED,
OceanBookingStatus.RELEASED, request, false);
@Transactional public Detail release(long id, long operatorId, String name, String key, ReleaseRequest request) {
validateRange(request.estimatedSailingAt(), request.estimatedArrivalAt());
String action = "RELEASE";
Detail replay = claimOrReplay(id, operatorId, action, key, hash(action, id, request));
if (replay != null) return replay;
OceanBookingEntity before = requiredVersion(id, request.expectedVersion());
requireStatus(before, OceanBookingStatus.CONFIRMED);
if (repository.release(id, request, operatorId, name) != 1) throw concurrent();
repository.syncReleasedTransport(id, request, operatorId, name);
OceanBookingEntity after = required(id);
addHistory(id, before.getStatus(), after.getStatus(), action, request.reason(), before, after,
operatorId, name);
return complete(id, operatorId, action, key, detail(after));
}
@Transactional
@@ -177,6 +198,7 @@ public class OceanBookingService {
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();
@@ -237,7 +259,11 @@ public class OceanBookingService {
private static void requireStatus(OceanBookingEntity value, OceanBookingStatus status) { if (!status.name().equals(value.getStatus())) throw conflict("订舱状态不允许当前操作"); }
private void validateDraft(DraftData data) {
validateRange(data.estimatedSailingAt(), data.estimatedArrivalAt());
if (data.estimatedSailingAt() != null || data.estimatedArrivalAt() != null
|| StringUtils.hasText(data.billOfLadingNo()) || StringUtils.hasText(data.vesselName())
|| StringUtils.hasText(data.voyageNo())) {
throw badRequest("真实运输数据请在确认放舱时录入");
}
Set<String> types = new HashSet<>();
for (PartyRequest party : data.parties()) {
String type = party.partyType().trim().toUpperCase();
@@ -254,17 +280,66 @@ public class OceanBookingService {
}
private static void validateRange(LocalDateTime from, LocalDateTime to) { if (from != null && to != null && to.isBefore(from)) throw badRequest("预计到港时间不能早于预计开船时间"); }
private static void copy(OceanBookingEntity v, DraftData d) {
v.setCustomerId(d.customerId()); v.setCustomerName(d.customerName().trim()); v.setBillOfLadingNo(trim(d.billOfLadingNo()));
v.setCustomerId(d.customerId()); v.setCustomerName(d.customerName().trim());
v.setChannelCode(d.channelCode().trim()); v.setChannelName(d.channelName().trim());
v.setWarehouseId(d.warehouseId()); v.setWarehouseName(d.warehouseName().trim());
v.setBillOfLadingNo(null);
v.setShippingCompanyId(d.shippingCompanyId()); v.setShippingCompanyName(trim(d.shippingCompanyName()));
v.setBookingAgentId(d.bookingAgentId()); v.setBookingAgentName(trim(d.bookingAgentName()));
v.setVesselName(trim(d.vesselName())); v.setVoyageNo(trim(d.voyageNo()));
v.setVesselName(null); v.setVoyageNo(null);
v.setDeparturePortCode(trim(d.departurePortCode())); v.setDeparturePortName(trim(d.departurePortName()));
v.setDestinationPortCode(trim(d.destinationPortCode())); v.setDestinationPortName(trim(d.destinationPortName()));
v.setPlaceOfDelivery(trim(d.placeOfDelivery())); v.setServiceTerm(trim(d.serviceTerm())); v.setFreightTerm(trim(d.freightTerm()));
v.setEstimatedSailingAt(d.estimatedSailingAt()); v.setEstimatedArrivalAt(d.estimatedArrivalAt()); v.setRemark(trim(d.remark()));
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 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.setQuantity(r.quantity()); v.setSortNo(sort[0]++); 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); } }
private String json(Object value) { if (value == null) return null; try { return objectMapper.writeValueAsString(value); } catch (JsonProcessingException ex) { throw new IllegalStateException("无法保存订舱历史快照", ex); } }
private static OceanBookingStatus parseStatus(String status) { try { return OceanBookingStatus.valueOf(status); } catch (RuntimeException ex) { throw badRequest("订舱状态无效"); } }
@@ -102,7 +102,8 @@ public class LoadingListRepository {
}
private PreloadSummary preload(AggregateRow row, List<Customer> customers) {
return new PreloadSummary(row.getPlanId(), row.getPlanNo(), row.getContainerNo(), row.getCabinetNo(),
return new PreloadSummary(row.getPlanId(), row.getContainerBookingId(), row.getPlanNo(),
row.getContainerNo(), row.getCabinetNo(),
customers, row.getWarehouseId(), row.getWarehouseName(), row.getVesselName(), row.getVoyageNo(),
row.getContainerType(), row.getPlanStatus(), value(row.getSkuCount()),
value(row.getTotalPackageCount()), decimal(row.getTotalVolumeCbm()),
@@ -110,7 +111,8 @@ public class LoadingListRepository {
}
private ConfirmedSummary confirmed(AggregateRow row) {
return new ConfirmedSummary(row.getPlanId(), row.getPlanNo(), row.getWarehouseId(), row.getWarehouseName(),
return new ConfirmedSummary(row.getPlanId(), row.getContainerBookingId(), row.getPlanNo(),
row.getWarehouseId(), row.getWarehouseName(),
row.getContainerNo(), row.getCabinetNo(), row.getPlanStatus(), value(row.getTotalPackageCount()),
decimal(row.getTotalGrossWeightKg()), decimal(row.getTotalNetWeightKg()),
decimal(row.getTotalVolumeCbm()), value(row.getTotalQuantity()), row.getRemark(),
@@ -13,6 +13,8 @@ import com.baiyue.firstleg.dto.ContainerDtos.SaveRequest;
import com.baiyue.firstleg.dto.ContainerDtos.TransportUpdateRequest;
import com.baiyue.firstleg.entity.ContainerBookingEntity;
import com.baiyue.firstleg.entity.LoadingPlanEntity;
import com.baiyue.firstleg.entity.ContainerTransportHistoryEntity;
import com.baiyue.firstleg.mapper.ContainerTransportHistoryMapper;
import com.baiyue.firstleg.repository.ContainerRepository;
import com.baiyue.firstleg.repository.SmartLoadingRepository;
import jakarta.annotation.Resource;
@@ -29,6 +31,7 @@ public class ContainerService {
@Resource private WarehouseMasterDataClient warehouses;
@Resource private SmartLoadingRepository smartLoadingRepository;
@Resource private LoadingPickService loadingPickService;
@Resource private ContainerTransportHistoryMapper transportHistories;
@Value("${baiyue.security.internal-service-token:}") private String internalToken;
@Value("${baiyue.warehouse.public-tenant-id:0}") private long publicWarehouseTenantId;
public PageResult<ContainerBookingEntity> page(Query query) {
@@ -87,6 +90,11 @@ public class ContainerService {
if (repository.updateTransport(id, containerNo, trim(request.voyageNo()), request.version(),
operatorId, operatorName) != 1) throw conflict("货柜状态或版本已变化,请刷新后重试");
} catch (DuplicateKeyException ex) { throw conflict("集装箱号已存在"); }
ContainerTransportHistoryEntity history = new ContainerTransportHistoryEntity();
history.setContainerBookingId(id); history.setBeforeContainerNo(current.getContainerNo());
history.setAfterContainerNo(containerNo); history.setBeforeVoyageNo(current.getVoyageNo());
history.setAfterVoyageNo(trim(request.voyageNo())); history.setReason(request.reason().trim());
history.setOperatorId(operatorId); history.setOperatorName(operatorName); transportHistories.insert(history);
ContainerBookingEntity updated = required(id);
beginPdaTaskAfterContainerBound(updated, containerNo, operatorId, operatorName);
return updated;
@@ -80,7 +80,10 @@ public class LoadingPickService {
if (task == null) {
task = new LoadingPickTaskEntity();
task.setLoadingPlanId(planId);
task.setContainerNo(containerNo);
var booking = smartLoadingRepository.booking(smartLoadingRepository.plan(planId).getContainerBookingId());
task.setContainerBookingId(booking.getId());
task.setCabinetNo(booking.getCabinetNo());
task.setContainerNo(null);
task.setTaskStatus("LOADING");
task.setStartedBy(operatorId);
task.setStartedByName(operatorName);
@@ -122,10 +125,15 @@ public class LoadingPickService {
return view(task, warehouseId);
}
public Task task(long taskId, long warehouseId) {
return view(requiredTask(taskId), warehouseId);
}
public List<PendingTask> pendingTasks(long operatorId, String keyword) {
String normalizedKeyword = StringUtils.hasText(keyword) ? keyword.trim() : null;
return repository.pendingTasks(operatorId, normalizedKeyword).stream()
.map(row -> new PendingTask(row.getTaskId(), row.getLoadingPlanId(), row.getContainerNo(),
.map(row -> new PendingTask(row.getTaskId(), row.getLoadingPlanId(), row.getContainerBookingId(),
row.getCabinetNo(), row.getContainerNo(),
row.getWarehouseId(), row.getWarehouseName(), row.getTaskStatus(), row.getWorkflowStatus(),
row.getVersion(), row.getAssignmentVersion(), row.getItemCount(), row.getPendingItemCount()))
.toList();
@@ -297,8 +305,9 @@ public class LoadingPickService {
if (!Objects.equals(request.expectedVersion(), task.getVersion())) {
throw conflict("PDA任务版本已变化,请刷新后重试");
}
if (!task.getContainerNo().equals(request.containerNo().trim())) {
throw conflict("柜号与PDA任务不一致");
if (!Objects.equals(request.loadingPlanId(), task.getLoadingPlanId())
|| !Objects.equals(request.containerBookingId(), task.getContainerBookingId())) {
throw conflict("装柜计划或柜次归属与PDA任务不一致");
}
LoadingPickAssignmentEntity assignment = repository.assignment(taskId);
if (assignment == null) throw conflict("请先领取PDA拣货任务");
@@ -501,7 +510,9 @@ public class LoadingPickService {
String workflowStatus = "COMPLETED".equals(task.getTaskStatus()) ? "COMPLETED"
: assigned == null ? "UNASSIGNED"
: repository.allTaskItemsResolved(task.getId()) ? "AWAITING_REVIEW" : "IN_PROGRESS";
return new Task(task.getId(), task.getLoadingPlanId(), task.getContainerNo(), task.getTaskStatus(),
var booking = smartLoadingRepository.booking(task.getContainerBookingId());
return new Task(task.getId(), task.getLoadingPlanId(), task.getContainerBookingId(), task.getCabinetNo(),
booking == null ? null : booking.getContainerNo(), task.getTaskStatus(),
workflowStatus, task.getVersion(), assignment, review, items);
}
@@ -613,7 +624,9 @@ public class LoadingPickService {
return new InboundInventory(inboundId, available, resources);
}).toList();
String token = inventoryToken(task, warehouseId, inbounds);
return new InventorySnapshot(task.getId(), task.getVersion(), task.getContainerNo(), warehouseId,
var booking = smartLoadingRepository.booking(task.getContainerBookingId());
return new InventorySnapshot(task.getId(), task.getLoadingPlanId(), task.getContainerBookingId(), task.getCabinetNo(),
task.getVersion(), booking == null ? null : booking.getContainerNo(), warehouseId,
LocalDateTime.now(), token, inbounds);
}
@@ -808,13 +821,13 @@ public class LoadingPickService {
}
private static String batchRequestHash(long taskId, BatchRequest request) {
if (request == null || request.warehouseId() == null || !StringUtils.hasText(request.containerNo())
if (request == null || request.warehouseId() == null
|| request.expectedVersion() == null || !StringUtils.hasText(request.inventorySnapshotToken())
|| request.items() == null || request.items().isEmpty()) {
throw new BizException(ResultCode.BAD_REQUEST, "整批提交参数不完整");
}
StringBuilder canonical = new StringBuilder("BATCH|").append(taskId).append('|')
.append(request.warehouseId()).append('|').append(request.containerNo().trim()).append('|')
.append(request.warehouseId()).append('|').append(request.loadingPlanId()).append('|')
.append(request.expectedVersion()).append('|').append(request.inventorySnapshotToken().trim());
request.items().stream().sorted(Comparator.comparing(BatchItemRequest::itemId,
Comparator.nullsFirst(Long::compareTo)))
@@ -93,6 +93,7 @@ public class SmartLoadingService {
ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId());
Candidate candidate = repository.candidate(booking.getWarehouseId(), request.inboundId(), request.inboundDetailId());
if (candidate == null) throw conflict("入库货物不存在或尚未完成入库");
requireSameChannel(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());
@@ -135,6 +136,7 @@ public class SmartLoadingService {
ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId());
for (Candidate candidate : candidates(booking.getWarehouseId())) {
if (!Objects.equals(candidate.warehouseId(), booking.getWarehouseId())) continue;
if (!sameChannel(booking, candidate)) continue;
if (StringUtils.hasText(booking.getDestinationWarehouseCode())
&& !Objects.equals(booking.getDestinationWarehouseCode(), candidate.destinationWarehouseCode())) continue;
int count = fittingBoxes(booking, repository.items(planId), candidate);
@@ -170,6 +172,7 @@ public class SmartLoadingService {
ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId());
Candidate candidate = repository.candidate(booking.getWarehouseId(), item.getInboundId(), item.getInboundDetailId());
if (candidate == null || request.packageCount() > candidate.availablePackageCount() + item.getPackageCount()) throw validation("调整箱数超过可用箱数");
requireSameChannel(booking, candidate);
OrderCargoClient.CargoBinding cargo = loadCargo(candidate);
List<LoadingPlanItemEntity> others = new ArrayList<>(repository.items(planId));
others.removeIf(x -> Objects.equals(x.getId(), itemId));
@@ -287,6 +290,7 @@ public class SmartLoadingService {
}
}
private void checkRouteAndCapacity(ContainerBookingEntity b,List<LoadingPlanItemEntity> lines,Candidate c,int count){
requireSameChannel(b, c);
if (!Objects.equals(b.getWarehouseId(), c.warehouseId())) throw validation("装柜仓库不一致");
if (StringUtils.hasText(b.getDestinationWarehouseCode()) && !Objects.equals(b.getDestinationWarehouseCode(), c.destinationWarehouseCode())) throw validation("目的仓/港不一致");
BigDecimal v=lines.stream().map(x->unit(x.getVolumeCbm())).reduce(BigDecimal.ZERO,BigDecimal::add).add(unit(c.unitVolumeCbm()).multiply(BigDecimal.valueOf(count)));
@@ -344,6 +348,16 @@ public class SmartLoadingService {
return p;
}
private ContainerBookingEntity requireBooking(long id){ContainerBookingEntity b=repository.booking(id);if(b==null)throw new BizException(ResultCode.NOT_FOUND,"货柜预订不存在");return b;}
private static boolean sameChannel(ContainerBookingEntity booking, Candidate candidate) {
return StringUtils.hasText(booking.getChannelCode()) && StringUtils.hasText(candidate.channelCode())
&& booking.getChannelCode().equals(candidate.channelCode());
}
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 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);