提交
This commit is contained in:
@@ -19,6 +19,14 @@ public interface OrderCargoClient {
|
||||
|
||||
record CargoBinding(Long cargoId, Long orderId, Integer itemNo, String cargoName,
|
||||
Integer originalPackageCount, BigDecimal originalGrossWeightKg,
|
||||
BigDecimal originalVolumeCbm) {
|
||||
BigDecimal originalVolumeCbm, String skuNo, String cargoNameEn,
|
||||
Integer unitsPerPackage, Integer totalQuantity, BigDecimal netWeightKg,
|
||||
String cargoRemark) {
|
||||
public CargoBinding(Long cargoId, Long orderId, Integer itemNo, String cargoName,
|
||||
Integer originalPackageCount, BigDecimal originalGrossWeightKg,
|
||||
BigDecimal originalVolumeCbm) {
|
||||
this(cargoId, orderId, itemNo, cargoName, originalPackageCount,
|
||||
originalGrossWeightKg, originalVolumeCbm, null, null, null, null, null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,11 @@ public interface WarehousePalletLocationClient {
|
||||
ApiResponse<List<ActiveResource>> release(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestBody ReleaseRequest request);
|
||||
|
||||
@PostMapping("/release-selected")
|
||||
ApiResponse<List<SelectiveReleaseResult>> releaseSelected(
|
||||
@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestBody SelectiveReleaseRequest request);
|
||||
|
||||
record ReserveRequest(long tenantId, long warehouseId, String sourceService, long businessId,
|
||||
String inboundCode, String idempotencyKey, long operatorId,
|
||||
BigDecimal totalVolumeCbm, BigDecimal palletCapacityCbm, String palletType,
|
||||
@@ -33,10 +38,16 @@ public interface WarehousePalletLocationClient {
|
||||
long operatorId, BigDecimal totalVolumeCbm, BigDecimal palletCapacityCbm) {}
|
||||
record ReleaseRequest(long tenantId, String sourceService, long businessId, long operatorId,
|
||||
String reason) {}
|
||||
record SelectiveReleaseRequest(long tenantId, String sourceService, long businessId,
|
||||
long operatorId, String reason, int totalPackageCount,
|
||||
List<Long> locationIds, Integer releaseLocationCount) {}
|
||||
record AllocationResult(long reservationId, String reservationNo, int allocationNo,
|
||||
long palletId, String palletCode, long locationId, String locationCode,
|
||||
BigDecimal allocatedVolumeCbm, String state) {}
|
||||
record BatchResult(int palletCount, List<AllocationResult> allocations) {}
|
||||
record ActiveResource(long reservationId, long warehouseId, long palletId, String palletCode,
|
||||
long locationId, String locationCode, String state) {}
|
||||
record SelectiveReleaseResult(long reservationId, long occupancyId, int allocationNo,
|
||||
long palletId, long locationId, String locationCode,
|
||||
int releasedPackageCount) {}
|
||||
}
|
||||
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package com.baiyue.firstleg.controller;
|
||||
|
||||
import com.baiyue.common.audit.BusinessAudit;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.firstleg.dto.ContainerDtos.FullUpdateRequest;
|
||||
import com.baiyue.firstleg.dto.ContainerDtos.Query;
|
||||
import com.baiyue.firstleg.dto.ContainerDtos.SaveRequest;
|
||||
import com.baiyue.firstleg.dto.ContainerDtos.TransportUpdateRequest;
|
||||
import com.baiyue.firstleg.entity.ContainerBookingEntity;
|
||||
import com.baiyue.firstleg.service.ContainerService;
|
||||
import com.baiyue.firstleg.service.FirstlegScopeGuard;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@Tag(name = "货柜基础资料") @Validated @RestController
|
||||
@RequestMapping("/api/platform/v1/containers")
|
||||
public class PlatformContainerController {
|
||||
@Resource private ContainerService service;
|
||||
@GetMapping @PreAuthorize("hasAuthority('platform:firstleg:container:list')")
|
||||
public ApiResponse<PageResult<ContainerBookingEntity>> page(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid Query query) {
|
||||
FirstlegScopeGuard.platform(user); return ApiResponse.success(service.page(query));
|
||||
}
|
||||
@GetMapping("/{id}") @PreAuthorize("hasAuthority('platform:firstleg:container:view')")
|
||||
public ApiResponse<ContainerBookingEntity> detail(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long id) {
|
||||
FirstlegScopeGuard.platform(user); return ApiResponse.success(service.detail(id));
|
||||
}
|
||||
@PostMapping @PreAuthorize("hasAuthority('platform:firstleg:container:create')")
|
||||
@BusinessAudit(event="CONTAINER_CREATE", resourceType="CONTAINER", description="新增货柜基础资料", captureBefore=false)
|
||||
public ApiResponse<ContainerBookingEntity> create(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody SaveRequest request) {
|
||||
FirstlegScopeGuard.platform(user); return ApiResponse.success(service.create(user.userId(), user.username(), request));
|
||||
}
|
||||
@PutMapping("/{id}") @PreAuthorize("hasAuthority('platform:firstleg:container:edit')")
|
||||
@BusinessAudit(event="CONTAINER_UPDATE", resourceType="CONTAINER", resourceId="#id", description="完整编辑货柜基础资料")
|
||||
@Operation(summary="完整编辑货柜", description="仅无装柜计划或计划为EDITING时允许")
|
||||
public ApiResponse<ContainerBookingEntity> updateDraft(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long id,
|
||||
@Valid @RequestBody FullUpdateRequest request) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.updateDraft(id, user.userId(), user.username(), request));
|
||||
}
|
||||
@PatchMapping("/{id}/transport") @PreAuthorize("hasAuthority('platform:firstleg:container:edit')")
|
||||
@BusinessAudit(event="CONTAINER_TRANSPORT_UPDATE", resourceType="CONTAINER", resourceId="#id",
|
||||
description="修改货柜集装箱号和航次")
|
||||
@Operation(summary="修改集装箱号和航次", description="LOADING仅开放此白名单;COMPLETED全部只读")
|
||||
public ApiResponse<ContainerBookingEntity> updateTransport(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long id,
|
||||
@Valid @RequestBody TransportUpdateRequest request) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.updateTransport(id, user.userId(), user.username(), request));
|
||||
}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
package com.baiyue.firstleg.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.AdvanceRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.ConfirmedDetail;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.ConfirmedQuery;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.ConfirmedSummary;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.PreloadDetail;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.PreloadQuery;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.PreloadSummary;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.TransportUpdateRequest;
|
||||
import com.baiyue.firstleg.service.FirstlegScopeGuard;
|
||||
import com.baiyue.firstleg.service.LoadingListService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PatchMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/** 基于同一装柜计划聚合提供平台预装清单和已确认装柜清单。 */
|
||||
@Tag(name = "平台预装与装柜清单", description = "预装单向推进和已确认装柜清单只读查询")
|
||||
@Validated
|
||||
@RestController
|
||||
@RequestMapping("/api/platform/v1/first-leg")
|
||||
public class PlatformLoadingListController {
|
||||
@Resource
|
||||
private LoadingListService service;
|
||||
|
||||
@Operation(summary = "分页查询预装清单")
|
||||
@GetMapping("/loading-plans")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:preload:list')")
|
||||
public ApiResponse<PageResult<PreloadSummary>> preloadPage(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @Valid PreloadQuery query) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.preloadPage(query));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询装柜详情", description = "装柜中可维护未拣货明细;完成装柜后永久只读。")
|
||||
@GetMapping("/loading-plans/{planId}")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:preload:view')")
|
||||
public ApiResponse<PreloadDetail> preloadDetail(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long planId) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.preloadDetail(planId));
|
||||
}
|
||||
|
||||
@Operation(summary = "修改装柜运输字段", description = "仅装柜中可修改集装箱号与航次。")
|
||||
@PatchMapping("/loading-plans/{planId}/transport")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:preload:edit')")
|
||||
public ApiResponse<PreloadDetail> updateTransport(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long planId,
|
||||
@RequestHeader("Idempotency-Key") String idempotencyKey,
|
||||
@Valid @RequestBody TransportUpdateRequest request) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.updateTransport(
|
||||
planId, user.userId(), user.username(), idempotencyKey, request));
|
||||
}
|
||||
|
||||
@Operation(summary = "完成装柜", description = "状态仅允许 LOADING 推进到 COMPLETED,完成后主单和明细冻结。")
|
||||
@PostMapping("/loading-plans/{planId}/complete")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:preload:complete')")
|
||||
public ApiResponse<ConfirmedDetail> complete(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long planId,
|
||||
@RequestHeader("Idempotency-Key") String idempotencyKey,
|
||||
@Valid @RequestBody AdvanceRequest request) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.complete(
|
||||
planId, user.userId(), user.username(), idempotencyKey, request));
|
||||
}
|
||||
|
||||
@Operation(summary = "分页查询完成装柜清单", description = "只读取 COMPLETED 状态的装柜计划。")
|
||||
@GetMapping("/loading-lists")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:loading-list:list')")
|
||||
public ApiResponse<PageResult<ConfirmedSummary>> confirmedPage(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @Valid ConfirmedQuery query) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.confirmedPage(query));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询完成装柜清单明细", description = "已完成主单和明细全部只读。")
|
||||
@GetMapping("/loading-lists/{planId}")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:loading-list:view')")
|
||||
public ApiResponse<ConfirmedDetail> confirmedDetail(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long planId) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.confirmedDetail(planId));
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package com.baiyue.firstleg.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.PickRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.Task;
|
||||
import com.baiyue.firstleg.service.FirstlegScopeGuard;
|
||||
import com.baiyue.firstleg.service.LoadingPickService;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/** PDA 按集装箱号执行装柜拣货。 */
|
||||
@Validated
|
||||
@RestController
|
||||
@RequestMapping("/api/platform/v1/first-leg/pda-loading")
|
||||
public class PlatformLoadingPdaController {
|
||||
@Resource
|
||||
private LoadingPickService service;
|
||||
|
||||
@GetMapping("/tasks")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:smart-loading:list')")
|
||||
public ApiResponse<Task> task(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam String containerNo) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.task(containerNo));
|
||||
}
|
||||
|
||||
@PostMapping("/tasks/{taskId}/items/{itemId}/pick")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:smart-loading:assign')")
|
||||
public ApiResponse<Task> pick(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long taskId,
|
||||
@PathVariable @Positive long itemId,
|
||||
@Valid @RequestBody PickRequest request) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.pick(taskId, itemId, request, user.userId(), user.username()));
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package com.baiyue.firstleg.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse; import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.firstleg.dto.SmartLoadingDtos.*; import com.baiyue.firstleg.service.FirstlegScopeGuard; import com.baiyue.firstleg.service.SmartLoadingService;
|
||||
import jakarta.validation.Valid; import jakarta.validation.constraints.Positive; import jakarta.annotation.Resource; import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.util.List;
|
||||
|
||||
@Validated @RestController @RequestMapping("/api/platform/v1/first-leg/smart-loading")
|
||||
public class PlatformSmartLoadingController {
|
||||
@Resource private SmartLoadingService service;
|
||||
@GetMapping("/candidates") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:list')")
|
||||
public ApiResponse<List<Candidate>> candidates(@AuthenticationPrincipal AuthenticatedUser u,@RequestParam @Positive long accountId){FirstlegScopeGuard.platform(u,accountId);return ApiResponse.success(service.candidates(accountId));}
|
||||
@GetMapping("/plans/by-booking/{bookingId}") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:list')")
|
||||
public ApiResponse<Plan> draftByBooking(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long bookingId){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.draftByBooking(bookingId));}
|
||||
@GetMapping("/plans/{planId}") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:list')")
|
||||
public ApiResponse<Plan> detail(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long planId){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.detail(planId));}
|
||||
@PostMapping("/plans") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:create')")
|
||||
public ApiResponse<Plan> create(@AuthenticationPrincipal AuthenticatedUser u,@Valid @RequestBody CreateRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.create(r.bookingId(),r.remark(),u.userId(),u.username()));}
|
||||
@PostMapping("/plans/{planId}/assignments") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:assign')")
|
||||
public ApiResponse<Plan> assign(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long planId,@Valid @RequestBody AssignRequest r){FirstlegScopeGuard.platform(u,r.accountId());return ApiResponse.success(service.assign(planId,r,u.userId(),u.username()));}
|
||||
@PostMapping("/plans/{planId}/recommend") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:assign')")
|
||||
public ApiResponse<Plan> recommend(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long planId,@Valid @RequestBody RecommendRequest r){FirstlegScopeGuard.platform(u,r.accountId());return ApiResponse.success(service.recommend(planId,r,u.userId(),u.username()));}
|
||||
@PatchMapping("/plans/{planId}/items/{itemId}") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:assign')")
|
||||
public ApiResponse<Plan> adjust(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long planId,@PathVariable @Positive long itemId,@Valid @RequestBody AdjustRequest r){FirstlegScopeGuard.platform(u,r.accountId());return ApiResponse.success(service.adjust(planId,itemId,r,u.userId(),u.username()));}
|
||||
@DeleteMapping("/plans/{planId}/items/{itemId}") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:delete')")
|
||||
public ApiResponse<Plan> delete(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long planId,@PathVariable @Positive long itemId,@Valid @RequestBody DeleteRequest r){FirstlegScopeGuard.platform(u,r.accountId());return ApiResponse.success(service.delete(planId,itemId,r,u.userId(),u.username()));}
|
||||
@PostMapping("/plans/{planId}/push-preload") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:push')")
|
||||
public ApiResponse<Plan> push(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long planId,@RequestHeader("Idempotency-Key") String key,@Valid @RequestBody PushRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.pushPreload(planId,key,r,u.userId(),u.username()));}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.baiyue.firstleg.domain;
|
||||
|
||||
/** 智能装柜状态,只允许沿此方向推进。 */
|
||||
public enum SmartLoadingStatus {
|
||||
EDITING,
|
||||
LOADING,
|
||||
COMPLETED;
|
||||
|
||||
/** 装柜完成前可维护集装箱号和航次。 */
|
||||
public boolean allowsTransportUpdate() { return this != COMPLETED; }
|
||||
|
||||
/** 编辑中和装柜中均允许维护尚未拣货的明细。 */
|
||||
public boolean mutable() { return this != COMPLETED; }
|
||||
|
||||
public boolean canAdvanceTo(SmartLoadingStatus next) {
|
||||
return next != null && next.ordinal() == ordinal() + 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.baiyue.firstleg.dto;
|
||||
|
||||
import com.baiyue.firstleg.domain.SmartLoadingStatus;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.PositiveOrZero;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** 平台级货柜基础资料请求模型。 */
|
||||
public final class ContainerDtos {
|
||||
private ContainerDtos() {}
|
||||
|
||||
@Schema(name = "ContainerQuery", description = "货柜分页筛选条件")
|
||||
public record Query(
|
||||
@Size(max = 64) String containerNo,
|
||||
@Size(max = 64) String cabinetNo,
|
||||
@Size(max = 128) String billOfLadingNo,
|
||||
@Size(max = 128) String keyword,
|
||||
@Positive Long warehouseId,
|
||||
@Size(max = 32) String containerType,
|
||||
@Positive Long shippingCompanyId,
|
||||
@Size(max = 32) String departurePortCode,
|
||||
@Size(max = 32) String destinationPortCode,
|
||||
@Size(max = 64) String destinationWarehouseCode,
|
||||
SmartLoadingStatus status,
|
||||
LocalDateTime estimatedSailingFrom,
|
||||
LocalDateTime estimatedSailingTo,
|
||||
@Min(1) long page,
|
||||
@Min(1) @Max(200) long size
|
||||
) {
|
||||
public Query {
|
||||
if (page == 0) page = 1;
|
||||
if (size == 0) size = 20;
|
||||
}
|
||||
}
|
||||
|
||||
@Schema(name = "ContainerSaveRequest", description = "货柜基础资料;智能装柜草稿阶段可完整编辑")
|
||||
public record SaveRequest(
|
||||
@Size(max = 64) String containerNo,
|
||||
@NotBlank @Size(max = 64) String cabinetNo,
|
||||
@NotNull @Positive Long warehouseId,
|
||||
@NotBlank @Size(max = 128) String warehouseName,
|
||||
@Size(max = 128) String billOfLadingNo,
|
||||
@Size(max = 128) String vesselName,
|
||||
@Size(max = 64) String voyageNo,
|
||||
@NotBlank @Size(max = 32) String containerType,
|
||||
@Positive Long shippingCompanyId,
|
||||
@Size(max = 128) String shippingCompanyName,
|
||||
@Size(max = 32) String departurePortCode,
|
||||
@Size(max = 128) String departurePortName,
|
||||
@Size(max = 32) String destinationPortCode,
|
||||
@Size(max = 128) String destinationPortName,
|
||||
@Size(max = 64) String destinationWarehouseCode,
|
||||
@Size(max = 128) String destinationWarehouseName,
|
||||
@DecimalMin(value = "0", inclusive = false) BigDecimal capacityCbm,
|
||||
@DecimalMin(value = "0", inclusive = false) BigDecimal maxWeightKg,
|
||||
@Size(max = 128) String terminalTruckingCompany,
|
||||
LocalDateTime loadingAt,
|
||||
LocalDateTime estimatedSailingAt,
|
||||
LocalDateTime actualSailingAt,
|
||||
LocalDateTime estimatedPortArrivalAt,
|
||||
LocalDateTime actualPortArrivalAt,
|
||||
LocalDateTime estimatedWarehouseArrivalAt,
|
||||
LocalDateTime actualWarehouseArrivalAt
|
||||
) {}
|
||||
|
||||
@Schema(name = "ContainerFullUpdateRequest", description = "智能装柜 EDITING 阶段完整编辑")
|
||||
public record FullUpdateRequest(
|
||||
@NotNull @PositiveOrZero Integer version,
|
||||
@Valid @NotNull SaveRequest data
|
||||
) {}
|
||||
|
||||
@Schema(name = "ContainerTransportUpdateRequest",
|
||||
description = "LOADING 阶段只允许修改集装箱号和航次")
|
||||
public record TransportUpdateRequest(
|
||||
@Size(max = 64) String containerNo,
|
||||
@Size(max = 64) String voyageNo,
|
||||
@NotNull @PositiveOrZero Integer version
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package com.baiyue.firstleg.dto;
|
||||
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/** 同一装柜计划聚合的预装清单和已确认装柜清单接口模型。 */
|
||||
public final class LoadingListDtos {
|
||||
private LoadingListDtos() {
|
||||
}
|
||||
|
||||
public record PreloadQuery(
|
||||
@Size(max = 128) String keyword,
|
||||
String status,
|
||||
@Min(1) Long accountId,
|
||||
@Min(1) Long warehouseId,
|
||||
LocalDateTime createdFrom,
|
||||
LocalDateTime createdTo,
|
||||
@Min(1) long page,
|
||||
@Min(1) @Max(200) long size
|
||||
) {
|
||||
public PreloadQuery {
|
||||
page = page == 0 ? 1 : page;
|
||||
size = size == 0 ? 20 : size;
|
||||
}
|
||||
}
|
||||
|
||||
public record ConfirmedQuery(
|
||||
@Size(max = 128) String keyword,
|
||||
@Min(1) Long accountId,
|
||||
@Min(1) Long warehouseId,
|
||||
LocalDateTime createdFrom,
|
||||
LocalDateTime createdTo,
|
||||
@Min(1) long page,
|
||||
@Min(1) @Max(200) long size
|
||||
) {
|
||||
public ConfirmedQuery {
|
||||
page = page == 0 ? 1 : page;
|
||||
size = size == 0 ? 20 : size;
|
||||
}
|
||||
}
|
||||
|
||||
public record TransportUpdateRequest(
|
||||
@Size(max = 64) String containerNo,
|
||||
@Size(max = 64) String voyageNo,
|
||||
@NotNull @Min(0) Integer expectedVersion,
|
||||
@Size(max = 500) String operationReason
|
||||
) {
|
||||
}
|
||||
|
||||
public record AdvanceRequest(
|
||||
@NotNull @Min(0) Integer expectedVersion,
|
||||
@Size(max = 500) String operationReason
|
||||
) {
|
||||
}
|
||||
|
||||
public record Customer(Long accountId, String customerName) {
|
||||
}
|
||||
|
||||
public record PreloadSummary(
|
||||
Long planId,
|
||||
String preloadNo,
|
||||
String containerNo,
|
||||
String internalContainerNo,
|
||||
List<Customer> customers,
|
||||
Long warehouseId,
|
||||
String warehouseName,
|
||||
String vesselName,
|
||||
String voyageNo,
|
||||
String containerType,
|
||||
String status,
|
||||
long skuCount,
|
||||
long totalPackageCount,
|
||||
BigDecimal totalVolumeCbm,
|
||||
BigDecimal totalGrossWeightKg,
|
||||
LocalDateTime createdAt,
|
||||
Integer version
|
||||
) {
|
||||
public PreloadSummary {
|
||||
customers = customers == null ? List.of() : List.copyOf(customers);
|
||||
}
|
||||
}
|
||||
|
||||
public record ConfirmedSummary(
|
||||
Long planId,
|
||||
String loadingListNo,
|
||||
Long warehouseId,
|
||||
String warehouseName,
|
||||
String containerNo,
|
||||
String internalContainerNo,
|
||||
String status,
|
||||
long totalPackageCount,
|
||||
BigDecimal totalGrossWeightKg,
|
||||
BigDecimal totalNetWeightKg,
|
||||
BigDecimal totalVolumeCbm,
|
||||
long totalQuantity,
|
||||
String remark,
|
||||
Long createdBy,
|
||||
String createdByName,
|
||||
LocalDateTime createdAt,
|
||||
Integer version
|
||||
) {
|
||||
}
|
||||
|
||||
public record Item(
|
||||
Long itemId,
|
||||
Long accountId,
|
||||
String customerName,
|
||||
Long inboundId,
|
||||
Long inboundDetailId,
|
||||
String inboundCode,
|
||||
String skuCode,
|
||||
String destinationWarehouse,
|
||||
String chineseName,
|
||||
String englishName,
|
||||
String goodsImageUrl,
|
||||
int packageCount,
|
||||
int palletCount,
|
||||
BigDecimal grossWeightKg,
|
||||
BigDecimal netWeightKg,
|
||||
BigDecimal volumeCbm,
|
||||
BigDecimal dimensionalWeightKg,
|
||||
long productQuantity,
|
||||
int unitsPerPackage,
|
||||
String fbaTrackingNo,
|
||||
String shipmentNo,
|
||||
String shippingMark,
|
||||
String internalRemark,
|
||||
String customerRemark,
|
||||
String warehouseRemark,
|
||||
int loadSequence,
|
||||
LocalDateTime frozenAt
|
||||
) {
|
||||
}
|
||||
|
||||
public record History(
|
||||
Long historyId,
|
||||
Long itemId,
|
||||
String actionType,
|
||||
String fromStatus,
|
||||
String toStatus,
|
||||
String beforeSnapshot,
|
||||
String afterSnapshot,
|
||||
Long operatorId,
|
||||
String operatorName,
|
||||
String operationReason,
|
||||
LocalDateTime occurredAt
|
||||
) {
|
||||
}
|
||||
|
||||
public record PreloadDetail(PreloadSummary summary, List<Item> items, List<History> histories) {
|
||||
public PreloadDetail {
|
||||
items = items == null ? List.of() : List.copyOf(items);
|
||||
histories = histories == null ? List.of() : List.copyOf(histories);
|
||||
}
|
||||
}
|
||||
|
||||
public record ConfirmedDetail(ConfirmedSummary summary, List<Item> items, List<History> histories) {
|
||||
public ConfirmedDetail {
|
||||
items = items == null ? List.of() : List.copyOf(items);
|
||||
histories = histories == null ? List.of() : List.copyOf(histories);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.baiyue.firstleg.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** PDA 装柜拣货接口模型。 */
|
||||
public final class LoadingPickDtos {
|
||||
private LoadingPickDtos() {
|
||||
}
|
||||
|
||||
public record PickRequest(
|
||||
@NotNull @Min(1) Integer pickedPackageCount,
|
||||
@JsonAlias("reservationIds") List<@Min(1) Long> locationIds,
|
||||
@NotNull @Min(0) Integer version,
|
||||
@NotBlank @Size(max = 128) String operationId
|
||||
) {
|
||||
}
|
||||
|
||||
public record PickItem(Long id, Long loadingPlanItemId, Long inboundId, Long inboundDetailId,
|
||||
Integer plannedPackageCount, Integer pickedPackageCount,
|
||||
String itemStatus, Integer version) {
|
||||
}
|
||||
|
||||
public record Task(Long id, Long loadingPlanId, String containerNo, String taskStatus,
|
||||
Integer version, List<PickItem> items) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
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,
|
||||
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);
|
||||
}
|
||||
}
|
||||
public record CreateRequest(@NotNull @Positive Long bookingId, @Size(max=500) String remark) {}
|
||||
public record AssignRequest(@NotNull @Positive Long accountId,@NotNull @Positive Long inboundId,@NotNull @Positive Long inboundDetailId,
|
||||
@NotNull @Positive Integer packageCount,@NotNull @Min(0) Integer expectedVersion,@NotBlank @Size(max=128) String operationId) {}
|
||||
public record RecommendRequest(@NotNull @Positive Long accountId,@NotNull @Min(0) Integer expectedVersion,
|
||||
@NotBlank @Size(max=128) String operationId) {}
|
||||
public record AdjustRequest(@NotNull @Positive Long accountId,@NotNull @Positive Integer packageCount,@NotNull @Min(0) Integer expectedPlanVersion,
|
||||
@NotNull @Min(0) Integer expectedItemVersion,@NotBlank @Size(max=128) String operationId) {}
|
||||
public record DeleteRequest(@NotNull @Positive Long accountId,@NotNull @Min(0) Integer expectedPlanVersion,
|
||||
@NotNull @Min(0) Integer expectedItemVersion,@NotBlank @Size(max=128) String operationId) {}
|
||||
public record PushRequest(@NotNull @Min(0) Integer expectedVersion) {}
|
||||
public record Plan(Long id,String planNo,String status,Long bookingId,String containerNo,String voyageNo,Integer version,List<LoadingPlanItemEntity> items) {}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.baiyue.firstleg.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.annotation.Version;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** 平台级货柜预订资料;预装状态由关联装柜计划聚合。 */
|
||||
@Getter @Setter
|
||||
@TableName("fl_container_booking")
|
||||
public class ContainerBookingEntity {
|
||||
@TableId(type = IdType.ASSIGN_ID) private Long id;
|
||||
private String bookingNo;
|
||||
private String containerNo;
|
||||
private String cabinetNo;
|
||||
private Long warehouseId;
|
||||
private String warehouseName;
|
||||
private String billOfLadingNo;
|
||||
private String vesselName;
|
||||
private String voyageNo;
|
||||
private String containerType;
|
||||
private Long shippingCompanyId;
|
||||
private String shippingCompanyName;
|
||||
private String departurePortCode;
|
||||
private String departurePortName;
|
||||
private String destinationPortCode;
|
||||
private String destinationPortName;
|
||||
private String destinationWarehouseCode;
|
||||
private String destinationWarehouseName;
|
||||
private BigDecimal capacityCbm;
|
||||
private BigDecimal maxWeightKg;
|
||||
private String terminalTruckingCompany;
|
||||
private LocalDateTime loadingAt;
|
||||
private LocalDateTime estimatedSailingAt;
|
||||
private LocalDateTime actualSailingAt;
|
||||
private LocalDateTime estimatedPortArrivalAt;
|
||||
private LocalDateTime actualPortArrivalAt;
|
||||
private LocalDateTime estimatedWarehouseArrivalAt;
|
||||
private LocalDateTime actualWarehouseArrivalAt;
|
||||
private Long createdBy;
|
||||
private String createdByName;
|
||||
@TableField(fill = FieldFill.INSERT) private LocalDateTime createdAt;
|
||||
private Long updatedBy;
|
||||
private String updatedByName;
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updatedAt;
|
||||
@Version @TableField(fill = FieldFill.INSERT) private Integer version;
|
||||
@TableField(exist = false) private Long planId;
|
||||
@TableField(exist = false) private String planStatus;
|
||||
}
|
||||
@@ -26,6 +26,7 @@ public class InboundPalletAllocationEntity {
|
||||
private BigDecimal palletCapacityCbm;
|
||||
private BigDecimal allocatedVolumeCbm;
|
||||
private Integer allocatedPackageCount;
|
||||
private Integer pickedPackageCount;
|
||||
private Long locationId;
|
||||
private String locationCode;
|
||||
private String reservationNo;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
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;
|
||||
|
||||
/** PDA 拣货操作幂等历史。 */
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("fl_loading_pick_history")
|
||||
public class LoadingPickHistoryEntity {
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long pickTaskId;
|
||||
private Long pickItemId;
|
||||
private String operationId;
|
||||
private String requestHash;
|
||||
private Integer pickedPackageCount;
|
||||
private Long operatorId;
|
||||
private String operatorName;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.baiyue.firstleg.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.annotation.Version;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** PDA 装柜拣货明细。 */
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("fl_loading_pick_item")
|
||||
public class LoadingPickItemEntity {
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long pickTaskId;
|
||||
private Long loadingPlanItemId;
|
||||
private Long inboundId;
|
||||
private Long inboundDetailId;
|
||||
private Integer plannedPackageCount;
|
||||
private Integer pickedPackageCount;
|
||||
private String itemStatus;
|
||||
@Version
|
||||
private Integer version;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.baiyue.firstleg.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.annotation.Version;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** PDA 装柜拣货任务。 */
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("fl_loading_pick_task")
|
||||
public class LoadingPickTaskEntity {
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long loadingPlanId;
|
||||
private String containerNo;
|
||||
private String taskStatus;
|
||||
private Long startedBy;
|
||||
private String startedByName;
|
||||
private LocalDateTime startedAt;
|
||||
private Long completedBy;
|
||||
private String completedByName;
|
||||
private LocalDateTime completedAt;
|
||||
@Version
|
||||
private Integer version;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.baiyue.firstleg.entity;
|
||||
import com.baomidou.mybatisplus.annotation.*; import lombok.Getter; import lombok.Setter; import java.time.LocalDateTime;
|
||||
@Getter @Setter @TableName("fl_loading_plan")
|
||||
public class LoadingPlanEntity {
|
||||
@TableId(type=IdType.ASSIGN_ID) private Long id; private String planNo; private Long containerBookingId; private String planStatus;
|
||||
private String pushIdempotencyKey; private Long preloadPushedBy; private String preloadPushedByName; private LocalDateTime preloadPushedAt;
|
||||
private Long preloadCompletedBy; private String preloadCompletedByName; private LocalDateTime preloadCompletedAt; private Long confirmedBy;
|
||||
private String confirmedByName; private LocalDateTime confirmedAt; private String remark; private Long createdBy; private String createdByName;
|
||||
private LocalDateTime createdAt; private Long updatedBy; private String updatedByName; private LocalDateTime updatedAt; @Version private Integer version;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.baiyue.firstleg.entity;
|
||||
import com.baomidou.mybatisplus.annotation.*; import lombok.Getter; import lombok.Setter; import java.time.LocalDateTime;
|
||||
@Getter @Setter @TableName("fl_loading_plan_history")
|
||||
public class LoadingPlanHistoryEntity {
|
||||
@TableId(type=IdType.ASSIGN_ID) private Long id; private Long loadingPlanId; private Long loadingPlanItemId; private String operationId;
|
||||
private String actionType; private String fromStatus; private String toStatus; private String beforeSnapshot; private String afterSnapshot;
|
||||
private Long operatorId; private String operatorName; private String operationReason; private LocalDateTime createdAt;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.baiyue.firstleg.entity;
|
||||
import com.baomidou.mybatisplus.annotation.*; import lombok.Getter; import lombok.Setter; import java.math.BigDecimal; import java.time.LocalDateTime;
|
||||
@Getter @Setter @TableName("fl_loading_plan_item")
|
||||
public class LoadingPlanItemEntity {
|
||||
@TableId(type=IdType.ASSIGN_ID) private Long id; private Long loadingPlanId; private Long accountId; private String customerName;
|
||||
private Long inboundId; private Long inboundDetailId; private Long orderId; private String orderNo; private String inboundCode; private Long destinationWarehouseId;
|
||||
private String destinationWarehouseCode; private String destinationWarehouseName; private String skuCode; private String productNameZh; private String productNameEn;
|
||||
private Long productImageFileId; private String productImageUrl; private Integer packageCount; private Integer productQuantity; private Integer unitsPerCarton;
|
||||
private Integer palletCount; private BigDecimal grossWeightKg; private BigDecimal netWeightKg; private BigDecimal dimensionalWeightKg; private BigDecimal volumeCbm;
|
||||
private String fbaTrackingNo; private String shipmentNo; private String shippingMark; private Integer loadSequence; private String positionSnapshot;
|
||||
private String internalRemark; private String customerRemark; private String warehouseRemark; private LocalDateTime frozenAt; private Long frozenBy;
|
||||
private Long removedBy; private String removedByName; private LocalDateTime removedAt; @TableLogic private Boolean deleted;
|
||||
@TableField(exist=false) private Long activeInboundDetailId; private Long createdBy; private String createdByName; private LocalDateTime createdAt;
|
||||
private Long updatedBy; private String updatedByName; private LocalDateTime updatedAt; @Version private Integer version;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.baiyue.firstleg.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baiyue.firstleg.dto.ContainerDtos.Query;
|
||||
import com.baiyue.firstleg.entity.ContainerBookingEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
@Mapper
|
||||
public interface ContainerBookingMapper extends BaseMapper<ContainerBookingEntity> {
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT b.*, p.id AS plan_id, COALESCE(p.plan_status, 'EDITING') AS plan_status
|
||||
FROM fl_container_booking b LEFT JOIN fl_loading_plan p ON p.container_booking_id = b.id
|
||||
WHERE (#{query.containerNo} IS NULL OR b.container_no = #{query.containerNo})
|
||||
AND (#{query.cabinetNo} IS NULL OR b.cabinet_no = #{query.cabinetNo})
|
||||
AND (#{query.billOfLadingNo} IS NULL OR b.bill_of_lading_no = #{query.billOfLadingNo})
|
||||
AND (#{query.warehouseId} IS NULL OR b.warehouse_id = #{query.warehouseId})
|
||||
AND (#{query.containerType} IS NULL OR b.container_type = #{query.containerType})
|
||||
AND (#{query.shippingCompanyId} IS NULL OR b.shipping_company_id = #{query.shippingCompanyId})
|
||||
AND (#{query.departurePortCode} IS NULL OR b.departure_port_code = #{query.departurePortCode})
|
||||
AND (#{query.destinationPortCode} IS NULL OR b.destination_port_code = #{query.destinationPortCode})
|
||||
AND (#{query.destinationWarehouseCode} IS NULL OR b.destination_warehouse_code = #{query.destinationWarehouseCode})
|
||||
AND (#{query.status} IS NULL OR COALESCE(p.plan_status, 'EDITING') = #{query.status})
|
||||
AND (#{query.estimatedSailingFrom} IS NULL OR b.estimated_sailing_at >= #{query.estimatedSailingFrom})
|
||||
AND (#{query.estimatedSailingTo} IS NULL OR b.estimated_sailing_at <= #{query.estimatedSailingTo})
|
||||
AND (#{query.keyword} IS NULL OR b.container_no LIKE CONCAT('%', #{query.keyword}, '%')
|
||||
OR b.cabinet_no LIKE CONCAT('%', #{query.keyword}, '%')
|
||||
OR b.bill_of_lading_no LIKE CONCAT('%', #{query.keyword}, '%')
|
||||
OR b.vessel_name LIKE CONCAT('%', #{query.keyword}, '%')
|
||||
OR b.voyage_no LIKE CONCAT('%', #{query.keyword}, '%'))
|
||||
ORDER BY b.updated_at DESC, b.id DESC
|
||||
</script>
|
||||
""")
|
||||
Page<ContainerBookingEntity> selectBookingPage(Page<ContainerBookingEntity> page, @Param("query") Query query);
|
||||
|
||||
@Select("""
|
||||
SELECT b.*, p.id AS plan_id, COALESCE(p.plan_status, 'EDITING') AS plan_status
|
||||
FROM fl_container_booking b LEFT JOIN fl_loading_plan p ON p.container_booking_id = b.id
|
||||
WHERE b.id = #{id}
|
||||
""")
|
||||
ContainerBookingEntity selectBooking(@Param("id") long id);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_container_booking SET
|
||||
container_no=#{value.containerNo}, cabinet_no=#{value.cabinetNo},
|
||||
warehouse_id=#{value.warehouseId}, warehouse_name=#{value.warehouseName},
|
||||
bill_of_lading_no=#{value.billOfLadingNo}, vessel_name=#{value.vesselName}, voyage_no=#{value.voyageNo},
|
||||
container_type=#{value.containerType}, shipping_company_id=#{value.shippingCompanyId},
|
||||
shipping_company_name=#{value.shippingCompanyName}, departure_port_code=#{value.departurePortCode},
|
||||
departure_port_name=#{value.departurePortName}, destination_port_code=#{value.destinationPortCode},
|
||||
destination_port_name=#{value.destinationPortName}, destination_warehouse_code=#{value.destinationWarehouseCode},
|
||||
destination_warehouse_name=#{value.destinationWarehouseName}, capacity_cbm=#{value.capacityCbm},
|
||||
max_weight_kg=#{value.maxWeightKg}, terminal_trucking_company=#{value.terminalTruckingCompany},
|
||||
loading_at=#{value.loadingAt}, estimated_sailing_at=#{value.estimatedSailingAt},
|
||||
actual_sailing_at=#{value.actualSailingAt}, estimated_port_arrival_at=#{value.estimatedPortArrivalAt},
|
||||
actual_port_arrival_at=#{value.actualPortArrivalAt},
|
||||
estimated_warehouse_arrival_at=#{value.estimatedWarehouseArrivalAt},
|
||||
actual_warehouse_arrival_at=#{value.actualWarehouseArrivalAt}, updated_by=#{operatorId},
|
||||
updated_by_name=#{operatorName}, updated_at=CURRENT_TIMESTAMP(3), version=version+1
|
||||
WHERE id=#{value.id} AND version=#{expectedVersion}
|
||||
AND NOT EXISTS (SELECT 1 FROM fl_loading_plan p
|
||||
WHERE p.container_booking_id=fl_container_booking.id AND p.plan_status<>'EDITING')
|
||||
""")
|
||||
int updateDraft(@Param("value") ContainerBookingEntity value, @Param("expectedVersion") int expectedVersion,
|
||||
@Param("operatorId") long operatorId, @Param("operatorName") String operatorName);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_container_booking SET container_no=#{containerNo}, voyage_no=#{voyageNo},
|
||||
updated_by=#{operatorId}, updated_by_name=#{operatorName},
|
||||
updated_at=CURRENT_TIMESTAMP(3), version=version+1
|
||||
WHERE id=#{id} AND version=#{expectedVersion}
|
||||
AND NOT EXISTS (SELECT 1 FROM fl_loading_plan p
|
||||
WHERE p.container_booking_id=fl_container_booking.id AND p.plan_status='COMPLETED')
|
||||
""")
|
||||
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);
|
||||
}
|
||||
@@ -3,7 +3,38 @@ package com.baiyue.firstleg.mapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.firstleg.entity.InboundPalletAllocationEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
@Mapper
|
||||
public interface InboundPalletAllocationMapper extends BaseMapper<InboundPalletAllocationEntity> {
|
||||
@Select("""
|
||||
SELECT *
|
||||
FROM fl_inbound_pallet_allocation
|
||||
WHERE tenant_id = #{tenantId}
|
||||
AND inbound_id = #{inboundId}
|
||||
AND allocation_status = 'CONFIRMED'
|
||||
ORDER BY allocation_no ASC
|
||||
FOR UPDATE
|
||||
""")
|
||||
java.util.List<InboundPalletAllocationEntity> selectConfirmedForUpdate(
|
||||
@Param("tenantId") long tenantId, @Param("inboundId") long inboundId);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_inbound_pallet_allocation
|
||||
SET picked_package_count = picked_package_count + #{count},
|
||||
allocation_status = CASE
|
||||
WHEN picked_package_count + #{count} = allocated_package_count THEN 'RELEASED'
|
||||
ELSE 'CONFIRMED'
|
||||
END,
|
||||
updated_at = CURRENT_TIMESTAMP(3)
|
||||
WHERE id = #{id}
|
||||
AND tenant_id = #{tenantId}
|
||||
AND inbound_id = #{inboundId}
|
||||
AND allocation_status = 'CONFIRMED'
|
||||
AND picked_package_count + #{count} <= allocated_package_count
|
||||
""")
|
||||
int consume(@Param("id") long id, @Param("tenantId") long tenantId,
|
||||
@Param("inboundId") long inboundId, @Param("count") int count);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
package com.baiyue.firstleg.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.ConfirmedQuery;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.PreloadQuery;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/** 基于共享装柜计划表提供清单查询投影和状态推进持久化操作。 */
|
||||
@Mapper
|
||||
public interface LoadingListQueryMapper {
|
||||
String AGGREGATE_COLUMNS = """
|
||||
p.id AS plan_id, p.plan_no, p.container_booking_id, p.plan_status, p.remark,
|
||||
p.created_by, p.created_by_name, p.created_at, p.version,
|
||||
b.container_no, b.cabinet_no, b.warehouse_id, b.warehouse_name,
|
||||
b.vessel_name, b.voyage_no, b.container_type,
|
||||
COUNT(DISTINCT NULLIF(i.sku_code, '')) AS sku_count,
|
||||
COALESCE(SUM(i.package_count), 0) AS total_package_count,
|
||||
COALESCE(SUM(i.gross_weight_kg), 0) AS total_gross_weight_kg,
|
||||
COALESCE(SUM(i.net_weight_kg), 0) AS total_net_weight_kg,
|
||||
COALESCE(SUM(i.volume_cbm), 0) AS total_volume_cbm,
|
||||
COALESCE(SUM(i.product_quantity), 0) AS total_quantity
|
||||
""";
|
||||
|
||||
String AGGREGATE_GROUP = """
|
||||
p.id, p.plan_no, p.container_booking_id, p.plan_status, p.remark,
|
||||
p.created_by, p.created_by_name, p.created_at, p.version,
|
||||
b.container_no, b.cabinet_no, b.warehouse_id, b.warehouse_name,
|
||||
b.vessel_name, b.voyage_no, b.container_type
|
||||
""";
|
||||
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT
|
||||
""" + AGGREGATE_COLUMNS + """
|
||||
FROM fl_loading_plan p
|
||||
JOIN fl_container_booking b ON b.id = p.container_booking_id
|
||||
LEFT JOIN fl_loading_plan_item i ON i.loading_plan_id = p.id AND i.deleted = 0
|
||||
WHERE p.plan_status = 'LOADING'
|
||||
<if test="query.status != null and query.status != ''">AND p.plan_status = #{query.status}</if>
|
||||
<if test="query.keyword != null and query.keyword != ''">
|
||||
AND (p.plan_no LIKE CONCAT('%', #{query.keyword}, '%')
|
||||
OR b.container_no LIKE CONCAT('%', #{query.keyword}, '%')
|
||||
OR b.cabinet_no LIKE CONCAT('%', #{query.keyword}, '%'))
|
||||
</if>
|
||||
<if test="query.warehouseId != null">AND b.warehouse_id = #{query.warehouseId}</if>
|
||||
<if test="query.accountId != null">
|
||||
AND EXISTS (SELECT 1 FROM fl_loading_plan_item ai
|
||||
WHERE ai.loading_plan_id = p.id AND ai.deleted = 0
|
||||
AND ai.account_id = #{query.accountId})
|
||||
</if>
|
||||
<if test="query.createdFrom != null">AND p.created_at >= #{query.createdFrom}</if>
|
||||
<if test="query.createdTo != null">AND p.created_at <= #{query.createdTo}</if>
|
||||
GROUP BY
|
||||
""" + AGGREGATE_GROUP + """
|
||||
ORDER BY p.created_at DESC, p.id DESC
|
||||
</script>
|
||||
""")
|
||||
Page<AggregateRow> selectPreloadPage(Page<?> page, @Param("query") PreloadQuery query);
|
||||
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT
|
||||
""" + AGGREGATE_COLUMNS + """
|
||||
FROM fl_loading_plan p
|
||||
JOIN fl_container_booking b ON b.id = p.container_booking_id
|
||||
LEFT JOIN fl_loading_plan_item i ON i.loading_plan_id = p.id AND i.deleted = 0
|
||||
WHERE p.plan_status = 'COMPLETED'
|
||||
<if test="query.keyword != null and query.keyword != ''">
|
||||
AND (p.plan_no LIKE CONCAT('%', #{query.keyword}, '%')
|
||||
OR b.container_no LIKE CONCAT('%', #{query.keyword}, '%')
|
||||
OR b.cabinet_no LIKE CONCAT('%', #{query.keyword}, '%'))
|
||||
</if>
|
||||
<if test="query.warehouseId != null">AND b.warehouse_id = #{query.warehouseId}</if>
|
||||
<if test="query.accountId != null">
|
||||
AND EXISTS (SELECT 1 FROM fl_loading_plan_item ai
|
||||
WHERE ai.loading_plan_id = p.id AND ai.deleted = 0
|
||||
AND ai.account_id = #{query.accountId})
|
||||
</if>
|
||||
<if test="query.createdFrom != null">AND p.created_at >= #{query.createdFrom}</if>
|
||||
<if test="query.createdTo != null">AND p.created_at <= #{query.createdTo}</if>
|
||||
GROUP BY
|
||||
""" + AGGREGATE_GROUP + """
|
||||
ORDER BY p.created_at DESC, p.id DESC
|
||||
</script>
|
||||
""")
|
||||
Page<AggregateRow> selectConfirmedPage(Page<?> page, @Param("query") ConfirmedQuery query);
|
||||
|
||||
@Select("""
|
||||
SELECT
|
||||
""" + AGGREGATE_COLUMNS + """
|
||||
FROM fl_loading_plan p
|
||||
JOIN fl_container_booking b ON b.id = p.container_booking_id
|
||||
LEFT JOIN fl_loading_plan_item i ON i.loading_plan_id = p.id AND i.deleted = 0
|
||||
WHERE p.id = #{planId}
|
||||
GROUP BY
|
||||
""" + AGGREGATE_GROUP)
|
||||
AggregateRow selectAggregate(@Param("planId") long planId);
|
||||
|
||||
@Select("""
|
||||
SELECT id AS item_id, account_id, customer_name, inbound_id, inbound_detail_id,
|
||||
inbound_code, sku_code, destination_warehouse_name, product_name_zh,
|
||||
product_name_en, product_image_url, package_count, pallet_count,
|
||||
gross_weight_kg, net_weight_kg, volume_cbm, dimensional_weight_kg,
|
||||
product_quantity, units_per_carton, fba_tracking_no, shipment_no,
|
||||
shipping_mark, internal_remark, customer_remark, warehouse_remark,
|
||||
load_sequence, frozen_at
|
||||
FROM fl_loading_plan_item
|
||||
WHERE loading_plan_id = #{planId} AND deleted = 0
|
||||
ORDER BY load_sequence, id
|
||||
""")
|
||||
List<ItemRow> selectItems(@Param("planId") long planId);
|
||||
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT DISTINCT loading_plan_id AS plan_id, account_id, customer_name
|
||||
FROM fl_loading_plan_item
|
||||
WHERE deleted = 0 AND loading_plan_id IN
|
||||
<foreach collection="planIds" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
ORDER BY loading_plan_id, account_id
|
||||
</script>
|
||||
""")
|
||||
List<CustomerRow> selectCustomers(@Param("planIds") List<Long> planIds);
|
||||
|
||||
@Select("""
|
||||
SELECT id AS history_id, loading_plan_item_id AS item_id, operation_id,
|
||||
action_type, from_status, to_status, before_snapshot, after_snapshot,
|
||||
operator_id, operator_name, operation_reason, created_at AS occurred_at
|
||||
FROM fl_loading_plan_history
|
||||
WHERE loading_plan_id = #{planId}
|
||||
ORDER BY created_at DESC, id DESC
|
||||
""")
|
||||
List<HistoryRow> selectHistories(@Param("planId") long planId);
|
||||
|
||||
@Select("""
|
||||
SELECT id AS history_id, loading_plan_item_id AS item_id, operation_id,
|
||||
action_type, from_status, to_status, before_snapshot, after_snapshot,
|
||||
operator_id, operator_name, operation_reason, created_at AS occurred_at
|
||||
FROM fl_loading_plan_history WHERE operation_id = #{operationId}
|
||||
""")
|
||||
HistoryRow selectHistoryByOperation(@Param("operationId") String operationId);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_loading_plan
|
||||
SET updated_by = #{operatorId}, updated_by_name = #{operatorName},
|
||||
updated_at = CURRENT_TIMESTAMP(3), version = version + 1
|
||||
WHERE id = #{planId} AND plan_status = 'LOADING'
|
||||
AND version = #{expectedVersion}
|
||||
""")
|
||||
int touchEditable(@Param("planId") long planId, @Param("expectedVersion") int expectedVersion,
|
||||
@Param("operatorId") long operatorId, @Param("operatorName") String operatorName);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_container_booking b
|
||||
JOIN fl_loading_plan p ON p.container_booking_id = b.id
|
||||
SET b.container_no = #{containerNo}, b.voyage_no = #{voyageNo},
|
||||
b.updated_by = #{operatorId}, b.updated_by_name = #{operatorName},
|
||||
b.updated_at = CURRENT_TIMESTAMP(3), b.version = b.version + 1
|
||||
WHERE p.id = #{planId} AND p.plan_status = 'LOADING'
|
||||
""")
|
||||
int updateTransport(@Param("planId") long planId, @Param("containerNo") String containerNo,
|
||||
@Param("voyageNo") String voyageNo, @Param("operatorId") long operatorId,
|
||||
@Param("operatorName") String operatorName);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_loading_plan
|
||||
SET plan_status = #{toStatus},
|
||||
preload_completed_by = CASE WHEN #{toStatus} = 'COMPLETED' THEN #{operatorId}
|
||||
ELSE preload_completed_by END,
|
||||
preload_completed_by_name = CASE WHEN #{toStatus} = 'COMPLETED' THEN #{operatorName}
|
||||
ELSE preload_completed_by_name END,
|
||||
preload_completed_at = CASE WHEN #{toStatus} = 'COMPLETED' THEN CURRENT_TIMESTAMP(3)
|
||||
ELSE preload_completed_at END,
|
||||
confirmed_by = CASE WHEN #{toStatus} = 'COMPLETED' THEN #{operatorId} ELSE confirmed_by END,
|
||||
confirmed_by_name = CASE WHEN #{toStatus} = 'COMPLETED' THEN #{operatorName}
|
||||
ELSE confirmed_by_name END,
|
||||
confirmed_at = CASE WHEN #{toStatus} = 'COMPLETED' THEN CURRENT_TIMESTAMP(3)
|
||||
ELSE confirmed_at END,
|
||||
updated_by = #{operatorId}, updated_by_name = #{operatorName},
|
||||
updated_at = CURRENT_TIMESTAMP(3), version = version + 1
|
||||
WHERE id = #{planId} AND plan_status = #{fromStatus} AND version = #{expectedVersion}
|
||||
""")
|
||||
int advance(@Param("planId") long planId, @Param("fromStatus") String fromStatus,
|
||||
@Param("toStatus") String toStatus, @Param("expectedVersion") int expectedVersion,
|
||||
@Param("operatorId") long operatorId, @Param("operatorName") String operatorName);
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO fl_loading_plan_history
|
||||
(id, loading_plan_id, loading_plan_item_id, operation_id, action_type,
|
||||
from_status, to_status, before_snapshot, after_snapshot,
|
||||
operator_id, operator_name, operation_reason, created_at)
|
||||
VALUES
|
||||
(#{id}, #{planId}, NULL, #{operationId}, #{actionType},
|
||||
#{fromStatus}, #{toStatus}, #{beforeSnapshot}, #{afterSnapshot},
|
||||
#{operatorId}, #{operatorName}, #{operationReason}, CURRENT_TIMESTAMP(3))
|
||||
""")
|
||||
int insertHistory(HistoryWrite value);
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
class AggregateRow {
|
||||
private Long planId;
|
||||
private String planNo;
|
||||
private Long containerBookingId;
|
||||
private String planStatus;
|
||||
private String remark;
|
||||
private Long createdBy;
|
||||
private String createdByName;
|
||||
private LocalDateTime createdAt;
|
||||
private Integer version;
|
||||
private String containerNo;
|
||||
private String cabinetNo;
|
||||
private Long warehouseId;
|
||||
private String warehouseName;
|
||||
private String vesselName;
|
||||
private String voyageNo;
|
||||
private String containerType;
|
||||
private Long skuCount;
|
||||
private Long totalPackageCount;
|
||||
private BigDecimal totalGrossWeightKg;
|
||||
private BigDecimal totalNetWeightKg;
|
||||
private BigDecimal totalVolumeCbm;
|
||||
private Long totalQuantity;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
class ItemRow {
|
||||
private Long itemId;
|
||||
private Long accountId;
|
||||
private String customerName;
|
||||
private Long inboundId;
|
||||
private Long inboundDetailId;
|
||||
private String inboundCode;
|
||||
private String skuCode;
|
||||
private String destinationWarehouseName;
|
||||
private String productNameZh;
|
||||
private String productNameEn;
|
||||
private String productImageUrl;
|
||||
private Integer packageCount;
|
||||
private Integer palletCount;
|
||||
private BigDecimal grossWeightKg;
|
||||
private BigDecimal netWeightKg;
|
||||
private BigDecimal volumeCbm;
|
||||
private BigDecimal dimensionalWeightKg;
|
||||
private Long productQuantity;
|
||||
private Integer unitsPerCarton;
|
||||
private String fbaTrackingNo;
|
||||
private String shipmentNo;
|
||||
private String shippingMark;
|
||||
private String internalRemark;
|
||||
private String customerRemark;
|
||||
private String warehouseRemark;
|
||||
private Integer loadSequence;
|
||||
private LocalDateTime frozenAt;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
class HistoryRow {
|
||||
private Long historyId;
|
||||
private Long itemId;
|
||||
private String operationId;
|
||||
private String actionType;
|
||||
private String fromStatus;
|
||||
private String toStatus;
|
||||
private String beforeSnapshot;
|
||||
private String afterSnapshot;
|
||||
private Long operatorId;
|
||||
private String operatorName;
|
||||
private String operationReason;
|
||||
private LocalDateTime occurredAt;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
class CustomerRow {
|
||||
private Long planId;
|
||||
private Long accountId;
|
||||
private String customerName;
|
||||
}
|
||||
|
||||
record HistoryWrite(long id, long planId, String operationId, String actionType,
|
||||
String fromStatus, String toStatus, String beforeSnapshot,
|
||||
String afterSnapshot, long operatorId, String operatorName,
|
||||
String operationReason) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.baiyue.firstleg.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.firstleg.entity.LoadingPickHistoryEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface LoadingPickHistoryMapper extends BaseMapper<LoadingPickHistoryEntity> {
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.baiyue.firstleg.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.firstleg.entity.LoadingPickItemEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
@Mapper
|
||||
public interface LoadingPickItemMapper extends BaseMapper<LoadingPickItemEntity> {
|
||||
@Update("""
|
||||
UPDATE fl_loading_pick_item
|
||||
SET picked_package_count = picked_package_count + #{count},
|
||||
item_status = CASE WHEN picked_package_count + #{count} = planned_package_count
|
||||
THEN 'PICKED' ELSE 'PICKING' END,
|
||||
version = version + 1
|
||||
WHERE id = #{id} AND version = #{version}
|
||||
AND picked_package_count + #{count} <= planned_package_count
|
||||
""")
|
||||
int pick(@Param("id") long id, @Param("count") int count, @Param("version") int version);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.baiyue.firstleg.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.firstleg.entity.LoadingPickTaskEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface LoadingPickTaskMapper extends BaseMapper<LoadingPickTaskEntity> {
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.baiyue.firstleg.mapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baiyue.firstleg.entity.LoadingPlanHistoryEntity; import org.apache.ibatis.annotations.Mapper;
|
||||
@Mapper public interface LoadingPlanHistoryMapper extends BaseMapper<LoadingPlanHistoryEntity> {}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.baiyue.firstleg.mapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baiyue.firstleg.entity.LoadingPlanItemEntity; import org.apache.ibatis.annotations.*;
|
||||
@Mapper public interface LoadingPlanItemMapper extends BaseMapper<LoadingPlanItemEntity> {
|
||||
@Update("""
|
||||
UPDATE fl_loading_plan_item SET deleted=1,removed_by=#{operatorId},removed_by_name=#{operatorName},
|
||||
removed_at=CURRENT_TIMESTAMP(3),updated_by=#{operatorId},updated_by_name=#{operatorName},
|
||||
updated_at=CURRENT_TIMESTAMP(3),version=version+1
|
||||
WHERE id=#{itemId} AND loading_plan_id=#{planId} AND deleted=0 AND frozen_at IS NULL
|
||||
AND version=#{expectedVersion}
|
||||
""")
|
||||
int removeEditing(@Param("planId") long planId,@Param("itemId") long itemId,@Param("expectedVersion") int expectedVersion,@Param("operatorId") long operatorId,@Param("operatorName") String operatorName);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.baiyue.firstleg.mapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baiyue.firstleg.entity.LoadingPlanEntity; import org.apache.ibatis.annotations.*;
|
||||
@Mapper public interface LoadingPlanMapper extends BaseMapper<LoadingPlanEntity> {
|
||||
@Update("""
|
||||
UPDATE fl_loading_plan SET updated_by=#{operatorId},updated_by_name=#{operatorName},
|
||||
updated_at=CURRENT_TIMESTAMP(3),version=version+1
|
||||
WHERE id=#{planId} AND plan_status IN ('EDITING','LOADING') AND version=#{expectedVersion}
|
||||
""")
|
||||
int touchEditing(@Param("planId") long planId,@Param("expectedVersion") int expectedVersion,@Param("operatorId") long operatorId,@Param("operatorName") String operatorName);
|
||||
@Update("""
|
||||
UPDATE fl_loading_plan SET plan_status='LOADING',push_idempotency_key=#{key},
|
||||
preload_pushed_by=#{operatorId},preload_pushed_by_name=#{operatorName},preload_pushed_at=CURRENT_TIMESTAMP(3),
|
||||
updated_by=#{operatorId},updated_by_name=#{operatorName},updated_at=CURRENT_TIMESTAMP(3),version=version+1
|
||||
WHERE id=#{planId} AND plan_status='EDITING' AND version=#{expectedVersion}
|
||||
""")
|
||||
int pushPreload(@Param("planId") long planId,@Param("key") String key,@Param("expectedVersion") int expectedVersion,@Param("operatorId") long operatorId,@Param("operatorName") String operatorName);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.baiyue.firstleg.mapper;
|
||||
import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate; import org.apache.ibatis.annotations.*; import java.util.List;
|
||||
@Mapper public interface SmartLoadingCandidateMapper {
|
||||
@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.destination_code destinationWarehouseCode,o.destination_name destinationWarehouseName,
|
||||
o.customer_name customerName,COALESCE(m.actual_goods_name,'') goodsName,
|
||||
i.package_count availablePackageCount,COALESCE(i.unit_weight_kg,0) unitWeightKg,
|
||||
COALESCE(i.unit_volume_cbm,0) unitVolumeCbm
|
||||
FROM fl_inbound_order o
|
||||
JOIN fl_inbound_measurement m ON m.tenant_id=o.tenant_id AND m.inbound_id=o.id
|
||||
AND m.status='APPROVED'
|
||||
AND m.id=(SELECT MAX(m2.id) FROM fl_inbound_measurement m2
|
||||
WHERE m2.tenant_id=o.tenant_id AND m2.inbound_id=o.id AND m2.status='APPROVED')
|
||||
JOIN fl_inbound_measurement_item i ON i.tenant_id=o.tenant_id AND i.measurement_id=m.id
|
||||
LEFT JOIN fl_loading_plan_item l
|
||||
ON l.account_id=o.tenant_id AND l.active_inbound_detail_id=i.inbound_detail_id
|
||||
WHERE o.tenant_id=#{accountId} AND o.process_status='INBOUNDED'
|
||||
AND i.inbound_detail_id IS NOT NULL AND l.id IS NULL
|
||||
ORDER BY o.inbounded_at ASC, o.id ASC, i.id ASC
|
||||
""")
|
||||
List<Candidate> candidates(@Param("accountId") long accountId);
|
||||
|
||||
@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.destination_code destinationWarehouseCode,o.destination_name destinationWarehouseName,
|
||||
o.customer_name customerName,COALESCE(m.actual_goods_name,'') goodsName,
|
||||
i.package_count availablePackageCount,COALESCE(i.unit_weight_kg,0) unitWeightKg,
|
||||
COALESCE(i.unit_volume_cbm,0) unitVolumeCbm
|
||||
FROM fl_inbound_order o
|
||||
JOIN fl_inbound_measurement m ON m.tenant_id=o.tenant_id AND m.inbound_id=o.id
|
||||
AND m.status='APPROVED'
|
||||
AND m.id=(SELECT MAX(m2.id) FROM fl_inbound_measurement m2
|
||||
WHERE m2.tenant_id=o.tenant_id AND m2.inbound_id=o.id AND m2.status='APPROVED')
|
||||
JOIN fl_inbound_measurement_item i ON i.tenant_id=o.tenant_id AND i.measurement_id=m.id
|
||||
WHERE o.tenant_id=#{accountId} AND o.id=#{inboundId} AND o.process_status='INBOUNDED'
|
||||
AND i.inbound_detail_id=#{detailId}
|
||||
ORDER BY m.id DESC LIMIT 1
|
||||
""")
|
||||
Candidate candidate(@Param("accountId") long accountId,@Param("inboundId") long inboundId,
|
||||
@Param("detailId") long detailId);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.baiyue.firstleg.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.firstleg.dto.ContainerDtos.Query;
|
||||
import com.baiyue.firstleg.entity.ContainerBookingEntity;
|
||||
import com.baiyue.firstleg.mapper.ContainerBookingMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public class ContainerRepository {
|
||||
@Resource private ContainerBookingMapper mapper;
|
||||
public PageResult<ContainerBookingEntity> page(Query query) {
|
||||
return PageResult.from(mapper.selectBookingPage(new Page<>(query.page(), query.size()), query));
|
||||
}
|
||||
public ContainerBookingEntity find(long id) { return mapper.selectBooking(id); }
|
||||
public boolean containerNoExists(String no, Long excludedId) {
|
||||
if (no == null) return false;
|
||||
return mapper.selectCount(new LambdaQueryWrapper<ContainerBookingEntity>()
|
||||
.eq(ContainerBookingEntity::getContainerNo, no)
|
||||
.ne(excludedId != null, ContainerBookingEntity::getId, excludedId)) > 0;
|
||||
}
|
||||
public boolean cabinetNoExists(String no, Long excludedId) {
|
||||
return mapper.selectCount(new LambdaQueryWrapper<ContainerBookingEntity>()
|
||||
.eq(ContainerBookingEntity::getCabinetNo, no)
|
||||
.ne(excludedId != null, ContainerBookingEntity::getId, excludedId)) > 0;
|
||||
}
|
||||
public void insert(ContainerBookingEntity value) { mapper.insert(value); }
|
||||
public int updateDraft(ContainerBookingEntity value, int version, long operatorId, String operatorName) {
|
||||
return mapper.updateDraft(value, version, operatorId, operatorName);
|
||||
}
|
||||
public int updateTransport(long id, String containerNo, String voyageNo, int version,
|
||||
long operatorId, String operatorName) {
|
||||
return mapper.updateTransport(id, containerNo, voyageNo, version, operatorId, operatorName);
|
||||
}
|
||||
}
|
||||
+22
-4
@@ -12,6 +12,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -21,8 +22,7 @@ public class InboundPalletAllocationRepository {
|
||||
|
||||
@Transactional
|
||||
public void replace(long tenantId, long inboundId, long warehouseId, long warehouseResourceTenantId,
|
||||
BigDecimal capacity,
|
||||
BatchResult result) {
|
||||
BigDecimal capacity, int totalPackageCount, BatchResult result) {
|
||||
Map<Long, String> reservationNos = mapper.selectList(new LambdaQueryWrapper<InboundPalletAllocationEntity>()
|
||||
.eq(InboundPalletAllocationEntity::getTenantId, tenantId)
|
||||
.eq(InboundPalletAllocationEntity::getInboundId, inboundId))
|
||||
@@ -32,7 +32,11 @@ public class InboundPalletAllocationRepository {
|
||||
.eq(InboundPalletAllocationEntity::getTenantId, tenantId)
|
||||
.eq(InboundPalletAllocationEntity::getInboundId, inboundId));
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
result.allocations().forEach(source -> {
|
||||
int allocationCount = result.allocations().size();
|
||||
int perAllocation = allocationCount == 0 ? 0
|
||||
: (int) Math.ceil((double) totalPackageCount / allocationCount);
|
||||
int remaining = totalPackageCount;
|
||||
for (var source : result.allocations()) {
|
||||
InboundPalletAllocationEntity value = new InboundPalletAllocationEntity();
|
||||
value.setTenantId(tenantId);
|
||||
value.setInboundId(inboundId);
|
||||
@@ -43,6 +47,10 @@ public class InboundPalletAllocationRepository {
|
||||
value.setPalletCode(source.palletCode());
|
||||
value.setPalletCapacityCbm(capacity);
|
||||
value.setAllocatedVolumeCbm(source.allocatedVolumeCbm());
|
||||
int allocatedPackages = Math.min(perAllocation, remaining);
|
||||
value.setAllocatedPackageCount(allocatedPackages);
|
||||
value.setPickedPackageCount(0);
|
||||
remaining -= allocatedPackages;
|
||||
value.setLocationId(source.locationId());
|
||||
value.setLocationCode(source.locationCode());
|
||||
// 仓储确认响应只返回 reservationId;沿用预占阶段的预占单号快照。
|
||||
@@ -52,7 +60,17 @@ public class InboundPalletAllocationRepository {
|
||||
value.setCreatedAt(now);
|
||||
value.setUpdatedAt(now);
|
||||
mapper.insert(value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public List<InboundPalletAllocationEntity> lockConfirmed(long tenantId, long inboundId) {
|
||||
return mapper.selectConfirmedForUpdate(tenantId, inboundId);
|
||||
}
|
||||
|
||||
public void consume(long tenantId, long inboundId, long allocationId, int packageCount) {
|
||||
if (mapper.consume(allocationId, tenantId, inboundId, packageCount) != 1) {
|
||||
throw new IllegalStateException("库位箱数已被并发操作修改");
|
||||
}
|
||||
}
|
||||
|
||||
private static String normalize(String state) {
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
package com.baiyue.firstleg.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.ConfirmedDetail;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.ConfirmedQuery;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.ConfirmedSummary;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.Customer;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.History;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.Item;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.PreloadDetail;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.PreloadQuery;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.PreloadSummary;
|
||||
import com.baiyue.firstleg.mapper.LoadingListQueryMapper;
|
||||
import com.baiyue.firstleg.mapper.LoadingListQueryMapper.AggregateRow;
|
||||
import com.baiyue.firstleg.mapper.LoadingListQueryMapper.CustomerRow;
|
||||
import com.baiyue.firstleg.mapper.LoadingListQueryMapper.HistoryRow;
|
||||
import com.baiyue.firstleg.mapper.LoadingListQueryMapper.HistoryWrite;
|
||||
import com.baiyue.firstleg.mapper.LoadingListQueryMapper.ItemRow;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Repository
|
||||
public class LoadingListRepository {
|
||||
@Resource
|
||||
private LoadingListQueryMapper mapper;
|
||||
|
||||
public PageResult<PreloadSummary> preloadPage(PreloadQuery query) {
|
||||
Page<AggregateRow> page = mapper.selectPreloadPage(new Page<>(query.page(), query.size()), query);
|
||||
Map<Long, List<Customer>> customers = customerMap(page.getRecords());
|
||||
List<PreloadSummary> records = page.getRecords().stream()
|
||||
.map(row -> preload(row, customers.get(row.getPlanId())))
|
||||
.toList();
|
||||
return new PageResult<>(records, page.getCurrent(), page.getSize(), page.getTotal(), page.getPages());
|
||||
}
|
||||
|
||||
public PageResult<ConfirmedSummary> confirmedPage(ConfirmedQuery query) {
|
||||
Page<AggregateRow> page = mapper.selectConfirmedPage(new Page<>(query.page(), query.size()), query);
|
||||
List<ConfirmedSummary> records = page.getRecords().stream().map(this::confirmed).toList();
|
||||
return new PageResult<>(records, page.getCurrent(), page.getSize(), page.getTotal(), page.getPages());
|
||||
}
|
||||
|
||||
public AggregateRow aggregate(long planId) {
|
||||
return mapper.selectAggregate(planId);
|
||||
}
|
||||
|
||||
public PreloadDetail preloadDetail(AggregateRow row) {
|
||||
List<ItemRow> items = mapper.selectItems(row.getPlanId());
|
||||
List<Customer> customers = items.stream()
|
||||
.collect(Collectors.toMap(ItemRow::getAccountId, ItemRow::getCustomerName,
|
||||
(left, right) -> left, java.util.LinkedHashMap::new))
|
||||
.entrySet().stream().map(entry -> new Customer(entry.getKey(), entry.getValue())).toList();
|
||||
return new PreloadDetail(preload(row, customers), items(items), histories(row.getPlanId()));
|
||||
}
|
||||
|
||||
public ConfirmedDetail confirmedDetail(AggregateRow row) {
|
||||
return new ConfirmedDetail(confirmed(row), items(mapper.selectItems(row.getPlanId())),
|
||||
histories(row.getPlanId()));
|
||||
}
|
||||
|
||||
public HistoryRow history(String operationId) {
|
||||
return mapper.selectHistoryByOperation(operationId);
|
||||
}
|
||||
|
||||
public int updateTransport(long planId, int expectedVersion, String containerNo, String voyageNo,
|
||||
long operatorId, String operatorName) {
|
||||
if (mapper.touchEditable(planId, expectedVersion, operatorId, operatorName) != 1) {
|
||||
return 0;
|
||||
}
|
||||
return mapper.updateTransport(planId, containerNo, voyageNo, operatorId, operatorName);
|
||||
}
|
||||
|
||||
public int advance(long planId, String fromStatus, String toStatus, int expectedVersion,
|
||||
long operatorId, String operatorName) {
|
||||
return mapper.advance(planId, fromStatus, toStatus, expectedVersion, operatorId, operatorName);
|
||||
}
|
||||
|
||||
public void appendHistory(long planId, String operationId, String actionType,
|
||||
String fromStatus, String toStatus, String beforeSnapshot,
|
||||
String afterSnapshot, long operatorId, String operatorName,
|
||||
String operationReason) {
|
||||
mapper.insertHistory(new HistoryWrite(IdWorker.getId(), planId, operationId, actionType,
|
||||
fromStatus, toStatus, beforeSnapshot, afterSnapshot, operatorId, operatorName,
|
||||
operationReason));
|
||||
}
|
||||
|
||||
private Map<Long, List<Customer>> customerMap(List<AggregateRow> rows) {
|
||||
List<Long> ids = rows.stream().map(AggregateRow::getPlanId).toList();
|
||||
if (ids.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
return mapper.selectCustomers(ids).stream().collect(Collectors.groupingBy(
|
||||
CustomerRow::getPlanId, java.util.LinkedHashMap::new,
|
||||
Collectors.mapping(row -> new Customer(row.getAccountId(), row.getCustomerName()),
|
||||
Collectors.toList())));
|
||||
}
|
||||
|
||||
private PreloadSummary preload(AggregateRow row, List<Customer> customers) {
|
||||
return new PreloadSummary(row.getPlanId(), 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()),
|
||||
decimal(row.getTotalGrossWeightKg()), row.getCreatedAt(), row.getVersion());
|
||||
}
|
||||
|
||||
private ConfirmedSummary confirmed(AggregateRow row) {
|
||||
return new ConfirmedSummary(row.getPlanId(), 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(),
|
||||
row.getCreatedBy(), row.getCreatedByName(), row.getCreatedAt(), row.getVersion());
|
||||
}
|
||||
|
||||
private List<Item> items(List<ItemRow> rows) {
|
||||
return rows.stream().map(row -> new Item(row.getItemId(), row.getAccountId(), row.getCustomerName(),
|
||||
row.getInboundId(), row.getInboundDetailId(), row.getInboundCode(), row.getSkuCode(),
|
||||
row.getDestinationWarehouseName(), row.getProductNameZh(), row.getProductNameEn(),
|
||||
row.getProductImageUrl(), integer(row.getPackageCount()), integer(row.getPalletCount()),
|
||||
decimal(row.getGrossWeightKg()), decimal(row.getNetWeightKg()), decimal(row.getVolumeCbm()),
|
||||
decimal(row.getDimensionalWeightKg()), value(row.getProductQuantity()),
|
||||
integer(row.getUnitsPerCarton()), row.getFbaTrackingNo(), row.getShipmentNo(), row.getShippingMark(),
|
||||
row.getInternalRemark(), row.getCustomerRemark(), row.getWarehouseRemark(),
|
||||
integer(row.getLoadSequence()), row.getFrozenAt())).toList();
|
||||
}
|
||||
|
||||
private List<History> histories(long planId) {
|
||||
return mapper.selectHistories(planId).stream().map(row -> new History(row.getHistoryId(), row.getItemId(),
|
||||
row.getActionType(), row.getFromStatus(), row.getToStatus(), row.getBeforeSnapshot(),
|
||||
row.getAfterSnapshot(), row.getOperatorId(), row.getOperatorName(), row.getOperationReason(),
|
||||
row.getOccurredAt())).toList();
|
||||
}
|
||||
|
||||
private static long value(Long value) {
|
||||
return value == null ? 0 : value;
|
||||
}
|
||||
|
||||
private static int integer(Integer value) {
|
||||
return value == null ? 0 : value;
|
||||
}
|
||||
|
||||
private static BigDecimal decimal(BigDecimal value) {
|
||||
return value == null ? BigDecimal.ZERO : value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.baiyue.firstleg.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baiyue.firstleg.entity.LoadingPickItemEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPickHistoryEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPickTaskEntity;
|
||||
import com.baiyue.firstleg.mapper.LoadingPickHistoryMapper;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanItemEntity;
|
||||
import com.baiyue.firstleg.mapper.LoadingPickItemMapper;
|
||||
import com.baiyue.firstleg.mapper.LoadingPickTaskMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** PDA 拣货任务持久化边界。 */
|
||||
@Repository
|
||||
public class LoadingPickRepository {
|
||||
@Resource
|
||||
private LoadingPickTaskMapper tasks;
|
||||
@Resource
|
||||
private LoadingPickItemMapper items;
|
||||
@Resource
|
||||
private LoadingPickHistoryMapper histories;
|
||||
|
||||
public LoadingPickTaskEntity task(long taskId) {
|
||||
return tasks.selectById(taskId);
|
||||
}
|
||||
|
||||
public LoadingPickTaskEntity taskByPlan(long planId) {
|
||||
return tasks.selectOne(new LambdaQueryWrapper<LoadingPickTaskEntity>()
|
||||
.eq(LoadingPickTaskEntity::getLoadingPlanId, planId));
|
||||
}
|
||||
|
||||
public LoadingPickTaskEntity taskByContainer(String containerNo) {
|
||||
return tasks.selectOne(new LambdaQueryWrapper<LoadingPickTaskEntity>()
|
||||
.eq(LoadingPickTaskEntity::getContainerNo, containerNo)
|
||||
.eq(LoadingPickTaskEntity::getTaskStatus, "LOADING"));
|
||||
}
|
||||
|
||||
public LoadingPickItemEntity item(long taskId, long itemId) {
|
||||
return items.selectOne(new LambdaQueryWrapper<LoadingPickItemEntity>()
|
||||
.eq(LoadingPickItemEntity::getPickTaskId, taskId)
|
||||
.eq(LoadingPickItemEntity::getId, itemId));
|
||||
}
|
||||
|
||||
public List<LoadingPickItemEntity> items(long taskId) {
|
||||
return items.selectList(new LambdaQueryWrapper<LoadingPickItemEntity>()
|
||||
.eq(LoadingPickItemEntity::getPickTaskId, taskId)
|
||||
.orderByAsc(LoadingPickItemEntity::getId));
|
||||
}
|
||||
|
||||
public void insert(LoadingPickTaskEntity task) {
|
||||
tasks.insert(task);
|
||||
}
|
||||
|
||||
public void insert(LoadingPickItemEntity item) {
|
||||
items.insert(item);
|
||||
}
|
||||
|
||||
public LoadingPickItemEntity itemByPlanItem(long planItemId) {
|
||||
return items.selectOne(new LambdaQueryWrapper<LoadingPickItemEntity>()
|
||||
.eq(LoadingPickItemEntity::getLoadingPlanItemId, planItemId));
|
||||
}
|
||||
|
||||
public int pick(long itemId, int count, int version) {
|
||||
return items.pick(itemId, count, version);
|
||||
}
|
||||
|
||||
public LoadingPickHistoryEntity history(String operationId) {
|
||||
return histories.selectOne(new LambdaQueryWrapper<LoadingPickHistoryEntity>()
|
||||
.eq(LoadingPickHistoryEntity::getOperationId, operationId));
|
||||
}
|
||||
|
||||
public void insert(LoadingPickHistoryEntity history) {
|
||||
histories.insert(history);
|
||||
}
|
||||
|
||||
public void update(LoadingPickItemEntity item) {
|
||||
items.updateById(item);
|
||||
}
|
||||
|
||||
public void deleteItem(long itemId) {
|
||||
items.deleteById(itemId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.baiyue.firstleg.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate;
|
||||
import com.baiyue.firstleg.entity.ContainerBookingEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanHistoryEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanItemEntity;
|
||||
import com.baiyue.firstleg.mapper.ContainerBookingMapper;
|
||||
import com.baiyue.firstleg.mapper.LoadingPlanHistoryMapper;
|
||||
import com.baiyue.firstleg.mapper.LoadingPlanItemMapper;
|
||||
import com.baiyue.firstleg.mapper.LoadingPlanMapper;
|
||||
import com.baiyue.firstleg.mapper.SmartLoadingCandidateMapper;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 智能装柜聚合的真实 MySQL 持久化边界。 */
|
||||
@Repository
|
||||
@ConditionalOnProperty(name="firstleg.smart-loading.repository.enabled", havingValue="true", matchIfMissing=true)
|
||||
public class SmartLoadingRepository {
|
||||
@Resource private LoadingPlanMapper plans;
|
||||
@Resource private LoadingPlanItemMapper items;
|
||||
@Resource private LoadingPlanHistoryMapper histories;
|
||||
@Resource private ContainerBookingMapper bookings;
|
||||
@Resource private SmartLoadingCandidateMapper candidates;
|
||||
|
||||
public List<Candidate> candidates(long accountId) { return candidates.candidates(accountId); }
|
||||
public Candidate candidate(long accountId, long inboundId, long detailId) {
|
||||
return candidates.candidate(accountId, inboundId, detailId);
|
||||
}
|
||||
public LoadingPlanEntity plan(long planId) { return plans.selectById(planId); }
|
||||
public LoadingPlanEntity planByBooking(long bookingId) {
|
||||
return plans.selectOne(new LambdaQueryWrapper<LoadingPlanEntity>()
|
||||
.eq(LoadingPlanEntity::getContainerBookingId, bookingId));
|
||||
}
|
||||
public ContainerBookingEntity booking(long bookingId) { return bookings.selectById(bookingId); }
|
||||
public List<LoadingPlanItemEntity> items(long planId) {
|
||||
return items.selectList(new LambdaQueryWrapper<LoadingPlanItemEntity>()
|
||||
.eq(LoadingPlanItemEntity::getLoadingPlanId, planId)
|
||||
.eq(LoadingPlanItemEntity::getDeleted, false)
|
||||
.orderByAsc(LoadingPlanItemEntity::getLoadSequence).orderByAsc(LoadingPlanItemEntity::getId));
|
||||
}
|
||||
public LoadingPlanItemEntity item(long planId, long itemId) {
|
||||
return items.selectOne(new LambdaQueryWrapper<LoadingPlanItemEntity>()
|
||||
.eq(LoadingPlanItemEntity::getLoadingPlanId, planId)
|
||||
.eq(LoadingPlanItemEntity::getId, itemId).eq(LoadingPlanItemEntity::getDeleted, false));
|
||||
}
|
||||
public LoadingPlanItemEntity activeItem(long accountId, long detailId) {
|
||||
return items.selectOne(new LambdaQueryWrapper<LoadingPlanItemEntity>()
|
||||
.eq(LoadingPlanItemEntity::getAccountId, accountId)
|
||||
.eq(LoadingPlanItemEntity::getInboundDetailId, detailId)
|
||||
.eq(LoadingPlanItemEntity::getDeleted, false));
|
||||
}
|
||||
public LoadingPlanHistoryEntity operation(String operationId) {
|
||||
return histories.selectOne(new LambdaQueryWrapper<LoadingPlanHistoryEntity>()
|
||||
.eq(LoadingPlanHistoryEntity::getOperationId, operationId));
|
||||
}
|
||||
public void insert(LoadingPlanEntity value) { plans.insert(value); }
|
||||
public void insert(LoadingPlanItemEntity value) { items.insert(value); }
|
||||
public void insert(LoadingPlanHistoryEntity value) { histories.insert(value); }
|
||||
public int update(LoadingPlanItemEntity value) { return items.updateById(value); }
|
||||
public int touch(long planId, int version, long operatorId, String operatorName) {
|
||||
return plans.touchEditing(planId, version, operatorId, operatorName);
|
||||
}
|
||||
public int remove(long planId, long itemId, int version, long operatorId, String operatorName) {
|
||||
return items.removeEditing(planId, itemId, version, operatorId, operatorName);
|
||||
}
|
||||
public int push(long planId, String key, int version, long operatorId, String operatorName) {
|
||||
return plans.pushPreload(planId, key, version, operatorId, operatorName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
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.domain.SmartLoadingStatus;
|
||||
import com.baiyue.firstleg.dto.ContainerDtos.FullUpdateRequest;
|
||||
import com.baiyue.firstleg.dto.ContainerDtos.Query;
|
||||
import com.baiyue.firstleg.dto.ContainerDtos.SaveRequest;
|
||||
import com.baiyue.firstleg.dto.ContainerDtos.TransportUpdateRequest;
|
||||
import com.baiyue.firstleg.entity.ContainerBookingEntity;
|
||||
import com.baiyue.firstleg.repository.ContainerRepository;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Service
|
||||
public class ContainerService {
|
||||
@Resource private ContainerRepository repository;
|
||||
public PageResult<ContainerBookingEntity> page(Query query) {
|
||||
validateRange(query.estimatedSailingFrom(), query.estimatedSailingTo(), "预计船期筛选");
|
||||
return repository.page(normalize(query));
|
||||
}
|
||||
public ContainerBookingEntity detail(long id) { return required(id); }
|
||||
|
||||
@Transactional
|
||||
public ContainerBookingEntity create(long operatorId, String operatorName, SaveRequest request) {
|
||||
validateSchedule(request);
|
||||
ensureUnique(trim(request.containerNo()), request.cabinetNo().trim(), null);
|
||||
ContainerBookingEntity value = new ContainerBookingEntity();
|
||||
value.setId(IdWorker.getId());
|
||||
value.setBookingNo("CB" + value.getId());
|
||||
copy(value, request);
|
||||
value.setCreatedBy(operatorId); value.setCreatedByName(operatorName);
|
||||
value.setUpdatedBy(operatorId); value.setUpdatedByName(operatorName); value.setVersion(0);
|
||||
try { repository.insert(value); } catch (DuplicateKeyException ex) { throw duplicate(); }
|
||||
value.setPlanStatus(SmartLoadingStatus.EDITING.name());
|
||||
return value;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ContainerBookingEntity updateDraft(long id, long operatorId, String operatorName,
|
||||
FullUpdateRequest request) {
|
||||
ContainerBookingEntity current = required(id);
|
||||
requireVersion(current, request.version());
|
||||
if (status(current) != SmartLoadingStatus.EDITING) {
|
||||
throw conflict("货柜已进入预装流程,只能修改集装箱号和航次");
|
||||
}
|
||||
validateSchedule(request.data());
|
||||
ensureUnique(trim(request.data().containerNo()), request.data().cabinetNo().trim(), id);
|
||||
copy(current, request.data());
|
||||
try {
|
||||
if (repository.updateDraft(current, request.version(), operatorId, operatorName) != 1) {
|
||||
throw conflict("货柜状态或版本已变化,请刷新后重试");
|
||||
}
|
||||
} catch (DuplicateKeyException ex) { throw duplicate(); }
|
||||
return required(id);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ContainerBookingEntity updateTransport(long id, long operatorId, String operatorName,
|
||||
TransportUpdateRequest request) {
|
||||
ContainerBookingEntity current = required(id);
|
||||
requireVersion(current, request.version());
|
||||
if (status(current) == SmartLoadingStatus.COMPLETED) throw conflict("装柜完成后货柜全部只读,不能修改");
|
||||
String containerNo = trim(request.containerNo());
|
||||
if (repository.containerNoExists(containerNo, id)) throw conflict("集装箱号已存在");
|
||||
try {
|
||||
if (repository.updateTransport(id, containerNo, trim(request.voyageNo()), request.version(),
|
||||
operatorId, operatorName) != 1) throw conflict("货柜状态或版本已变化,请刷新后重试");
|
||||
} catch (DuplicateKeyException ex) { throw conflict("集装箱号已存在"); }
|
||||
return required(id);
|
||||
}
|
||||
|
||||
private ContainerBookingEntity required(long id) {
|
||||
ContainerBookingEntity value = repository.find(id);
|
||||
if (value == null) throw new BizException(ResultCode.NOT_FOUND, "货柜不存在");
|
||||
return value;
|
||||
}
|
||||
private void ensureUnique(String containerNo, String cabinetNo, Long excludedId) {
|
||||
if (repository.containerNoExists(containerNo, excludedId)) throw conflict("集装箱号已存在");
|
||||
if (repository.cabinetNoExists(cabinetNo, excludedId)) throw conflict("内部柜号已存在");
|
||||
}
|
||||
private static void requireVersion(ContainerBookingEntity value, Integer version) {
|
||||
if (!version.equals(value.getVersion())) throw conflict("货柜已被其他人修改,请刷新后重试");
|
||||
}
|
||||
private static SmartLoadingStatus status(ContainerBookingEntity value) {
|
||||
try { return SmartLoadingStatus.valueOf(value.getPlanStatus()); }
|
||||
catch (RuntimeException ex) { throw conflict("装柜计划状态无效,不能修改货柜"); }
|
||||
}
|
||||
private static void copy(ContainerBookingEntity v, SaveRequest r) {
|
||||
v.setContainerNo(trim(r.containerNo())); v.setCabinetNo(r.cabinetNo().trim());
|
||||
v.setWarehouseId(r.warehouseId()); v.setWarehouseName(r.warehouseName().trim());
|
||||
v.setBillOfLadingNo(trim(r.billOfLadingNo())); v.setVesselName(trim(r.vesselName()));
|
||||
v.setVoyageNo(trim(r.voyageNo())); v.setContainerType(r.containerType().trim());
|
||||
v.setShippingCompanyId(r.shippingCompanyId()); v.setShippingCompanyName(trim(r.shippingCompanyName()));
|
||||
v.setDeparturePortCode(trim(r.departurePortCode())); v.setDeparturePortName(trim(r.departurePortName()));
|
||||
v.setDestinationPortCode(trim(r.destinationPortCode())); v.setDestinationPortName(trim(r.destinationPortName()));
|
||||
v.setDestinationWarehouseCode(trim(r.destinationWarehouseCode()));
|
||||
v.setDestinationWarehouseName(trim(r.destinationWarehouseName()));
|
||||
v.setCapacityCbm(r.capacityCbm()); v.setMaxWeightKg(r.maxWeightKg());
|
||||
v.setTerminalTruckingCompany(trim(r.terminalTruckingCompany())); v.setLoadingAt(r.loadingAt());
|
||||
v.setEstimatedSailingAt(r.estimatedSailingAt()); v.setActualSailingAt(r.actualSailingAt());
|
||||
v.setEstimatedPortArrivalAt(r.estimatedPortArrivalAt()); v.setActualPortArrivalAt(r.actualPortArrivalAt());
|
||||
v.setEstimatedWarehouseArrivalAt(r.estimatedWarehouseArrivalAt());
|
||||
v.setActualWarehouseArrivalAt(r.actualWarehouseArrivalAt());
|
||||
}
|
||||
private static void validateSchedule(SaveRequest r) {
|
||||
validateRange(r.estimatedSailingAt(), r.estimatedPortArrivalAt(), "预计船期和预计到港时间");
|
||||
validateRange(r.estimatedPortArrivalAt(), r.estimatedWarehouseArrivalAt(), "预计到港和预计到库时间");
|
||||
validateRange(r.actualSailingAt(), r.actualPortArrivalAt(), "实际船期和实际到港时间");
|
||||
validateRange(r.actualPortArrivalAt(), r.actualWarehouseArrivalAt(), "实际到港和实际到库时间");
|
||||
}
|
||||
private static void validateRange(LocalDateTime from, LocalDateTime to, String label) {
|
||||
if (from != null && to != null && to.isBefore(from))
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, label + "先后顺序不正确");
|
||||
}
|
||||
private static Query normalize(Query q) {
|
||||
return new Query(trim(q.containerNo()), trim(q.cabinetNo()), trim(q.billOfLadingNo()), trim(q.keyword()),
|
||||
q.warehouseId(), trim(q.containerType()), q.shippingCompanyId(), trim(q.departurePortCode()),
|
||||
trim(q.destinationPortCode()), trim(q.destinationWarehouseCode()), q.status(), q.estimatedSailingFrom(),
|
||||
q.estimatedSailingTo(), q.page(), q.size());
|
||||
}
|
||||
private static String trim(String value) {
|
||||
if (value == null) return null;
|
||||
String result = value.trim(); return result.isEmpty() ? null : result;
|
||||
}
|
||||
private static BizException duplicate() { return conflict("集装箱号或内部柜号已存在"); }
|
||||
private static BizException conflict(String message) { return new BizException(ResultCode.CONFLICT, message); }
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.audit.BusinessAudit;
|
||||
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.dto.LoadingListDtos.AdvanceRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.ConfirmedDetail;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.ConfirmedQuery;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.ConfirmedSummary;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.PreloadDetail;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.PreloadQuery;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.PreloadSummary;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.TransportUpdateRequest;
|
||||
import com.baiyue.firstleg.mapper.LoadingListQueryMapper.AggregateRow;
|
||||
import com.baiyue.firstleg.mapper.LoadingListQueryMapper.HistoryRow;
|
||||
import com.baiyue.firstleg.repository.LoadingListRepository;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.HexFormat;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
public class LoadingListService {
|
||||
private static final Set<String> LOADING_STATUSES = Set.of("LOADING");
|
||||
@Resource
|
||||
private LoadingListRepository repository;
|
||||
@Resource
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
public PageResult<PreloadSummary> preloadPage(PreloadQuery query) {
|
||||
validateRange(query.createdFrom(), query.createdTo());
|
||||
String status = trim(query.status());
|
||||
if (StringUtils.hasText(status) && !LOADING_STATUSES.contains(status)) {
|
||||
throw badRequest("装柜清单状态只能是 LOADING");
|
||||
}
|
||||
return repository.preloadPage(new PreloadQuery(trim(query.keyword()), status, query.accountId(),
|
||||
query.warehouseId(), query.createdFrom(), query.createdTo(), query.page(), query.size()));
|
||||
}
|
||||
|
||||
public PageResult<ConfirmedSummary> confirmedPage(ConfirmedQuery query) {
|
||||
validateRange(query.createdFrom(), query.createdTo());
|
||||
return repository.confirmedPage(new ConfirmedQuery(trim(query.keyword()), query.accountId(),
|
||||
query.warehouseId(), query.createdFrom(), query.createdTo(), query.page(), query.size()));
|
||||
}
|
||||
|
||||
public PreloadDetail preloadDetail(long planId) {
|
||||
AggregateRow row = requirePlan(planId);
|
||||
if (!LOADING_STATUSES.contains(row.getPlanStatus())) {
|
||||
throw notFound("装柜中清单不存在");
|
||||
}
|
||||
return repository.preloadDetail(row);
|
||||
}
|
||||
|
||||
public ConfirmedDetail confirmedDetail(long planId) {
|
||||
AggregateRow row = requirePlan(planId);
|
||||
if (!"COMPLETED".equals(row.getPlanStatus())) {
|
||||
throw notFound("装柜清单不存在");
|
||||
}
|
||||
return repository.confirmedDetail(row);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "PRELOAD_TRANSPORT_UPDATE", resourceType = "LOADING_PLAN",
|
||||
resourceId = "#planId", description = "修改预装集装箱号与航次")
|
||||
public PreloadDetail updateTransport(long planId, long operatorId, String operatorName,
|
||||
String idempotencyKey, TransportUpdateRequest request) {
|
||||
String containerNo = nullableTrim(request.containerNo());
|
||||
String voyageNo = nullableTrim(request.voyageNo());
|
||||
String requestHash = sha256("TRANSPORT|" + planId + '|' + request.expectedVersion()
|
||||
+ '|' + containerNo + '|' + voyageNo + '|' + trim(request.operationReason()));
|
||||
String operationId = operationId("TRANSPORT", planId, idempotencyKey);
|
||||
HistoryRow replay = repository.history(operationId);
|
||||
if (replay != null) {
|
||||
requireSameRequest(replay, requestHash);
|
||||
return repository.preloadDetail(requirePlan(planId));
|
||||
}
|
||||
|
||||
AggregateRow before = requirePlan(planId);
|
||||
requireLoadingMutable(before);
|
||||
if (repository.updateTransport(planId, request.expectedVersion(), containerNo, voyageNo,
|
||||
operatorId, trim(operatorName)) != 1) {
|
||||
throw conflict("预装计划已被其他操作修改,请刷新后重试");
|
||||
}
|
||||
repository.appendHistory(planId, operationId, "UPDATE_PRELOAD_TRANSPORT",
|
||||
before.getPlanStatus(), before.getPlanStatus(), json(snapshot(before)),
|
||||
json(values("requestHash", requestHash, "status", before.getPlanStatus(),
|
||||
"containerNo", containerNo, "voyageNo", voyageNo,
|
||||
"version", request.expectedVersion() + 1)), operatorId, trim(operatorName),
|
||||
trim(request.operationReason()));
|
||||
return repository.preloadDetail(requirePlan(planId));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "PRELOAD_COMPLETE", resourceType = "LOADING_PLAN",
|
||||
resourceId = "#planId", description = "完成预装")
|
||||
public ConfirmedDetail complete(long planId, long operatorId, String operatorName,
|
||||
String idempotencyKey, AdvanceRequest request) {
|
||||
advance(planId, operatorId, operatorName, idempotencyKey, request,
|
||||
"LOADING", "COMPLETED");
|
||||
return repository.confirmedDetail(requirePlan(planId));
|
||||
}
|
||||
|
||||
private void advance(long planId, long operatorId, String operatorName, String idempotencyKey,
|
||||
AdvanceRequest request, String fromStatus, String toStatus) {
|
||||
String requestHash = sha256(toStatus + '|' + planId + '|' + request.expectedVersion()
|
||||
+ '|' + trim(request.operationReason()));
|
||||
String operationId = operationId(toStatus, planId, idempotencyKey);
|
||||
HistoryRow replay = repository.history(operationId);
|
||||
if (replay != null) {
|
||||
requireSameRequest(replay, requestHash);
|
||||
return;
|
||||
}
|
||||
|
||||
AggregateRow before = requirePlan(planId);
|
||||
if (!fromStatus.equals(before.getPlanStatus())) {
|
||||
throw conflict("当前状态为 " + before.getPlanStatus() + ",不能推进到 " + toStatus);
|
||||
}
|
||||
if (repository.advance(planId, fromStatus, toStatus, request.expectedVersion(),
|
||||
operatorId, trim(operatorName)) != 1) {
|
||||
throw conflict("预装计划已被其他操作修改,请刷新后重试");
|
||||
}
|
||||
repository.appendHistory(planId, operationId, "ADVANCE_PRELOAD_STATUS",
|
||||
fromStatus, toStatus, json(snapshot(before)),
|
||||
json(values("requestHash", requestHash, "status", toStatus,
|
||||
"version", request.expectedVersion() + 1)), operatorId, trim(operatorName),
|
||||
trim(request.operationReason()));
|
||||
}
|
||||
|
||||
private AggregateRow requirePlan(long planId) {
|
||||
AggregateRow row = repository.aggregate(planId);
|
||||
if (row == null) {
|
||||
throw notFound("装柜计划不存在");
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
private static void requireLoadingMutable(AggregateRow row) {
|
||||
if (!LOADING_STATUSES.contains(row.getPlanStatus())) {
|
||||
throw conflict("仅装柜中可修改集装箱号与航次");
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateRange(java.time.LocalDateTime from, java.time.LocalDateTime to) {
|
||||
if (from != null && to != null && from.isAfter(to)) {
|
||||
throw badRequest("创建开始时间不能晚于结束时间");
|
||||
}
|
||||
}
|
||||
|
||||
private String operationId(String operation, long planId, String key) {
|
||||
String normalized = trim(key);
|
||||
if (!StringUtils.hasText(normalized)) {
|
||||
throw badRequest("必须提供 Idempotency-Key");
|
||||
}
|
||||
if (normalized.length() > 128) {
|
||||
throw badRequest("Idempotency-Key 长度不能超过128");
|
||||
}
|
||||
return "PL-" + sha256(operation + '|' + planId + '|' + normalized);
|
||||
}
|
||||
|
||||
private static void requireSameRequest(HistoryRow replay, String requestHash) {
|
||||
String snapshot = replay.getAfterSnapshot();
|
||||
if (snapshot == null || !snapshot.contains("\"requestHash\":\"" + requestHash + "\"")) {
|
||||
throw conflict("Idempotency-Key 已用于不同请求");
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> snapshot(AggregateRow row) {
|
||||
return values("status", row.getPlanStatus(), "containerNo", row.getContainerNo(),
|
||||
"voyageNo", row.getVoyageNo(), "version", row.getVersion());
|
||||
}
|
||||
|
||||
private static Map<String, Object> values(Object... values) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
for (int index = 0; index < values.length; index += 2) {
|
||||
result.put((String) values[index], values[index + 1]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private String json(Object value) {
|
||||
try {
|
||||
return objectMapper.writeValueAsString(value);
|
||||
} catch (JsonProcessingException exception) {
|
||||
throw new IllegalStateException("无法生成装柜审计快照", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static String sha256(String value) {
|
||||
try {
|
||||
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
|
||||
.digest(value.getBytes(StandardCharsets.UTF_8)));
|
||||
} catch (NoSuchAlgorithmException impossible) {
|
||||
throw new IllegalStateException("JVM 不支持 SHA-256", impossible);
|
||||
}
|
||||
}
|
||||
|
||||
private static String nullableTrim(String value) {
|
||||
String trimmed = trim(value);
|
||||
return StringUtils.hasText(trimmed) ? trimmed : null;
|
||||
}
|
||||
|
||||
private static String trim(String value) {
|
||||
return value == null ? null : value.trim();
|
||||
}
|
||||
|
||||
private static BizException badRequest(String message) {
|
||||
return new BizException(ResultCode.BAD_REQUEST, message);
|
||||
}
|
||||
|
||||
private static BizException notFound(String message) {
|
||||
return new BizException(ResultCode.NOT_FOUND, message);
|
||||
}
|
||||
|
||||
private static BizException conflict(String message) {
|
||||
return new BizException(ResultCode.CONFLICT, message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.PickItem;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.PickRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingPickDtos.Task;
|
||||
import com.baiyue.firstleg.entity.LoadingPickHistoryEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPickItemEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPickTaskEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanItemEntity;
|
||||
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.List;
|
||||
|
||||
/** 装柜中 PDA 拣货任务及明细同步。 */
|
||||
@Service
|
||||
public class LoadingPickService {
|
||||
@Resource
|
||||
private LoadingPickRepository repository;
|
||||
|
||||
@Transactional
|
||||
public void begin(long planId, String containerNo, long operatorId, String operatorName,
|
||||
List<LoadingPlanItemEntity> planItems) {
|
||||
LoadingPickTaskEntity task = repository.taskByPlan(planId);
|
||||
if (task == null) {
|
||||
task = new LoadingPickTaskEntity();
|
||||
task.setLoadingPlanId(planId);
|
||||
task.setContainerNo(containerNo);
|
||||
task.setTaskStatus("LOADING");
|
||||
task.setStartedBy(operatorId);
|
||||
task.setStartedByName(operatorName);
|
||||
task.setVersion(0);
|
||||
try {
|
||||
repository.insert(task);
|
||||
} catch (DuplicateKeyException exception) {
|
||||
task = repository.taskByPlan(planId);
|
||||
}
|
||||
}
|
||||
if (task == null || !"LOADING".equals(task.getTaskStatus())) {
|
||||
throw conflict("装柜任务已完成,不能继续同步明细");
|
||||
}
|
||||
for (LoadingPlanItemEntity planItem : planItems) {
|
||||
syncItem(task.getId(), planItem);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void sync(long planId, LoadingPlanItemEntity planItem) {
|
||||
LoadingPickTaskEntity task = repository.taskByPlan(planId);
|
||||
if (task != null && "LOADING".equals(task.getTaskStatus())) {
|
||||
syncItem(task.getId(), planItem);
|
||||
}
|
||||
}
|
||||
|
||||
public Task task(String containerNo) {
|
||||
if (!StringUtils.hasText(containerNo)) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "集装箱号不能为空");
|
||||
}
|
||||
LoadingPickTaskEntity task = repository.taskByContainer(containerNo.trim());
|
||||
if (task == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "未找到装柜中的PDA拣货任务");
|
||||
}
|
||||
return view(task);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Task pick(long taskId, long itemId, PickRequest request, long operatorId, String operatorName) {
|
||||
String operationId = request.operationId().trim();
|
||||
String requestHash = hash(taskId + "|" + itemId + "|" + request.pickedPackageCount());
|
||||
LoadingPickHistoryEntity replay = repository.history(operationId);
|
||||
if (replay != null) {
|
||||
if (!replay.getPickTaskId().equals(taskId) || !replay.getPickItemId().equals(itemId)
|
||||
|| !replay.getRequestHash().equals(requestHash)) {
|
||||
throw conflict("operationId 已用于不同拣货请求");
|
||||
}
|
||||
return view(requiredTask(taskId));
|
||||
}
|
||||
LoadingPickTaskEntity task = requiredTask(taskId);
|
||||
if (!"LOADING".equals(task.getTaskStatus())) {
|
||||
throw conflict("装柜任务已完成,不能继续拣货");
|
||||
}
|
||||
LoadingPickItemEntity item = repository.item(taskId, itemId);
|
||||
if (item == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "PDA拣货明细不存在");
|
||||
}
|
||||
if (repository.pick(itemId, request.pickedPackageCount(), request.version()) != 1) {
|
||||
throw conflict("拣货数量超过计划箱数或明细版本已变化");
|
||||
}
|
||||
LoadingPickHistoryEntity history = new LoadingPickHistoryEntity();
|
||||
history.setPickTaskId(taskId);
|
||||
history.setPickItemId(itemId);
|
||||
history.setOperationId(operationId);
|
||||
history.setRequestHash(requestHash);
|
||||
history.setPickedPackageCount(request.pickedPackageCount());
|
||||
history.setOperatorId(operatorId);
|
||||
history.setOperatorName(operatorName);
|
||||
repository.insert(history);
|
||||
return view(requiredTask(taskId));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void remove(long planItemId) {
|
||||
LoadingPickItemEntity item = repository.itemByPlanItem(planItemId);
|
||||
if (item == null) {
|
||||
return;
|
||||
}
|
||||
if (item.getPickedPackageCount() != null && item.getPickedPackageCount() > 0) {
|
||||
throw conflict("已有拣货记录的明细不能删除");
|
||||
}
|
||||
repository.deleteItem(item.getId());
|
||||
}
|
||||
|
||||
private void syncItem(long taskId, LoadingPlanItemEntity planItem) {
|
||||
LoadingPickItemEntity item = repository.itemByPlanItem(planItem.getId());
|
||||
if (item == null) {
|
||||
item = new LoadingPickItemEntity();
|
||||
item.setPickTaskId(taskId);
|
||||
item.setLoadingPlanItemId(planItem.getId());
|
||||
item.setInboundId(planItem.getInboundId());
|
||||
item.setInboundDetailId(planItem.getInboundDetailId());
|
||||
item.setPlannedPackageCount(planItem.getPackageCount());
|
||||
item.setPickedPackageCount(0);
|
||||
item.setItemStatus("PENDING");
|
||||
item.setVersion(0);
|
||||
repository.insert(item);
|
||||
return;
|
||||
}
|
||||
if (item.getPickedPackageCount() > planItem.getPackageCount()) {
|
||||
throw conflict("已拣货箱数不能低于计划箱数");
|
||||
}
|
||||
item.setPlannedPackageCount(planItem.getPackageCount());
|
||||
item.setItemStatus(item.getPickedPackageCount().equals(planItem.getPackageCount()) ? "PICKED" : "PICKING");
|
||||
repository.update(item);
|
||||
}
|
||||
|
||||
private LoadingPickTaskEntity requiredTask(long taskId) {
|
||||
LoadingPickTaskEntity task = repository.task(taskId);
|
||||
if (task == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "PDA拣货任务不存在");
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
private Task view(LoadingPickTaskEntity task) {
|
||||
List<PickItem> items = repository.items(task.getId()).stream()
|
||||
.map(item -> new PickItem(item.getId(), item.getLoadingPlanItemId(), item.getInboundId(),
|
||||
item.getInboundDetailId(), item.getPlannedPackageCount(), item.getPickedPackageCount(),
|
||||
item.getItemStatus(), item.getVersion())).toList();
|
||||
return new Task(task.getId(), task.getLoadingPlanId(), task.getContainerNo(), task.getTaskStatus(),
|
||||
task.getVersion(), items);
|
||||
}
|
||||
|
||||
private static String hash(String value) {
|
||||
try {
|
||||
return java.util.HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
|
||||
.digest(value.getBytes(StandardCharsets.UTF_8)));
|
||||
} catch (NoSuchAlgorithmException exception) {
|
||||
throw new IllegalStateException("JVM 不支持 SHA-256", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static BizException conflict(String message) {
|
||||
return new BizException(ResultCode.CONFLICT, message);
|
||||
}
|
||||
}
|
||||
+10
-2
@@ -45,15 +45,16 @@ public class MeasurementLocationOccupationService {
|
||||
properties.getDefaultPalletCapacityCbm(), "STANDARD",
|
||||
LocalDateTime.now().plus(properties.getReservationTtl()), true))),
|
||||
"测量完成后库位预占失败");
|
||||
int packageCount = requirePackageCount(order);
|
||||
allocations.replace(tenantId, inboundId, warehouseId, resourceTenantId,
|
||||
properties.getDefaultPalletCapacityCbm(), reserved);
|
||||
properties.getDefaultPalletCapacityCbm(), packageCount, reserved);
|
||||
try {
|
||||
WarehousePalletLocationClient.BatchResult occupied = requireSuccess(call(() -> warehouse.confirm(
|
||||
internalToken, new WarehousePalletLocationClient.ConfirmRequest(resourceTenantId, warehouseId,
|
||||
SOURCE_SERVICE, inboundId, operatorId, volume,
|
||||
properties.getDefaultPalletCapacityCbm()))), "测量完成后库位占用失败");
|
||||
allocations.replace(tenantId, inboundId, warehouseId, resourceTenantId,
|
||||
properties.getDefaultPalletCapacityCbm(), occupied);
|
||||
properties.getDefaultPalletCapacityCbm(), packageCount, occupied);
|
||||
} catch (RuntimeException failure) {
|
||||
try {
|
||||
warehouse.release(internalToken, new WarehousePalletLocationClient.ReleaseRequest(
|
||||
@@ -85,6 +86,13 @@ public class MeasurementLocationOccupationService {
|
||||
return order.getActualVolumeCbm();
|
||||
}
|
||||
|
||||
private static int requirePackageCount(InboundOrderEntity order) {
|
||||
if (order.getTotalPackageCount() == null || order.getTotalPackageCount() <= 0) {
|
||||
throw new BizException(ResultCode.CONFLICT, "入库单缺少有效总箱数");
|
||||
}
|
||||
return order.getTotalPackageCount();
|
||||
}
|
||||
|
||||
private static <T> ApiResponse<T> call(RemoteCall<T> call) {
|
||||
try { return call.invoke(); }
|
||||
catch (FeignException exception) {
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.common.audit.BusinessAudit;
|
||||
import com.baiyue.firstleg.dto.SmartLoadingDtos.*;
|
||||
import com.baiyue.firstleg.client.OrderCargoClient;
|
||||
import com.baiyue.firstleg.entity.*;
|
||||
import com.baiyue.firstleg.repository.SmartLoadingRepository;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import feign.FeignException;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
|
||||
/** 智能装柜的候选、推荐/人工分配、删除和不可逆推送编排。 */
|
||||
@Service
|
||||
public class SmartLoadingService {
|
||||
@Resource private SmartLoadingRepository repository;
|
||||
@Resource private OrderCargoClient orderCargoClient;
|
||||
@Resource private LoadingPickService loadingPickService;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
@Value("${baiyue.security.internal-service-token:}") private String internalToken;
|
||||
public SmartLoadingService() { }
|
||||
|
||||
public List<Candidate> candidates(long accountId) {
|
||||
requireAccount(accountId);
|
||||
return repository.candidates(accountId);
|
||||
}
|
||||
|
||||
public Plan draftByBooking(long bookingId) {
|
||||
ContainerBookingEntity booking = requireBooking(bookingId);
|
||||
LoadingPlanEntity plan = repository.planByBooking(bookingId);
|
||||
return draftView(plan, booking);
|
||||
}
|
||||
|
||||
public Plan detail(long planId) {
|
||||
LoadingPlanEntity plan = repository.plan(planId);
|
||||
if (plan == null) throw new BizException(ResultCode.NOT_FOUND, "装柜计划不存在");
|
||||
return draftView(plan, requireBooking(plan.getContainerBookingId()));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "SMART_LOADING_PLAN_CREATE", resourceType = "LOADING_PLAN",
|
||||
description = "创建智能装柜计划", captureBefore = false)
|
||||
public Plan create(long bookingId, String remark, long operatorId, String operatorName) {
|
||||
ContainerBookingEntity booking = requireBooking(bookingId);
|
||||
LoadingPlanEntity existing = repository.planByBooking(bookingId);
|
||||
if (existing != null) return draftView(existing, booking);
|
||||
LoadingPlanEntity plan = new LoadingPlanEntity();
|
||||
plan.setPlanNo("LP-" + UUID.randomUUID().toString().replace("-", "").substring(0, 20));
|
||||
plan.setContainerBookingId(bookingId); plan.setPlanStatus("EDITING"); plan.setRemark(trim(remark));
|
||||
plan.setCreatedBy(operatorId); plan.setCreatedByName(operatorName); plan.setUpdatedBy(operatorId);
|
||||
plan.setUpdatedByName(operatorName); plan.setVersion(0);
|
||||
try {
|
||||
repository.insert(plan);
|
||||
} catch (DuplicateKeyException exception) {
|
||||
LoadingPlanEntity concurrent = repository.planByBooking(bookingId);
|
||||
if (concurrent != null) return draftView(concurrent, booking);
|
||||
throw conflict("装柜计划创建冲突,请刷新后重试");
|
||||
}
|
||||
history(plan.getId(), null, "CREATE_PLAN", null, "EDITING", UUID.randomUUID().toString(), operatorId, operatorName);
|
||||
return view(plan, booking);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "SMART_LOADING_ITEM_ASSIGN", resourceType = "LOADING_PLAN",
|
||||
resourceId = "#planId", description = "分配智能装柜明细")
|
||||
public Plan assign(long planId, AssignRequest request, long operatorId, String operatorName) {
|
||||
String operation = operation(request.operationId());
|
||||
String requestHash = sha256("ASSIGN|" + planId + '|' + request.accountId() + '|'
|
||||
+ request.inboundId() + '|' + request.inboundDetailId() + '|' + request.packageCount()
|
||||
+ '|' + request.expectedVersion());
|
||||
Plan replay = replay(operation, planId, "ADD_ITEM", null, requestHash);
|
||||
if (replay != null) return replay;
|
||||
LoadingPlanEntity plan = editing(planId, request.expectedVersion());
|
||||
Candidate candidate = repository.candidate(request.accountId(), request.inboundId(), request.inboundDetailId());
|
||||
if (candidate == null) throw conflict("入库货物不存在或尚未完成入库");
|
||||
if (!Objects.equals(candidate.accountId(), request.accountId())) throw forbidden();
|
||||
OrderCargoClient.CargoBinding cargo = loadCargo(candidate);
|
||||
LoadingPlanItemEntity existing = repository.activeItem(request.accountId(), request.inboundDetailId());
|
||||
if (existing != null && !Objects.equals(existing.getLoadingPlanId(), planId)) throw conflict("货物明细已被有效计划锁定");
|
||||
int total = request.packageCount() + (existing == null ? 0 : existing.getPackageCount());
|
||||
if (total > candidate.availablePackageCount()) throw validation("累计分配箱数超过可用箱数");
|
||||
ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId());
|
||||
List<LoadingPlanItemEntity> lines = new ArrayList<>(repository.items(planId));
|
||||
if (existing != null) lines.removeIf(x -> Objects.equals(x.getId(), existing.getId()));
|
||||
checkRouteAndCapacity(booking, lines, candidate, total);
|
||||
touch(plan, operatorId, operatorName);
|
||||
LoadingPlanItemEntity changed;
|
||||
if (existing == null) {
|
||||
LoadingPlanItemEntity item = snapshot(planId, candidate, cargo, request.packageCount(), operatorId, operatorName, lines.size() + 1);
|
||||
try { repository.insert(item); } catch (DuplicateKeyException e) { throw conflict("货物明细已被并发锁定"); }
|
||||
changed = item;
|
||||
} else {
|
||||
existing.setPackageCount(total); existing.setGrossWeightKg(unit(candidate.unitWeightKg()).multiply(BigDecimal.valueOf(total)));
|
||||
existing.setVolumeCbm(unit(candidate.unitVolumeCbm()).multiply(BigDecimal.valueOf(total))); applyCargo(existing, cargo, total);
|
||||
existing.setUpdatedBy(operatorId); existing.setUpdatedByName(operatorName);
|
||||
if (repository.update(existing) != 1) throw conflict("装柜草稿已被并发修改");
|
||||
changed = existing;
|
||||
}
|
||||
history(planId, changed.getId(), "ADD_ITEM", "EDITING", "EDITING", operation,
|
||||
json(values("version", request.expectedVersion())),
|
||||
json(values("requestHash", requestHash, "itemId", changed.getId(),
|
||||
"packageCount", changed.getPackageCount())), operatorId, operatorName);
|
||||
loadingPickService.sync(planId, changed);
|
||||
return view(repository.plan(planId), booking);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "SMART_LOADING_RECOMMEND", resourceType = "LOADING_PLAN",
|
||||
resourceId = "#planId", description = "执行智能装柜推荐")
|
||||
public Plan recommend(long planId, RecommendRequest request, long operatorId, String operatorName) {
|
||||
String operation = operation(request.operationId());
|
||||
String requestHash = sha256("RECOMMEND|" + planId + '|' + request.accountId()
|
||||
+ '|' + request.expectedVersion());
|
||||
Plan replay = replay(operation, planId, "SMART_RECOMMEND", null, requestHash);
|
||||
if (replay != null) return replay;
|
||||
LoadingPlanEntity plan = editing(planId, request.expectedVersion());
|
||||
ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId());
|
||||
for (Candidate candidate : candidates(request.accountId())) {
|
||||
if (!Objects.equals(candidate.warehouseId(), booking.getWarehouseId())) continue;
|
||||
if (StringUtils.hasText(booking.getDestinationWarehouseCode())
|
||||
&& !Objects.equals(booking.getDestinationWarehouseCode(), candidate.destinationWarehouseCode())) continue;
|
||||
int count = fittingBoxes(booking, repository.items(planId), candidate);
|
||||
if (count > 0) {
|
||||
plan = repository.plan(planId);
|
||||
assign(planId, new AssignRequest(request.accountId(), candidate.inboundId(), candidate.inboundDetailId(),
|
||||
count, plan.getVersion(), "REC-" + sha256(operation + '|' + candidate.inboundId()
|
||||
+ '|' + candidate.inboundDetailId())), operatorId, operatorName);
|
||||
}
|
||||
}
|
||||
LoadingPlanEntity recommended = repository.plan(planId);
|
||||
history(planId, null, "SMART_RECOMMEND", "EDITING", "EDITING", operation,
|
||||
json(values("version", request.expectedVersion())),
|
||||
json(values("requestHash", requestHash, "version", recommended.getVersion())),
|
||||
operatorId, operatorName);
|
||||
return view(recommended, requireBooking(plan.getContainerBookingId()));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "SMART_LOADING_ITEM_ADJUST", resourceType = "LOADING_PLAN_ITEM",
|
||||
resourceId = "#itemId", description = "调整智能装柜明细")
|
||||
public Plan adjust(long planId, long itemId, AdjustRequest request, long operatorId, String operatorName) {
|
||||
String operation = operation(request.operationId());
|
||||
String requestHash = sha256("ADJUST|" + planId + '|' + itemId + '|' + request.accountId()
|
||||
+ '|' + request.packageCount() + '|' + request.expectedPlanVersion()
|
||||
+ '|' + request.expectedItemVersion());
|
||||
Plan replay = replay(operation, planId, "ADJUST_ITEM", itemId, requestHash);
|
||||
if (replay != null) return replay;
|
||||
LoadingPlanEntity plan = editing(planId, request.expectedPlanVersion());
|
||||
LoadingPlanItemEntity item = repository.item(planId, itemId);
|
||||
if (item == null) throw new BizException(ResultCode.NOT_FOUND, "装柜明细不存在");
|
||||
if (!Objects.equals(item.getAccountId(), request.accountId())) throw forbidden();
|
||||
if (!Objects.equals(item.getVersion(), request.expectedItemVersion())) throw conflict("装柜明细版本冲突");
|
||||
Candidate candidate = repository.candidate(item.getAccountId(), item.getInboundId(), item.getInboundDetailId());
|
||||
if (candidate == null || request.packageCount() > candidate.availablePackageCount()) throw validation("调整箱数超过可用箱数");
|
||||
OrderCargoClient.CargoBinding cargo = loadCargo(candidate);
|
||||
List<LoadingPlanItemEntity> others = new ArrayList<>(repository.items(planId));
|
||||
others.removeIf(x -> Objects.equals(x.getId(), itemId));
|
||||
checkRouteAndCapacity(requireBooking(plan.getContainerBookingId()), others, candidate, request.packageCount());
|
||||
touch(plan, operatorId, operatorName);
|
||||
item.setPackageCount(request.packageCount());
|
||||
item.setGrossWeightKg(unit(candidate.unitWeightKg()).multiply(BigDecimal.valueOf(request.packageCount())));
|
||||
item.setVolumeCbm(unit(candidate.unitVolumeCbm()).multiply(BigDecimal.valueOf(request.packageCount()))); applyCargo(item, cargo, request.packageCount());
|
||||
item.setUpdatedBy(operatorId); item.setUpdatedByName(operatorName);
|
||||
if (repository.update(item) != 1) throw conflict("装柜明细已被并发修改");
|
||||
history(planId, itemId, "ADJUST_ITEM", "EDITING", "EDITING", operation,
|
||||
json(values("version", request.expectedPlanVersion(), "packageCount", item.getPackageCount())),
|
||||
json(values("requestHash", requestHash, "packageCount", request.packageCount())),
|
||||
operatorId, operatorName);
|
||||
loadingPickService.sync(planId, item);
|
||||
return view(repository.plan(planId), requireBooking(plan.getContainerBookingId()));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "SMART_LOADING_ITEM_DELETE", resourceType = "LOADING_PLAN_ITEM",
|
||||
resourceId = "#itemId", description = "删除智能装柜明细")
|
||||
public Plan delete(long planId, long itemId, DeleteRequest request, long operatorId, String operatorName) {
|
||||
String operation = operation(request.operationId());
|
||||
String requestHash = sha256("DELETE|" + planId + '|' + itemId + '|' + request.accountId()
|
||||
+ '|' + request.expectedPlanVersion() + '|' + request.expectedItemVersion());
|
||||
Plan replay = replay(operation, planId, "REMOVE_ITEM", itemId, requestHash);
|
||||
if (replay != null) return replay;
|
||||
LoadingPlanEntity plan = editing(planId, request.expectedPlanVersion());
|
||||
LoadingPlanItemEntity item = repository.item(planId, itemId);
|
||||
if (item == null) throw new BizException(ResultCode.NOT_FOUND, "装柜明细不存在");
|
||||
if (!Objects.equals(item.getAccountId(), request.accountId())) throw forbidden();
|
||||
loadingPickService.remove(itemId);
|
||||
touch(plan, operatorId, operatorName);
|
||||
if (repository.remove(planId, itemId, request.expectedItemVersion(), operatorId, operatorName) != 1) throw conflict("装柜草稿已被并发修改");
|
||||
history(planId, itemId, "REMOVE_ITEM", "EDITING", "EDITING", operation,
|
||||
json(values("version", request.expectedPlanVersion(), "packageCount", item.getPackageCount())),
|
||||
json(values("requestHash", requestHash, "deleted", true)), operatorId, operatorName);
|
||||
return view(repository.plan(planId), requireBooking(plan.getContainerBookingId()));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "SMART_LOADING_PUSH_PRELOAD", resourceType = "LOADING_PLAN",
|
||||
resourceId = "#planId", description = "推送智能装柜计划到预装清单")
|
||||
public Plan pushPreload(long planId, String idempotencyKey, PushRequest request, long operatorId, String operatorName) {
|
||||
String key = trim(idempotencyKey);
|
||||
if (!StringUtils.hasText(key) || key.length() > 128) throw new BizException(ResultCode.BAD_REQUEST, "必须提供有效 Idempotency-Key");
|
||||
LoadingPlanEntity plan = repository.plan(planId); if (plan == null) throw new BizException(ResultCode.NOT_FOUND, "装柜计划不存在");
|
||||
if ("LOADING".equals(plan.getPlanStatus()) && key.equals(plan.getPushIdempotencyKey())) return view(plan, requireBooking(plan.getContainerBookingId()));
|
||||
if (!"EDITING".equals(plan.getPlanStatus()) || !Objects.equals(plan.getVersion(), request.expectedVersion())) throw conflict("仅编辑中的计划可开始装柜且版本必须匹配");
|
||||
ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId());
|
||||
if (!StringUtils.hasText(booking.getContainerNo())) throw validation("开始装柜前必须填写集装箱号");
|
||||
if (repository.items(planId).isEmpty()) throw validation("装柜明细不能为空");
|
||||
loadingPickService.begin(planId, booking.getContainerNo(), operatorId, operatorName, repository.items(planId));
|
||||
try {
|
||||
if (repository.push(planId, key, request.expectedVersion(), operatorId, operatorName) != 1) throw conflict("装柜计划已被并发修改");
|
||||
} catch (DuplicateKeyException e) { throw conflict("幂等键已用于其他推送"); }
|
||||
history(planId, null, "START_LOADING", "EDITING", "LOADING", key, operatorId, operatorName);
|
||||
LoadingPlanEntity pushed = repository.plan(planId);
|
||||
return view(pushed, requireBooking(pushed.getContainerBookingId()));
|
||||
}
|
||||
|
||||
private LoadingPlanItemEntity snapshot(long planId, Candidate c, OrderCargoClient.CargoBinding cargo, int count, long op, String name, int seq) {
|
||||
LoadingPlanItemEntity i = new LoadingPlanItemEntity(); i.setLoadingPlanId(planId); i.setAccountId(c.accountId()); i.setCustomerName(c.customerName());
|
||||
i.setInboundId(c.inboundId()); i.setInboundDetailId(c.inboundDetailId()); i.setOrderId(c.orderId()); i.setOrderNo(c.orderNo()); i.setInboundCode(c.inboundCode());
|
||||
i.setDestinationWarehouseId(c.destinationWarehouseId()); i.setDestinationWarehouseCode(c.destinationWarehouseCode());
|
||||
i.setDestinationWarehouseName(c.destinationWarehouseName()); i.setProductNameZh(cargo.cargoName()); i.setPackageCount(count);
|
||||
i.setGrossWeightKg(unit(c.unitWeightKg()).multiply(BigDecimal.valueOf(count))); i.setVolumeCbm(unit(c.unitVolumeCbm()).multiply(BigDecimal.valueOf(count)));
|
||||
applyCargo(i, cargo, count); i.setLoadSequence(seq); i.setDeleted(false); i.setCreatedBy(op); i.setCreatedByName(name); i.setUpdatedBy(op); i.setUpdatedByName(name); i.setVersion(0); return i;
|
||||
}
|
||||
private void applyCargo(LoadingPlanItemEntity item, OrderCargoClient.CargoBinding cargo, int boxes) {
|
||||
item.setSkuCode(cargo.skuNo());
|
||||
item.setProductNameEn(cargo.cargoNameEn());
|
||||
item.setUnitsPerCarton(cargo.unitsPerPackage());
|
||||
item.setInternalRemark(cargo.cargoRemark());
|
||||
int original = cargo.originalPackageCount() == null || cargo.originalPackageCount() <= 0
|
||||
? boxes : cargo.originalPackageCount();
|
||||
if (cargo.unitsPerPackage() != null) {
|
||||
item.setProductQuantity(Math.multiplyExact(cargo.unitsPerPackage(), boxes));
|
||||
} else if (cargo.totalQuantity() != null) {
|
||||
item.setProductQuantity(BigDecimal.valueOf(cargo.totalQuantity())
|
||||
.multiply(BigDecimal.valueOf(boxes))
|
||||
.divide(BigDecimal.valueOf(original), 0, RoundingMode.HALF_UP).intValueExact());
|
||||
}
|
||||
if (cargo.netWeightKg() != null) {
|
||||
item.setNetWeightKg(cargo.netWeightKg().multiply(BigDecimal.valueOf(boxes))
|
||||
.divide(BigDecimal.valueOf(original), 6, RoundingMode.HALF_UP));
|
||||
}
|
||||
}
|
||||
|
||||
private OrderCargoClient.CargoBinding loadCargo(Candidate candidate) {
|
||||
try {
|
||||
ApiResponse<OrderCargoClient.CargoBinding> response = orderCargoClient.detail(internalToken,
|
||||
candidate.accountId(), candidate.orderId(), candidate.inboundDetailId());
|
||||
if (response == null) {
|
||||
throw unavailable();
|
||||
}
|
||||
if (!response.successful()) {
|
||||
throw new BizException(ResultCode.of(response.code()), response.message());
|
||||
}
|
||||
OrderCargoClient.CargoBinding cargo = response.data();
|
||||
if (cargo == null || !Objects.equals(cargo.cargoId(), candidate.inboundDetailId())
|
||||
|| !Objects.equals(cargo.orderId(), candidate.orderId())) {
|
||||
throw conflict("订单货物明细返回与入库单不匹配");
|
||||
}
|
||||
return cargo;
|
||||
} catch (BizException exception) {
|
||||
throw exception;
|
||||
} catch (FeignException exception) {
|
||||
throw unavailable();
|
||||
} catch (RuntimeException exception) {
|
||||
throw unavailable();
|
||||
}
|
||||
}
|
||||
private void checkRouteAndCapacity(ContainerBookingEntity b,List<LoadingPlanItemEntity> lines,Candidate c,int count){
|
||||
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)));
|
||||
BigDecimal w=lines.stream().map(x->unit(x.getGrossWeightKg())).reduce(BigDecimal.ZERO,BigDecimal::add).add(unit(c.unitWeightKg()).multiply(BigDecimal.valueOf(count)));
|
||||
if (b.getCapacityCbm()==null || b.getMaxWeightKg()==null) throw validation("货柜容量和最大承重未配置");
|
||||
if (v.compareTo(b.getCapacityCbm())>0 || w.compareTo(b.getMaxWeightKg())>0) throw validation("超过货柜容积或重量上限");
|
||||
}
|
||||
private int fittingBoxes(ContainerBookingEntity b,List<LoadingPlanItemEntity> lines,Candidate c){
|
||||
if(b.getCapacityCbm()==null||b.getMaxWeightKg()==null)return 0;
|
||||
BigDecimal usedV=lines.stream().map(x->unit(x.getVolumeCbm())).reduce(BigDecimal.ZERO,BigDecimal::add);
|
||||
BigDecimal usedW=lines.stream().map(x->unit(x.getGrossWeightKg())).reduce(BigDecimal.ZERO,BigDecimal::add);
|
||||
BigDecimal leftV=b.getCapacityCbm().subtract(usedV),leftW=b.getMaxWeightKg().subtract(usedW);
|
||||
int byV=unit(c.unitVolumeCbm()).signum()==0?c.availablePackageCount():leftV.max(BigDecimal.ZERO).divideToIntegralValue(c.unitVolumeCbm()).intValue();
|
||||
int byW=unit(c.unitWeightKg()).signum()==0?c.availablePackageCount():leftW.max(BigDecimal.ZERO).divideToIntegralValue(c.unitWeightKg()).intValue();
|
||||
return Math.max(0,Math.min(c.availablePackageCount(),Math.min(byV,byW)));
|
||||
}
|
||||
private Plan view(LoadingPlanEntity p,ContainerBookingEntity b){return new Plan(p.getId(),p.getPlanNo(),p.getPlanStatus(),p.getContainerBookingId(),b.getContainerNo(),b.getVoyageNo(),p.getVersion(),repository.items(p.getId()));}
|
||||
private Plan draftView(LoadingPlanEntity plan, ContainerBookingEntity booking) {
|
||||
if (plan == null) throw new BizException(ResultCode.NOT_FOUND, "该货柜尚未创建智能装柜草稿");
|
||||
if (!"EDITING".equals(plan.getPlanStatus())) throw conflict("装柜计划已推送预装,请前往预装清单查看");
|
||||
return view(plan, booking);
|
||||
}
|
||||
private LoadingPlanEntity editing(long id,int version){LoadingPlanEntity p=repository.plan(id);if(p==null)throw new BizException(ResultCode.NOT_FOUND,"装柜计划不存在");if(!Set.of("EDITING", "LOADING").contains(p.getPlanStatus())||!Objects.equals(p.getVersion(),version))throw conflict("计划已完成或版本冲突");return p;}
|
||||
private ContainerBookingEntity requireBooking(long id){ContainerBookingEntity b=repository.booking(id);if(b==null)throw new BizException(ResultCode.NOT_FOUND,"货柜预订不存在");return b;}
|
||||
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);
|
||||
if (history == null) return null;
|
||||
if (!Objects.equals(history.getLoadingPlanId(), planId)
|
||||
|| !Objects.equals(history.getActionType(), action)
|
||||
|| itemId != null && !Objects.equals(history.getLoadingPlanItemId(), itemId)
|
||||
|| history.getAfterSnapshot() == null
|
||||
|| !history.getAfterSnapshot().contains("\"requestHash\":\"" + requestHash + "\"")) {
|
||||
throw conflict("operationId 已用于不同请求");
|
||||
}
|
||||
LoadingPlanEntity plan = repository.plan(planId);
|
||||
if (plan == null) throw new BizException(ResultCode.NOT_FOUND, "装柜计划不存在");
|
||||
return view(plan, requireBooking(plan.getContainerBookingId()));
|
||||
}
|
||||
|
||||
private void history(long planId, Long itemId, String action, String from, String to,
|
||||
String operation, long operatorId, String operatorName) {
|
||||
history(planId, itemId, action, from, to, operation, null, null, operatorId, operatorName);
|
||||
}
|
||||
|
||||
private void history(long planId, Long itemId, String action, String from, String to,
|
||||
String operation, String before, String after,
|
||||
long operatorId, String operatorName) {
|
||||
LoadingPlanHistoryEntity history = new LoadingPlanHistoryEntity();
|
||||
history.setLoadingPlanId(planId); history.setLoadingPlanItemId(itemId);
|
||||
history.setActionType(action); history.setFromStatus(from); history.setToStatus(to);
|
||||
history.setOperationId(operation); history.setBeforeSnapshot(before); history.setAfterSnapshot(after);
|
||||
history.setOperatorId(operatorId); history.setOperatorName(operatorName);
|
||||
history.setCreatedAt(LocalDateTime.now()); repository.insert(history);
|
||||
}
|
||||
|
||||
private static String operation(String value) {
|
||||
String normalized = trim(value);
|
||||
if (!StringUtils.hasText(normalized)) throw new BizException(ResultCode.BAD_REQUEST, "必须提供 operationId");
|
||||
if (normalized.length() > 128) throw new BizException(ResultCode.BAD_REQUEST, "operationId 长度不能超过128");
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private String json(Object value) {
|
||||
try {
|
||||
return objectMapper.writeValueAsString(value);
|
||||
} catch (JsonProcessingException exception) {
|
||||
throw new IllegalStateException("无法生成智能装柜审计快照", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, Object> values(Object... values) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
for (int index = 0; index < values.length; index += 2) {
|
||||
result.put((String) values[index], values[index + 1]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String sha256(String value) {
|
||||
try {
|
||||
return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
|
||||
.digest(value.getBytes(StandardCharsets.UTF_8)));
|
||||
} catch (NoSuchAlgorithmException impossible) {
|
||||
throw new IllegalStateException("JVM 不支持 SHA-256", impossible);
|
||||
}
|
||||
}
|
||||
|
||||
private static BizException unavailable() {
|
||||
return new BizException(ResultCode.SERVICE_UNAVAILABLE, "订单货物服务暂不可用");
|
||||
}
|
||||
|
||||
private static String trim(String x){return x==null?null:x.trim();} private static BigDecimal unit(BigDecimal x){return x==null?BigDecimal.ZERO:x.max(BigDecimal.ZERO);}
|
||||
private static void requireAccount(long x){if(x<=0)throw new BizException(ResultCode.BAD_REQUEST,"accountId必须大于零");} private static BizException conflict(String x){return new BizException(ResultCode.CONFLICT,x);} private static BizException validation(String x){return new BizException(ResultCode.VALIDATION_FAILED,x);} private static BizException forbidden(){return new BizException(ResultCode.FORBIDDEN,"客户归属不匹配");}
|
||||
}
|
||||
Reference in New Issue
Block a user