Merge remote-tracking branch 'origin/test' into dev
This commit is contained in:
+59
-22
@@ -6,10 +6,12 @@ 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.terminaldrayage.dto.ContainerWorkbenchDtos.WorkbenchItem;
|
||||
import com.baiyue.firstleg.dto.ContainerDtos.SimulationCreateRequest;
|
||||
import com.baiyue.firstleg.dto.ContainerDtos.OceanBookingLinkRequest;
|
||||
import com.baiyue.firstleg.entity.ContainerBookingEntity;
|
||||
import com.baiyue.firstleg.service.ContainerService;
|
||||
import com.baiyue.firstleg.terminaldrayage.service.ContainerWorkbenchService;
|
||||
import com.baiyue.firstleg.service.FirstlegScopeGuard;
|
||||
import com.baiyue.firstleg.service.LoadingListService;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.BusinessDocument;
|
||||
@@ -39,21 +41,50 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
@RequestMapping("/api/platform/v1/containers")
|
||||
public class PlatformContainerController {
|
||||
@Resource private ContainerService service;
|
||||
@Resource private ContainerWorkbenchService workbenches;
|
||||
@Resource private LoadingListService loadingLists;
|
||||
@Resource private ContainerOverviewService overview;
|
||||
@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("/domestic") @PreAuthorize("hasAuthority('platform:firstleg:container:list')")
|
||||
public ApiResponse<PageResult<WorkbenchItem>> domesticPage(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid Query query) {
|
||||
FirstlegScopeGuard.platform(user); return ApiResponse.success(workbenches.domesticPage(query, user.permissions()));
|
||||
}
|
||||
@GetMapping("/{id}") @PreAuthorize("hasAuthority('platform:firstleg:container:list')")
|
||||
public ApiResponse<ContainerBookingEntity> detail(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long id) {
|
||||
FirstlegScopeGuard.platform(user); return ApiResponse.success(service.detail(id));
|
||||
@GetMapping("/overseas") @PreAuthorize("hasAuthority('platform:firstleg:terminal-drayage:list')")
|
||||
public ApiResponse<PageResult<WorkbenchItem>> overseasPage(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid Query query) {
|
||||
FirstlegScopeGuard.platform(user); return ApiResponse.success(workbenches.overseasPage(query, user.permissions()));
|
||||
}
|
||||
@GetMapping("/domestic/{id}") @PreAuthorize("hasAuthority('platform:firstleg:container:list')")
|
||||
public ApiResponse<WorkbenchItem> domesticDetail(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long id) {
|
||||
FirstlegScopeGuard.platform(user); return ApiResponse.success(workbenches.domesticDetail(id, user.permissions()));
|
||||
}
|
||||
@GetMapping("/overseas/{id}") @PreAuthorize("hasAuthority('platform:firstleg:terminal-drayage:list')")
|
||||
public ApiResponse<WorkbenchItem> overseasDetail(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long id) {
|
||||
FirstlegScopeGuard.platform(user); return ApiResponse.success(workbenches.overseasDetail(id, user.permissions()));
|
||||
}
|
||||
@GetMapping("/{id}/overview") @PreAuthorize("hasAuthority('platform:firstleg:container:list')")
|
||||
public ApiResponse<Overview> overview(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable @Positive long id) {
|
||||
FirstlegScopeGuard.platform(user); return ApiResponse.success(overview.overview(id));
|
||||
FirstlegScopeGuard.platform(user); workbenches.domesticDetail(id, user.permissions());
|
||||
return ApiResponse.success(overview.overview(id));
|
||||
}
|
||||
@GetMapping("/overseas/{id}/overview")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:terminal-drayage:list')")
|
||||
public ApiResponse<Overview> overseasOverview(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long id) {
|
||||
FirstlegScopeGuard.platform(user); service.requireOverseas(id);
|
||||
return ApiResponse.success(overview.overview(id));
|
||||
}
|
||||
@PutMapping("/{id}/pickup-code")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:terminal-drayage:document:edit')")
|
||||
@BusinessAudit(event="CONTAINER_PICKUP_CODE_UPDATE", resourceType="CONTAINER", resourceId="#id", description="填写PU码")
|
||||
public ApiResponse<Overview> updatePickupCode(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long id,
|
||||
@Valid @RequestBody PickupCodeRequest request) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
service.requireOverseas(id);
|
||||
return ApiResponse.success(overview.updatePickupCode(id, user.userId(), user.username(), request));
|
||||
}
|
||||
@GetMapping("/{id}/pda/photos/{fileId}/download")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:container:list')")
|
||||
@@ -61,6 +92,7 @@ public class PlatformContainerController {
|
||||
@PathVariable @Positive long id,
|
||||
@PathVariable @Positive long fileId) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
service.requireDomestic(id);
|
||||
var value = overview.downloadPhoto(id, fileId);
|
||||
return ResponseEntity.ok().cacheControl(CacheControl.noStore())
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, org.springframework.http.ContentDisposition.inline()
|
||||
@@ -71,20 +103,20 @@ public class PlatformContainerController {
|
||||
public ApiResponse<Void> markException(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable @Positive long id,
|
||||
@RequestHeader("Idempotency-Key") String key,
|
||||
@Valid @RequestBody ExceptionRequest request) {
|
||||
FirstlegScopeGuard.platform(user); overview.mark(id,user.userId(),user.username(),key,request); return ApiResponse.success(null);
|
||||
FirstlegScopeGuard.platform(user); service.requireDomestic(id); overview.mark(id,user.userId(),user.username(),key,request); return ApiResponse.success(null);
|
||||
}
|
||||
@DeleteMapping("/{id}/exception") @PreAuthorize("hasAuthority('platform:firstleg:container:edit')")
|
||||
public ApiResponse<Void> cancelException(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable @Positive long id,
|
||||
@RequestHeader("Idempotency-Key") String key,
|
||||
@Valid @RequestBody ExceptionRequest request) {
|
||||
FirstlegScopeGuard.platform(user); overview.cancel(id,user.userId(),user.username(),key,request); return ApiResponse.success(null);
|
||||
FirstlegScopeGuard.platform(user); service.requireDomestic(id); overview.cancel(id,user.userId(),user.username(),key,request); return ApiResponse.success(null);
|
||||
}
|
||||
@GetMapping("/{id}/documents/{documentType}") @PreAuthorize("hasAuthority('platform:firstleg:container:list')")
|
||||
public ApiResponse<BusinessDocument> logisticsDocument(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable @Positive long id,@PathVariable String documentType){FirstlegScopeGuard.platform(user);return ApiResponse.success(loadingLists.containerLogisticsDocument(id,documentType));}
|
||||
@PostMapping(value="/{id}/documents/{documentType}/upload",consumes=MediaType.MULTIPART_FORM_DATA_VALUE) @PreAuthorize("hasAuthority('platform:firstleg:container:edit')")
|
||||
public ApiResponse<BusinessDocument> uploadLogisticsDocument(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable @Positive long id,@PathVariable String documentType,@RequestHeader("Idempotency-Key") String key,@RequestPart("file") MultipartFile file,@RequestParam("expectedVersion") @Min(0) int version,@RequestParam(value="operationReason",required=false) String reason){FirstlegScopeGuard.platform(user);return ApiResponse.success(loadingLists.uploadContainerLogisticsDocument(id,documentType,user.userId(),user.username(),key,version,reason,file));}
|
||||
@GetMapping("/{id}/documents/{documentType}/{fileId}/download") @PreAuthorize("hasAuthority('platform:firstleg:container:list')")
|
||||
public ResponseEntity<byte[]> downloadDocument(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable @Positive long id,@PathVariable String documentType,@PathVariable @Positive long fileId){FirstlegScopeGuard.platform(user);var d=loadingLists.downloadContainerDocument(id,documentType,fileId);return ResponseEntity.ok().cacheControl(CacheControl.noStore()).header(HttpHeaders.CONTENT_DISPOSITION,org.springframework.http.ContentDisposition.attachment().filename(d.fileName(),StandardCharsets.UTF_8).build().toString()).contentType(d.contentType()).contentLength(d.content().length).body(d.content());}
|
||||
@GetMapping("/{id}/documents/{documentType}") @PreAuthorize("hasAuthority('platform:firstleg:terminal-drayage:list')")
|
||||
public ApiResponse<BusinessDocument> logisticsDocument(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable @Positive long id,@PathVariable String documentType){FirstlegScopeGuard.platform(user);service.requireOverseas(id);return ApiResponse.success(loadingLists.containerLogisticsDocument(id,documentType));}
|
||||
@PostMapping(value="/{id}/documents/{documentType}/upload",consumes=MediaType.MULTIPART_FORM_DATA_VALUE) @PreAuthorize("hasAuthority('platform:firstleg:terminal-drayage:document:edit')")
|
||||
public ApiResponse<BusinessDocument> uploadLogisticsDocument(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable @Positive long id,@PathVariable String documentType,@RequestHeader("Idempotency-Key") String key,@RequestPart("file") MultipartFile file,@RequestParam("expectedVersion") @Min(0) int version,@RequestParam(value="operationReason",required=false) String reason){FirstlegScopeGuard.platform(user);service.requireOverseas(id);return ApiResponse.success(loadingLists.uploadContainerLogisticsDocument(id,documentType,user.userId(),user.username(),key,version,reason,file));}
|
||||
@GetMapping("/{id}/documents/{documentType}/{fileId}/download") @PreAuthorize("hasAuthority('platform:firstleg:terminal-drayage:list')")
|
||||
public ResponseEntity<byte[]> downloadDocument(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable @Positive long id,@PathVariable String documentType,@PathVariable @Positive long fileId){FirstlegScopeGuard.platform(user);service.requireOverseas(id);var d=loadingLists.downloadContainerDocument(id,documentType,fileId);return ResponseEntity.ok().cacheControl(CacheControl.noStore()).header(HttpHeaders.CONTENT_DISPOSITION,org.springframework.http.ContentDisposition.attachment().filename(d.fileName(),StandardCharsets.UTF_8).build().toString()).contentType(d.contentType()).contentLength(d.content().length).body(d.content());}
|
||||
@PostMapping @PreAuthorize("hasAuthority('platform:firstleg:container:create')")
|
||||
@BusinessAudit(event="CONTAINER_CREATE", resourceType="CONTAINER", description="新增货柜基础资料", captureBefore=false)
|
||||
public ApiResponse<ContainerBookingEntity> create(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@@ -104,21 +136,23 @@ public class PlatformContainerController {
|
||||
return ApiResponse.success(service.updateDraft(id, user.userId(), user.username(), request));
|
||||
}
|
||||
@GetMapping("/{id}/clearance")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:customs:manage')")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:terminal-drayage:list')")
|
||||
@Operation(summary = "查询货柜目的港清关资料")
|
||||
public ApiResponse<BusinessDocument> clearance(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long id) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
service.requireOverseas(id);
|
||||
return ApiResponse.success(loadingLists.containerClearance(id));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/clearance/document/{fileId}/download")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:customs:manage')")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:terminal-drayage:list')")
|
||||
@Operation(summary = "下载货柜目的港清关附件", description = "校验当前清关附件和 File 业务归属,完成态仍可下载。")
|
||||
public ResponseEntity<byte[]> downloadClearance(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long id,
|
||||
@PathVariable @Positive long fileId) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
service.requireOverseas(id);
|
||||
var download = loadingLists.downloadContainerClearance(id, fileId);
|
||||
return ResponseEntity.ok().cacheControl(CacheControl.noStore())
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, org.springframework.http.ContentDisposition.attachment()
|
||||
@@ -128,19 +162,20 @@ public class PlatformContainerController {
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/clearance/document")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:customs:manage')")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:terminal-drayage:document:edit')")
|
||||
@Operation(summary = "上传或替换货柜目的港清关资料")
|
||||
public ApiResponse<BusinessDocument> attachClearance(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long id,
|
||||
@RequestHeader("Idempotency-Key") String key,
|
||||
@Valid @RequestBody DocumentAttachRequest request) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
service.requireOverseas(id);
|
||||
return ApiResponse.success(loadingLists.attachContainerClearance(
|
||||
id, user.userId(), user.username(), key, request));
|
||||
}
|
||||
|
||||
@PostMapping(value = "/{id}/clearance/document/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:customs:manage')")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:terminal-drayage:document:edit')")
|
||||
@Operation(summary = "直接上传或替换货柜目的港清关资料")
|
||||
public ApiResponse<BusinessDocument> uploadClearance(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long id,
|
||||
@@ -149,18 +184,20 @@ public class PlatformContainerController {
|
||||
@RequestParam("expectedVersion") @Min(0) int expectedVersion,
|
||||
@RequestParam(value = "operationReason", required = false) @Size(max = 500) String operationReason) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
service.requireOverseas(id);
|
||||
return ApiResponse.success(loadingLists.uploadContainerClearance(
|
||||
id, user.userId(), user.username(), key, expectedVersion, operationReason, file));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/clearance/complete")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:customs:manage')")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:terminal-drayage:document:edit')")
|
||||
@Operation(summary = "确认货柜目的港清关完成")
|
||||
public ApiResponse<BusinessDocument> completeClearance(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long id,
|
||||
@RequestHeader("Idempotency-Key") String key,
|
||||
@Valid @RequestBody DocumentCompleteRequest request) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
service.requireOverseas(id);
|
||||
return ApiResponse.success(loadingLists.completeContainerClearance(
|
||||
id, user.userId(), user.username(), key, request));
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ import java.time.LocalDateTime;
|
||||
public final class ContainerDtos {
|
||||
private ContainerDtos() {}
|
||||
|
||||
public enum WorkbenchScope { ACTIVE, HISTORY }
|
||||
|
||||
@Schema(name = "ContainerQuery", description = "货柜分页筛选条件")
|
||||
public record Query(
|
||||
@Size(max = 64) String containerNo,
|
||||
@@ -35,6 +37,7 @@ public final class ContainerDtos {
|
||||
SmartLoadingStatus status,
|
||||
LocalDateTime estimatedSailingFrom,
|
||||
LocalDateTime estimatedSailingTo,
|
||||
@NotNull WorkbenchScope scope,
|
||||
@Min(1) long page,
|
||||
@Min(1) @Max(200) long size
|
||||
) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@@ -21,7 +22,8 @@ public final class ContainerOverviewDtos {
|
||||
LocalDateTime estimatedWarehouseArrivalAt, LocalDateTime actualWarehouseArrivalAt,
|
||||
LocalDateTime unloadingCompletedAt, LocalDateTime unloadingPhotoUploadedAt,
|
||||
LocalDateTime estimatedEmptyReportedAt, LocalDateTime actualEmptyReportedAt,
|
||||
Long emptyReportSourceVersion,
|
||||
Long emptyReportSourceVersion, String pickupCode, Long pickupCodeUpdatedBy,
|
||||
String pickupCodeUpdatedByName, LocalDateTime pickupCodeUpdatedAt,
|
||||
Integer version) {}
|
||||
public record PdaSummary(Long taskId, Long loadingPlanId, Long containerBookingId, String taskStatus,
|
||||
String workflowStatus, Integer taskVersion, Integer itemCount, Integer completedItemCount, Integer unresolvedItemCount,
|
||||
@@ -35,4 +37,6 @@ public final class ContainerOverviewDtos {
|
||||
public record ExceptionHistory(Long id, String action, String reason, Long operatorId, String operatorName, LocalDateTime occurredAt) {}
|
||||
public record TimelineEntry(String eventType, String detail, Long operatorId, String operatorName, LocalDateTime occurredAt) {}
|
||||
public record ExceptionRequest(@NotNull @Min(0) Integer expectedVersion, @NotBlank @Size(max=500) String reason) {}
|
||||
public record PickupCodeRequest(@NotBlank @Size(max=64) @Pattern(regexp="^[0-9]+$", message="PU码只能包含数字") String pickupCode,
|
||||
@NotNull @Min(0) Integer expectedVersion) {}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,10 @@ public class ContainerBookingEntity {
|
||||
private LocalDateTime estimatedEmptyReportedAt;
|
||||
private LocalDateTime actualEmptyReportedAt;
|
||||
private Long emptyReportSourceVersion;
|
||||
private String pickupCode;
|
||||
private Long pickupCodeUpdatedBy;
|
||||
private String pickupCodeUpdatedByName;
|
||||
private LocalDateTime pickupCodeUpdatedAt;
|
||||
private Long createdBy;
|
||||
private String createdByName;
|
||||
@TableField(fill = FieldFill.INSERT) private LocalDateTime createdAt;
|
||||
@@ -76,5 +80,6 @@ public class ContainerBookingEntity {
|
||||
@TableField(exist = false) private String destinationClearanceDocumentStatus;
|
||||
@TableField(exist = false) private String customsDeclarationDocumentStatus;
|
||||
@TableField(exist = false) private String deliveryOrderDocumentStatus;
|
||||
@TableField(exist = false) private String pickupOrderDocumentStatus;
|
||||
@TableField(exist = false) private String outboundDispatchStatus;
|
||||
@TableField(exist = false) private LocalDateTime outboundConfirmedAt;
|
||||
}
|
||||
|
||||
@@ -40,8 +40,14 @@ public interface ContainerBookingMapper extends BaseMapper<ContainerBookingEntit
|
||||
COALESCE((SELECT c.status FROM fl_customs_container c WHERE c.container_booking_id=b.id LIMIT 1),'MISSING') AS customs_declaration_document_status,
|
||||
COALESCE((SELECT 'UPLOADED' FROM fl_container_document d WHERE d.container_booking_id=b.id
|
||||
AND d.document_type='DELIVERY_ORDER' LIMIT 1),'MISSING') AS delivery_order_document_status,
|
||||
COALESCE((SELECT 'UPLOADED' FROM fl_container_document d WHERE d.container_booking_id=b.id
|
||||
AND d.document_type='PICKUP_ORDER' LIMIT 1),'MISSING') AS pickup_order_document_status,
|
||||
COALESCE((SELECT CASE WHEN MIN(d.confirmed_at) IS NOT NULL
|
||||
AND MAX(d.reversed_at) IS NULL THEN 'DEPARTED'
|
||||
ELSE MIN(d.status) END
|
||||
FROM fl_outbound_dispatch d WHERE d.container_booking_id=b.id),'NOT_CREATED')
|
||||
AS outbound_dispatch_status,
|
||||
(SELECT MIN(d.confirmed_at) FROM fl_outbound_dispatch d
|
||||
WHERE d.container_booking_id=b.id AND d.confirmed_at IS NOT NULL
|
||||
AND d.reversed_at IS NULL AND d.status='WAITING_FOR_LOADING') AS outbound_confirmed_at,
|
||||
CASE WHEN p.id IS NULL THEN 'WAIT_LOADING'
|
||||
WHEN p.plan_status != 'COMPLETED' AND NOT EXISTS (
|
||||
SELECT 1 FROM fl_loading_plan_item active_item
|
||||
@@ -78,6 +84,24 @@ public interface ContainerBookingMapper extends BaseMapper<ContainerBookingEntit
|
||||
ELSE p.plan_status END = #{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 ((#{workbench}='DOMESTIC' AND (
|
||||
(#{query.scope}='ACTIVE' AND NOT EXISTS (
|
||||
SELECT 1 FROM fl_outbound_dispatch departure
|
||||
WHERE departure.container_booking_id=b.id
|
||||
AND departure.confirmed_at IS NOT NULL AND departure.reversed_at IS NULL
|
||||
AND departure.status='WAITING_FOR_LOADING'))
|
||||
OR (#{query.scope}='HISTORY' AND EXISTS (
|
||||
SELECT 1 FROM fl_outbound_dispatch departure
|
||||
WHERE departure.container_booking_id=b.id
|
||||
AND departure.confirmed_at IS NOT NULL AND departure.reversed_at IS NULL
|
||||
AND departure.status='WAITING_FOR_LOADING'))))
|
||||
OR (#{workbench}='OVERSEAS' AND EXISTS (
|
||||
SELECT 1 FROM fl_outbound_dispatch departure
|
||||
WHERE departure.container_booking_id=b.id
|
||||
AND departure.confirmed_at IS NOT NULL AND departure.reversed_at IS NULL
|
||||
AND departure.status='WAITING_FOR_LOADING')
|
||||
AND ((#{query.scope}='ACTIVE' AND b.actual_empty_reported_at IS NULL)
|
||||
OR (#{query.scope}='HISTORY' AND b.actual_empty_reported_at IS NOT NULL))))
|
||||
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}, '%')
|
||||
@@ -86,7 +110,9 @@ public interface ContainerBookingMapper extends BaseMapper<ContainerBookingEntit
|
||||
ORDER BY b.updated_at DESC, b.id DESC
|
||||
</script>
|
||||
""")
|
||||
Page<ContainerBookingEntity> selectBookingPage(Page<ContainerBookingEntity> page, @Param("query") Query query);
|
||||
Page<ContainerBookingEntity> selectBookingPage(Page<ContainerBookingEntity> page,
|
||||
@Param("query") Query query,
|
||||
@Param("workbench") String workbench);
|
||||
|
||||
@Select("""
|
||||
SELECT b.*, p.id AS plan_id, p.plan_no AS plan_no,
|
||||
@@ -100,8 +126,14 @@ public interface ContainerBookingMapper extends BaseMapper<ContainerBookingEntit
|
||||
COALESCE((SELECT c.status FROM fl_customs_container c WHERE c.container_booking_id=b.id LIMIT 1),'MISSING') AS customs_declaration_document_status,
|
||||
COALESCE((SELECT 'UPLOADED' FROM fl_container_document d WHERE d.container_booking_id=b.id
|
||||
AND d.document_type='DELIVERY_ORDER' LIMIT 1),'MISSING') AS delivery_order_document_status,
|
||||
COALESCE((SELECT 'UPLOADED' FROM fl_container_document d WHERE d.container_booking_id=b.id
|
||||
AND d.document_type='PICKUP_ORDER' LIMIT 1),'MISSING') AS pickup_order_document_status,
|
||||
COALESCE((SELECT CASE WHEN MIN(d.confirmed_at) IS NOT NULL
|
||||
AND MAX(d.reversed_at) IS NULL THEN 'DEPARTED'
|
||||
ELSE MIN(d.status) END
|
||||
FROM fl_outbound_dispatch d WHERE d.container_booking_id=b.id),'NOT_CREATED')
|
||||
AS outbound_dispatch_status,
|
||||
(SELECT MIN(d.confirmed_at) FROM fl_outbound_dispatch d
|
||||
WHERE d.container_booking_id=b.id AND d.confirmed_at IS NOT NULL
|
||||
AND d.reversed_at IS NULL AND d.status='WAITING_FOR_LOADING') AS outbound_confirmed_at,
|
||||
CASE WHEN p.id IS NULL THEN 'WAIT_LOADING'
|
||||
WHEN p.plan_status != 'COMPLETED' AND NOT EXISTS (
|
||||
SELECT 1 FROM fl_loading_plan_item active_item
|
||||
@@ -119,6 +151,23 @@ public interface ContainerBookingMapper extends BaseMapper<ContainerBookingEntit
|
||||
@Select("SELECT * FROM fl_container_booking WHERE id=#{id} AND record_status='ACTIVE' FOR UPDATE")
|
||||
ContainerBookingEntity selectActiveBookingForUpdate(@Param("id") long id);
|
||||
|
||||
@Select("""
|
||||
SELECT COUNT(1) FROM fl_outbound_dispatch
|
||||
WHERE container_booking_id=#{id} AND confirmed_at IS NOT NULL AND reversed_at IS NULL
|
||||
AND status='WAITING_FOR_LOADING'
|
||||
""")
|
||||
int hasActiveOutboundDeparture(@Param("id") long id);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_container_booking SET pickup_code=#{pickupCode},pickup_code_updated_by=#{operatorId},
|
||||
pickup_code_updated_by_name=#{operatorName},pickup_code_updated_at=CURRENT_TIMESTAMP(3),
|
||||
updated_by=#{operatorId},updated_by_name=#{operatorName},updated_at=CURRENT_TIMESTAMP(3),version=version+1
|
||||
WHERE id=#{id} AND record_status='ACTIVE' AND version=#{version}
|
||||
""")
|
||||
int updatePickupCode(@Param("id") long id, @Param("pickupCode") String pickupCode,
|
||||
@Param("version") int version, @Param("operatorId") long operatorId,
|
||||
@Param("operatorName") String operatorName);
|
||||
|
||||
@Select("SELECT * FROM fl_container_booking WHERE container_no=#{containerNo} AND record_status='ACTIVE'")
|
||||
ContainerBookingEntity selectActiveByContainerNo(@Param("containerNo") String containerNo);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import com.baiyue.firstleg.entity.ContainerDocumentEntity;
|
||||
import org.apache.ibatis.annotations.*;
|
||||
import java.util.List;
|
||||
@Mapper public interface ContainerOverviewMapper {
|
||||
@Select("SELECT * FROM fl_container_document WHERE container_booking_id=#{id} AND document_type IN ('DESTINATION_CLEARANCE','DELIVERY_ORDER','PICKUP_ORDER') ORDER BY document_type") List<ContainerDocumentEntity> documents(@Param("id") long id);
|
||||
@Select("SELECT * FROM fl_container_document WHERE container_booking_id=#{id} AND document_type IN ('DESTINATION_CLEARANCE','DELIVERY_ORDER') ORDER BY document_type") List<ContainerDocumentEntity> documents(@Param("id") long id);
|
||||
@Select("""
|
||||
SELECT history.id,plan.container_booking_id containerBookingId,
|
||||
JSON_UNQUOTE(JSON_EXTRACT(history.before_snapshot,'$.containerNo')) beforeContainerNo,
|
||||
|
||||
+14
@@ -5,6 +5,7 @@ import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.CarrierDescriptor;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ContainerActualFacts;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.CreateRequest;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.Detail;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ManualResultRequest;
|
||||
@@ -19,6 +20,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
@@ -28,6 +30,7 @@ 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.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
@@ -47,6 +50,17 @@ public class PlatformCarrierScheduleQueryController {
|
||||
return ApiResponse.success(service.carriers());
|
||||
}
|
||||
|
||||
@Operation(summary = "按货柜批量查询实际船节点摘要",
|
||||
description = "最多200柜;仅返回实际节点时间,不返回或推断预计时间;没有船节点任务的货柜不返回行。")
|
||||
@GetMapping("/container-actual-facts")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:port-schedule-query:list')")
|
||||
public ApiResponse<List<ContainerActualFacts>> containerActualFacts(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam("containerBookingIds") @Size(min = 1, max = 200) List<@Positive Long> containerBookingIds) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.containerActualFacts(containerBookingIds));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:port-schedule-query:list')")
|
||||
public ApiResponse<PageResult<QueryView>> page(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
|
||||
+11
@@ -80,6 +80,17 @@ public final class CarrierScheduleQueryDtos {
|
||||
|
||||
public record QueryView(Task query, boolean expired) {}
|
||||
|
||||
/** Actual-only summary used by container workbenches; estimated facts are intentionally excluded. */
|
||||
public record ContainerActualFacts(Long containerBookingId, Long queryId, Integer queryVersion,
|
||||
String queryStatus, String latestActualNode,
|
||||
LocalDateTime latestActualAt,
|
||||
LocalDateTime departedWarehouseAt,
|
||||
LocalDateTime departedOriginPortAt,
|
||||
LocalDateTime arrivedDestinationPortAt,
|
||||
LocalDateTime pickupReadyAt,
|
||||
LocalDateTime pickedUpAt,
|
||||
LocalDateTime emptyReturnedAt) {}
|
||||
|
||||
public record NodeFact(Long id, String providerCode, String providerEventId,
|
||||
String providerNodeCode, String standardNode,
|
||||
int nodeSequence, LocalDateTime observedAt, LocalDateTime estimatedAt,
|
||||
|
||||
+28
@@ -3,6 +3,7 @@ package com.baiyue.firstleg.portschedule.mapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.Query;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ContainerActualFacts;
|
||||
import com.baiyue.firstleg.portschedule.entity.CarrierScheduleQueryEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
@@ -46,6 +47,33 @@ public interface CarrierScheduleQueryMapper extends BaseMapper<CarrierScheduleQu
|
||||
@Param("oceanBookingId") long oceanBookingId,
|
||||
@Param("containerBookingId") long containerBookingId);
|
||||
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT q.container_booking_id AS containerBookingId,q.id AS queryId,q.version AS queryVersion,
|
||||
q.status AS queryStatus,q.latest_standard_node AS latestActualNode,
|
||||
q.latest_node_observed_at AS latestActualAt,
|
||||
MAX(CASE WHEN f.standard_node='DEPARTED_WAREHOUSE' THEN f.actual_at END) AS departedWarehouseAt,
|
||||
MAX(CASE WHEN f.standard_node='DEPARTED_ORIGIN_PORT' THEN f.actual_at END) AS departedOriginPortAt,
|
||||
MAX(CASE WHEN f.standard_node='ARRIVED_DESTINATION_PORT' THEN f.actual_at END) AS arrivedDestinationPortAt,
|
||||
MAX(CASE WHEN f.standard_node='PICKUP_READY' THEN f.actual_at END) AS pickupReadyAt,
|
||||
MAX(CASE WHEN f.standard_node='PICKED_UP' THEN f.actual_at END) AS pickedUpAt,
|
||||
MAX(CASE WHEN f.standard_node='EMPTY_RETURNED' THEN f.actual_at END) AS emptyReturnedAt
|
||||
FROM fl_carrier_schedule_query q
|
||||
JOIN (
|
||||
SELECT container_booking_id,MAX(id) AS query_id
|
||||
FROM fl_carrier_schedule_query
|
||||
WHERE container_booking_id IN
|
||||
<foreach collection="containerBookingIds" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
GROUP BY container_booking_id
|
||||
) latest ON latest.query_id=q.id
|
||||
LEFT JOIN fl_carrier_schedule_node_fact f ON f.query_id=q.id AND f.actual_at IS NOT NULL
|
||||
GROUP BY q.container_booking_id,q.id,q.version,q.status,q.latest_standard_node,q.latest_node_observed_at
|
||||
ORDER BY q.container_booking_id
|
||||
</script>
|
||||
""")
|
||||
List<ContainerActualFacts> selectContainerActualFacts(
|
||||
@Param("containerBookingIds") List<Long> containerBookingIds);
|
||||
|
||||
@Select("""
|
||||
SELECT * FROM fl_carrier_schedule_query
|
||||
WHERE status IN ('READY','MANUAL_REQUIRED')
|
||||
|
||||
+4
@@ -5,6 +5,7 @@ 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.portschedule.dto.CarrierScheduleQueryDtos.Query;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ContainerActualFacts;
|
||||
import com.baiyue.firstleg.portschedule.entity.CarrierScheduleQueryAttemptEntity;
|
||||
import com.baiyue.firstleg.portschedule.entity.CarrierScheduleQueryEntity;
|
||||
import com.baiyue.firstleg.portschedule.entity.CarrierScheduleQueryHistoryEntity;
|
||||
@@ -40,6 +41,9 @@ public class CarrierScheduleQueryRepository {
|
||||
public CarrierScheduleQueryEntity findByBusinessScope(long customerId, long oceanBookingId, long containerBookingId) {
|
||||
return queries.findByBusinessScope(customerId, oceanBookingId, containerBookingId);
|
||||
}
|
||||
public List<ContainerActualFacts> containerActualFacts(List<Long> containerBookingIds) {
|
||||
return queries.selectContainerActualFacts(containerBookingIds);
|
||||
}
|
||||
public List<CarrierScheduleQueryEntity> due(LocalDateTime now, int limit) { return queries.selectDue(now, limit); }
|
||||
public boolean claimLease(long id, String owner, LocalDateTime now, LocalDateTime leaseUntil) {
|
||||
return queries.claimLease(id, owner, now, leaseUntil) == 1;
|
||||
|
||||
+14
@@ -7,6 +7,7 @@ import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.common.event.ContainerNumbers;
|
||||
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingEntity;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.CarrierDescriptor;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ContainerActualFacts;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.CreateRequest;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.Detail;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ManualResultRequest;
|
||||
@@ -48,6 +49,7 @@ import java.time.Clock;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.HexFormat;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
@@ -76,6 +78,18 @@ public class CarrierScheduleQueryService {
|
||||
source.page(), source.size(), source.total(), source.pages());
|
||||
}
|
||||
|
||||
public List<ContainerActualFacts> containerActualFacts(List<Long> containerBookingIds) {
|
||||
if (containerBookingIds == null || containerBookingIds.isEmpty() || containerBookingIds.size() > 200) {
|
||||
throw badRequest("containerBookingIds数量必须在1到200之间");
|
||||
}
|
||||
Set<Long> unique = new HashSet<>();
|
||||
for (Long id : containerBookingIds) {
|
||||
if (id == null || id <= 0) throw badRequest("containerBookingIds必须全部为正数");
|
||||
if (!unique.add(id)) throw badRequest("containerBookingIds不能包含重复值");
|
||||
}
|
||||
return repository.containerActualFacts(containerBookingIds);
|
||||
}
|
||||
|
||||
public Detail detail(long id) { return detail(required(id)); }
|
||||
|
||||
public List<History> history(long id) {
|
||||
|
||||
@@ -12,15 +12,16 @@ import org.springframework.stereotype.Repository;
|
||||
@Repository
|
||||
public class ContainerRepository {
|
||||
@Resource private ContainerBookingMapper mapper;
|
||||
public PageResult<ContainerBookingEntity> page(Query query) {
|
||||
public PageResult<ContainerBookingEntity> page(String workbench, Query query) {
|
||||
Page<ContainerBookingEntity> result = mapper.selectBookingPage(
|
||||
new Page<>(query.page(), query.size()), query);
|
||||
new Page<>(query.page(), query.size()), query, workbench);
|
||||
result.getRecords().forEach(this::resolveLoadingTime);
|
||||
return PageResult.from(result);
|
||||
}
|
||||
public ContainerBookingEntity find(long id) { return resolveLoadingTime(mapper.selectBooking(id)); }
|
||||
public ContainerBookingEntity findForUpdate(long id) { return mapper.selectBookingForUpdate(id); }
|
||||
public ContainerBookingEntity findActiveForUpdate(long id) { return mapper.selectActiveBookingForUpdate(id); }
|
||||
public boolean departed(long id) { return mapper.hasActiveOutboundDeparture(id) > 0; }
|
||||
public boolean containerNoExists(String no, Long excludedId) {
|
||||
if (no == null) return false;
|
||||
return mapper.selectCount(new LambdaQueryWrapper<ContainerBookingEntity>()
|
||||
@@ -45,6 +46,9 @@ public class ContainerRepository {
|
||||
public int updateDraft(ContainerBookingEntity value, int version, long operatorId, String operatorName) {
|
||||
return mapper.updateDraft(value, version, operatorId, operatorName);
|
||||
}
|
||||
public int updatePickupCode(long id, String pickupCode, int version, long operatorId, String operatorName) {
|
||||
return mapper.updatePickupCode(id, pickupCode, version, operatorId, operatorName);
|
||||
}
|
||||
public boolean hasLoadingPlan(long id) { return mapper.hasLoadingPlan(id) > 0; }
|
||||
public boolean hasActiveLoadingItems(long id) { return mapper.hasActiveLoadingItems(id) > 0; }
|
||||
public boolean hasPickTask(long id) { return mapper.hasPickTask(id) > 0; }
|
||||
|
||||
@@ -51,7 +51,9 @@ public class ContainerOverviewService {
|
||||
booking.getActualPortArrivalAt(), booking.getEstimatedWarehouseArrivalAt(),
|
||||
booking.getActualWarehouseArrivalAt(), booking.getUnloadingCompletedAt(),
|
||||
booking.getUnloadingPhotoUploadedAt(), booking.getEstimatedEmptyReportedAt(),
|
||||
booking.getActualEmptyReportedAt(), booking.getEmptyReportSourceVersion(), booking.getVersion()),
|
||||
booking.getActualEmptyReportedAt(), booking.getEmptyReportSourceVersion(), booking.getPickupCode(),
|
||||
booking.getPickupCodeUpdatedBy(), booking.getPickupCodeUpdatedByName(), booking.getPickupCodeUpdatedAt(),
|
||||
booking.getVersion()),
|
||||
task == null ? booking.getPlanId() : task.loadingPlanId(), pda, documents,
|
||||
new ExceptionState(latestException != null && "MARK".equals(latestException.action()),
|
||||
latestException == null ? null : latestException.reason(),
|
||||
@@ -72,6 +74,18 @@ public class ContainerOverviewService {
|
||||
changeException(id, operatorId, operatorName, idempotencyKey, request, "CANCEL");
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Overview updatePickupCode(long id, long operatorId, String operatorName, PickupCodeRequest request) {
|
||||
ContainerBookingEntity booking = containers.findActiveForUpdate(id);
|
||||
if (booking == null) throw new BizException(ResultCode.NOT_FOUND, "货柜不存在");
|
||||
if (!request.expectedVersion().equals(booking.getVersion())) throw conflict("货柜版本已变化,请刷新后重试");
|
||||
String pickupCode = request.pickupCode().trim();
|
||||
if (containers.updatePickupCode(id, pickupCode, request.expectedVersion(), operatorId, operatorName) != 1) {
|
||||
throw conflict("货柜版本已变化,请刷新后重试");
|
||||
}
|
||||
return overview(id);
|
||||
}
|
||||
|
||||
private void changeException(long id, long operatorId, String operatorName, String rawKey,
|
||||
ExceptionRequest request, String action) {
|
||||
String operationId = operationId(rawKey);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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.ApiResponse;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
@@ -9,7 +8,6 @@ import com.baiyue.firstleg.client.WarehouseMasterDataClient;
|
||||
import com.baiyue.firstleg.domain.SmartLoadingStatus;
|
||||
import com.baiyue.firstleg.domain.ContainerSpecificationCatalog;
|
||||
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.SimulationCreateRequest;
|
||||
import com.baiyue.firstleg.dto.ContainerDtos.OceanBookingLinkRequest;
|
||||
@@ -41,11 +39,15 @@ public class ContainerService {
|
||||
@Resource private SmartLoadingService smartLoadingService;
|
||||
@Value("${baiyue.security.internal-service-token:}") private String internalToken;
|
||||
@Value("${baiyue.warehouse.public-tenant-id:0}") private long publicWarehouseTenantId;
|
||||
public PageResult<ContainerBookingEntity> page(Query query) {
|
||||
validateRange(query.estimatedSailingFrom(), query.estimatedSailingTo(), "预计船期筛选");
|
||||
return repository.page(normalize(query));
|
||||
public void requireOverseas(long id) {
|
||||
required(id);
|
||||
if (!repository.departed(id)) throw new BizException(ResultCode.NOT_FOUND, "国外工作台货柜不存在");
|
||||
}
|
||||
|
||||
public void requireDomestic(long id) {
|
||||
required(id);
|
||||
if (repository.departed(id)) throw new BizException(ResultCode.NOT_FOUND, "国内工作台货柜不存在");
|
||||
}
|
||||
public ContainerBookingEntity detail(long id) { return required(id); }
|
||||
|
||||
@Transactional
|
||||
public ContainerBookingEntity createSimulation(long operatorId, String operatorName, SimulationCreateRequest request) {
|
||||
@@ -220,12 +222,6 @@ public class ContainerService {
|
||||
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.customerId(), 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;
|
||||
|
||||
@@ -56,10 +56,8 @@ public class LoadingListService {
|
||||
private static final Set<String> PRELOAD_STATUSES = Set.of("EDITING", "LOADING");
|
||||
public static final String DESTINATION_CLEARANCE = "DESTINATION_CLEARANCE";
|
||||
public static final String DELIVERY_ORDER = "DELIVERY_ORDER";
|
||||
public static final String PICKUP_ORDER = "PICKUP_ORDER";
|
||||
private static final String CLEARANCE_FILE_TYPE = "CONTAINER_CLEARANCE_DOCUMENT";
|
||||
private static final String DO_FILE_TYPE = "CONTAINER_DELIVERY_ORDER";
|
||||
private static final String PU_FILE_TYPE = "CONTAINER_PICKUP_ORDER";
|
||||
@Resource
|
||||
private LoadingListRepository repository;
|
||||
@Resource
|
||||
@@ -190,8 +188,8 @@ public class LoadingListService {
|
||||
}
|
||||
}
|
||||
public record DocumentDownload(String fileName,MediaType contentType,byte[] content) {}
|
||||
private static void requireLogisticsType(String type){ if(!DELIVERY_ORDER.equals(type)&&!PICKUP_ORDER.equals(type)) throw badRequest("documentType只能是DELIVERY_ORDER或PICKUP_ORDER"); }
|
||||
private static String fileType(String type){ return switch(type){case DELIVERY_ORDER->DO_FILE_TYPE; case PICKUP_ORDER->PU_FILE_TYPE; default->throw badRequest("documentType只能是DELIVERY_ORDER或PICKUP_ORDER");}; }
|
||||
private static void requireLogisticsType(String type){ if(!DELIVERY_ORDER.equals(type)) throw badRequest("documentType只能是DELIVERY_ORDER"); }
|
||||
private static String fileType(String type){ if (DELIVERY_ORDER.equals(type)) return DO_FILE_TYPE; throw badRequest("documentType只能是DELIVERY_ORDER"); }
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "CONTAINER_CLEARANCE_DOCUMENT_ATTACH", resourceType = "CONTAINER",
|
||||
|
||||
+3
-2
@@ -63,8 +63,9 @@ public class OverseasContainerEventConsumer {
|
||||
|
||||
ContainerBookingEntity container = requiredContainer(payload.sourceContainerBookingId(), payload.containerNo());
|
||||
LocalDateTime uploadedAt = utc(payload.unloadingPhotoUploadedAt());
|
||||
containers.applyEmptyReport(container.getId(), utc(payload.unloadingCompletedAt()), uploadedAt,
|
||||
uploadedAt.plusDays(2), utc(payload.actualEmptyReportedAt()), event.aggregateVersion());
|
||||
LocalDateTime unloadingCompletedAt = utc(payload.unloadingCompletedAt());
|
||||
containers.applyEmptyReport(container.getId(), unloadingCompletedAt, uploadedAt,
|
||||
unloadingCompletedAt.plusHours(72), utc(payload.actualEmptyReportedAt()), event.aggregateVersion());
|
||||
row.setStatus("PROCESSED");
|
||||
row.setProcessedAt(LocalDateTime.now(ZoneOffset.UTC));
|
||||
inbox.updateById(row);
|
||||
|
||||
+17
-13
@@ -39,24 +39,24 @@ public class PlatformTerminalDrayageController {
|
||||
private final TerminalDrayageService service;
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:container:list')")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:terminal-drayage:list')")
|
||||
public ApiResponse<List<View>> views(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam @Size(min = 1, max = 200)
|
||||
List<@Positive Long> containerBookingIds) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.views(containerBookingIds));
|
||||
return ApiResponse.success(service.authorize(service.views(containerBookingIds), user.permissions()));
|
||||
}
|
||||
|
||||
@GetMapping("/{containerBookingId}")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:container:list')")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:terminal-drayage:list')")
|
||||
public ApiResponse<View> detail(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long containerBookingId) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.detail(containerBookingId));
|
||||
return ApiResponse.success(service.authorize(service.detail(containerBookingId), user.permissions()));
|
||||
}
|
||||
|
||||
@GetMapping("/{containerBookingId}/history")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:container:list')")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:terminal-drayage:list')")
|
||||
public ApiResponse<List<History>> history(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long containerBookingId) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
@@ -64,7 +64,7 @@ public class PlatformTerminalDrayageController {
|
||||
}
|
||||
|
||||
@PostMapping("/{containerBookingId}/fleet")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:container:edit')")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:terminal-drayage:fleet:edit')")
|
||||
@Operation(summary = "选择或更换目的港码头车队")
|
||||
@BusinessAudit(event = "TERMINAL_DRAYAGE_SELECT_FLEET", resourceType = "CONTAINER",
|
||||
resourceId = "#containerBookingId", description = "选择目的港码头车队")
|
||||
@@ -73,11 +73,12 @@ public class PlatformTerminalDrayageController {
|
||||
@RequestHeader("Idempotency-Key") String key,
|
||||
@Valid @RequestBody FleetSelectionRequest request) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.selectFleet(containerBookingId, user.userId(), user.username(), key, request));
|
||||
return ApiResponse.success(service.authorize(service.selectFleet(
|
||||
containerBookingId, user.userId(), user.username(), key, request), user.permissions()));
|
||||
}
|
||||
|
||||
@PostMapping("/{containerBookingId}/pickup-appointment")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:container:edit')")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:terminal-drayage:pickup-appointment:edit')")
|
||||
@Operation(summary = "预约目的港提柜时间")
|
||||
@BusinessAudit(event = "TERMINAL_DRAYAGE_SCHEDULE_PICKUP", resourceType = "CONTAINER",
|
||||
resourceId = "#containerBookingId", description = "预约目的港提柜")
|
||||
@@ -86,11 +87,12 @@ public class PlatformTerminalDrayageController {
|
||||
@RequestHeader("Idempotency-Key") String key,
|
||||
@Valid @RequestBody PickupAppointmentRequest request) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.schedulePickup(containerBookingId, user.userId(), user.username(), key, request));
|
||||
return ApiResponse.success(service.authorize(service.schedulePickup(
|
||||
containerBookingId, user.userId(), user.username(), key, request), user.permissions()));
|
||||
}
|
||||
|
||||
@PostMapping("/{containerBookingId}/pickup")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:container:edit')")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:terminal-drayage:pickup:confirm')")
|
||||
@Operation(summary = "确认目的港实际提柜")
|
||||
@BusinessAudit(event = "TERMINAL_DRAYAGE_RECORD_PICKUP", resourceType = "CONTAINER",
|
||||
resourceId = "#containerBookingId", description = "确认目的港实际提柜")
|
||||
@@ -99,11 +101,12 @@ public class PlatformTerminalDrayageController {
|
||||
@RequestHeader("Idempotency-Key") String key,
|
||||
@Valid @RequestBody ActualTimeRequest request) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.recordPickup(containerBookingId, user.userId(), user.username(), key, request));
|
||||
return ApiResponse.success(service.authorize(service.recordPickup(
|
||||
containerBookingId, user.userId(), user.username(), key, request), user.permissions()));
|
||||
}
|
||||
|
||||
@PostMapping("/{containerBookingId}/empty-return")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:container:edit')")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:terminal-drayage:empty-return:confirm')")
|
||||
@Operation(summary = "确认实际还空柜")
|
||||
@BusinessAudit(event = "TERMINAL_DRAYAGE_RECORD_EMPTY_RETURN", resourceType = "CONTAINER",
|
||||
resourceId = "#containerBookingId", description = "确认实际还空柜")
|
||||
@@ -112,6 +115,7 @@ public class PlatformTerminalDrayageController {
|
||||
@RequestHeader("Idempotency-Key") String key,
|
||||
@Valid @RequestBody ActualTimeRequest request) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.recordEmptyReturn(containerBookingId, user.userId(), user.username(), key, request));
|
||||
return ApiResponse.success(service.authorize(service.recordEmptyReturn(
|
||||
containerBookingId, user.userId(), user.username(), key, request), user.permissions()));
|
||||
}
|
||||
}
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package com.baiyue.firstleg.terminaldrayage.dto;
|
||||
|
||||
import com.baiyue.firstleg.entity.ContainerBookingEntity;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ContainerActualFacts;
|
||||
import com.baiyue.firstleg.terminaldrayage.dto.TerminalDrayageDtos.View;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/** 国内、国外货柜工作台共享只读模型。 */
|
||||
public final class ContainerWorkbenchDtos {
|
||||
private ContainerWorkbenchDtos() {}
|
||||
|
||||
@Schema(name = "ContainerWorkbenchStatusSummary", description = "柜级各履约域只读状态摘要")
|
||||
public record StatusSummary(String loadingStatus, String customsStatus, String outboundStatus,
|
||||
String destinationClearanceStatus, String deliveryOrderStatus,
|
||||
String pickupCodeStatus, String latestActualNode,
|
||||
LocalDateTime latestActualAt, String terminalDrayageStatus,
|
||||
String emptyReturnWarningStatus) {}
|
||||
|
||||
@Schema(name = "ContainerWorkbenchItem", description = "同一containerBookingId的国内或国外工作台读模型")
|
||||
public record WorkbenchItem(ContainerBookingEntity container, String workbenchStage,
|
||||
StatusSummary statusSummary, List<String> allowedActions,
|
||||
ContainerActualFacts actualFacts, View terminalDrayage) {}
|
||||
}
|
||||
+3
-1
@@ -7,6 +7,7 @@ import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ContainerActualFacts;
|
||||
|
||||
public final class TerminalDrayageDtos {
|
||||
private TerminalDrayageDtos() {}
|
||||
@@ -33,7 +34,8 @@ public final class TerminalDrayageDtos {
|
||||
LocalDateTime pickupAppointmentAt, LocalDateTime actualPickupAt,
|
||||
LocalDateTime estimatedEmptyReturnAt, LocalDateTime actualEmptyReturnAt,
|
||||
LocalDateTime unloadingCompletedAt, Warning emptyReturnWarning,
|
||||
List<String> allowedActions, LocalDateTime updatedAt) {}
|
||||
List<String> allowedActions, ContainerActualFacts actualFacts,
|
||||
LocalDateTime updatedAt) {}
|
||||
|
||||
public record History(String action, String fromStatus, String toStatus,
|
||||
Integer fromVersion, Integer toVersion, String reason,
|
||||
|
||||
+4
-39
@@ -16,38 +16,16 @@ import java.util.List;
|
||||
public interface TerminalDrayageMapper extends BaseMapper<TerminalDrayageEntity> {
|
||||
@Select("""
|
||||
SELECT b.id AS containerBookingId,b.container_no AS containerNo,b.ocean_booking_id AS oceanBookingId,
|
||||
b.record_status AS recordStatus,b.unloading_completed_at AS unloadingCompletedAt,
|
||||
q.id AS queryId,q.latest_standard_node AS latestActualNode,
|
||||
q.latest_node_observed_at AS latestActualAt,q.version AS queryVersion,
|
||||
(SELECT MAX(f.actual_at) FROM fl_carrier_schedule_node_fact f WHERE f.query_id=q.id
|
||||
AND f.standard_node='ARRIVED_DESTINATION_PORT') AS actualArrivalNodeAt,
|
||||
(SELECT MAX(f.actual_at) FROM fl_carrier_schedule_node_fact f WHERE f.query_id=q.id
|
||||
AND f.standard_node='PICKED_UP') AS actualPickupNodeAt,
|
||||
(SELECT MAX(f.actual_at) FROM fl_carrier_schedule_node_fact f WHERE f.query_id=q.id
|
||||
AND f.standard_node='EMPTY_RETURNED') AS actualEmptyReturnNodeAt
|
||||
b.record_status AS recordStatus,b.unloading_completed_at AS unloadingCompletedAt
|
||||
FROM fl_container_booking b
|
||||
LEFT JOIN fl_carrier_schedule_query q ON q.id=(
|
||||
SELECT q2.id FROM fl_carrier_schedule_query q2
|
||||
WHERE q2.container_booking_id=b.id ORDER BY q2.id DESC LIMIT 1)
|
||||
WHERE b.id=#{containerBookingId} FOR UPDATE
|
||||
""")
|
||||
Context lockContext(@Param("containerBookingId") long containerBookingId);
|
||||
|
||||
@Select("""
|
||||
SELECT b.id AS containerBookingId,b.container_no AS containerNo,b.ocean_booking_id AS oceanBookingId,
|
||||
b.record_status AS recordStatus,b.unloading_completed_at AS unloadingCompletedAt,
|
||||
q.id AS queryId,q.latest_standard_node AS latestActualNode,
|
||||
q.latest_node_observed_at AS latestActualAt,q.version AS queryVersion,
|
||||
(SELECT MAX(f.actual_at) FROM fl_carrier_schedule_node_fact f WHERE f.query_id=q.id
|
||||
AND f.standard_node='ARRIVED_DESTINATION_PORT') AS actualArrivalNodeAt,
|
||||
(SELECT MAX(f.actual_at) FROM fl_carrier_schedule_node_fact f WHERE f.query_id=q.id
|
||||
AND f.standard_node='PICKED_UP') AS actualPickupNodeAt,
|
||||
(SELECT MAX(f.actual_at) FROM fl_carrier_schedule_node_fact f WHERE f.query_id=q.id
|
||||
AND f.standard_node='EMPTY_RETURNED') AS actualEmptyReturnNodeAt
|
||||
b.record_status AS recordStatus,b.unloading_completed_at AS unloadingCompletedAt
|
||||
FROM fl_container_booking b
|
||||
LEFT JOIN fl_carrier_schedule_query q ON q.id=(
|
||||
SELECT q2.id FROM fl_carrier_schedule_query q2
|
||||
WHERE q2.container_booking_id=b.id ORDER BY q2.id DESC LIMIT 1)
|
||||
WHERE b.id=#{containerBookingId}
|
||||
""")
|
||||
Context context(@Param("containerBookingId") long containerBookingId);
|
||||
@@ -55,19 +33,8 @@ public interface TerminalDrayageMapper extends BaseMapper<TerminalDrayageEntity>
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT b.id AS containerBookingId,b.container_no AS containerNo,b.ocean_booking_id AS oceanBookingId,
|
||||
b.record_status AS recordStatus,b.unloading_completed_at AS unloadingCompletedAt,
|
||||
q.id AS queryId,q.latest_standard_node AS latestActualNode,
|
||||
q.latest_node_observed_at AS latestActualAt,q.version AS queryVersion,
|
||||
(SELECT MAX(f.actual_at) FROM fl_carrier_schedule_node_fact f WHERE f.query_id=q.id
|
||||
AND f.standard_node='ARRIVED_DESTINATION_PORT') AS actualArrivalNodeAt,
|
||||
(SELECT MAX(f.actual_at) FROM fl_carrier_schedule_node_fact f WHERE f.query_id=q.id
|
||||
AND f.standard_node='PICKED_UP') AS actualPickupNodeAt,
|
||||
(SELECT MAX(f.actual_at) FROM fl_carrier_schedule_node_fact f WHERE f.query_id=q.id
|
||||
AND f.standard_node='EMPTY_RETURNED') AS actualEmptyReturnNodeAt
|
||||
b.record_status AS recordStatus,b.unloading_completed_at AS unloadingCompletedAt
|
||||
FROM fl_container_booking b
|
||||
LEFT JOIN fl_carrier_schedule_query q ON q.id=(
|
||||
SELECT q2.id FROM fl_carrier_schedule_query q2
|
||||
WHERE q2.container_booking_id=b.id ORDER BY q2.id DESC LIMIT 1)
|
||||
WHERE b.id IN
|
||||
<foreach collection="containerBookingIds" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
ORDER BY b.id
|
||||
@@ -141,8 +108,6 @@ public interface TerminalDrayageMapper extends BaseMapper<TerminalDrayageEntity>
|
||||
@Param("operatorId") long operatorId);
|
||||
|
||||
record Context(Long containerBookingId, String containerNo, Long oceanBookingId, String recordStatus,
|
||||
LocalDateTime unloadingCompletedAt, Long queryId, String latestActualNode,
|
||||
LocalDateTime latestActualAt, Integer queryVersion, LocalDateTime actualArrivalNodeAt,
|
||||
LocalDateTime actualPickupNodeAt, LocalDateTime actualEmptyReturnNodeAt) {}
|
||||
LocalDateTime unloadingCompletedAt) {}
|
||||
record Operation(String operation, String requestHash, Integer resultVersion) {}
|
||||
}
|
||||
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
package com.baiyue.firstleg.terminaldrayage.service;
|
||||
|
||||
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.ContainerDtos.Query;
|
||||
import com.baiyue.firstleg.entity.ContainerBookingEntity;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ContainerActualFacts;
|
||||
import com.baiyue.firstleg.portschedule.service.CarrierScheduleQueryService;
|
||||
import com.baiyue.firstleg.repository.ContainerRepository;
|
||||
import com.baiyue.firstleg.terminaldrayage.dto.ContainerWorkbenchDtos.StatusSummary;
|
||||
import com.baiyue.firstleg.terminaldrayage.dto.ContainerWorkbenchDtos.WorkbenchItem;
|
||||
import com.baiyue.firstleg.terminaldrayage.dto.TerminalDrayageDtos.View;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/** 基于同一货柜主档聚合国内、国外工作台读模型。 */
|
||||
@Service
|
||||
public class ContainerWorkbenchService {
|
||||
@Resource private ContainerRepository repository;
|
||||
@Resource private CarrierScheduleQueryService carrierSchedules;
|
||||
@Resource private TerminalDrayageService terminalDrayages;
|
||||
|
||||
public PageResult<WorkbenchItem> domesticPage(Query query, Set<String> permissions) {
|
||||
return page("DOMESTIC", query, permissions);
|
||||
}
|
||||
|
||||
public PageResult<WorkbenchItem> overseasPage(Query query, Set<String> permissions) {
|
||||
return page("OVERSEAS", query, permissions);
|
||||
}
|
||||
|
||||
public WorkbenchItem domesticDetail(long id, Set<String> permissions) {
|
||||
ContainerBookingEntity value = required(id);
|
||||
if (repository.departed(id)) throw new BizException(ResultCode.NOT_FOUND, "国内工作台货柜不存在");
|
||||
ContainerActualFacts facts = actualFacts(List.of(id)).get(id);
|
||||
return item("DOMESTIC", value, facts, null, permissions);
|
||||
}
|
||||
|
||||
public WorkbenchItem overseasDetail(long id, Set<String> permissions) {
|
||||
ContainerBookingEntity value = required(id);
|
||||
if (!repository.departed(id)) throw new BizException(ResultCode.NOT_FOUND, "国外工作台货柜不存在");
|
||||
View drayage = terminalDrayages.authorize(terminalDrayages.detail(id), permissions);
|
||||
return item("OVERSEAS", value, drayage.actualFacts(), drayage, permissions);
|
||||
}
|
||||
|
||||
private PageResult<WorkbenchItem> page(String workbench, Query query, Set<String> permissions) {
|
||||
validateRange(query.estimatedSailingFrom(), query.estimatedSailingTo(), "预计船期筛选");
|
||||
Query normalized = normalize(query);
|
||||
PageResult<ContainerBookingEntity> source = repository.page(workbench, normalized);
|
||||
List<Long> ids = source.records().stream().map(ContainerBookingEntity::getId).toList();
|
||||
Map<Long, ContainerActualFacts> facts = new HashMap<>();
|
||||
Map<Long, View> drayages = new HashMap<>();
|
||||
if (!ids.isEmpty() && "OVERSEAS".equals(workbench)) {
|
||||
terminalDrayages.authorize(terminalDrayages.views(ids), permissions).forEach(value -> {
|
||||
drayages.put(value.containerBookingId(), value);
|
||||
facts.put(value.containerBookingId(), value.actualFacts());
|
||||
});
|
||||
} else if (!ids.isEmpty()) {
|
||||
facts.putAll(actualFacts(ids));
|
||||
}
|
||||
List<WorkbenchItem> records = source.records().stream()
|
||||
.map(value -> item(workbench, value, facts.get(value.getId()), drayages.get(value.getId()), permissions))
|
||||
.toList();
|
||||
return new PageResult<>(records, source.page(), source.size(), source.total(), source.pages());
|
||||
}
|
||||
|
||||
private ContainerBookingEntity required(long id) {
|
||||
ContainerBookingEntity value = repository.find(id);
|
||||
if (value == null) throw new BizException(ResultCode.NOT_FOUND, "货柜不存在");
|
||||
return value;
|
||||
}
|
||||
|
||||
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 query) {
|
||||
return new Query(trim(query.containerNo()), trim(query.cabinetNo()), trim(query.billOfLadingNo()),
|
||||
trim(query.keyword()), query.customerId(), query.warehouseId(), trim(query.containerType()),
|
||||
query.shippingCompanyId(), trim(query.departurePortCode()), trim(query.destinationPortCode()),
|
||||
trim(query.destinationWarehouseCode()), query.status(), query.estimatedSailingFrom(),
|
||||
query.estimatedSailingTo(), query.scope(), query.page(), query.size());
|
||||
}
|
||||
|
||||
private Map<Long, ContainerActualFacts> actualFacts(List<Long> ids) {
|
||||
Map<Long, ContainerActualFacts> result = new HashMap<>();
|
||||
carrierSchedules.containerActualFacts(ids).forEach(value -> result.put(value.containerBookingId(), value));
|
||||
return result;
|
||||
}
|
||||
|
||||
private WorkbenchItem item(String workbench, ContainerBookingEntity container, ContainerActualFacts facts,
|
||||
View drayage, Set<String> permissions) {
|
||||
String stage = "DOMESTIC".equals(workbench) ? domesticStage(container) : overseasStage(container, facts, drayage);
|
||||
StatusSummary summary = new StatusSummary(container.getPlanStatus(),
|
||||
container.getCustomsDeclarationDocumentStatus(), container.getOutboundDispatchStatus(),
|
||||
container.getDestinationClearanceDocumentStatus(), container.getDeliveryOrderDocumentStatus(),
|
||||
trim(container.getPickupCode()) == null ? "MISSING" : "UPLOADED",
|
||||
facts == null ? null : facts.latestActualNode(), facts == null ? null : facts.latestActualAt(),
|
||||
drayage == null ? null : drayage.status(),
|
||||
drayage == null || drayage.emptyReturnWarning() == null ? null : drayage.emptyReturnWarning().status());
|
||||
List<String> actions = "DOMESTIC".equals(workbench)
|
||||
? domesticActions(container, permissions) : overseasActions(drayage, permissions);
|
||||
return new WorkbenchItem(container, stage, summary, actions, facts, drayage);
|
||||
}
|
||||
|
||||
private static String domesticStage(ContainerBookingEntity value) {
|
||||
if (value.getOutboundConfirmedAt() != null) return "DEPARTED_WAREHOUSE";
|
||||
if (value.getPlanStatus() == null || "WAIT_LOADING".equals(value.getPlanStatus())) return "WAIT_LOADING";
|
||||
if (!"COMPLETED".equals(value.getPlanStatus())) return "LOADING";
|
||||
if (!"COMPLETED".equals(value.getCustomsDeclarationDocumentStatus())) return "WAIT_CUSTOMS";
|
||||
return "WAIT_OUTBOUND";
|
||||
}
|
||||
|
||||
private static String overseasStage(ContainerBookingEntity value, ContainerActualFacts facts, View drayage) {
|
||||
if (drayage != null && "EMPTY_RETURNED".equals(drayage.status())) return "EMPTY_RETURNED";
|
||||
if (value.getUnloadingCompletedAt() != null) return "UNLOADED";
|
||||
if (drayage != null && "PICKED_UP".equals(drayage.status())) return "PICKED_UP";
|
||||
if (facts != null && facts.pickupReadyAt() != null) return "PICKUP_READY";
|
||||
if (facts != null && facts.arrivedDestinationPortAt() != null) return "ARRIVED_DESTINATION_PORT";
|
||||
if (facts != null && facts.departedOriginPortAt() != null) return "IN_TRANSIT";
|
||||
return "DEPARTED_WAREHOUSE";
|
||||
}
|
||||
|
||||
private static List<String> domesticActions(ContainerBookingEntity value, Set<String> permissions) {
|
||||
Set<String> granted = permissions == null ? Set.of() : permissions;
|
||||
List<String> actions = new ArrayList<>();
|
||||
if (granted.contains("platform:firstleg:container:edit")
|
||||
&& ("WAIT_LOADING".equals(value.getPlanStatus()) || "EDITING".equals(value.getPlanStatus()))) {
|
||||
actions.add("EDIT_CONTAINER");
|
||||
}
|
||||
if (granted.contains("platform:firstleg:customs:manage") && "COMPLETED".equals(value.getPlanStatus())
|
||||
&& value.getOutboundConfirmedAt() == null) actions.add("MANAGE_CUSTOMS");
|
||||
if (granted.contains("platform:firstleg:outbound-dispatch:confirm")
|
||||
&& "COMPLETED".equals(value.getPlanStatus())
|
||||
&& "COMPLETED".equals(value.getCustomsDeclarationDocumentStatus())
|
||||
&& value.getOutboundConfirmedAt() == null) actions.add("CONFIRM_OUTBOUND");
|
||||
return List.copyOf(actions);
|
||||
}
|
||||
|
||||
private static List<String> overseasActions(View drayage, Set<String> permissions) {
|
||||
Set<String> granted = permissions == null ? Set.of() : permissions;
|
||||
List<String> actions = new ArrayList<>();
|
||||
boolean returned = drayage != null && "EMPTY_RETURNED".equals(drayage.status());
|
||||
if (!returned && granted.contains("platform:firstleg:terminal-drayage:document:edit")) {
|
||||
actions.add("MANAGE_DESTINATION_DOCUMENTS");
|
||||
}
|
||||
if (drayage != null) actions.addAll(drayage.allowedActions());
|
||||
return List.copyOf(actions);
|
||||
}
|
||||
|
||||
private static String trim(String value) {
|
||||
if (value == null) return null;
|
||||
String result = value.trim();
|
||||
return result.isEmpty() ? null : result;
|
||||
}
|
||||
}
|
||||
+96
-49
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.portschedule.domain.CarrierScheduleNode;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ContainerActualFacts;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ManualNodeRequest;
|
||||
import com.baiyue.firstleg.portschedule.service.CarrierScheduleQueryService;
|
||||
import com.baiyue.firstleg.service.WarehouseSupplierSnapshotService;
|
||||
@@ -35,6 +36,7 @@ import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -56,7 +58,10 @@ public class TerminalDrayageService {
|
||||
if (ids.size() != containerBookingIds.size()) throw badRequest("containerBookingIds不能包含空值或重复值");
|
||||
Map<Long, TerminalDrayageEntity> rows = new HashMap<>();
|
||||
mapper.findAll(ids).forEach(value -> rows.put(value.getContainerBookingId(), value));
|
||||
return mapper.contexts(ids).stream().map(value -> view(value, rows.get(value.containerBookingId()))).toList();
|
||||
Map<Long, ContainerActualFacts> facts = actualFacts(ids);
|
||||
return mapper.contexts(ids).stream().map(value -> context(value, facts.get(value.containerBookingId())))
|
||||
.filter(Objects::nonNull)
|
||||
.map(value -> view(value, rows.get(value.base().containerBookingId()))).toList();
|
||||
}
|
||||
|
||||
public List<History> histories(long containerBookingId) {
|
||||
@@ -64,11 +69,25 @@ public class TerminalDrayageService {
|
||||
return mapper.histories(containerBookingId);
|
||||
}
|
||||
|
||||
public List<View> authorize(List<View> values, Set<String> permissions) {
|
||||
return values.stream().map(value -> authorize(value, permissions)).toList();
|
||||
}
|
||||
|
||||
public View authorize(View value, Set<String> permissions) {
|
||||
Set<String> granted = permissions == null ? Set.of() : permissions;
|
||||
List<String> actions = value.allowedActions().stream()
|
||||
.filter(action -> granted.contains(permission(action))).toList();
|
||||
return new View(value.containerBookingId(), value.containerNo(), value.oceanBookingId(), value.status(),
|
||||
value.version(), value.fleetSupplierId(), value.fleetSupplierName(), value.pickupAppointmentAt(),
|
||||
value.actualPickupAt(), value.estimatedEmptyReturnAt(), value.actualEmptyReturnAt(),
|
||||
value.unloadingCompletedAt(), value.emptyReturnWarning(), actions, value.actualFacts(), value.updatedAt());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public View selectFleet(long containerBookingId, long operatorId, String operatorName, String key,
|
||||
FleetSelectionRequest request) {
|
||||
String action = "SELECT_FLEET";
|
||||
Context context = requiredLockedContext(containerBookingId);
|
||||
WorkContext context = requiredLockedContext(containerBookingId);
|
||||
View replay = replay(context, action, key, request);
|
||||
if (replay != null) return replay;
|
||||
requireArrived(context);
|
||||
@@ -90,7 +109,7 @@ public class TerminalDrayageService {
|
||||
public View schedulePickup(long containerBookingId, long operatorId, String operatorName, String key,
|
||||
PickupAppointmentRequest request) {
|
||||
String action = "SCHEDULE_PICKUP";
|
||||
Context context = requiredLockedContext(containerBookingId);
|
||||
WorkContext context = requiredLockedContext(containerBookingId);
|
||||
View replay = replay(context, action, key, request);
|
||||
if (replay != null) return replay;
|
||||
requireArrived(context);
|
||||
@@ -98,7 +117,8 @@ public class TerminalDrayageService {
|
||||
requireVersion(value, request.expectedVersion());
|
||||
if (value.getFleetSupplierId() == null) throw conflict("请先选择有效码头车队");
|
||||
if (actualPickupAt(context, value) != null) throw conflict("实际提柜后不可修改预约提柜时间");
|
||||
if (context.actualArrivalNodeAt() != null && request.pickupAppointmentAt().isBefore(context.actualArrivalNodeAt())) {
|
||||
if (context.facts().arrivedDestinationPortAt() != null
|
||||
&& request.pickupAppointmentAt().isBefore(context.facts().arrivedDestinationPortAt())) {
|
||||
throw badRequest("预约提柜时间不能早于实际到港时间");
|
||||
}
|
||||
int fromVersion = version(value);
|
||||
@@ -113,7 +133,7 @@ public class TerminalDrayageService {
|
||||
public View recordPickup(long containerBookingId, long operatorId, String operatorName, String key,
|
||||
ActualTimeRequest request) {
|
||||
String action = "RECORD_PICKUP";
|
||||
Context context = requiredLockedContext(containerBookingId);
|
||||
WorkContext context = requiredLockedContext(containerBookingId);
|
||||
View replay = replay(context, action, key, request);
|
||||
if (replay != null) return replay;
|
||||
requireAtLeast(context, CarrierScheduleNode.PICKUP_READY, "货柜尚未清关放行,不允许确认实际提柜");
|
||||
@@ -123,7 +143,7 @@ public class TerminalDrayageService {
|
||||
throw conflict("选择码头车队并预约提柜后才能确认实际提柜");
|
||||
}
|
||||
if (actualPickupAt(context, value) != null) throw conflict("实际提柜时间已确认");
|
||||
if (context.latestActualAt() != null && request.actualAt().isBefore(context.latestActualAt())) {
|
||||
if (context.facts().latestActualAt() != null && request.actualAt().isBefore(context.facts().latestActualAt())) {
|
||||
throw badRequest("实际提柜时间不能早于当前实际船节点时间");
|
||||
}
|
||||
advanceNode(context, CarrierScheduleNode.PICKED_UP, request.actualAt(), action, key,
|
||||
@@ -141,16 +161,16 @@ public class TerminalDrayageService {
|
||||
public View recordEmptyReturn(long containerBookingId, long operatorId, String operatorName, String key,
|
||||
ActualTimeRequest request) {
|
||||
String action = "RECORD_EMPTY_RETURN";
|
||||
Context context = requiredLockedContext(containerBookingId);
|
||||
WorkContext context = requiredLockedContext(containerBookingId);
|
||||
View replay = replay(context, action, key, request);
|
||||
if (replay != null) return replay;
|
||||
TerminalDrayageEntity value = requiredAggregate(containerBookingId);
|
||||
requireVersion(value, request.expectedVersion());
|
||||
LocalDateTime pickupAt = actualPickupAt(context, value);
|
||||
if (pickupAt == null) throw conflict("实际提柜后才能确认还柜");
|
||||
if (context.unloadingCompletedAt() == null) throw conflict("海外卸柜收货完成后才能确认还柜");
|
||||
LocalDateTime earliest = pickupAt.isAfter(context.unloadingCompletedAt())
|
||||
? pickupAt : context.unloadingCompletedAt();
|
||||
if (context.base().unloadingCompletedAt() == null) throw conflict("海外卸柜收货完成后才能确认还柜");
|
||||
LocalDateTime earliest = pickupAt.isAfter(context.base().unloadingCompletedAt())
|
||||
? pickupAt : context.base().unloadingCompletedAt();
|
||||
if (request.actualAt().isBefore(earliest)) throw badRequest("实际还柜时间不能早于提柜或卸柜完成时间");
|
||||
if (actualEmptyReturnAt(context, value) != null) throw conflict("实际还柜时间已确认");
|
||||
advanceNode(context, CarrierScheduleNode.EMPTY_RETURNED, request.actualAt(), action, key,
|
||||
@@ -164,40 +184,40 @@ public class TerminalDrayageService {
|
||||
fromVersion, request.reason(), operatorId, operatorName);
|
||||
}
|
||||
|
||||
private void advanceNode(Context context, CarrierScheduleNode target, LocalDateTime actualAt,
|
||||
private void advanceNode(WorkContext context, CarrierScheduleNode target, LocalDateTime actualAt,
|
||||
String action, String key, String reason, long operatorId, String operatorName) {
|
||||
if (context.queryId() == null || context.queryVersion() == null) {
|
||||
if (context.facts().queryId() == null || context.facts().queryVersion() == null) {
|
||||
throw conflict("货柜尚未建立船节点跟踪任务");
|
||||
}
|
||||
CarrierScheduleNode current = node(context.latestActualNode());
|
||||
CarrierScheduleNode current = node(context.facts().latestActualNode());
|
||||
if (current != null && current.sequence() >= target.sequence()) return;
|
||||
carrierSchedules.addManualNode(context.queryId(), operatorId, operatorName,
|
||||
"td-" + hash(context.containerBookingId() + "|" + action + "|" + key.trim()),
|
||||
new ManualNodeRequest(context.queryVersion(), target.name(), LocalDateTime.now(clock), null,
|
||||
carrierSchedules.addManualNode(context.facts().queryId(), operatorId, operatorName,
|
||||
"td-" + hash(context.base().containerBookingId() + "|" + action + "|" + key.trim()),
|
||||
new ManualNodeRequest(context.facts().queryVersion(), target.name(), LocalDateTime.now(clock), null,
|
||||
actualAt, null, null, StringUtils.hasText(reason) ? reason.trim() : "码头拖车操作同步"));
|
||||
}
|
||||
|
||||
private View finish(Context context, TerminalDrayageEntity value, String action, String key, Object request,
|
||||
private View finish(WorkContext context, TerminalDrayageEntity value, String action, String key, Object request,
|
||||
String fromStatus, int fromVersion, String reason,
|
||||
long operatorId, String operatorName) {
|
||||
TerminalDrayageEntity stored = mapper.find(context.containerBookingId());
|
||||
Context currentContext = requiredContext(context.containerBookingId());
|
||||
TerminalDrayageEntity stored = mapper.find(context.base().containerBookingId());
|
||||
WorkContext currentContext = requiredContext(context.base().containerBookingId());
|
||||
String toStatus = status(currentContext, stored);
|
||||
mapper.insertHistory(IdWorker.getId(), stored.getId(), context.containerBookingId(), action,
|
||||
mapper.insertHistory(IdWorker.getId(), stored.getId(), context.base().containerBookingId(), action,
|
||||
fromStatus, toStatus, fromVersion, version(stored), trim(reason), operatorId, operatorName);
|
||||
mapper.insertOperation(IdWorker.getId(), context.containerBookingId(), action, key.trim(), hash(request),
|
||||
mapper.insertOperation(IdWorker.getId(), context.base().containerBookingId(), action, key.trim(), hash(request),
|
||||
version(stored), operatorId);
|
||||
return view(currentContext, stored);
|
||||
}
|
||||
|
||||
private View replay(Context context, String action, String key, Object request) {
|
||||
private View replay(WorkContext context, String action, String key, Object request) {
|
||||
if (!StringUtils.hasText(key) || key.length() > 255) throw badRequest("Idempotency-Key不能为空且不能超过255字符");
|
||||
Operation operation = mapper.operation(context.containerBookingId(), key.trim());
|
||||
Operation operation = mapper.operation(context.base().containerBookingId(), key.trim());
|
||||
if (operation == null) return null;
|
||||
if (!action.equals(operation.operation()) || !hash(request).equals(operation.requestHash())) {
|
||||
throw conflict("Idempotency-Key已被不同的码头拖车操作占用");
|
||||
}
|
||||
return view(context, mapper.find(context.containerBookingId()));
|
||||
return view(context, mapper.find(context.base().containerBookingId()));
|
||||
}
|
||||
|
||||
private TerminalDrayageEntity create(long containerBookingId, long operatorId, String operatorName) {
|
||||
@@ -216,16 +236,16 @@ public class TerminalDrayageService {
|
||||
if (mapper.updateAggregate(value, expectedVersion, operatorId, operatorName) != 1) throw concurrent();
|
||||
}
|
||||
|
||||
private void applyAuthoritativeDeadline(Context context, TerminalDrayageEntity value) {
|
||||
if (context.unloadingCompletedAt() != null) {
|
||||
value.setEstimatedEmptyReturnAt(context.unloadingCompletedAt().plusHours(EMPTY_RETURN_WARNING_HOURS));
|
||||
private void applyAuthoritativeDeadline(WorkContext context, TerminalDrayageEntity value) {
|
||||
if (context.base().unloadingCompletedAt() != null) {
|
||||
value.setEstimatedEmptyReturnAt(context.base().unloadingCompletedAt().plusHours(EMPTY_RETURN_WARNING_HOURS));
|
||||
}
|
||||
}
|
||||
|
||||
private View view(Context context, TerminalDrayageEntity value) {
|
||||
private View view(WorkContext context, TerminalDrayageEntity value) {
|
||||
String status = status(context, value);
|
||||
LocalDateTime deadline = context.unloadingCompletedAt() == null ? null
|
||||
: context.unloadingCompletedAt().plusHours(EMPTY_RETURN_WARNING_HOURS);
|
||||
LocalDateTime deadline = context.base().unloadingCompletedAt() == null ? null
|
||||
: context.base().unloadingCompletedAt().plusHours(EMPTY_RETURN_WARNING_HOURS);
|
||||
LocalDateTime returnedAt = actualEmptyReturnAt(context, value);
|
||||
String warningStatus;
|
||||
long overdueHours = 0;
|
||||
@@ -236,18 +256,18 @@ public class TerminalDrayageService {
|
||||
overdueHours = Math.max(0, Duration.between(deadline, LocalDateTime.now(clock)).toHours());
|
||||
} else warningStatus = "PENDING";
|
||||
Warning warning = new Warning(warningStatus, deadline, overdueHours, overdueHours / 24);
|
||||
return new View(context.containerBookingId(), context.containerNo(), context.oceanBookingId(), status,
|
||||
return new View(context.base().containerBookingId(), context.base().containerNo(), context.base().oceanBookingId(), status,
|
||||
version(value), value == null ? null : value.getFleetSupplierId(),
|
||||
value == null ? null : value.getFleetSupplierName(),
|
||||
value == null ? null : value.getPickupAppointmentAt(),
|
||||
actualPickupAt(context, value), deadline, returnedAt,
|
||||
context.unloadingCompletedAt(), warning, allowedActions(context, value),
|
||||
context.base().unloadingCompletedAt(), warning, allowedActions(context, value), context.facts(),
|
||||
value == null ? null : value.getUpdatedAt());
|
||||
}
|
||||
|
||||
private List<String> allowedActions(Context context, TerminalDrayageEntity value) {
|
||||
private List<String> allowedActions(WorkContext context, TerminalDrayageEntity value) {
|
||||
List<String> result = new ArrayList<>();
|
||||
CarrierScheduleNode current = node(context.latestActualNode());
|
||||
CarrierScheduleNode current = node(context.facts().latestActualNode());
|
||||
if (current != null && current.sequence() >= CarrierScheduleNode.ARRIVED_DESTINATION_PORT.sequence()
|
||||
&& current.sequence() < CarrierScheduleNode.PICKED_UP.sequence()
|
||||
&& actualPickupAt(context, value) == null) {
|
||||
@@ -257,27 +277,27 @@ public class TerminalDrayageService {
|
||||
if (current != null && current.sequence() >= CarrierScheduleNode.PICKUP_READY.sequence()
|
||||
&& value != null && value.getFleetSupplierId() != null && value.getPickupAppointmentAt() != null
|
||||
&& actualPickupAt(context, value) == null) result.add("RECORD_PICKUP");
|
||||
if (value != null && context.unloadingCompletedAt() != null && actualPickupAt(context, value) != null
|
||||
if (value != null && context.base().unloadingCompletedAt() != null && actualPickupAt(context, value) != null
|
||||
&& actualEmptyReturnAt(context, value) == null) result.add("RECORD_EMPTY_RETURN");
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
private String status(Context context, TerminalDrayageEntity value) {
|
||||
private String status(WorkContext context, TerminalDrayageEntity value) {
|
||||
if (actualEmptyReturnAt(context, value) != null) return "EMPTY_RETURNED";
|
||||
if (context.unloadingCompletedAt() != null && actualPickupAt(context, value) != null) return "UNLOADED";
|
||||
if (context.base().unloadingCompletedAt() != null && actualPickupAt(context, value) != null) return "UNLOADED";
|
||||
if (actualPickupAt(context, value) != null) return "PICKED_UP";
|
||||
if (value != null && value.getPickupAppointmentAt() != null) return "PICKUP_SCHEDULED";
|
||||
CarrierScheduleNode current = node(context.latestActualNode());
|
||||
CarrierScheduleNode current = node(context.facts().latestActualNode());
|
||||
return current != null && current.sequence() >= CarrierScheduleNode.ARRIVED_DESTINATION_PORT.sequence()
|
||||
? "ARRIVED" : "WAITING_FOR_ARRIVAL";
|
||||
}
|
||||
|
||||
private void requireArrived(Context context) {
|
||||
private void requireArrived(WorkContext context) {
|
||||
requireAtLeast(context, CarrierScheduleNode.ARRIVED_DESTINATION_PORT, "货柜实际到达目的港后才能安排码头车队");
|
||||
}
|
||||
|
||||
private void requireAtLeast(Context context, CarrierScheduleNode required, String message) {
|
||||
CarrierScheduleNode current = node(context.latestActualNode());
|
||||
private void requireAtLeast(WorkContext context, CarrierScheduleNode required, String message) {
|
||||
CarrierScheduleNode current = node(context.facts().latestActualNode());
|
||||
if (current == null || current.sequence() < required.sequence()) throw conflict(message);
|
||||
}
|
||||
|
||||
@@ -285,27 +305,45 @@ public class TerminalDrayageService {
|
||||
return StringUtils.hasText(value) ? CarrierScheduleNode.require(value) : null;
|
||||
}
|
||||
|
||||
private LocalDateTime actualPickupAt(Context context, TerminalDrayageEntity value) {
|
||||
return value != null && value.getActualPickupAt() != null ? value.getActualPickupAt() : context.actualPickupNodeAt();
|
||||
private LocalDateTime actualPickupAt(WorkContext context, TerminalDrayageEntity value) {
|
||||
return value != null && value.getActualPickupAt() != null ? value.getActualPickupAt() : context.facts().pickedUpAt();
|
||||
}
|
||||
|
||||
private LocalDateTime actualEmptyReturnAt(Context context, TerminalDrayageEntity value) {
|
||||
private LocalDateTime actualEmptyReturnAt(WorkContext context, TerminalDrayageEntity value) {
|
||||
return value != null && value.getActualEmptyReturnAt() != null
|
||||
? value.getActualEmptyReturnAt() : context.actualEmptyReturnNodeAt();
|
||||
? value.getActualEmptyReturnAt() : context.facts().emptyReturnedAt();
|
||||
}
|
||||
|
||||
private Context requiredContext(long id) {
|
||||
private WorkContext requiredContext(long id) {
|
||||
Context value = mapper.context(id);
|
||||
if (value == null || !"ACTIVE".equals(value.recordStatus())) throw notFound();
|
||||
return value;
|
||||
WorkContext context = context(value, actualFacts(List.of(id)).get(id));
|
||||
if (context == null) throw notFound();
|
||||
return context;
|
||||
}
|
||||
|
||||
private Context requiredLockedContext(long id) {
|
||||
private WorkContext requiredLockedContext(long id) {
|
||||
Context value = mapper.lockContext(id);
|
||||
if (value == null || !"ACTIVE".equals(value.recordStatus())) throw notFound();
|
||||
return value;
|
||||
WorkContext context = context(value, actualFacts(List.of(id)).get(id));
|
||||
if (context == null) throw notFound();
|
||||
return context;
|
||||
}
|
||||
|
||||
private Map<Long, ContainerActualFacts> actualFacts(List<Long> ids) {
|
||||
Map<Long, ContainerActualFacts> result = new HashMap<>();
|
||||
carrierSchedules.containerActualFacts(ids).forEach(value -> result.put(value.containerBookingId(), value));
|
||||
return result;
|
||||
}
|
||||
|
||||
private WorkContext context(Context base, ContainerActualFacts facts) {
|
||||
if (base == null || !"ACTIVE".equals(base.recordStatus()) || facts == null
|
||||
|| facts.departedWarehouseAt() == null) return null;
|
||||
return new WorkContext(base, facts);
|
||||
}
|
||||
|
||||
private record WorkContext(Context base, ContainerActualFacts facts) {}
|
||||
|
||||
private TerminalDrayageEntity requiredAggregate(long id) {
|
||||
TerminalDrayageEntity value = mapper.lock(id);
|
||||
if (value == null) throw conflict("请先选择码头车队");
|
||||
@@ -330,6 +368,15 @@ public class TerminalDrayageService {
|
||||
}
|
||||
|
||||
private static String trim(String value) { return StringUtils.hasText(value) ? value.trim() : null; }
|
||||
private static String permission(String action) {
|
||||
return switch (action) {
|
||||
case "SELECT_FLEET" -> "platform:firstleg:terminal-drayage:fleet:edit";
|
||||
case "SCHEDULE_PICKUP" -> "platform:firstleg:terminal-drayage:pickup-appointment:edit";
|
||||
case "RECORD_PICKUP" -> "platform:firstleg:terminal-drayage:pickup:confirm";
|
||||
case "RECORD_EMPTY_RETURN" -> "platform:firstleg:terminal-drayage:empty-return:confirm";
|
||||
default -> "";
|
||||
};
|
||||
}
|
||||
private static BizException badRequest(String message) { return new BizException(ResultCode.BAD_REQUEST, message); }
|
||||
private static BizException conflict(String message) { return new BizException(ResultCode.CONFLICT, message); }
|
||||
private static BizException concurrent() { return conflict("码头拖车记录已变化,请刷新后重试"); }
|
||||
|
||||
Reference in New Issue
Block a user