优化
This commit is contained in:
+9
@@ -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;
|
||||
|
||||
+21
@@ -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
|
||||
|
||||
+1
-1
@@ -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,
|
||||
|
||||
+3
@@ -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;
|
||||
|
||||
+15
@@ -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},
|
||||
|
||||
+15
@@ -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; }
|
||||
|
||||
+85
-10
@@ -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);
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
SET NAMES utf8mb4;
|
||||
USE baiyue_firstleg;
|
||||
|
||||
DROP PROCEDURE IF EXISTS migrate_phase3_channel_containers;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE migrate_phase3_channel_containers()
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='fl_ocean_booking' AND column_name='channel_code') THEN
|
||||
ALTER TABLE fl_ocean_booking ADD COLUMN channel_code VARCHAR(64) NULL COMMENT '订舱渠道编码快照' AFTER customer_name,
|
||||
ADD COLUMN channel_name VARCHAR(128) NULL COMMENT '订舱渠道名称快照' AFTER channel_code,
|
||||
ADD COLUMN warehouse_id BIGINT UNSIGNED NULL COMMENT '国内中转仓ID快照' AFTER channel_name,
|
||||
ADD COLUMN warehouse_name VARCHAR(128) NULL COMMENT '国内中转仓名称快照' AFTER warehouse_id;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='fl_ocean_booking_container_requirement' AND column_name='capacity_cbm') THEN
|
||||
ALTER TABLE fl_ocean_booking_container_requirement ADD COLUMN capacity_cbm DECIMAL(18,4) NULL COMMENT '柜型容量快照m3' AFTER container_kind,
|
||||
ADD COLUMN max_weight_kg DECIMAL(18,3) NULL COMMENT '柜型最大承重快照kg' AFTER capacity_cbm;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='fl_container_booking' AND column_name='channel_code') THEN
|
||||
ALTER TABLE fl_container_booking ADD COLUMN channel_code VARCHAR(64) NULL COMMENT '订舱渠道编码快照' AFTER cabinet_no,
|
||||
ADD COLUMN channel_name VARCHAR(128) NULL COMMENT '订舱渠道名称快照' AFTER channel_code,
|
||||
ADD COLUMN ocean_booking_id BIGINT UNSIGNED NULL COMMENT '来源订舱ID' AFTER channel_name,
|
||||
ADD COLUMN ocean_booking_requirement_id BIGINT UNSIGNED NULL COMMENT '来源订舱箱型需求ID' AFTER ocean_booking_id;
|
||||
END IF;
|
||||
UPDATE fl_container_booking c
|
||||
JOIN fl_ocean_booking_container_allocation a ON a.container_booking_id=c.id
|
||||
JOIN fl_ocean_booking b ON b.id=a.ocean_booking_id
|
||||
SET c.ocean_booking_id=b.id,c.ocean_booking_requirement_id=a.requirement_id,
|
||||
c.channel_code=COALESCE(c.channel_code,b.channel_code),c.channel_name=COALESCE(c.channel_name,b.channel_name)
|
||||
WHERE c.ocean_booking_id IS NULL OR c.ocean_booking_requirement_id IS NULL OR c.channel_code IS NULL;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='fl_loading_pick_task' AND column_name='container_booking_id') THEN
|
||||
ALTER TABLE fl_loading_pick_task ADD COLUMN container_booking_id BIGINT UNSIGNED NULL COMMENT '稳定柜次ID' AFTER loading_plan_id,
|
||||
ADD COLUMN cabinet_no VARCHAR(64) NULL COMMENT '稳定内部柜次号快照' AFTER container_booking_id,
|
||||
MODIFY COLUMN container_no VARCHAR(64) NULL COMMENT '废弃兼容字段;不得作为任务身份';
|
||||
END IF;
|
||||
UPDATE fl_loading_pick_task t JOIN fl_loading_plan p ON p.id=t.loading_plan_id
|
||||
JOIN fl_container_booking c ON c.id=p.container_booking_id
|
||||
SET t.container_booking_id=c.id,t.cabinet_no=c.cabinet_no
|
||||
WHERE t.container_booking_id IS NULL OR t.cabinet_no IS NULL;
|
||||
IF EXISTS (SELECT 1 FROM fl_loading_pick_task WHERE container_booking_id IS NULL OR cabinet_no IS NULL) THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='渠道柜迁移失败:PDA任务无法回填稳定柜次归属';
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name='fl_loading_pick_task' AND index_name='uk_loading_pick_task_booking') THEN
|
||||
ALTER TABLE fl_loading_pick_task ADD UNIQUE KEY uk_loading_pick_task_booking (container_booking_id);
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
CALL migrate_phase3_channel_containers();
|
||||
DROP PROCEDURE IF EXISTS migrate_phase3_channel_containers;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_container_transport_history (
|
||||
id BIGINT UNSIGNED NOT NULL, container_booking_id BIGINT UNSIGNED NOT NULL,
|
||||
before_container_no VARCHAR(64) NULL, after_container_no VARCHAR(64) NULL,
|
||||
before_voyage_no VARCHAR(64) NULL, after_voyage_no VARCHAR(64) NULL,
|
||||
reason VARCHAR(500) NULL, operator_id BIGINT UNSIGNED NOT NULL, operator_name VARCHAR(64) NULL,
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), PRIMARY KEY(id),
|
||||
KEY idx_container_transport_history (container_booking_id,created_at,id)
|
||||
) ENGINE=InnoDB COMMENT='柜次真实运输字段不可变变更审计';
|
||||
@@ -7,6 +7,10 @@ CREATE TABLE IF NOT EXISTS fl_ocean_booking (
|
||||
status VARCHAR(24) NOT NULL DEFAULT 'DRAFT' COMMENT '订舱状态:DRAFT草稿、SUBMITTED已提交、CONFIRMED已确认、RELEASED已放舱、FAILED失败、CHANGE_PENDING船期待变更、CANCELLED已取消',
|
||||
customer_id BIGINT UNSIGNED NOT NULL COMMENT '客户ID和数据隔离键',
|
||||
customer_name VARCHAR(128) NOT NULL COMMENT '客户名称快照',
|
||||
channel_code VARCHAR(64) NOT NULL COMMENT '订舱渠道编码快照',
|
||||
channel_name VARCHAR(128) NOT NULL COMMENT '订舱渠道名称快照',
|
||||
warehouse_id BIGINT UNSIGNED NOT NULL COMMENT '国内中转仓ID快照',
|
||||
warehouse_name VARCHAR(128) NOT NULL COMMENT '国内中转仓名称快照',
|
||||
bill_of_lading_no VARCHAR(128) NULL COMMENT '提单号',
|
||||
shipping_company_id BIGINT UNSIGNED NULL COMMENT '船公司逻辑ID快照',
|
||||
shipping_company_name VARCHAR(128) NULL COMMENT '船公司名称快照',
|
||||
@@ -71,6 +75,8 @@ CREATE TABLE IF NOT EXISTS fl_ocean_booking_container_requirement (
|
||||
ocean_booking_id BIGINT UNSIGNED NOT NULL COMMENT '所属海运订舱ID',
|
||||
container_type VARCHAR(32) NOT NULL COMMENT '所需集装箱尺寸类型编码',
|
||||
container_kind VARCHAR(32) NOT NULL COMMENT '所需集装箱箱种编码',
|
||||
capacity_cbm DECIMAL(18,4) NOT NULL COMMENT '该需求柜型可用容量快照m3',
|
||||
max_weight_kg DECIMAL(18,3) NOT NULL COMMENT '该需求柜型最大承重快照kg',
|
||||
quantity INT UNSIGNED NOT NULL COMMENT '该箱型箱种所需柜数',
|
||||
sort_no INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '订舱需求展示顺序',
|
||||
PRIMARY KEY (id),
|
||||
|
||||
@@ -31,6 +31,10 @@ CREATE TABLE IF NOT EXISTS fl_container_booking (
|
||||
booking_no VARCHAR(40) NOT NULL COMMENT '平台唯一货柜预订单号',
|
||||
container_no VARCHAR(64) NULL COMMENT '集装箱号;确认前允许修订',
|
||||
cabinet_no VARCHAR(64) NOT NULL COMMENT '平台唯一内部柜号',
|
||||
channel_code VARCHAR(64) NULL COMMENT '订舱渠道编码快照;存量回填前禁止混装',
|
||||
channel_name VARCHAR(128) NULL COMMENT '订舱渠道名称快照',
|
||||
ocean_booking_id BIGINT UNSIGNED NULL COMMENT '来源订舱ID',
|
||||
ocean_booking_requirement_id BIGINT UNSIGNED NULL COMMENT '来源订舱箱型需求ID',
|
||||
warehouse_id BIGINT UNSIGNED NOT NULL COMMENT '装柜仓库ID;平台公共资源',
|
||||
warehouse_name VARCHAR(128) NOT NULL COMMENT '装柜仓库名称快照',
|
||||
bill_of_lading_no VARCHAR(128) NULL COMMENT '提单号',
|
||||
@@ -396,7 +400,9 @@ DROP PROCEDURE migrate_legacy_loading_pick_task;
|
||||
CREATE TABLE IF NOT EXISTS fl_loading_pick_task (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT 'PDA拣货任务ID',
|
||||
loading_plan_id BIGINT UNSIGNED NOT NULL COMMENT '装柜计划ID',
|
||||
container_no VARCHAR(64) NOT NULL COMMENT '集装箱号快照',
|
||||
container_booking_id BIGINT UNSIGNED NOT NULL COMMENT '稳定柜次ID',
|
||||
cabinet_no VARCHAR(64) NOT NULL COMMENT '稳定内部柜次号快照',
|
||||
container_no VARCHAR(64) NULL COMMENT '废弃兼容字段;不得作为任务身份或随运输号变更重写',
|
||||
task_status VARCHAR(16) NOT NULL DEFAULT 'LOADING' COMMENT 'PDA任务状态:LOADING拣货装柜中、COMPLETED已完成',
|
||||
started_by BIGINT UNSIGNED NOT NULL COMMENT '开始装柜人',
|
||||
started_by_name VARCHAR(64) NULL COMMENT '开始装柜人名称',
|
||||
@@ -407,6 +413,7 @@ CREATE TABLE IF NOT EXISTS fl_loading_pick_task (
|
||||
version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_loading_pick_task_plan (loading_plan_id),
|
||||
UNIQUE KEY uk_loading_pick_task_booking (container_booking_id),
|
||||
KEY idx_loading_pick_container (container_no, task_status, started_at),
|
||||
CONSTRAINT fk_loading_pick_task_plan FOREIGN KEY (loading_plan_id) REFERENCES fl_loading_plan (id),
|
||||
CONSTRAINT chk_loading_pick_task_status CHECK (task_status IN ('LOADING', 'COMPLETED'))
|
||||
|
||||
@@ -953,6 +953,10 @@ components:
|
||||
bookingNo: {type: string, maxLength: 40}
|
||||
containerNo: {type: string, maxLength: 64, nullable: true}
|
||||
cabinetNo: {type: string, maxLength: 64}
|
||||
channelCode: {type: string, maxLength: 64, nullable: true, description: Missing legacy snapshots are blocked from loading until explicitly backfilled}
|
||||
channelName: {type: string, maxLength: 128, nullable: true}
|
||||
oceanBookingId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true}
|
||||
oceanBookingRequirementId: {type: string, pattern: '^[1-9][0-9]*$', nullable: true}
|
||||
warehouseId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
warehouseName: {type: string, maxLength: 128}
|
||||
billOfLadingNo: {type: string, maxLength: 128, nullable: true}
|
||||
@@ -1017,11 +1021,12 @@ components:
|
||||
data: {$ref: '#/components/schemas/ContainerSaveRequest'}
|
||||
ContainerTransportUpdateRequest:
|
||||
type: object
|
||||
required: [version]
|
||||
required: [version, reason]
|
||||
properties:
|
||||
containerNo: {type: string, maxLength: 64, nullable: true}
|
||||
voyageNo: {type: string, maxLength: 64, nullable: true}
|
||||
version: {type: integer, minimum: 0}
|
||||
reason: {type: string, maxLength: 500}
|
||||
MerchantInboundBillingEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -208,9 +208,10 @@ components:
|
||||
customerName: {type: string, nullable: true, description: 客户名称}
|
||||
PreloadSummary:
|
||||
type: object
|
||||
required: [planId, preloadNo, customers, status, skuCount, totalPackageCount, totalVolumeCbm, totalGrossWeightKg, createdAt, version]
|
||||
required: [planId, containerBookingId, preloadNo, customers, status, skuCount, totalPackageCount, totalVolumeCbm, totalGrossWeightKg, createdAt, version]
|
||||
properties:
|
||||
planId: {$ref: '#/components/schemas/LongId'}
|
||||
containerBookingId: {$ref: '#/components/schemas/LongId'}
|
||||
preloadNo: {type: string, description: 预装单号}
|
||||
containerNo: {type: string, nullable: true, description: 集装箱号}
|
||||
internalContainerNo: {type: string, nullable: true, description: 内部柜号}
|
||||
@@ -229,9 +230,10 @@ components:
|
||||
version: {type: integer, format: int32, minimum: 0, description: 乐观锁版本}
|
||||
ConfirmedSummary:
|
||||
type: object
|
||||
required: [planId, loadingListNo, status, totalPackageCount, totalGrossWeightKg, totalNetWeightKg, totalVolumeCbm, totalQuantity, createdAt, version]
|
||||
required: [planId, containerBookingId, loadingListNo, status, totalPackageCount, totalGrossWeightKg, totalNetWeightKg, totalVolumeCbm, totalQuantity, createdAt, version]
|
||||
properties:
|
||||
planId: {$ref: '#/components/schemas/LongId'}
|
||||
containerBookingId: {$ref: '#/components/schemas/LongId'}
|
||||
loadingListNo: {type: string, description: 装柜清单号}
|
||||
warehouseId: {$ref: '#/components/schemas/NullableLongId'}
|
||||
warehouseName: {type: string, nullable: true, description: 仓库名称}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
openapi: 3.0.3
|
||||
info: {title: Phase 3 Ocean Booking API, version: 1.1.0}
|
||||
paths:
|
||||
/api/platform/v1/ocean-bookings:
|
||||
post:
|
||||
summary: Create an ocean booking draft with immutable channel and warehouse snapshots
|
||||
parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
|
||||
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/CreateRequest'}}}}
|
||||
responses: {'200': {description: Draft detail}}
|
||||
/api/platform/v1/ocean-bookings/{oceanBookingId}/confirm:
|
||||
post:
|
||||
summary: Confirm booking and atomically create one stable cabinet and editing loading plan per requested quantity
|
||||
parameters: [{$ref: '#/components/parameters/OceanBookingId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
|
||||
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/ActionRequest'}}}}
|
||||
responses: {'200': {description: CONFIRMED detail; no real container number required}}
|
||||
/api/platform/v1/ocean-bookings/{oceanBookingId}/release:
|
||||
post:
|
||||
summary: Confirm released transport data and synchronize associated cabinet transport snapshots
|
||||
description: Does not create cabinets, change channels, replace cabinetNo, or recreate loading plans.
|
||||
parameters: [{$ref: '#/components/parameters/OceanBookingId'}, {$ref: '#/components/parameters/IdempotencyKey'}]
|
||||
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/ReleaseRequest'}}}}
|
||||
responses: {'200': {description: RELEASED detail}}
|
||||
components:
|
||||
parameters:
|
||||
OceanBookingId: {name: oceanBookingId, in: path, required: true, schema: {type: integer, format: int64, minimum: 1}}
|
||||
IdempotencyKey: {name: Idempotency-Key, in: header, required: true, schema: {type: string, maxLength: 128}}
|
||||
schemas:
|
||||
CreateRequest:
|
||||
type: object
|
||||
required: [expectedVersion, data]
|
||||
properties:
|
||||
expectedVersion: {type: integer, enum: [0]}
|
||||
data: {$ref: '#/components/schemas/DraftData'}
|
||||
DraftData:
|
||||
type: object
|
||||
required: [customerId, customerName, channelCode, channelName, warehouseId, warehouseName, parties, containerRequirements]
|
||||
properties:
|
||||
customerId: {type: integer, format: int64, minimum: 1}
|
||||
customerName: {type: string, maxLength: 128}
|
||||
channelCode: {type: string, maxLength: 64}
|
||||
channelName: {type: string, maxLength: 128}
|
||||
warehouseId: {type: integer, format: int64, minimum: 1}
|
||||
warehouseName: {type: string, maxLength: 128}
|
||||
billOfLadingNo: {type: string, nullable: true, deprecated: true, description: Must be omitted until release}
|
||||
vesselName: {type: string, nullable: true, deprecated: true, description: Must be omitted until release}
|
||||
voyageNo: {type: string, nullable: true, deprecated: true, description: Must be omitted until release}
|
||||
estimatedSailingAt: {type: string, format: date-time, nullable: true, deprecated: true, description: Must be omitted until release}
|
||||
estimatedArrivalAt: {type: string, format: date-time, nullable: true, deprecated: true, description: Must be omitted until release}
|
||||
parties: {type: array, minItems: 3, items: {type: object}}
|
||||
containerRequirements: {type: array, minItems: 1, maxItems: 20, items: {$ref: '#/components/schemas/ContainerRequirement'}}
|
||||
ContainerRequirement:
|
||||
type: object
|
||||
required: [containerType, containerKind, capacityCbm, maxWeightKg, quantity]
|
||||
properties:
|
||||
containerType: {type: string, maxLength: 32}
|
||||
containerKind: {type: string, maxLength: 32}
|
||||
capacityCbm: {type: number, exclusiveMinimum: true, minimum: 0}
|
||||
maxWeightKg: {type: number, exclusiveMinimum: true, minimum: 0}
|
||||
quantity: {type: integer, minimum: 1}
|
||||
ActionRequest:
|
||||
type: object
|
||||
required: [expectedVersion]
|
||||
properties: {expectedVersion: {type: integer, minimum: 0}, reason: {type: string, maxLength: 1000}}
|
||||
ReleaseRequest:
|
||||
type: object
|
||||
required: [expectedVersion, billOfLadingNo, vesselName, voyageNo, estimatedSailingAt, estimatedArrivalAt]
|
||||
properties:
|
||||
expectedVersion: {type: integer, minimum: 0}
|
||||
billOfLadingNo: {type: string, maxLength: 128}
|
||||
vesselName: {type: string, maxLength: 128}
|
||||
voyageNo: {type: string, maxLength: 64}
|
||||
estimatedSailingAt: {type: string, format: date-time}
|
||||
estimatedArrivalAt: {type: string, format: date-time}
|
||||
reason: {type: string, maxLength: 1000}
|
||||
@@ -7,7 +7,7 @@ paths:
|
||||
get:
|
||||
summary: Find the active container picking task in one platform warehouse
|
||||
parameters:
|
||||
- {name: containerNo, in: query, required: true, schema: {type: string}}
|
||||
- {name: containerNo, in: query, required: true, deprecated: true, description: Compatibility lookup only; taskId is the stable identity, schema: {type: string}}
|
||||
- {name: warehouseId, in: query, required: true, schema: {type: integer, format: int64, minimum: 1}}
|
||||
responses:
|
||||
'200':
|
||||
@@ -26,6 +26,14 @@ paths:
|
||||
schema:
|
||||
type: array
|
||||
items: {$ref: '#/components/schemas/PendingTask'}
|
||||
/api/platform/v1/first-leg/pda-loading/tasks/{taskId}:
|
||||
get:
|
||||
summary: Read one PDA task by stable task identity
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/TaskId'}
|
||||
- {name: warehouseId, in: query, required: true, schema: {type: integer, format: int64, minimum: 1}}
|
||||
responses:
|
||||
'200': {description: Task with current transport display fields}
|
||||
/api/platform/v1/first-leg/pda-loading/tasks/{taskId}/claim:
|
||||
post:
|
||||
summary: Atomically claim an unassigned task for the authenticated operator
|
||||
@@ -112,11 +120,13 @@ components:
|
||||
schemas:
|
||||
Task:
|
||||
type: object
|
||||
required: [id, loadingPlanId, containerNo, taskStatus, workflowStatus, version, items]
|
||||
required: [id, loadingPlanId, containerBookingId, cabinetNo, taskStatus, workflowStatus, version, items]
|
||||
properties:
|
||||
id: {type: integer, format: int64}
|
||||
loadingPlanId: {type: integer, format: int64}
|
||||
containerNo: {type: string}
|
||||
containerBookingId: {type: integer, format: int64}
|
||||
cabinetNo: {type: string}
|
||||
currentContainerNo: {type: string, nullable: true, description: Mutable transport display field; never task identity}
|
||||
taskStatus: {type: string, enum: [LOADING, COMPLETED]}
|
||||
workflowStatus: {type: string, enum: [UNASSIGNED, IN_PROGRESS, AWAITING_REVIEW, COMPLETED]}
|
||||
version: {type: integer, minimum: 0}
|
||||
@@ -147,11 +157,13 @@ components:
|
||||
version: {type: integer, minimum: 0}
|
||||
PendingTask:
|
||||
type: object
|
||||
required: [taskId, loadingPlanId, containerNo, warehouseId, taskStatus, workflowStatus, version, assignmentVersion, itemCount, pendingItemCount]
|
||||
required: [taskId, loadingPlanId, containerBookingId, cabinetNo, warehouseId, taskStatus, workflowStatus, version, assignmentVersion, itemCount, pendingItemCount]
|
||||
properties:
|
||||
taskId: {type: integer, format: int64}
|
||||
loadingPlanId: {type: integer, format: int64}
|
||||
containerNo: {type: string}
|
||||
containerBookingId: {type: integer, format: int64}
|
||||
cabinetNo: {type: string}
|
||||
currentContainerNo: {type: string, nullable: true}
|
||||
warehouseId: {type: integer, format: int64}
|
||||
warehouseName: {type: string, nullable: true}
|
||||
taskStatus: {type: string, enum: [LOADING]}
|
||||
@@ -176,10 +188,12 @@ components:
|
||||
operationId: {type: string, maxLength: 128}
|
||||
BatchRequest:
|
||||
type: object
|
||||
required: [warehouseId, containerNo, expectedVersion, inventorySnapshotToken, items]
|
||||
required: [warehouseId, loadingPlanId, containerBookingId, expectedVersion, inventorySnapshotToken, items]
|
||||
properties:
|
||||
warehouseId: {type: integer, format: int64, minimum: 1}
|
||||
containerNo: {type: string, maxLength: 64}
|
||||
loadingPlanId: {type: integer, format: int64, minimum: 1}
|
||||
containerBookingId: {type: integer, format: int64, minimum: 1}
|
||||
containerNo: {type: string, maxLength: 64, nullable: true, deprecated: true, description: Accepted during compatibility period but ignored for identity and idempotency}
|
||||
expectedVersion: {type: integer, minimum: 0}
|
||||
inventorySnapshotToken: {type: string, minLength: 64, maxLength: 64}
|
||||
items:
|
||||
@@ -217,11 +231,14 @@ components:
|
||||
remark: {type: string, maxLength: 500}
|
||||
InventorySnapshot:
|
||||
type: object
|
||||
required: [taskId, taskVersion, containerNo, warehouseId, capturedAt, snapshotToken, inbounds]
|
||||
required: [taskId, loadingPlanId, containerBookingId, cabinetNo, taskVersion, warehouseId, capturedAt, snapshotToken, inbounds]
|
||||
properties:
|
||||
taskId: {type: integer, format: int64}
|
||||
loadingPlanId: {type: integer, format: int64}
|
||||
containerBookingId: {type: integer, format: int64}
|
||||
cabinetNo: {type: string}
|
||||
taskVersion: {type: integer, minimum: 0}
|
||||
containerNo: {type: string}
|
||||
currentContainerNo: {type: string, nullable: true}
|
||||
warehouseId: {type: integer, format: int64}
|
||||
capturedAt: {type: string, format: date-time}
|
||||
snapshotToken: {type: string, minLength: 64, maxLength: 64}
|
||||
|
||||
+4
-1
@@ -102,7 +102,10 @@ class LoadingListControllerContractTest {
|
||||
.doesNotContainKey("/api/platform/v1/first-leg/loading-plans/{planId}/complete");
|
||||
assertThat(stringMap(schemas.get("LongId"))).containsEntry("type", "string");
|
||||
assertThat(properties(schemas, "PreloadPage")).containsKeys("records", "page", "size", "total", "pages");
|
||||
assertThat(properties(schemas, "PreloadSummary")).containsKeys("planId", "customers", "skuCount");
|
||||
assertThat(properties(schemas, "PreloadSummary"))
|
||||
.containsKeys("planId", "containerBookingId", "customers", "skuCount");
|
||||
assertThat(properties(schemas, "ConfirmedSummary"))
|
||||
.containsKeys("planId", "containerBookingId", "loadingListNo");
|
||||
assertThat(properties(schemas, "LoadingItem")).containsKeys(
|
||||
"itemId", "accountId", "inboundId", "inboundDetailId", "skuCode",
|
||||
"destinationWarehouse", "productQuantity", "frozenAt");
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.baiyue.firstleg.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.ConfirmedQuery;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.PreloadQuery;
|
||||
import com.baiyue.firstleg.mapper.LoadingListQueryMapper;
|
||||
import com.baiyue.firstleg.mapper.LoadingListQueryMapper.AggregateRow;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class LoadingListRepositoryTest {
|
||||
@Mock
|
||||
private LoadingListQueryMapper mapper;
|
||||
|
||||
private LoadingListRepository repository;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
repository = new LoadingListRepository();
|
||||
ReflectionTestUtils.setField(repository, "mapper", mapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapsRealContainerBookingIdIntoPreloadAndConfirmedSummaries() {
|
||||
AggregateRow row = new AggregateRow();
|
||||
row.setPlanId(41L);
|
||||
row.setContainerBookingId(61L);
|
||||
row.setPlanNo("LP-41");
|
||||
row.setPlanStatus("LOADING");
|
||||
row.setVersion(1);
|
||||
|
||||
Page<AggregateRow> preloadPage = new Page<>(1, 20);
|
||||
preloadPage.setRecords(List.of(row));
|
||||
preloadPage.setTotal(1);
|
||||
when(mapper.selectPreloadPage(any(), any())).thenReturn(preloadPage);
|
||||
when(mapper.selectCustomers(List.of(41L))).thenReturn(List.of());
|
||||
|
||||
Page<AggregateRow> confirmedPage = new Page<>(1, 20);
|
||||
confirmedPage.setRecords(List.of(row));
|
||||
confirmedPage.setTotal(1);
|
||||
when(mapper.selectConfirmedPage(any(), any())).thenReturn(confirmedPage);
|
||||
|
||||
var preload = repository.preloadPage(new PreloadQuery(null, null, null, null, null, 1, 20))
|
||||
.records().get(0);
|
||||
var confirmed = repository.confirmedPage(new ConfirmedQuery(null, null, null, null, 1, 20))
|
||||
.records().get(0);
|
||||
|
||||
assertThat(preload.planId()).isEqualTo(41L);
|
||||
assertThat(preload.containerBookingId()).isEqualTo(61L);
|
||||
assertThat(confirmed.planId()).isEqualTo(41L);
|
||||
assertThat(confirmed.containerBookingId()).isEqualTo(61L);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user