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

35 KiB
Raw Blame History

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

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:

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:

    /**
     * 货物高度等级: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:

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:

    @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:

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:

    Map<String, Object> lockInboundCargoSpace(
        String trayCode,
        String warehouseId,
        String roadwayId,
        BigDecimal height);

Change the matching implementation signature to:

    @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:

                cargoSpace = pdaService.lockInboundCargoSpace(
                    job.getTrayCode(),
                    job.getWarehouseId(),
                    job.getRoadwayId(),
                    request == null ? null : request.getHeight());
  • Step 8: Run both contract tests and verify GREEN

Run:

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:

    @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:

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:

        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:

        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:

    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:

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:

    @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:

        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:

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:

        if (!isSupportedCargoHeight(height)) {
            throw new RuntimeException("height必须为1或2");
        }

Add the mapper parameter:

        lockMap.put("v_height", height);

Replace the existing locked-row return block with:

        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:

    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:

                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:

                  and bcs.the_height &gt;= #{v_height}

Change the candidate ordering to:

            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:

        select top 1 cargo_space_id, cargo_space_name, roadway_id, the_height
  • Step 6: Run service and mapper tests and verify GREEN

Run:

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:

    @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:

        verify(basCargoSpaceMapper).selectAvailableInboundCargoSpaceInRoadway(
            argThat(candidate -> candidate.getHeight().compareTo(BigDecimal.ONE) == 0));
  • Step 2: Add the failing reallocation SQL test

Extend putOccupiedReallocationChoosesPlatoonWithMostAvailableCargoSpaces with:

        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:

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:

        if (!isSupportedCargoHeight(job.getHeight())) {
            return AjaxResult.error("入库作业height必须为1或2");
        }

Add this private helper near rollbackError:

    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:

                   case when bcs.the_height = #{height} then 0 else 1 end as height_priority,

Add this predicate after the roadway predicate:

              and bcs.the_height &gt;= #{height}

Change the final ordering to:

        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:

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

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:

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
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:

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:

$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:

$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:

$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:

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:

mvn -pl ruoyi-admin test

Expected: BUILD SUCCESS, 0 failures and 0 errors.

  • Step 3: Compile/package all required backend modules

Run:

mvn -pl ruoyi-admin -am package -DskipTests

Expected: BUILD SUCCESS for the reactor.

  • Step 4: Validate XML and diff hygiene

Run:

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.