feat: add KPI dense lane relocation
This commit is contained in:
@@ -10,6 +10,8 @@ import com.ruoyi.kpi.service.KpiService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@@ -63,6 +65,27 @@ public class KpiController extends BaseController
|
||||
return AjaxResult.success(kpiService.getDenseCargoSpacePlanData(warehouseId));
|
||||
}
|
||||
|
||||
@GetMapping("/denseRelocation/options")
|
||||
@PreAuthorize("@ss.hasPermi('kpi:cargo:space:plan')")
|
||||
public AjaxResult denseRelocationOptions(
|
||||
com.ruoyi.out.relocation.domain.RelocationGenerateRequest request) {
|
||||
return AjaxResult.success(kpiService.getDenseRelocationOptions(request));
|
||||
}
|
||||
|
||||
@PostMapping("/denseRelocation/preview")
|
||||
@PreAuthorize("@ss.hasPermi('kpi:cargo:space:plan')")
|
||||
public AjaxResult previewDenseRelocation(
|
||||
@RequestBody com.ruoyi.out.relocation.domain.RelocationGenerateRequest request) {
|
||||
return AjaxResult.success(kpiService.previewDenseRelocation(request));
|
||||
}
|
||||
|
||||
@PostMapping("/denseRelocation/generate")
|
||||
@PreAuthorize("@ss.hasPermi('kpi:cargo:space:plan')")
|
||||
public AjaxResult generateDenseRelocation(
|
||||
@RequestBody com.ruoyi.out.relocation.domain.RelocationGenerateRequest request) {
|
||||
return AjaxResult.success(kpiService.generateDenseRelocation(request));
|
||||
}
|
||||
|
||||
/**
|
||||
* 货位平面图-货位详情
|
||||
*/
|
||||
|
||||
@@ -63,4 +63,13 @@ public interface KpiService
|
||||
* @return 包含 msg 的Map
|
||||
*/
|
||||
Map<String, Object> getCargoSpaceMag(String warehouseId, String thePlatoon, String theColumn, String theFloor);
|
||||
|
||||
Map<String, Object> getDenseRelocationOptions(
|
||||
com.ruoyi.out.relocation.domain.RelocationGenerateRequest request);
|
||||
|
||||
com.ruoyi.out.relocation.domain.RelocationPreview previewDenseRelocation(
|
||||
com.ruoyi.out.relocation.domain.RelocationGenerateRequest request);
|
||||
|
||||
List<String> generateDenseRelocation(
|
||||
com.ruoyi.out.relocation.domain.RelocationGenerateRequest request);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,11 @@ import com.ruoyi.in.job.mapper.MydoJobMapper;
|
||||
import com.ruoyi.kpi.domain.*;
|
||||
import com.ruoyi.kpi.mapper.KpiMapper;
|
||||
import com.ruoyi.kpi.service.KpiService;
|
||||
import com.ruoyi.out.dense.domain.DenseLaneOption;
|
||||
import com.ruoyi.out.dense.mapper.DenseLaneMapper;
|
||||
import com.ruoyi.out.relocation.domain.RelocationGenerateRequest;
|
||||
import com.ruoyi.out.relocation.domain.RelocationPreview;
|
||||
import com.ruoyi.out.relocation.service.IRelocationJobService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -43,6 +48,12 @@ public class KpiServiceImpl implements KpiService {
|
||||
@Autowired
|
||||
private MydoJobMapper mydoJobMapper;
|
||||
|
||||
@Autowired
|
||||
private DenseLaneMapper denseLaneMapper;
|
||||
|
||||
@Autowired
|
||||
private IRelocationJobService relocationJobService;
|
||||
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
private static final String UNASSIGNED_ROADWAY = "未分配";
|
||||
@@ -420,4 +431,54 @@ public class KpiServiceImpl implements KpiService {
|
||||
result.put("msg", msg.toString());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getDenseRelocationOptions(RelocationGenerateRequest request)
|
||||
{
|
||||
validateDenseRelocationBase(request);
|
||||
List<DenseLaneOption> options = denseLaneMapper.selectLaneOptions(
|
||||
request.getWarehouseId(), request.getTheFloor());
|
||||
if (options == null) {
|
||||
options = Collections.emptyList();
|
||||
}
|
||||
int requiredCapacity = request.getTrayCount() == null || request.getTrayCount() <= 0
|
||||
? 1 : request.getTrayCount();
|
||||
List<DenseLaneOption> sourceLanes = options.stream()
|
||||
.filter(item -> item.getFullTrayCount() != null && item.getFullTrayCount() > 0)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
List<DenseLaneOption> targetLanes = options.stream()
|
||||
.filter(item -> !StringUtils.hasText(request.getSourceGroupCoding())
|
||||
|| !request.getSourceGroupCoding().equals(item.getGroupCoding()))
|
||||
.filter(item -> item.getEmptyLocationCount() != null
|
||||
&& item.getEmptyLocationCount() >= requiredCapacity)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("sourceLanes", sourceLanes);
|
||||
result.put("targetLanes", targetLanes);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RelocationPreview previewDenseRelocation(RelocationGenerateRequest request)
|
||||
{
|
||||
validateDenseRelocationBase(request);
|
||||
return relocationJobService.preview(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> generateDenseRelocation(RelocationGenerateRequest request)
|
||||
{
|
||||
validateDenseRelocationBase(request);
|
||||
return relocationJobService.generate(request);
|
||||
}
|
||||
|
||||
private void validateDenseRelocationBase(RelocationGenerateRequest request)
|
||||
{
|
||||
if (request == null || !"03".equals(request.getWarehouseId())) {
|
||||
throw new RuntimeException("指定移库仅支持03仓密集库");
|
||||
}
|
||||
if (request.getTheFloor() == null) {
|
||||
throw new RuntimeException("指定移库必须选择当前层");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.ruoyi.out.dense.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 密集库通道移库选项。
|
||||
*/
|
||||
public class DenseLaneOption
|
||||
{
|
||||
private String groupCoding;
|
||||
private String cargoArea;
|
||||
private Long thePlatoon;
|
||||
private Long theFloor;
|
||||
private String directionType;
|
||||
private Integer fullTrayCount;
|
||||
private Integer emptyLocationCount;
|
||||
private BigDecimal totalQuantity;
|
||||
|
||||
public String getGroupCoding() { return groupCoding; }
|
||||
public void setGroupCoding(String groupCoding) { this.groupCoding = groupCoding; }
|
||||
public String getCargoArea() { return cargoArea; }
|
||||
public void setCargoArea(String cargoArea) { this.cargoArea = cargoArea; }
|
||||
public Long getThePlatoon() { return thePlatoon; }
|
||||
public void setThePlatoon(Long thePlatoon) { this.thePlatoon = thePlatoon; }
|
||||
public Long getTheFloor() { return theFloor; }
|
||||
public void setTheFloor(Long theFloor) { this.theFloor = theFloor; }
|
||||
public String getDirectionType() { return directionType; }
|
||||
public void setDirectionType(String directionType) { this.directionType = directionType; }
|
||||
public Integer getFullTrayCount() { return fullTrayCount; }
|
||||
public void setFullTrayCount(Integer fullTrayCount) { this.fullTrayCount = fullTrayCount; }
|
||||
public Integer getEmptyLocationCount() { return emptyLocationCount; }
|
||||
public void setEmptyLocationCount(Integer emptyLocationCount) { this.emptyLocationCount = emptyLocationCount; }
|
||||
public BigDecimal getTotalQuantity() { return totalQuantity; }
|
||||
public void setTotalQuantity(BigDecimal totalQuantity) { this.totalQuantity = totalQuantity; }
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.ruoyi.out.dense.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 密集库通道货位及其几何信息。
|
||||
*/
|
||||
@@ -19,6 +21,7 @@ public class DenseLanePosition
|
||||
private String directionType;
|
||||
private Long minColumn;
|
||||
private Long maxColumn;
|
||||
private BigDecimal stockQuantity;
|
||||
|
||||
public String getCargoSpaceId() { return cargoSpaceId; }
|
||||
public void setCargoSpaceId(String cargoSpaceId) { this.cargoSpaceId = cargoSpaceId; }
|
||||
@@ -48,4 +51,6 @@ public class DenseLanePosition
|
||||
public void setMinColumn(Long minColumn) { this.minColumn = minColumn; }
|
||||
public Long getMaxColumn() { return maxColumn; }
|
||||
public void setMaxColumn(Long maxColumn) { this.maxColumn = maxColumn; }
|
||||
public BigDecimal getStockQuantity() { return stockQuantity; }
|
||||
public void setStockQuantity(BigDecimal stockQuantity) { this.stockQuantity = stockQuantity; }
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.ruoyi.out.dense.mapper;
|
||||
import java.util.List;
|
||||
import com.ruoyi.bas.cargospace.domain.BasCargoSpace;
|
||||
import com.ruoyi.out.dense.domain.DenseLanePosition;
|
||||
import com.ruoyi.out.dense.domain.DenseLaneOption;
|
||||
import com.ruoyi.out.dense.domain.RelocationTargetQuery;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
@@ -20,4 +21,7 @@ public interface DenseLaneMapper
|
||||
List<DenseLanePosition> selectBlockersForUpdate(@Param("cargoSpaceId") String cargoSpaceId);
|
||||
|
||||
BasCargoSpace selectRelocationTargetForUpdate(RelocationTargetQuery query);
|
||||
|
||||
List<DenseLaneOption> selectLaneOptions(@Param("warehouseId") String warehouseId,
|
||||
@Param("theFloor") Long theFloor);
|
||||
}
|
||||
|
||||
@@ -219,6 +219,53 @@ 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">
|
||||
;with reserved_targets as
|
||||
(
|
||||
select distinct target_cargo_space_id as cargo_space_id
|
||||
from mydo_job
|
||||
where status in ('1', '2', '3')
|
||||
and target_cargo_space_id is not null
|
||||
), stock_totals as
|
||||
(
|
||||
select stock_space.group_coding,
|
||||
stock_space.the_floor,
|
||||
sum(isnull(detail.quantity, 0)) as total_quantity
|
||||
from bas_cargo_space stock_space
|
||||
inner join bas_stock stock on stock.cargo_space_id = stock_space.cargo_space_id
|
||||
inner join bas_stock_detail detail on detail.stock_id = stock.stock_id
|
||||
where stock_space.warehouse_id = #{warehouseId}
|
||||
and stock_space.the_floor = #{theFloor}
|
||||
group by stock_space.group_coding, stock_space.the_floor
|
||||
)
|
||||
select lane.group_coding,
|
||||
min(lane.this_cargo_area) as cargo_area,
|
||||
min(lane.the_platoon) as the_platoon,
|
||||
lane.the_floor,
|
||||
isnull(min(config.direction_type), '1') as direction_type,
|
||||
sum(case when lane.status = '2' and lane.tray_code is not null then 1 else 0 end) as full_tray_count,
|
||||
sum(case when lane.status = '1'
|
||||
and isnull(lane.locked, 'N') = 'N'
|
||||
and lane.tray_code is null
|
||||
and reserved.cargo_space_id is null then 1 else 0 end) as empty_location_count,
|
||||
isnull(max(stock_totals.total_quantity), 0) as total_quantity
|
||||
from bas_cargo_space lane
|
||||
left join bas_cargo_area_config config
|
||||
on config.warehouse_id = lane.warehouse_id
|
||||
and config.cargo_area_code = lane.this_cargo_area
|
||||
and config.is_active = '1'
|
||||
left join reserved_targets reserved on reserved.cargo_space_id = lane.cargo_space_id
|
||||
left join stock_totals
|
||||
on stock_totals.group_coding = lane.group_coding
|
||||
and stock_totals.the_floor = lane.the_floor
|
||||
where lane.warehouse_id = #{warehouseId}
|
||||
and lane.the_floor = #{theFloor}
|
||||
and lane.group_coding is not null
|
||||
and ltrim(rtrim(lane.group_coding)) != ''
|
||||
group by lane.group_coding, lane.the_floor
|
||||
order by lane.group_coding
|
||||
</select>
|
||||
|
||||
<select id="isCurrentOutermost" resultType="java.lang.Integer">
|
||||
select case
|
||||
when source_space.warehouse_id != '03'
|
||||
|
||||
@@ -48,7 +48,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
cargo.tray_code,
|
||||
isnull(config.direction_type, '1') as direction_type,
|
||||
boundary.min_column,
|
||||
boundary.max_column
|
||||
boundary.max_column,
|
||||
isnull(stock_total.stock_quantity, 0) as stock_quantity
|
||||
from bas_cargo_space cargo with (UPDLOCK,HOLDLOCK)
|
||||
left join bas_cargo_area_config config
|
||||
on config.warehouse_id = cargo.warehouse_id
|
||||
@@ -63,6 +64,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
and lane.group_coding = cargo.group_coding
|
||||
and lane.the_floor = cargo.the_floor
|
||||
) boundary
|
||||
outer apply
|
||||
(
|
||||
select sum(isnull(detail.quantity, 0)) as stock_quantity
|
||||
from bas_stock stock
|
||||
inner join bas_stock_detail detail on detail.stock_id = stock.stock_id
|
||||
where stock.cargo_space_id = cargo.cargo_space_id
|
||||
and stock.tray_code = cargo.tray_code
|
||||
) stock_total
|
||||
where cargo.warehouse_id = #{warehouseId}
|
||||
and cargo.the_floor = #{theFloor}
|
||||
and cargo.group_coding = #{sourceGroupCoding}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.ruoyi.kpi.service.impl;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.ruoyi.bas.cargospace.mapper.BasCargoSpaceMapper;
|
||||
import com.ruoyi.bas.stock.mapper.BasStockDetailMapper;
|
||||
import com.ruoyi.in.job.mapper.MydoJobMapper;
|
||||
import com.ruoyi.kpi.mapper.KpiMapper;
|
||||
import com.ruoyi.out.dense.domain.DenseLaneOption;
|
||||
import com.ruoyi.out.dense.mapper.DenseLaneMapper;
|
||||
import com.ruoyi.out.relocation.domain.RelocationGenerateRequest;
|
||||
import com.ruoyi.out.relocation.domain.RelocationPreview;
|
||||
import com.ruoyi.out.relocation.service.IRelocationJobService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class KpiDenseRelocationServiceTest
|
||||
{
|
||||
@Mock private KpiMapper kpiMapper;
|
||||
@Mock private BasCargoSpaceMapper basCargoSpaceMapper;
|
||||
@Mock private BasStockDetailMapper basStockDetailMapper;
|
||||
@Mock private MydoJobMapper mydoJobMapper;
|
||||
@Mock private DenseLaneMapper denseLaneMapper;
|
||||
@Mock private IRelocationJobService relocationJobService;
|
||||
|
||||
@InjectMocks
|
||||
private KpiServiceImpl service;
|
||||
|
||||
@Test
|
||||
void denseRelocationOptionsOnlyAcceptWarehouse03()
|
||||
{
|
||||
RelocationGenerateRequest request = request("02", 1L, "A", null, 1);
|
||||
|
||||
assertThatThrownBy(() -> service.getDenseRelocationOptions(request))
|
||||
.hasMessageContaining("03仓");
|
||||
}
|
||||
|
||||
@Test
|
||||
void optionsReturnFullSourceLanesAndSameFloorTargetsWithEnoughCapacity()
|
||||
{
|
||||
DenseLaneOption source = lane("A", 2L, 3, 0);
|
||||
DenseLaneOption enough = lane("B", 2L, 0, 2);
|
||||
DenseLaneOption insufficient = lane("C", 2L, 0, 1);
|
||||
when(denseLaneMapper.selectLaneOptions("03", 2L))
|
||||
.thenReturn(List.of(source, enough, insufficient));
|
||||
RelocationGenerateRequest request = request("03", 2L, "A", null, 2);
|
||||
|
||||
Map<String, Object> result = service.getDenseRelocationOptions(request);
|
||||
|
||||
assertThat(result.get("sourceLanes")).asList().containsExactly(source);
|
||||
assertThat(result.get("targetLanes")).asList().containsExactly(enough);
|
||||
}
|
||||
|
||||
@Test
|
||||
void previewReturnsExactOuterTraysAndInventoryTotal()
|
||||
{
|
||||
RelocationGenerateRequest request = request("03", 2L, "A", "B", 2);
|
||||
RelocationPreview preview = new RelocationPreview();
|
||||
preview.setTotalQuantity(new BigDecimal("25"));
|
||||
when(relocationJobService.preview(request)).thenReturn(preview);
|
||||
|
||||
assertThat(service.previewDenseRelocation(request).getTotalQuantity())
|
||||
.isEqualByComparingTo("25");
|
||||
verify(relocationJobService).preview(request);
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateRevalidatesThroughTransactionalRelocationService()
|
||||
{
|
||||
RelocationGenerateRequest request = request("03", 2L, "A", "B", 2);
|
||||
when(relocationJobService.generate(request)).thenReturn(List.of("MOVE-1", "MOVE-2"));
|
||||
|
||||
assertThat(service.generateDenseRelocation(request))
|
||||
.containsExactly("MOVE-1", "MOVE-2");
|
||||
verify(relocationJobService).generate(request);
|
||||
}
|
||||
|
||||
private RelocationGenerateRequest request(String warehouseId, Long floor,
|
||||
String source, String target, int count)
|
||||
{
|
||||
RelocationGenerateRequest request = new RelocationGenerateRequest();
|
||||
request.setWarehouseId(warehouseId);
|
||||
request.setTheFloor(floor);
|
||||
request.setSourceGroupCoding(source);
|
||||
request.setTargetGroupCoding(target);
|
||||
request.setTrayCount(count);
|
||||
return request;
|
||||
}
|
||||
|
||||
private DenseLaneOption lane(String groupCoding, Long floor, int full, int empty)
|
||||
{
|
||||
DenseLaneOption option = new DenseLaneOption();
|
||||
option.setGroupCoding(groupCoding);
|
||||
option.setTheFloor(floor);
|
||||
option.setFullTrayCount(full);
|
||||
option.setEmptyLocationCount(empty);
|
||||
return option;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user