# Inbound Cargo Space Height Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Persist the WCS `height` level and enforce height-compatible, normal-space-first inbound allocation for warehouses 01 and 02, including `PUT_OCCUPIED` reallocation. **Architecture:** Add `height` to the WCS request contract and persist it in the existing `mydo_job.height` column. Pass the value into the normal SQL Server lock query, apply `the_height >= requested height`, and sort exact-height candidates before oversized candidates. Reuse the persisted job height in the existing transactional `PUT_OCCUPIED` path. Apply the 01/02 cargo-space master-data update through an idempotent, assertion-guarded SQL script. **Tech Stack:** Java 17, Spring Boot, MyBatis XML, SQL Server 2016, JUnit 5, Mockito, AssertJ, Maven, PowerShell `System.Data.SqlClient`. --- ## File Map - `ruoyi-admin/src/main/java/com/ruoyi/api/domain/WcsInboundLocationRequest.java`: WCS request `height` property. - `ruoyi-admin/src/main/java/com/ruoyi/api/controller/ExternalApiController.java`: request validation, idempotency, lock invocation, and height persistence. - `ruoyi-admin/src/main/java/com/ruoyi/pda/service/PdaService.java`: height-aware lock contract. - `ruoyi-admin/src/main/java/com/ruoyi/pda/service/impl/PdaServiceImpl.java`: mapper parameter propagation and stale-lock compatibility check. - `ruoyi-admin/src/main/resources/mapper/pda/PdaMapper.xml`: normal allocation capacity filter and exact-height priority. - `ruoyi-admin/src/main/java/com/ruoyi/in/job/service/impl/MydoJobServiceImpl.java`: reject invalid legacy heights before `PUT_OCCUPIED` changes the old location. - `ruoyi-admin/src/main/resources/mapper/bas/BasCargoSpaceMapper.xml`: height-compatible `PUT_OCCUPIED` candidate filter and ordering. - `ruoyi-admin/src/test/java/com/ruoyi/api/domain/WcsInboundLocationRequestTest.java`: DTO contract test. - `ruoyi-admin/src/test/java/com/ruoyi/api/controller/ExternalApiControllerWcsCodeTest.java`: validation, persistence, and retry behavior. - `ruoyi-admin/src/test/java/com/ruoyi/pda/service/impl/PdaServiceImplTest.java`: height propagation and stale-lock behavior. - `ruoyi-admin/src/test/java/com/ruoyi/pda/mapper/PdaMapperSqlTest.java`: normal allocation SQL assertions. - `ruoyi-admin/src/test/java/com/ruoyi/in/job/service/impl/MydoJobServiceImplTest.java`: invalid-height and inherited-height reallocation tests. - `ruoyi-admin/src/test/java/com/ruoyi/bas/cargospace/mapper/BasCargoSpaceMapperSqlTest.java`: reallocation SQL assertions. - `ruoyi-admin/src/test/java/com/ruoyi/bas/cargospace/mapper/CargoSpaceHeightMigrationSqlTest.java`: migration contract test. - `sql/20260710_cargo_space_height.sql`: guarded, repeatable 01/02 master-data update. The worktree already contains unrelated edits in `PdaServiceImpl.java`, `PdaMapper.xml`, `PdaServiceImplTest.java`, `MydoJobServiceImpl.java`, and other files. Preserve those edits and do not stage or commit an entire overlapping file. The implementation stays uncommitted for review unless the task-specific hunks can be isolated without staging user-owned changes. ### Task 1: Add the Height Contracts **Files:** - Create: `ruoyi-admin/src/test/java/com/ruoyi/api/domain/WcsInboundLocationRequestTest.java` - Modify: `ruoyi-admin/src/main/java/com/ruoyi/api/domain/WcsInboundLocationRequest.java` - Modify: `ruoyi-admin/src/test/java/com/ruoyi/pda/service/impl/PdaServiceImplTest.java` - Modify: `ruoyi-admin/src/main/java/com/ruoyi/pda/service/PdaService.java` - Modify: `ruoyi-admin/src/main/java/com/ruoyi/pda/service/impl/PdaServiceImpl.java` - [ ] **Step 1: Write the failing DTO contract test** ```java package com.ruoyi.api.domain; import org.junit.jupiter.api.Test; import java.lang.reflect.Method; import java.math.BigDecimal; import static org.assertj.core.api.Assertions.assertThat; class WcsInboundLocationRequestTest { @Test void carriesHeightLevelAsBigDecimal() throws Exception { WcsInboundLocationRequest request = new WcsInboundLocationRequest(); Method setter = WcsInboundLocationRequest.class.getMethod("setHeight", BigDecimal.class); Method getter = WcsInboundLocationRequest.class.getMethod("getHeight"); setter.invoke(request, BigDecimal.valueOf(2)); assertThat(getter.invoke(request)).isEqualTo(BigDecimal.valueOf(2)); } } ``` - [ ] **Step 2: Run the DTO test and verify RED** Run: ```powershell mvn -pl ruoyi-admin -Dtest=WcsInboundLocationRequestTest test ``` Expected: FAIL with `NoSuchMethodException: ...setHeight(java.math.BigDecimal)`. - [ ] **Step 3: Add the DTO field and accessors** Add `import java.math.BigDecimal;`, then add this field after `warehouseId` and these accessors before the closing brace: ```java /** * 货物高度等级:1=普通,2=高货。 */ private BigDecimal height; public BigDecimal getHeight() { return height; } public void setHeight(BigDecimal height) { this.height = height; } ``` - [ ] **Step 4: Run the DTO test and verify GREEN** Run: ```powershell mvn -pl ruoyi-admin -Dtest=WcsInboundLocationRequestTest test ``` Expected: PASS, 1 test and 0 failures. - [ ] **Step 5: Write the failing service-contract test** Add `import com.ruoyi.pda.service.PdaService;`, `import java.lang.reflect.Method;`, and `import java.util.Map;`, then add this test to `PdaServiceImplTest`: ```java @Test void inboundCargoSpaceLockContractAcceptsHeightLevel() throws Exception { Method method = PdaService.class.getMethod( "lockInboundCargoSpace", String.class, String.class, String.class, BigDecimal.class); assertThat(method.getReturnType()).isEqualTo(Map.class); } ``` - [ ] **Step 6: Run the service-contract test and verify RED** Run: ```powershell mvn -pl ruoyi-admin '-Dtest=PdaServiceImplTest#inboundCargoSpaceLockContractAcceptsHeightLevel' test ``` Expected: FAIL with `NoSuchMethodException` for the four-argument method. - [ ] **Step 7: Change the service contract without changing allocation behavior yet** Add `import java.math.BigDecimal;` to `PdaService.java` and change the declaration to: ```java Map lockInboundCargoSpace( String trayCode, String warehouseId, String roadwayId, BigDecimal height); ``` Change the matching implementation signature to: ```java @Override public Map lockInboundCargoSpace( String trayCode, String warehouseId, String roadwayId, BigDecimal height) { ``` Change the controller invocation so the project continues to compile with the new contract; Task 2 adds validation and persistence around this call: ```java cargoSpace = pdaService.lockInboundCargoSpace( job.getTrayCode(), job.getWarehouseId(), job.getRoadwayId(), request == null ? null : request.getHeight()); ``` - [ ] **Step 8: Run both contract tests and verify GREEN** Run: ```powershell mvn -pl ruoyi-admin '-Dtest=WcsInboundLocationRequestTest,PdaServiceImplTest#inboundCargoSpaceLockContractAcceptsHeightLevel' test ``` Expected: PASS, 2 tests and 0 failures. ### Task 2: Validate and Persist WCS Height **Files:** - Modify: `ruoyi-admin/src/test/java/com/ruoyi/api/controller/ExternalApiControllerWcsCodeTest.java` - Modify: `ruoyi-admin/src/main/java/com/ruoyi/api/controller/ExternalApiController.java` - [ ] **Step 1: Add controller tests for invalid height, first allocation, and retries** Add imports for `WcsInboundLocationRequest`, `ArgumentCaptor`, `BigDecimal`, `verifyNoInteractions`, and the following tests: ```java @Test void inboundLocationRejectsMissingHeightBeforeLookingUpJob() { WcsInboundLocationRequest request = new WcsInboundLocationRequest(); request.setJobId("RK001"); AjaxResult result = controller.inboundLocation(request); assertWcsError(result, "height必须为1或2"); verifyNoInteractions(mydoJobService, pdaService); } @Test void inboundLocationRejectsUnsupportedHeightBeforeLookingUpJob() { WcsInboundLocationRequest request = new WcsInboundLocationRequest(); request.setJobId("RK001"); request.setHeight(BigDecimal.valueOf(3)); AjaxResult result = controller.inboundLocation(request); assertWcsError(result, "height必须为1或2"); verifyNoInteractions(mydoJobService, pdaService); } @Test void inboundLocationPersistsHeightAfterLockingFirstCargoSpace() { WcsInboundLocationRequest request = new WcsInboundLocationRequest(); request.setJobId("RK001"); request.setHeight(BigDecimal.ONE); MydoJob job = new MydoJob(); job.setJobId("RK001"); job.setTrayCode("A001"); job.setWarehouseId("01"); job.setRoadwayId("R01"); when(mydoJobService.selectMydoJobByJobId("RK001")).thenReturn(job); when(pdaService.lockInboundCargoSpace("A001", "01", "R01", BigDecimal.ONE)) .thenReturn(Map.of( "roadway_id", "R01", "cargo_space_id", "CS001", "cargo_space_name", "01-01-01")); when(mydoJobService.updateMydoJob(job)).thenReturn(1); AjaxResult result = controller.inboundLocation(request); assertWcsSuccess(result); ArgumentCaptor captor = ArgumentCaptor.forClass(MydoJob.class); verify(mydoJobService).updateMydoJob(captor.capture()); assertThat(captor.getValue().getHeight()).isEqualByComparingTo("1"); assertThat(captor.getValue().getCargoSpaceId()).isEqualTo("CS001"); } @Test void inboundLocationReusesAssignedCargoSpaceForSameHeight() { WcsInboundLocationRequest request = new WcsInboundLocationRequest(); request.setJobId("RK001"); request.setHeight(BigDecimal.ONE); MydoJob job = new MydoJob(); job.setJobId("RK001"); job.setCargoSpaceId("CS001"); job.setHeight(new BigDecimal("1.00")); when(mydoJobService.selectMydoJobByJobId("RK001")).thenReturn(job); AjaxResult result = controller.inboundLocation(request); assertWcsSuccess(result); verifyNoInteractions(pdaService); verify(mydoJobService, never()).updateMydoJob(job); } @Test void inboundLocationRejectsDifferentHeightForAssignedCargoSpace() { WcsInboundLocationRequest request = new WcsInboundLocationRequest(); request.setJobId("RK001"); request.setHeight(BigDecimal.valueOf(2)); MydoJob job = new MydoJob(); job.setJobId("RK001"); job.setCargoSpaceId("CS001"); job.setHeight(BigDecimal.ONE); when(mydoJobService.selectMydoJobByJobId("RK001")).thenReturn(job); AjaxResult result = controller.inboundLocation(request); assertWcsError(result, "请求height与已分配作业不一致"); verifyNoInteractions(pdaService); verify(mydoJobService, never()).updateMydoJob(job); } ``` - [ ] **Step 2: Run the controller tests and verify RED** Run: ```powershell mvn -pl ruoyi-admin -Dtest=ExternalApiControllerWcsCodeTest test ``` Expected: the new tests FAIL because height is not validated, passed to the service, persisted, or compared on retries. - [ ] **Step 3: Implement validation, persistence, and retry checks** Add `import java.math.BigDecimal;`. At the start of `inboundLocation`, validate the request: ```java BigDecimal requestedHeight = request == null ? null : request.getHeight(); if (!isSupportedHeight(requestedHeight)) { return wcsError("height必须为1或2"); } ``` After resolving `job` and before locking, replace the existing cargo-space branch with: ```java if (job.getCargoSpaceId() != null && !job.getCargoSpaceId().trim().isEmpty()) { if (!isSupportedHeight(job.getHeight())) { return wcsError("已分配入库作业height无效"); } if (job.getHeight().compareTo(requestedHeight) != 0) { return wcsError("请求height与已分配作业不一致"); } return wcsSuccess(job); } Map cargoSpace; try { cargoSpace = pdaService.lockInboundCargoSpace( job.getTrayCode(), job.getWarehouseId(), job.getRoadwayId(), requestedHeight); } catch (RuntimeException e) { return wcsError(e.getMessage()); } job.setRoadwayId(String.valueOf(cargoSpace.get("roadway_id"))); job.setCargoSpaceId(String.valueOf(cargoSpace.get("cargo_space_id"))); job.setCargoSpaceName(String.valueOf(cargoSpace.get("cargo_space_name"))); job.setHeight(requestedHeight); int updateRows = mydoJobService.updateMydoJob(job); if (updateRows <= 0) { pdaService.releaseInboundCargoSpace(job.getTrayCode(), job.getWarehouseId()); return wcsError("入库货位回写作业失败"); } ``` Add this helper near the existing private response helpers: ```java private boolean isSupportedHeight(BigDecimal height) { return height != null && (height.compareTo(BigDecimal.ONE) == 0 || height.compareTo(BigDecimal.valueOf(2)) == 0); } ``` - [ ] **Step 4: Run the controller tests and verify GREEN** Run: ```powershell mvn -pl ruoyi-admin -Dtest=ExternalApiControllerWcsCodeTest test ``` Expected: PASS, all controller tests and 0 failures. ### Task 3: Enforce Height in Normal Cargo-Space Locking **Files:** - Modify: `ruoyi-admin/src/test/java/com/ruoyi/pda/service/impl/PdaServiceImplTest.java` - Modify: `ruoyi-admin/src/test/java/com/ruoyi/pda/mapper/PdaMapperSqlTest.java` - Modify: `ruoyi-admin/src/main/java/com/ruoyi/pda/service/impl/PdaServiceImpl.java` - Modify: `ruoyi-admin/src/main/resources/mapper/pda/PdaMapper.xml` - [ ] **Step 1: Add failing service tests** Add `import static org.mockito.Mockito.verifyNoInteractions;` if it is not already present, then add these tests to `PdaServiceImplTest`: ```java @Test void lockInboundCargoSpacePassesHeightAndReusesCompatibleLock() { HashMap rule = new HashMap<>(); rule.put("allowed_cargo_type", "A"); when(pdaMapper.selectMatchedTrayRule(any(HashMap.class))).thenReturn(rule); HashMap locked = new HashMap<>(); locked.put("cargo_space_id", "CS-HIGH"); locked.put("the_height", 2.0D); when(pdaMapper.selectLockedCargoSpaceByTrayCode(any(HashMap.class))).thenReturn(locked); Map result = service.lockInboundCargoSpace( "A001", "01", "R01", BigDecimal.ONE); assertThat(result).isSameAs(locked); ArgumentCaptor> captor = ArgumentCaptor.forClass(HashMap.class); verify(pdaMapper).selectLockedCargoSpaceByTrayCode(captor.capture()); assertThat(captor.getValue()).containsEntry("v_height", BigDecimal.ONE); verify(pdaMapper, never()).lockInboundCargoSpace(any(HashMap.class)); } @Test void lockInboundCargoSpaceRejectsIncompatibleExistingLock() { HashMap rule = new HashMap<>(); rule.put("allowed_cargo_type", "A"); when(pdaMapper.selectMatchedTrayRule(any(HashMap.class))).thenReturn(rule); HashMap locked = new HashMap<>(); locked.put("cargo_space_id", "CS-NORMAL"); locked.put("the_height", 1.0D); when(pdaMapper.selectLockedCargoSpaceByTrayCode(any(HashMap.class))).thenReturn(locked); assertThatThrownBy(() -> service.lockInboundCargoSpace( "A001", "01", "R01", BigDecimal.valueOf(2))) .isInstanceOf(RuntimeException.class) .hasMessageContaining("已锁定货位高度不满足"); verify(pdaMapper, never()).lockInboundCargoSpace(any(HashMap.class)); } @Test void lockInboundCargoSpaceRejectsUnsupportedHeight() { assertThatThrownBy(() -> service.lockInboundCargoSpace( "A001", "01", "R01", BigDecimal.ZERO)) .isInstanceOf(RuntimeException.class) .hasMessage("height必须为1或2"); verifyNoInteractions(pdaMapper); } ``` - [ ] **Step 2: Add the failing normal-allocation SQL test** Replace the existing `inboundCargoSpaceLockChoosesPlatoonWithMostEmptySpacesFirst` assertion body with: ```java assertThat(lockSql) .contains("and bcs.the_height >= #{v_height}") .contains("case when bcs.the_height = #{v_height} then 0 else 1 end as height_priority") .contains("count(1) over (partition by bcs.the_platoon) as platoon_empty_count") .contains("order by height_priority asc, platoon_empty_count desc"); String lockedSql = section( xml, "