From 302cc0135f35bc8171d727147caa464fba6664dd Mon Sep 17 00:00:00 2001 From: zgw <13623675548@163.com> Date: Fri, 10 Jul 2026 10:43:11 +0800 Subject: [PATCH] docs: plan cargo space height allocation --- .../2026-07-10-inbound-cargo-space-height.md | 970 ++++++++++++++++++ 1 file changed, 970 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-10-inbound-cargo-space-height.md diff --git a/docs/superpowers/plans/2026-07-10-inbound-cargo-space-height.md b/docs/superpowers/plans/2026-07-10-inbound-cargo-space-height.md new file mode 100644 index 0000000..7d39252 --- /dev/null +++ b/docs/superpowers/plans/2026-07-10-inbound-cargo-space-height.md @@ -0,0 +1,970 @@ +# 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, + "