Files
FTC25009/docs/superpowers/plans/2026-07-10-inbound-cargo-space-height.md

971 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<String, Object> lockInboundCargoSpace(
String trayCode,
String warehouseId,
String roadwayId,
BigDecimal height);
```
Change the matching implementation signature to:
```java
@Override
public Map<String, Object> 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<MydoJob> 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<String, Object> 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<String, Object> rule = new HashMap<>();
rule.put("allowed_cargo_type", "A");
when(pdaMapper.selectMatchedTrayRule(any(HashMap.class))).thenReturn(rule);
HashMap<String, Object> 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<String, Object> result = service.lockInboundCargoSpace(
"A001", "01", "R01", BigDecimal.ONE);
assertThat(result).isSameAs(locked);
ArgumentCaptor<HashMap<String, Object>> 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<String, Object> rule = new HashMap<>();
rule.put("allowed_cargo_type", "A");
when(pdaMapper.selectMatchedTrayRule(any(HashMap.class))).thenReturn(rule);
HashMap<String, Object> 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 &gt;= #{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,
"<select id=\"selectLockedCargoSpaceByTrayCode\"",
"</select>");
assertThat(lockedSql).contains("the_height");
```
- [ ] **Step 3: Run service and mapper tests and verify RED**
Run:
```powershell
mvn -pl ruoyi-admin '-Dtest=PdaServiceImplTest#lockInboundCargoSpace*,PdaMapperSqlTest#inboundCargoSpaceLockChoosesPlatoonWithMostEmptySpacesFirst' test
```
Expected: FAIL because `v_height`, compatibility validation, capacity filtering, priority ordering, and locked-row height are absent.
- [ ] **Step 4: Implement service validation and mapper propagation**
At the start of `lockInboundCargoSpace`, before the roadway validation, add:
```java
if (!isSupportedCargoHeight(height)) {
throw new RuntimeException("height必须为1或2");
}
```
Add the mapper parameter:
```java
lockMap.put("v_height", height);
```
Replace the existing locked-row return block with:
```java
if (locked != null && !locked.isEmpty()) {
if (toBigDecimal(locked.get("the_height")).compareTo(height) < 0) {
throw new RuntimeException("已锁定货位高度不满足本次请求");
}
return locked;
}
```
Add this helper beside the existing `toBigDecimal` helper:
```java
private boolean isSupportedCargoHeight(BigDecimal height) {
return height != null
&& (height.compareTo(BigDecimal.ONE) == 0
|| height.compareTo(BigDecimal.valueOf(2)) == 0);
}
```
- [ ] **Step 5: Implement SQL capacity filtering and exact-height priority**
In the lock subquery, add `the_height` and `height_priority`:
```xml
select bcs.cargo_space_id,
bcs.the_platoon,
bcs.the_column,
bcs.the_floor,
bcs.the_height,
case when bcs.the_height = #{v_height} then 0 else 1 end as height_priority,
count(1) over (partition by bcs.the_platoon) as platoon_empty_count
```
Add this candidate condition after the roadway condition:
```xml
and bcs.the_height &gt;= #{v_height}
```
Change the candidate ordering to:
```xml
order by height_priority asc, platoon_empty_count desc,
the_platoon asc, the_floor asc, the_column asc, cargo_space_id asc
```
Return the locked row's capacity:
```xml
select top 1 cargo_space_id, cargo_space_name, roadway_id, the_height
```
- [ ] **Step 6: Run service and mapper tests and verify GREEN**
Run:
```powershell
mvn -pl ruoyi-admin '-Dtest=PdaServiceImplTest#lockInboundCargoSpace*,PdaMapperSqlTest#inboundCargoSpaceLockChoosesPlatoonWithMostEmptySpacesFirst' test
```
Expected: PASS, all selected tests and 0 failures.
### Task 4: Preserve Height During `PUT_OCCUPIED` Reallocation
**Files:**
- Modify: `ruoyi-admin/src/test/java/com/ruoyi/in/job/service/impl/MydoJobServiceImplTest.java`
- Modify: `ruoyi-admin/src/test/java/com/ruoyi/bas/cargospace/mapper/BasCargoSpaceMapperSqlTest.java`
- Modify: `ruoyi-admin/src/main/java/com/ruoyi/in/job/service/impl/MydoJobServiceImpl.java`
- Modify: `ruoyi-admin/src/main/resources/mapper/bas/BasCargoSpaceMapper.xml`
- [ ] **Step 1: Add the failing invalid-height reallocation test**
Add `import java.math.BigDecimal;` and this test:
```java
@Test
void handleWcsPutOccupiedExceptionRejectsUnsupportedJobHeightBeforeChangingLocation() {
MydoJob job = new MydoJob();
job.setJobId("RK002");
job.setInOrOut(10L);
job.setTrayCode("TRAY002");
job.setWarehouseId("01");
job.setRoadwayId("R01");
job.setCargoSpaceId("OLD-CS");
job.setHeight(BigDecimal.ZERO);
when(mydoJobMapper.selectMydoJobByJobId("RK002")).thenReturn(job);
AjaxResult result = service.handleWcsPutOccupiedException("RK002");
assertThat(result.isSuccess()).isFalse();
assertThat(result.get(AjaxResult.MSG_TAG)).isEqualTo("入库作业height必须为1或2");
verifyNoInteractions(basCargoSpaceMapper);
}
```
Set `job.setHeight(BigDecimal.ONE);` in the existing successful `handleWcsPutOccupiedExceptionMarksOldLocationAndReturnsReassignedInboundJob` fixture and add this verification after the call:
```java
verify(basCargoSpaceMapper).selectAvailableInboundCargoSpaceInRoadway(
argThat(candidate -> candidate.getHeight().compareTo(BigDecimal.ONE) == 0));
```
- [ ] **Step 2: Add the failing reallocation SQL test**
Extend `putOccupiedReallocationChoosesPlatoonWithMostAvailableCargoSpaces` with:
```java
assertThat(selectSql)
.contains("and bcs.the_height &gt;= #{height}")
.contains("case when bcs.the_height = #{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");
```
- [ ] **Step 3: Run the reallocation tests and verify RED**
Run:
```powershell
mvn -pl ruoyi-admin '-Dtest=MydoJobServiceImplTest#handleWcsPutOccupiedException*,BasCargoSpaceMapperSqlTest#putOccupiedReallocationChoosesPlatoonWithMostAvailableCargoSpaces' test
```
Expected: FAIL because invalid job height is not rejected and the reallocation SQL has no capacity or priority rule.
- [ ] **Step 4: Validate persisted height before changing the old location**
Add `import java.math.BigDecimal;`. After warehouse/roadway validation and before `markInboundNeedCheck`, add:
```java
if (!isSupportedCargoHeight(job.getHeight())) {
return AjaxResult.error("入库作业height必须为1或2");
}
```
Add this private helper near `rollbackError`:
```java
private boolean isSupportedCargoHeight(BigDecimal height) {
return height != null
&& (height.compareTo(BigDecimal.ONE) == 0
|| height.compareTo(BigDecimal.valueOf(2)) == 0);
}
```
- [ ] **Step 5: Add the same capacity and ordering rules to reallocation SQL**
Add this expression to the candidate projection before `platoon_empty_count`:
```xml
case when bcs.the_height = #{height} then 0 else 1 end as height_priority,
```
Add this predicate after the roadway predicate:
```xml
and bcs.the_height &gt;= #{height}
```
Change the final ordering to:
```xml
order by height_priority asc, platoon_empty_count desc,
the_platoon asc, the_floor asc, the_column asc, cargo_space_id asc
```
- [ ] **Step 6: Run the reallocation tests and verify GREEN**
Run:
```powershell
mvn -pl ruoyi-admin '-Dtest=MydoJobServiceImplTest#handleWcsPutOccupiedException*,BasCargoSpaceMapperSqlTest#putOccupiedReallocationChoosesPlatoonWithMostAvailableCargoSpaces' test
```
Expected: PASS, all selected tests and 0 failures.
### Task 5: Create and Execute the Guarded Master-Data Migration
**Files:**
- Create: `ruoyi-admin/src/test/java/com/ruoyi/bas/cargospace/mapper/CargoSpaceHeightMigrationSqlTest.java`
- Create: `sql/20260710_cargo_space_height.sql`
- [ ] **Step 1: Write the failing migration contract test**
```java
package com.ruoyi.bas.cargospace.mapper;
import org.junit.jupiter.api.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
class CargoSpaceHeightMigrationSqlTest {
@Test
void assignsHeightLevelsOnlyToWarehouses01And02WithSafetyChecks() throws Exception {
String sql = Files.readString(resolveSql()).toLowerCase();
assertThat(sql)
.contains("set xact_abort on")
.contains("begin transaction")
.contains("when the_floor in (1, 5) then 2 else 1 end")
.contains("where warehouse_id in ('01', '02')")
.contains("@target_count <> 340")
.contains("@high_count <> 136")
.contains("@normal_count <> 204")
.contains("rollback transaction")
.contains("commit transaction");
}
private Path resolveSql() {
return Stream.of(
Path.of("sql/20260710_cargo_space_height.sql"),
Path.of("../sql/20260710_cargo_space_height.sql"))
.filter(Files::exists)
.findFirst()
.orElseThrow();
}
}
```
- [ ] **Step 2: Run the migration contract test and verify RED**
Run:
```powershell
mvn -pl ruoyi-admin -Dtest=CargoSpaceHeightMigrationSqlTest test
```
Expected: FAIL with `NoSuchElementException` because the SQL file does not exist.
- [ ] **Step 3: Create the guarded, repeatable SQL script**
```sql
set nocount on;
set xact_abort on;
begin try
begin transaction;
declare @target_count int;
declare @high_count int;
declare @normal_count int;
declare @updated_rows int;
select @target_count = count(*)
from dbo.bas_cargo_space
where warehouse_id in ('01', '02');
select @high_count = count(*)
from dbo.bas_cargo_space
where warehouse_id in ('01', '02')
and the_floor in (1, 5);
select @normal_count = count(*)
from dbo.bas_cargo_space
where warehouse_id in ('01', '02')
and the_floor not in (1, 5);
if @target_count <> 340
begin
;throw 51001, 'Unexpected 01/02 cargo-space count; no changes committed.', 1;
end;
if @high_count <> 136
begin
;throw 51002, 'Unexpected high cargo-space count; no changes committed.', 1;
end;
if @normal_count <> 204
begin
;throw 51003, 'Unexpected normal cargo-space count; no changes committed.', 1;
end;
update dbo.bas_cargo_space
set the_height = case when the_floor in (1, 5) then 2 else 1 end
where warehouse_id in ('01', '02');
set @updated_rows = @@rowcount;
if @updated_rows <> @target_count
begin
;throw 51004, 'Not all 01/02 cargo spaces were updated; no changes committed.', 1;
end;
if exists (
select 1
from dbo.bas_cargo_space
where warehouse_id in ('01', '02')
and (
the_height is null
or the_height <> case when the_floor in (1, 5) then 2 else 1 end
)
)
begin
;throw 51005, 'Cargo-space height verification failed; no changes committed.', 1;
end;
commit transaction;
select warehouse_id,
the_floor,
the_height,
count(*) as cargo_space_count
from dbo.bas_cargo_space
where warehouse_id in ('01', '02')
group by warehouse_id, the_floor, the_height
order by warehouse_id, the_floor, the_height;
end try
begin catch
if @@trancount > 0
rollback transaction;
throw;
end catch;
```
- [ ] **Step 4: Run the migration contract test and verify GREEN**
Run:
```powershell
mvn -pl ruoyi-admin -Dtest=CargoSpaceHeightMigrationSqlTest test
```
Expected: PASS, 1 test and 0 failures.
- [ ] **Step 5: Re-run the read-only preflight immediately before writing**
Run this read-only PowerShell block from the repository root. It extracts the configured master connection without printing credentials:
```powershell
$config = Get-Content -Raw 'ruoyi-admin/src/main/resources/application-druid.yml'
$master = [regex]::Match($config, '(?ms)^[ \t]*master:[ \t]*\r?\n(?<body>.*?)(?=^[ \t]*slave:)').Groups['body'].Value
$url = [regex]::Match($master, '(?m)^[ \t]*url:[ \t]*(.+)$').Groups[1].Value.Trim()
$username = [regex]::Match($master, '(?m)^[ \t]*username:[ \t]*(.+)$').Groups[1].Value.Trim()
$password = [regex]::Match($master, '(?m)^[ \t]*password:[ \t]*(.+)$').Groups[1].Value.Trim()
$jdbc = [regex]::Match($url, 'jdbc:sqlserver://(?<host>[^:;]+):(?<port>\d+);databaseName=(?<db>[^;]+)')
$builder = [System.Data.SqlClient.SqlConnectionStringBuilder]::new()
$builder.DataSource = '{0},{1}' -f $jdbc.Groups['host'].Value, $jdbc.Groups['port'].Value
$builder.InitialCatalog = $jdbc.Groups['db'].Value
$builder.UserID = $username
$builder.Password = $password
$builder.Encrypt = $false
$builder.TrustServerCertificate = $true
$builder.ApplicationIntent = [System.Data.SqlClient.ApplicationIntent]::ReadOnly
$connection = [System.Data.SqlClient.SqlConnection]::new($builder.ConnectionString)
try {
$connection.Open()
$command = $connection.CreateCommand()
$command.CommandText = @'
select warehouse_id,
the_floor,
the_height,
count(*) as cargo_space_count
from dbo.bas_cargo_space
where warehouse_id in ('01', '02')
group by warehouse_id, the_floor, the_height
order by warehouse_id, the_floor, the_height;
'@
$table = [System.Data.DataTable]::new()
$adapter = [System.Data.SqlClient.SqlDataAdapter]::new($command)
[void] $adapter.Fill($table)
$table | Format-Table -AutoSize
} finally {
$connection.Dispose()
}
```
Expected before first execution: 340 rows total, all currently `NULL`; 136 rows on floors 1/5 and 204 rows on floors 2/3/4.
- [ ] **Step 6: Execute the migration against `FTC25009-WMS` without printing credentials**
Run this PowerShell block from the repository root:
```powershell
$config = Get-Content -Raw 'ruoyi-admin/src/main/resources/application-druid.yml'
$master = [regex]::Match($config, '(?ms)^[ \t]*master:[ \t]*\r?\n(?<body>.*?)(?=^[ \t]*slave:)').Groups['body'].Value
$url = [regex]::Match($master, '(?m)^[ \t]*url:[ \t]*(.+)$').Groups[1].Value.Trim()
$username = [regex]::Match($master, '(?m)^[ \t]*username:[ \t]*(.+)$').Groups[1].Value.Trim()
$password = [regex]::Match($master, '(?m)^[ \t]*password:[ \t]*(.+)$').Groups[1].Value.Trim()
$jdbc = [regex]::Match($url, 'jdbc:sqlserver://(?<host>[^:;]+):(?<port>\d+);databaseName=(?<db>[^;]+)')
$builder = [System.Data.SqlClient.SqlConnectionStringBuilder]::new()
$builder.DataSource = '{0},{1}' -f $jdbc.Groups['host'].Value, $jdbc.Groups['port'].Value
$builder.InitialCatalog = $jdbc.Groups['db'].Value
$builder.UserID = $username
$builder.Password = $password
$builder.Encrypt = $false
$builder.TrustServerCertificate = $true
$connection = [System.Data.SqlClient.SqlConnection]::new($builder.ConnectionString)
try {
$connection.Open()
$command = $connection.CreateCommand()
$command.CommandTimeout = 60
$command.CommandText = Get-Content -Raw 'sql/20260710_cargo_space_height.sql'
[void] $command.ExecuteNonQuery()
} finally {
$connection.Dispose()
}
```
Expected: no exception. Any count or invariant mismatch raises SQL error 51001-51005 and rolls back.
- [ ] **Step 7: Query and record the live post-migration distribution**
Run this read-only PowerShell block:
```powershell
$config = Get-Content -Raw 'ruoyi-admin/src/main/resources/application-druid.yml'
$master = [regex]::Match($config, '(?ms)^[ \t]*master:[ \t]*\r?\n(?<body>.*?)(?=^[ \t]*slave:)').Groups['body'].Value
$url = [regex]::Match($master, '(?m)^[ \t]*url:[ \t]*(.+)$').Groups[1].Value.Trim()
$username = [regex]::Match($master, '(?m)^[ \t]*username:[ \t]*(.+)$').Groups[1].Value.Trim()
$password = [regex]::Match($master, '(?m)^[ \t]*password:[ \t]*(.+)$').Groups[1].Value.Trim()
$jdbc = [regex]::Match($url, 'jdbc:sqlserver://(?<host>[^:;]+):(?<port>\d+);databaseName=(?<db>[^;]+)')
$builder = [System.Data.SqlClient.SqlConnectionStringBuilder]::new()
$builder.DataSource = '{0},{1}' -f $jdbc.Groups['host'].Value, $jdbc.Groups['port'].Value
$builder.InitialCatalog = $jdbc.Groups['db'].Value
$builder.UserID = $username
$builder.Password = $password
$builder.Encrypt = $false
$builder.TrustServerCertificate = $true
$builder.ApplicationIntent = [System.Data.SqlClient.ApplicationIntent]::ReadOnly
$connection = [System.Data.SqlClient.SqlConnection]::new($builder.ConnectionString)
try {
$connection.Open()
$command = $connection.CreateCommand()
$command.CommandText = @'
select warehouse_id,
the_floor,
the_height,
count(*) as cargo_space_count
from dbo.bas_cargo_space
where warehouse_id in ('01', '02')
group by warehouse_id, the_floor, the_height
order by warehouse_id, the_floor, the_height;
'@
$table = [System.Data.DataTable]::new()
$adapter = [System.Data.SqlClient.SqlDataAdapter]::new($command)
[void] $adapter.Fill($table)
$table | Format-Table -AutoSize
} finally {
$connection.Dispose()
}
```
Expected:
- Warehouse 01: floors 1 and 5 each have 48 rows at height 2; floors 2, 3, and 4 each have 48 rows at height 1.
- Warehouse 02: floors 1 and 5 each have 20 rows at height 2; floors 2, 3, and 4 each have 20 rows at height 1.
- Totals: 136 height-2 rows, 204 height-1 rows, 0 null/other rows.
### Task 6: Full Verification and Completion Audit
**Files:**
- Verify all files listed above.
- Do not stage unrelated worktree changes.
- [ ] **Step 1: Run focused tests for every new rule**
Run:
```powershell
mvn -pl ruoyi-admin -Dtest=WcsInboundLocationRequestTest,ExternalApiControllerWcsCodeTest,PdaServiceImplTest,PdaMapperSqlTest,MydoJobServiceImplTest,BasCargoSpaceMapperSqlTest,CargoSpaceHeightMigrationSqlTest test
```
Expected: PASS, 0 failures and 0 errors.
- [ ] **Step 2: Run the complete backend test suite**
Run:
```powershell
mvn -pl ruoyi-admin test
```
Expected: `BUILD SUCCESS`, 0 failures and 0 errors.
- [ ] **Step 3: Compile/package all required backend modules**
Run:
```powershell
mvn -pl ruoyi-admin -am package -DskipTests
```
Expected: `BUILD SUCCESS` for the reactor.
- [ ] **Step 4: Validate XML and diff hygiene**
Run:
```powershell
git diff --check
git status --short
git diff -- ruoyi-admin/src/main/java/com/ruoyi/api/domain/WcsInboundLocationRequest.java ruoyi-admin/src/main/java/com/ruoyi/api/controller/ExternalApiController.java ruoyi-admin/src/main/java/com/ruoyi/pda/service/PdaService.java ruoyi-admin/src/main/java/com/ruoyi/pda/service/impl/PdaServiceImpl.java ruoyi-admin/src/main/resources/mapper/pda/PdaMapper.xml ruoyi-admin/src/main/java/com/ruoyi/in/job/service/impl/MydoJobServiceImpl.java ruoyi-admin/src/main/resources/mapper/bas/BasCargoSpaceMapper.xml sql/20260710_cargo_space_height.sql
```
Expected: no whitespace errors. Review the diff to confirm existing inventory/PDA changes remain intact and only task-specific hunks were added.
- [ ] **Step 5: Audit each requirement against authoritative evidence**
Confirm all of the following:
- Live DB query proves 01/02 floors 1/5 are `the_height=2` and all other 01/02 floors are `the_height=1`.
- Controller tests prove missing/invalid height rejection, persistence, same-height idempotency, and different-height conflict.
- Normal mapper SQL proves height 2 cannot select height 1 and height 1 ranks height 1 before height 2.
- `PUT_OCCUPIED` tests and SQL prove reallocation uses the persisted job height with the same rules.
- Full test and package commands both have fresh successful output.
Do not claim completion or mark the goal complete until all five evidence items are present.