test: verify dense outbound dispatch workflows

This commit is contained in:
zgw
2026-07-16 12:04:32 +08:00
parent 10ce28f8ac
commit cd2ceef8e6
19 changed files with 513 additions and 35 deletions

View File

@@ -173,11 +173,14 @@ public class DenseLaneOutboundServiceImpl implements IDenseLaneOutboundService
position.getCargoSpaceId(), position.getTrayCode());
position.setStockDetails(details == null ? new ArrayList<>() : details);
});
String[] trayCodes = selected.stream().map(DenseLanePosition::getTrayCode).toArray(String[]::new);
BigDecimal total = relocationJobMapper.sumTrayStockQuantity(trayCodes);
BigDecimal total = selected.stream()
.flatMap(position -> position.getStockDetails().stream())
.map(MydoJobDetail::getStockQuantity)
.filter(quantity -> quantity != null)
.reduce(BigDecimal.ZERO, BigDecimal::add);
DenseLaneOutboundPreview preview = new DenseLaneOutboundPreview();
preview.setTrays(selected);
preview.setTotalQuantity(total == null ? BigDecimal.ZERO : total);
preview.setTotalQuantity(total);
preview.setPreviewToken(previewToken(request, selected));
return preview;
}

View File

@@ -1,6 +1,5 @@
package com.ruoyi.out.relocation.mapper;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import com.ruoyi.in.job.domain.MydoJob;
@@ -21,8 +20,6 @@ public interface RelocationJobMapper
List<DenseLanePosition> selectRelocationSourcesForUpdate(RelocationGenerateRequest request);
BigDecimal sumTrayStockQuantity(@Param("trayCodes") String[] trayCodes);
int reserveTarget(Map<String, Object> params);
int reserveSource(Map<String, Object> params);

View File

@@ -82,9 +82,11 @@ public class RelocationJobServiceImpl implements IRelocationJobService
.stream().limit(request.getTrayCount()).collect(Collectors.toList());
RelocationPreview preview = new RelocationPreview();
preview.setTrays(selected);
String[] trayCodes = selected.stream().map(DenseLanePosition::getTrayCode).toArray(String[]::new);
BigDecimal total = mapper.sumTrayStockQuantity(trayCodes);
preview.setTotalQuantity(total == null ? BigDecimal.ZERO : total);
BigDecimal total = selected.stream()
.map(DenseLanePosition::getStockQuantity)
.filter(quantity -> quantity != null)
.reduce(BigDecimal.ZERO, BigDecimal::add);
preview.setTotalQuantity(total);
return preview;
}

View File

@@ -21,6 +21,30 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="maxColumn" column="max_column" />
</resultMap>
<resultMap id="DenseLaneOptionResult" type="com.ruoyi.out.dense.domain.DenseLaneOption">
<id property="groupCoding" column="group_coding" />
<result property="cargoArea" column="cargo_area" />
<result property="thePlatoon" column="the_platoon" />
<result property="theFloor" column="the_floor" />
<result property="directionType" column="direction_type" />
<result property="fullTrayCount" column="full_tray_count" />
<result property="emptyLocationCount" column="empty_location_count" />
<result property="totalQuantity" column="total_quantity" />
</resultMap>
<resultMap id="DenseLaneSummaryResult" type="com.ruoyi.out.dense.domain.DenseLaneSummary">
<id property="groupCoding" column="group_coding" />
<result property="cargoArea" column="cargo_area" />
<result property="thePlatoon" column="the_platoon" />
<result property="theFloor" column="the_floor" />
<result property="directionType" column="direction_type" />
<result property="fullTrayCount" column="full_tray_count" />
<result property="availableTrayCount" column="available_tray_count" />
<result property="totalQuantity" column="total_quantity" />
<result property="outermostTrayCode" column="outermost_tray_code" />
<result property="outermostCargoSpaceId" column="outermost_cargo_space_id" />
</resultMap>
<select id="selectPosition" resultMap="DenseLanePositionResult">
select source_space.cargo_space_id,
source_space.cargo_space_name,
@@ -178,7 +202,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<select id="selectRelocationTargetForUpdate"
parameterType="com.ruoyi.out.dense.domain.RelocationTargetQuery"
resultType="com.ruoyi.bas.cargospace.domain.BasCargoSpace">
resultMap="com.ruoyi.bas.cargospace.mapper.BasCargoSpaceMapper.BasCargoSpaceResult">
select top 1 target_space.*
from bas_cargo_space target_space with (UPDLOCK,HOLDLOCK,ROWLOCK)
left join bas_cargo_area_config area_config
@@ -235,7 +259,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
target_space.cargo_space_id asc
</select>
<select id="selectLaneOptions" resultType="com.ruoyi.out.dense.domain.DenseLaneOption">
<select id="selectLaneOptions" resultMap="DenseLaneOptionResult">
;with reserved_targets as
(
select distinct target_cargo_space_id as cargo_space_id
@@ -282,7 +306,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
order by lane.group_coding
</select>
<select id="selectLaneSummaries" resultType="com.ruoyi.out.dense.domain.DenseLaneSummary">
<select id="selectLaneSummaries" resultMap="DenseLaneSummaryResult">
;with lane_positions as
(
select cargo.cargo_space_id,

View File

@@ -4,14 +4,49 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.out.relocation.mapper.RelocationJobMapper">
<select id="selectJobForUpdate" resultType="com.ruoyi.in.job.domain.MydoJob">
<resultMap id="DenseLanePositionResult" type="com.ruoyi.out.dense.domain.DenseLanePosition">
<id property="cargoSpaceId" column="cargo_space_id" />
<result property="cargoSpaceName" column="cargo_space_name" />
<result property="warehouseId" column="warehouse_id" />
<result property="groupCoding" column="group_coding" />
<result property="cargoArea" column="cargo_area" />
<result property="thePlatoon" column="the_platoon" />
<result property="theColumn" column="the_column" />
<result property="theFloor" column="the_floor" />
<result property="status" column="status" />
<result property="locked" column="locked" />
<result property="trayCode" column="tray_code" />
<result property="directionType" column="direction_type" />
<result property="minColumn" column="min_column" />
<result property="maxColumn" column="max_column" />
<result property="stockQuantity" column="stock_quantity" />
</resultMap>
<resultMap id="StockDetailResult" type="com.ruoyi.in.jobdetail.domain.MydoJobDetail">
<result property="stockId" column="stock_id" />
<result property="lotNo" column="lot_no" />
<result property="inDate" column="in_date" />
<result property="stockinId" column="stockin_id" />
<result property="stockinDetailId" column="stockin_detail_id" />
<result property="productId" column="product_id" />
<result property="productName" column="product_name" />
<result property="stockQuantity" column="stock_quantity" />
<result property="customerId" column="customer_id" />
<result property="customerName" column="customer_name" />
<result property="supplierId" column="supplier_id" />
<result property="supplierName" column="supplier_name" />
<result property="projectNo" column="project_no" />
</resultMap>
<select id="selectJobForUpdate"
resultMap="com.ruoyi.in.job.mapper.MydoJobMapper.MydoJobResult">
select *
from mydo_job with (UPDLOCK,HOLDLOCK)
where job_id = #{jobId}
</select>
<select id="selectStockDetailsForUpdate"
resultType="com.ruoyi.in.jobdetail.domain.MydoJobDetail">
resultMap="StockDetailResult">
select detail.stock_id,
detail.lot_no,
convert(varchar(19), stock.in_date, 120) as in_date,
@@ -22,8 +57,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
detail.quantity as stock_quantity,
cast(null as nvarchar(255)) as customer_id,
cast(null as nvarchar(255)) as customer_name,
stock.suplier_id as supplier_id,
stock.suplier_name as supplier_name,
stock.supplier_id as supplier_id,
stock.supplier_name as supplier_name,
detail.project_no
from bas_stock stock with (UPDLOCK,HOLDLOCK)
inner join bas_stock_detail detail with (UPDLOCK,HOLDLOCK)
@@ -35,7 +70,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</select>
<select id="selectRelocationSourcesForUpdate"
resultType="com.ruoyi.out.dense.domain.DenseLanePosition">
resultMap="DenseLanePositionResult">
select cargo.cargo_space_id,
cargo.cargo_space_name,
cargo.warehouse_id,
@@ -87,16 +122,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
)
</select>
<select id="sumTrayStockQuantity" resultType="java.math.BigDecimal">
select isnull(sum(isnull(detail.quantity, 0)), 0)
from bas_stock stock
inner join bas_stock_detail detail on detail.stock_id = stock.stock_id
where stock.tray_code in
<foreach collection="trayCodes" item="trayCode" open="(" separator="," close=")">
#{trayCode}
</foreach>
</select>
<update id="reserveTarget" parameterType="java.util.Map">
update bas_cargo_space
set status = '3', tray_code = #{trayCode}

View File

@@ -51,6 +51,7 @@ class DenseLaneMapperSqlTest
String sql = section(xml, "<select id=\"selectRelocationTargetForUpdate\"", "</select>");
assertThat(sql)
.contains("resultMap=\"com.ruoyi.bas.cargospace.mapper.BasCargoSpaceMapper.BasCargoSpaceResult\"")
.containsIgnoringCase("UPDLOCK")
.containsIgnoringCase("HOLDLOCK")
.contains("warehouse_id = #{warehouseId}")
@@ -82,6 +83,25 @@ class DenseLaneMapperSqlTest
.doesNotContain("abs(target_space.the_column - 1)");
}
@Test
void laneOptionsAndSummariesUseExplicitSnakeCaseResultMaps() throws Exception
{
String xml = Files.readString(resolveMapperXml());
String optionMap = section(xml, "<resultMap id=\"DenseLaneOptionResult\"", "</resultMap>");
String summaryMap = section(xml, "<resultMap id=\"DenseLaneSummaryResult\"", "</resultMap>");
String optionSelect = section(xml, "<select id=\"selectLaneOptions\"", "</select>");
String summarySelect = section(xml, "<select id=\"selectLaneSummaries\"", "</select>");
assertThat(optionMap)
.contains("property=\"groupCoding\" column=\"group_coding\"")
.contains("property=\"emptyLocationCount\" column=\"empty_location_count\"");
assertThat(summaryMap)
.contains("property=\"groupCoding\" column=\"group_coding\"")
.contains("property=\"outermostTrayCode\" column=\"outermost_tray_code\"");
assertThat(optionSelect).contains("resultMap=\"DenseLaneOptionResult\"");
assertThat(summarySelect).contains("resultMap=\"DenseLaneSummaryResult\"");
}
private Path resolveMapperXml()
{
return Stream.of(

View File

@@ -106,8 +106,10 @@ class DenseLaneOutboundServiceImplTest
when(relocationJobMapper.selectRelocationSourcesForUpdate(any())).thenReturn(positions);
when(denseLaneService.orderOutermostFirst(positions))
.thenReturn(List.of(positions.get(1), positions.get(0), positions.get(2)));
when(relocationJobMapper.sumTrayStockQuantity(any(String[].class)))
.thenReturn(new BigDecimal("25"));
when(relocationJobMapper.selectStockDetailsForUpdate("CS-3", "A003"))
.thenReturn(List.of(detail("STOCK-3", "D-3", "P-3", "15")));
when(relocationJobMapper.selectStockDetailsForUpdate("CS-2", "A002"))
.thenReturn(List.of(detail("STOCK-2", "D-2", "P-2", "10")));
DenseLaneOutboundPreview preview = service.preview(request);
@@ -128,8 +130,6 @@ class DenseLaneOutboundServiceImplTest
when(relocationJobMapper.selectRelocationSourcesForUpdate(any())).thenReturn(List.of(position));
when(denseLaneService.orderOutermostFirst(any())).thenReturn(List.of(position));
when(relocationJobMapper.selectStockDetailsForUpdate("CS-1", "A001")).thenReturn(details);
when(relocationJobMapper.sumTrayStockQuantity(any(String[].class)))
.thenReturn(new BigDecimal("12"));
DenseLaneOutboundPreview preview = service.preview(request);
@@ -137,6 +137,22 @@ class DenseLaneOutboundServiceImplTest
assertThat(tray.getStockDetails()).containsExactlyElementsOf(details));
}
@Test
void previewTotalUsesOnlySelectedCargoSpaceInventoryDetails()
{
DenseLaneOutboundRequest request = request(1);
DenseLanePosition position = position("CS-1", 1L, "DUPLICATE-TRAY");
when(relocationJobMapper.selectRelocationSourcesForUpdate(any())).thenReturn(List.of(position));
when(denseLaneService.orderOutermostFirst(any())).thenReturn(List.of(position));
when(relocationJobMapper.selectStockDetailsForUpdate("CS-1", "DUPLICATE-TRAY"))
.thenReturn(List.of(detail("STOCK-1", "D-1", "P-1", "7"),
detail("STOCK-1", "D-2", "P-2", "5")));
DenseLaneOutboundPreview preview = service.preview(request);
assertThat(preview.getTotalQuantity()).isEqualByComparingTo("12");
}
@Test
void previewRejectsQuantityOverAvailableCapacity()
{
@@ -154,7 +170,6 @@ class DenseLaneOutboundServiceImplTest
DenseLanePosition position = position("CS-1", 1L, "A001");
when(relocationJobMapper.selectRelocationSourcesForUpdate(any())).thenReturn(List.of(position));
when(denseLaneService.orderOutermostFirst(any())).thenReturn(List.of(position));
when(relocationJobMapper.sumTrayStockQuantity(any(String[].class))).thenReturn(BigDecimal.TEN);
when(relocationJobMapper.selectStockDetailsForUpdate("CS-1", "A001"))
.thenReturn(List.of(detail("STOCK-1", "D-1", "P-1", "10")))
.thenReturn(List.of(detail("STOCK-1", "D-1", "P-1", "9")));
@@ -172,7 +187,6 @@ class DenseLaneOutboundServiceImplTest
DenseLanePosition first = position("CS-1", 1L, "A001");
when(relocationJobMapper.selectRelocationSourcesForUpdate(any())).thenReturn(List.of(first));
when(denseLaneService.orderOutermostFirst(any())).thenAnswer(invocation -> invocation.getArgument(0));
when(relocationJobMapper.sumTrayStockQuantity(any(String[].class))).thenReturn(BigDecimal.TEN);
request.setPreviewToken(service.preview(request).getPreviewToken());
DenseLanePosition changed = position("CS-2", 2L, "A002");
when(relocationJobMapper.selectRelocationSourcesForUpdate(any())).thenReturn(List.of(changed));
@@ -187,7 +201,6 @@ class DenseLaneOutboundServiceImplTest
DenseLanePosition position = position("CS-1", 1L, "A001");
when(relocationJobMapper.selectRelocationSourcesForUpdate(any())).thenReturn(List.of(position));
when(denseLaneService.orderOutermostFirst(any())).thenAnswer(invocation -> invocation.getArgument(0));
when(relocationJobMapper.sumTrayStockQuantity(any(String[].class))).thenReturn(new BigDecimal("12"));
List<MydoJobDetail> stockDetails = List.of(detail("STOCK-1", "D-1", "P-1", "7"),
detail("STOCK-1", "D-2", "P-2", "5"));
when(relocationJobMapper.selectStockDetailsForUpdate("CS-1", "A001"))

View File

@@ -0,0 +1,66 @@
package com.ruoyi.out.relocation.mapper;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class RelocationJobMapperSqlTest
{
@Test
void relocationRuntimeQueriesUseExplicitSnakeCaseResultMaps() throws Exception
{
String xml = Files.readString(resolveMapperXml());
String positionMap = section(xml, "<resultMap id=\"DenseLanePositionResult\"", "</resultMap>");
String stockMap = section(xml, "<resultMap id=\"StockDetailResult\"", "</resultMap>");
String jobSelect = section(xml, "<select id=\"selectJobForUpdate\"", "</select>");
String stockSelect = section(xml, "<select id=\"selectStockDetailsForUpdate\"", "</select>");
String sourceSelect = section(xml, "<select id=\"selectRelocationSourcesForUpdate\"", "</select>");
assertThat(positionMap)
.contains("property=\"cargoSpaceId\" column=\"cargo_space_id\"")
.contains("property=\"stockQuantity\" column=\"stock_quantity\"");
assertThat(stockMap)
.contains("property=\"stockId\" column=\"stock_id\"")
.contains("property=\"stockinDetailId\" column=\"stockin_detail_id\"")
.contains("property=\"projectNo\" column=\"project_no\"");
assertThat(jobSelect)
.contains("resultMap=\"com.ruoyi.in.job.mapper.MydoJobMapper.MydoJobResult\"");
assertThat(stockSelect).contains("resultMap=\"StockDetailResult\"");
assertThat(sourceSelect).contains("resultMap=\"DenseLanePositionResult\"");
}
@Test
void stockDetailQueryUsesActualBasStockSupplierColumns() throws Exception
{
String xml = Files.readString(resolveMapperXml());
String stockSelect = section(xml, "<select id=\"selectStockDetailsForUpdate\"", "</select>");
assertThat(stockSelect)
.contains("stock.supplier_id as supplier_id")
.contains("stock.supplier_name as supplier_name")
.doesNotContain("stock.suplier_id")
.doesNotContain("stock.suplier_name");
}
private Path resolveMapperXml()
{
return Stream.of(
Path.of("src/main/resources/mapper/out/RelocationJobMapper.xml"),
Path.of("ruoyi-admin/src/main/resources/mapper/out/RelocationJobMapper.xml"))
.filter(Files::exists)
.findFirst()
.orElseThrow();
}
private String section(String text, String startMarker, String endMarker)
{
int start = text.indexOf(startMarker);
assertThat(start).isGreaterThanOrEqualTo(0);
int end = text.indexOf(endMarker, start);
assertThat(end).isGreaterThan(start);
return text.substring(start, end);
}
}

View File

@@ -118,6 +118,24 @@ class RelocationJobServiceImplTest
.hasMessageContaining("不同通道");
}
@Test
void previewTotalUsesExactSelectedCargoSpaceQuantities()
{
RelocationGenerateRequest request = new RelocationGenerateRequest();
request.setWarehouseId("03");
request.setTheFloor(1L);
request.setSourceGroupCoding("SOURCE-GROUP");
request.setTargetGroupCoding("TARGET-GROUP");
request.setTrayCount(2);
DenseLanePosition first = source("CS-1", "DUPLICATE-TRAY");
first.setStockQuantity(new BigDecimal("7"));
DenseLanePosition second = source("CS-2", "UNIQUE-TRAY");
second.setStockQuantity(new BigDecimal("5"));
when(mapper.selectRelocationSourcesForUpdate(request)).thenReturn(List.of(first, second));
when(denseLaneService.orderOutermostFirst(any())).thenReturn(List.of(first, second));
assertThat(service.preview(request).getTotalQuantity()).isEqualByComparingTo("12");
}
@Test
void completionMovesInventorySwapsCargoSpacesArchivesAndWakesNextJob()
{