diff --git a/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformContainerController.java b/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformContainerController.java index dd10cd1a4..e4e43d904 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformContainerController.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformContainerController.java @@ -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> 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> 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 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> 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 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 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(@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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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)); } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/ContainerDtos.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/ContainerDtos.java index 4a1cbb272..b20580354 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/dto/ContainerDtos.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/ContainerDtos.java @@ -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 ) { diff --git a/firstleg/src/main/java/com/baiyue/firstleg/dto/ContainerOverviewDtos.java b/firstleg/src/main/java/com/baiyue/firstleg/dto/ContainerOverviewDtos.java index 07375297d..0a742df39 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/dto/ContainerOverviewDtos.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/dto/ContainerOverviewDtos.java @@ -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) {} } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/entity/ContainerBookingEntity.java b/firstleg/src/main/java/com/baiyue/firstleg/entity/ContainerBookingEntity.java index 1279f8a48..6f6244d5c 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/entity/ContainerBookingEntity.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/entity/ContainerBookingEntity.java @@ -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; } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/mapper/ContainerBookingMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/mapper/ContainerBookingMapper.java index 138e5e138..16335a476 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/mapper/ContainerBookingMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/mapper/ContainerBookingMapper.java @@ -40,8 +40,14 @@ public interface ContainerBookingMapper extends BaseMapper """) - Page selectBookingPage(Page page, @Param("query") Query query); + Page selectBookingPage(Page 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 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 documents(@Param("id") long id); @Select(""" SELECT history.id,plan.container_booking_id containerBookingId, JSON_UNQUOTE(JSON_EXTRACT(history.before_snapshot,'$.containerNo')) beforeContainerNo, diff --git a/firstleg/src/main/java/com/baiyue/firstleg/repository/ContainerRepository.java b/firstleg/src/main/java/com/baiyue/firstleg/repository/ContainerRepository.java index e6024bc4c..cfc60b9d2 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/repository/ContainerRepository.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/repository/ContainerRepository.java @@ -12,15 +12,16 @@ import org.springframework.stereotype.Repository; @Repository public class ContainerRepository { @Resource private ContainerBookingMapper mapper; - public PageResult page(Query query) { + public PageResult page(String workbench, Query query) { Page 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() @@ -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; } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/ContainerOverviewService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/ContainerOverviewService.java index 8192fba2b..6556b6a4c 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/ContainerOverviewService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/ContainerOverviewService.java @@ -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); diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/ContainerService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/ContainerService.java index afa4f201a..4fbae5f56 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/ContainerService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/ContainerService.java @@ -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 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; diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingListService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingListService.java index db4f97006..769a69693 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingListService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/LoadingListService.java @@ -56,10 +56,8 @@ public class LoadingListService { private static final Set 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", diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/OverseasContainerEventConsumer.java b/firstleg/src/main/java/com/baiyue/firstleg/service/OverseasContainerEventConsumer.java index 1873105dd..5bdd15cc2 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/OverseasContainerEventConsumer.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/OverseasContainerEventConsumer.java @@ -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); diff --git a/firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/controller/PlatformTerminalDrayageController.java b/firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/controller/PlatformTerminalDrayageController.java index 777a9648f..3bcf2952d 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/controller/PlatformTerminalDrayageController.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/controller/PlatformTerminalDrayageController.java @@ -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> 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 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> 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())); } } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/dto/ContainerWorkbenchDtos.java b/firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/dto/ContainerWorkbenchDtos.java new file mode 100644 index 000000000..969e9f8bb --- /dev/null +++ b/firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/dto/ContainerWorkbenchDtos.java @@ -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 allowedActions, + ContainerActualFacts actualFacts, View terminalDrayage) {} +} diff --git a/firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/dto/TerminalDrayageDtos.java b/firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/dto/TerminalDrayageDtos.java index b193cf6b5..2148b61b2 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/dto/TerminalDrayageDtos.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/dto/TerminalDrayageDtos.java @@ -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 allowedActions, LocalDateTime updatedAt) {} + List allowedActions, ContainerActualFacts actualFacts, + LocalDateTime updatedAt) {} public record History(String action, String fromStatus, String toStatus, Integer fromVersion, Integer toVersion, String reason, diff --git a/firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/mapper/TerminalDrayageMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/mapper/TerminalDrayageMapper.java index f9124be45..6bf23c0dd 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/mapper/TerminalDrayageMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/terminaldrayage/mapper/TerminalDrayageMapper.java @@ -16,38 +16,16 @@ import java.util.List; public interface TerminalDrayageMapper extends BaseMapper { @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 @Select("""