diff --git a/deploy/sql/README.md b/deploy/sql/README.md index 9894151dd..8d8cd4021 100644 --- a/deploy/sql/README.md +++ b/deploy/sql/README.md @@ -230,9 +230,9 @@ SELECT ## 海外排车、出库与派送权限 - 国内固定入口已在海外仓业务 `lastleg-permissions.sql` 权限段之后完整内联 `iam/src/main/resources/db/phase5-delivery-permissions.sql`;测试和部署只执行固定入口,不再单独执行模块脚本。 -- 平台端固定使用排车列表/编辑/取消/转派送、出库列表/创建/取消、派送列表/退回/状态、异常订单列表/创建和客户回访列表/编辑十四项权限;列表权限包含详情及只读辅助接口,写操作保持独立授权。 +- 平台端固定使用排车列表/编辑/取消/转派送、出库列表/创建/取消、派送列表/退回/状态、异常订单列表/创建/处理和客户回访列表/编辑十五项权限;列表权限包含详情及只读辅助接口,写操作保持独立授权。 - 客户端固定使用 `merchant:warehouse:delivery:list` 和 `merchant:warehouse:delivery:create`;客户默认管理员获得两项权限,其他客户角色不自动扩权。 -- `WAREHOUSE_OPERATOR` 获得十四项后台作业权限,`WAREHOUSE_TALLY` 只获得排车、出库、派送、异常订单和客户回访五项列表只读;国内、海外 PDA 岗位不得获得上述后台权限。 +- `WAREHOUSE_OPERATOR` 获得十五项后台作业权限,`WAREHOUSE_TALLY` 只获得排车、出库、派送、异常订单和客户回访五项列表只读;国内、海外 PDA 岗位不得获得上述后台权限。 ## 海外排车与出库结构升级 diff --git a/deploy/sql/头程数据库统一增量-MySQL8.sql b/deploy/sql/头程数据库统一增量-MySQL8.sql index a11bed90f..0826af4d1 100644 --- a/deploy/sql/头程数据库统一增量-MySQL8.sql +++ b/deploy/sql/头程数据库统一增量-MySQL8.sql @@ -16966,6 +16966,7 @@ BEGIN OR (id = 5107093 AND permission_code <> 'platform:overseas-warehouse:outbound:cancel') OR (id = 5107070 AND permission_code <> 'platform:overseas-warehouse:exception-order:list') OR (id = 5107071 AND permission_code <> 'platform:overseas-warehouse:exception-order:create') + OR (id = 5107073 AND permission_code <> 'platform:overseas-warehouse:exception-order:process') OR (id = 5107072 AND permission_code <> 'platform:overseas-warehouse:delivery:return') OR (id = 5107080 AND permission_code <> 'platform:overseas-warehouse:customer-visit:list') OR (id = 5107081 AND permission_code <> 'platform:overseas-warehouse:customer-visit:edit') @@ -16984,6 +16985,7 @@ BEGIN OR (permission_code = 'platform:overseas-warehouse:outbound:cancel' AND id <> 5107093) OR (permission_code = 'platform:overseas-warehouse:exception-order:list' AND id <> 5107070) OR (permission_code = 'platform:overseas-warehouse:exception-order:create' AND id <> 5107071) + OR (permission_code = 'platform:overseas-warehouse:exception-order:process' AND id <> 5107073) OR (permission_code = 'platform:overseas-warehouse:delivery:return' AND id <> 5107072) OR (permission_code = 'platform:overseas-warehouse:customer-visit:list' AND id <> 5107080) OR (permission_code = 'platform:overseas-warehouse:customer-visit:edit' AND id <> 5107081) @@ -17008,6 +17010,7 @@ VALUES (5107018,0,'PLATFORM','BUTTON',5107064,'platform:overseas-warehouse:dispatch:transfer-delivery','排车转派送',NULL,'POST',718,'ACTIVE',1,0), (5107070,0,'PLATFORM','MENU',0,'platform:overseas-warehouse:exception-order:list','海外异常订单','/overseas-warehouse/exception-orders','GET',770,'ACTIVE',1,0), (5107071,0,'PLATFORM','BUTTON',5107070,'platform:overseas-warehouse:exception-order:create','新增海外异常订单',NULL,'POST',771,'ACTIVE',1,0), + (5107073,0,'PLATFORM','BUTTON',5107070,'platform:overseas-warehouse:exception-order:process','处理海外异常订单',NULL,'POST',773,'ACTIVE',1,0), (5107072,0,'PLATFORM','BUTTON',5107092,'platform:overseas-warehouse:delivery:return','派送单标记退货',NULL,'POST',772,'ACTIVE',1,0), (5107080,0,'PLATFORM','MENU',0,'platform:overseas-warehouse:customer-visit:list','客户回访','/overseas-warehouse/customer-visits','GET',780,'ACTIVE',1,0), (5107081,0,'PLATFORM','BUTTON',5107080,'platform:overseas-warehouse:customer-visit:edit','保存客户回访',NULL,'PUT',781,'ACTIVE',1,0), @@ -17043,6 +17046,7 @@ SELECT 0, role.id, permission.id, CURRENT_TIMESTAMP(3) 'platform:overseas-warehouse:delivery:list', 'platform:overseas-warehouse:exception-order:list', 'platform:overseas-warehouse:exception-order:create', + 'platform:overseas-warehouse:exception-order:process', 'platform:overseas-warehouse:delivery:return', 'platform:overseas-warehouse:delivery:status', 'platform:overseas-warehouse:customer-visit:list', @@ -17067,6 +17071,7 @@ INSERT INTO tmp_phase5_delivery_role_permission (role_code, permission_code) VAL ('WAREHOUSE_OPERATOR','platform:overseas-warehouse:delivery:list'), ('WAREHOUSE_OPERATOR','platform:overseas-warehouse:exception-order:list'), ('WAREHOUSE_OPERATOR','platform:overseas-warehouse:exception-order:create'), + ('WAREHOUSE_OPERATOR','platform:overseas-warehouse:exception-order:process'), ('WAREHOUSE_OPERATOR','platform:overseas-warehouse:delivery:return'), ('WAREHOUSE_OPERATOR','platform:overseas-warehouse:delivery:status'), ('WAREHOUSE_OPERATOR','platform:overseas-warehouse:customer-visit:list'), @@ -17095,6 +17100,7 @@ WHERE role.tenant_id=0 AND role.app_code='PLATFORM' 'platform:overseas-warehouse:delivery:list', 'platform:overseas-warehouse:exception-order:list', 'platform:overseas-warehouse:exception-order:create', + 'platform:overseas-warehouse:exception-order:process', 'platform:overseas-warehouse:delivery:return', 'platform:overseas-warehouse:delivery:status', 'platform:overseas-warehouse:customer-visit:list', @@ -17142,12 +17148,13 @@ BEGIN 'platform:overseas-warehouse:delivery:list', 'platform:overseas-warehouse:exception-order:list', 'platform:overseas-warehouse:exception-order:create', + 'platform:overseas-warehouse:exception-order:process', 'platform:overseas-warehouse:delivery:return', 'platform:overseas-warehouse:delivery:status', 'platform:overseas-warehouse:customer-visit:list', 'platform:overseas-warehouse:customer-visit:edit', 'merchant:warehouse:delivery:list', - 'merchant:warehouse:delivery:create')) <> 16 THEN + 'merchant:warehouse:delivery:create')) <> 17 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='五期海外派送权限初始化失败:权限目录不完整'; END IF; IF (SELECT COUNT(*) FROM iam_role_permission grant_row @@ -17165,10 +17172,11 @@ BEGIN 'platform:overseas-warehouse:delivery:list', 'platform:overseas-warehouse:exception-order:list', 'platform:overseas-warehouse:exception-order:create', + 'platform:overseas-warehouse:exception-order:process', 'platform:overseas-warehouse:delivery:return', 'platform:overseas-warehouse:delivery:status', 'platform:overseas-warehouse:customer-visit:list', - 'platform:overseas-warehouse:customer-visit:edit')) <> 14 THEN + 'platform:overseas-warehouse:customer-visit:edit')) <> 15 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='五期海外派送权限初始化失败:平台管理员授权不完整'; END IF; IF EXISTS (SELECT 1 FROM iam_role role @@ -17189,17 +17197,18 @@ BEGIN 'platform:overseas-warehouse:delivery:list', 'platform:overseas-warehouse:exception-order:list', 'platform:overseas-warehouse:exception-order:create', + 'platform:overseas-warehouse:exception-order:process', 'platform:overseas-warehouse:delivery:return', 'platform:overseas-warehouse:delivery:status', 'platform:overseas-warehouse:customer-visit:list', - 'platform:overseas-warehouse:customer-visit:edit')) <> 14) THEN + 'platform:overseas-warehouse:customer-visit:edit')) <> 15) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='五期海外派送权限初始化失败:平台自定义管理员授权不完整'; END IF; SELECT COALESCE(SUM(role_code='WAREHOUSE_OPERATOR'),0), COALESCE(SUM(role_code='WAREHOUSE_TALLY'),0) INTO v_warehouse_operator_permission_count, v_warehouse_tally_permission_count FROM tmp_phase5_delivery_role_permission; - IF v_warehouse_operator_permission_count <> 14 + IF v_warehouse_operator_permission_count <> 15 OR v_warehouse_tally_permission_count <> 5 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='五期海外派送权限初始化失败:仓库岗位最小权限矩阵异常'; END IF; @@ -17218,6 +17227,7 @@ BEGIN 'platform:overseas-warehouse:delivery:list', 'platform:overseas-warehouse:exception-order:list', 'platform:overseas-warehouse:exception-order:create', + 'platform:overseas-warehouse:exception-order:process', 'platform:overseas-warehouse:delivery:return', 'platform:overseas-warehouse:delivery:status', 'platform:overseas-warehouse:customer-visit:list', @@ -18287,6 +18297,149 @@ DELIMITER ; CALL assert_terminal_drayage_schema(); DROP PROCEDURE IF EXISTS assert_terminal_drayage_schema; +-- 货柜双工作台权限开始(与 iam/db/container-workspace-permissions.sql 保持一致) +-- 国内送柜报关与国外清关提柜双工作台权限;国内沿用现有货柜权限,国外直接收敛到独立权限族。 +USE baiyue_iam; +SET NAMES utf8mb4; + +DROP PROCEDURE IF EXISTS migrate_container_workspace_permissions; +DELIMITER $$ +CREATE PROCEDURE migrate_container_workspace_permissions() +BEGIN + IF EXISTS ( + SELECT 1 FROM iam_permission + WHERE (id = 5102090 AND permission_code <> 'platform:firstleg:container:list') + OR (id = 5102092 AND permission_code <> 'platform:firstleg:container:create') + OR (id = 5102093 AND permission_code <> 'platform:firstleg:container:edit') + OR (id = 5102200 AND permission_code <> 'platform:firstleg:terminal-drayage:list') + OR (id = 5102201 AND permission_code <> 'platform:firstleg:terminal-drayage:document:edit') + OR (id = 5102202 AND permission_code <> 'platform:firstleg:terminal-drayage:fleet:edit') + OR (id = 5102203 AND permission_code <> 'platform:firstleg:terminal-drayage:pickup-appointment:edit') + OR (id = 5102204 AND permission_code <> 'platform:firstleg:terminal-drayage:pickup:confirm') + OR (id = 5102205 AND permission_code <> 'platform:firstleg:terminal-drayage:empty-return:confirm') + ) OR EXISTS ( + SELECT 1 FROM iam_permission + WHERE (permission_code = 'platform:firstleg:container:list' AND id <> 5102090) + OR (permission_code = 'platform:firstleg:container:create' AND id <> 5102092) + OR (permission_code = 'platform:firstleg:container:edit' AND id <> 5102093) + OR (permission_code = 'platform:firstleg:terminal-drayage:list' AND id <> 5102200) + OR (permission_code = 'platform:firstleg:terminal-drayage:document:edit' AND id <> 5102201) + OR (permission_code = 'platform:firstleg:terminal-drayage:fleet:edit' AND id <> 5102202) + OR (permission_code = 'platform:firstleg:terminal-drayage:pickup-appointment:edit' AND id <> 5102203) + OR (permission_code = 'platform:firstleg:terminal-drayage:pickup:confirm' AND id <> 5102204) + OR (permission_code = 'platform:firstleg:terminal-drayage:empty-return:confirm' AND id <> 5102205) + ) THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='货柜双工作台权限初始化失败:权限ID或编码冲突'; + END IF; + + INSERT INTO iam_permission + (id, tenant_id, app_code, permission_type, parent_id, permission_code, + permission_name, route_path, http_method, sort_no, status, built_in, deleted) + VALUES + (5102090,0,'PLATFORM','MENU',5102000,'platform:firstleg:container:list', + '国内送柜报关','/container-loading/domestic','GET',40,'ACTIVE',1,0), + (5102092,0,'PLATFORM','BUTTON',5102090,'platform:firstleg:container:create', + '建立国内货柜',NULL,'POST',20,'ACTIVE',1,0), + (5102093,0,'PLATFORM','BUTTON',5102090,'platform:firstleg:container:edit', + '维护国内货柜资料',NULL,'PUT',30,'ACTIVE',1,0), + (5102200,0,'PLATFORM','MENU',5102000,'platform:firstleg:terminal-drayage:list', + '国外清关提柜','/container-loading/overseas','GET',41,'ACTIVE',1,0), + (5102201,0,'PLATFORM','BUTTON',5102200,'platform:firstleg:terminal-drayage:document:edit', + '维护目的港清关与DO/PU',NULL,'POST',10,'ACTIVE',1,0), + (5102202,0,'PLATFORM','BUTTON',5102200,'platform:firstleg:terminal-drayage:fleet:edit', + '维护目的港码头车队',NULL,'POST',20,'ACTIVE',1,0), + (5102203,0,'PLATFORM','BUTTON',5102200,'platform:firstleg:terminal-drayage:pickup-appointment:edit', + '预约目的港提柜',NULL,'POST',30,'ACTIVE',1,0), + (5102204,0,'PLATFORM','BUTTON',5102200,'platform:firstleg:terminal-drayage:pickup:confirm', + '确认目的港实际提柜',NULL,'POST',40,'ACTIVE',1,0), + (5102205,0,'PLATFORM','BUTTON',5102200,'platform:firstleg:terminal-drayage:empty-return:confirm', + '确认目的港实际还柜',NULL,'POST',50,'ACTIVE',1,0) + ON DUPLICATE KEY UPDATE + permission_type=VALUES(permission_type), parent_id=VALUES(parent_id), + permission_name=VALUES(permission_name), route_path=VALUES(route_path), + http_method=VALUES(http_method), sort_no=VALUES(sort_no), status='ACTIVE', + built_in=1, deleted=0; + + -- list 已包含详情只读;旧 container:view 不再作为别名或双权限入口。 + DELETE grant_row FROM iam_role_permission grant_row + JOIN iam_permission permission + ON permission.id=grant_row.permission_id AND permission.tenant_id=grant_row.tenant_id + WHERE permission.tenant_id=0 + AND permission.permission_code='platform:firstleg:container:view'; + UPDATE iam_permission + SET status='INACTIVE', deleted=1 + WHERE tenant_id=0 AND permission_code='platform:firstleg:container:view'; + + -- 平台超级管理员和现有订单运营保持货柜全链路能力;其他角色按两个菜单独立授权。 + INSERT IGNORE INTO iam_role_permission (tenant_id, role_id, permission_id, created_at) + SELECT 0, role.id, permission.id, CURRENT_TIMESTAMP(3) + FROM iam_role role + JOIN iam_permission permission + ON permission.tenant_id=0 AND permission.app_code='PLATFORM' + AND permission.permission_code IN ( + 'platform:firstleg:container:list', + 'platform:firstleg:container:create', + 'platform:firstleg:container:edit', + 'platform:firstleg:terminal-drayage:list', + 'platform:firstleg:terminal-drayage:document:edit', + 'platform:firstleg:terminal-drayage:fleet:edit', + 'platform:firstleg:terminal-drayage:pickup-appointment:edit', + 'platform:firstleg:terminal-drayage:pickup:confirm', + 'platform:firstleg:terminal-drayage:empty-return:confirm') + WHERE role.tenant_id=0 AND role.app_code='PLATFORM' + AND role.role_code IN ('PLATFORM_ADMIN','ORDER_OPERATOR') + AND role.status='ACTIVE' AND role.deleted=0 + AND permission.status='ACTIVE' AND permission.deleted=0; + + IF (SELECT COUNT(*) FROM iam_permission + WHERE tenant_id=0 AND app_code='PLATFORM' AND status='ACTIVE' AND deleted=0 + AND permission_code IN ( + 'platform:firstleg:container:list', + 'platform:firstleg:container:create', + 'platform:firstleg:container:edit', + 'platform:firstleg:terminal-drayage:list', + 'platform:firstleg:terminal-drayage:document:edit', + 'platform:firstleg:terminal-drayage:fleet:edit', + 'platform:firstleg:terminal-drayage:pickup-appointment:edit', + 'platform:firstleg:terminal-drayage:pickup:confirm', + 'platform:firstleg:terminal-drayage:empty-return:confirm')) <> 9 THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='货柜双工作台权限初始化失败:权限目录不完整'; + END IF; + + IF (SELECT COUNT(DISTINCT CONCAT(role.role_code, ':', permission.permission_code)) + FROM iam_role_permission grant_row + JOIN iam_role role + ON role.id=grant_row.role_id AND role.tenant_id=grant_row.tenant_id + JOIN iam_permission permission + ON permission.id=grant_row.permission_id AND permission.tenant_id=grant_row.tenant_id + WHERE role.tenant_id=0 AND role.app_code='PLATFORM' + AND role.role_code IN ('PLATFORM_ADMIN','ORDER_OPERATOR') + AND permission.permission_code IN ( + 'platform:firstleg:container:list', + 'platform:firstleg:container:create', + 'platform:firstleg:container:edit', + 'platform:firstleg:terminal-drayage:list', + 'platform:firstleg:terminal-drayage:document:edit', + 'platform:firstleg:terminal-drayage:fleet:edit', + 'platform:firstleg:terminal-drayage:pickup-appointment:edit', + 'platform:firstleg:terminal-drayage:pickup:confirm', + 'platform:firstleg:terminal-drayage:empty-return:confirm')) <> 18 THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='货柜双工作台权限初始化失败:平台默认角色授权不完整'; + END IF; + + IF EXISTS (SELECT 1 FROM iam_permission + WHERE tenant_id=0 AND permission_code='platform:firstleg:container:view' + AND (status='ACTIVE' OR deleted=0)) THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='货柜双工作台权限初始化失败:旧查看权限仍然有效'; + END IF; +END$$ +DELIMITER ; + +CALL migrate_container_workspace_permissions(); +DROP PROCEDURE IF EXISTS migrate_container_workspace_permissions; +-- 货柜双工作台权限结束,恢复头程数据库上下文 +USE baiyue_firstleg; + CALL baiyue_firstleg.assert_unified_final_schema(); DROP PROCEDURE IF EXISTS baiyue_firstleg.assert_unified_final_schema; SELECT 'PASS' AS migration_status, 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/portschedule/controller/PlatformCarrierScheduleQueryController.java b/firstleg/src/main/java/com/baiyue/firstleg/portschedule/controller/PlatformCarrierScheduleQueryController.java index 4b76ab3a0..de6190798 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/portschedule/controller/PlatformCarrierScheduleQueryController.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/portschedule/controller/PlatformCarrierScheduleQueryController.java @@ -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> 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> page(@AuthenticationPrincipal AuthenticatedUser user, diff --git a/firstleg/src/main/java/com/baiyue/firstleg/portschedule/dto/CarrierScheduleQueryDtos.java b/firstleg/src/main/java/com/baiyue/firstleg/portschedule/dto/CarrierScheduleQueryDtos.java index 6ff80c834..0bbfb2713 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/portschedule/dto/CarrierScheduleQueryDtos.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/portschedule/dto/CarrierScheduleQueryDtos.java @@ -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, diff --git a/firstleg/src/main/java/com/baiyue/firstleg/portschedule/mapper/CarrierScheduleQueryMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/portschedule/mapper/CarrierScheduleQueryMapper.java index 274bff0d6..283efa2e0 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/portschedule/mapper/CarrierScheduleQueryMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/portschedule/mapper/CarrierScheduleQueryMapper.java @@ -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 + 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 + #{id} + 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 + + """) + List selectContainerActualFacts( + @Param("containerBookingIds") List containerBookingIds); + @Select(""" SELECT * FROM fl_carrier_schedule_query WHERE status IN ('READY','MANUAL_REQUIRED') diff --git a/firstleg/src/main/java/com/baiyue/firstleg/portschedule/repository/CarrierScheduleQueryRepository.java b/firstleg/src/main/java/com/baiyue/firstleg/portschedule/repository/CarrierScheduleQueryRepository.java index 7df760374..c83cb6239 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/portschedule/repository/CarrierScheduleQueryRepository.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/portschedule/repository/CarrierScheduleQueryRepository.java @@ -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(List containerBookingIds) { + return queries.selectContainerActualFacts(containerBookingIds); + } public List 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; diff --git a/firstleg/src/main/java/com/baiyue/firstleg/portschedule/service/CarrierScheduleQueryService.java b/firstleg/src/main/java/com/baiyue/firstleg/portschedule/service/CarrierScheduleQueryService.java index 9a0bc0916..308f71969 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/portschedule/service/CarrierScheduleQueryService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/portschedule/service/CarrierScheduleQueryService.java @@ -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(List containerBookingIds) { + if (containerBookingIds == null || containerBookingIds.isEmpty() || containerBookingIds.size() > 200) { + throw badRequest("containerBookingIds数量必须在1到200之间"); + } + Set 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(long id) { 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("""