From 1a34d72cbb13858bedd2a3cc840827f78280e28c Mon Sep 17 00:00:00 2001 From: zgw <13623675548@163.com> Date: Tue, 8 Sep 2026 14:35:56 +0800 Subject: [PATCH] fix(pda): ignore empty identifiers when checking half-tray jobs --- .../com/ruoyi/pda/service/PdaServiceImp.java | 14 +- .../resources/mapper/job/MydoJobMapper.xml | 12 +- .../pda/service/HalfTrayEligibilityTest.java | 136 ++++++++++++++++++ .../ruoyi/pda/service/PdaHalfTrayOutTest.java | 113 +++++++++++++++ 4 files changed, 272 insertions(+), 3 deletions(-) create mode 100644 ruoyi-admin/src/test/java/com/ruoyi/pda/service/HalfTrayEligibilityTest.java create mode 100644 ruoyi-admin/src/test/java/com/ruoyi/pda/service/PdaHalfTrayOutTest.java diff --git a/ruoyi-admin/src/main/java/com/ruoyi/pda/service/PdaServiceImp.java b/ruoyi-admin/src/main/java/com/ruoyi/pda/service/PdaServiceImp.java index dd0b2eb..857467d 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/pda/service/PdaServiceImp.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/pda/service/PdaServiceImp.java @@ -85,10 +85,20 @@ public class PdaServiceImp implements PdaService { @Override public AjaxResult halfTrayOut(TrayCodeMsg trayCodeMsg) { + if (!"1".equals(trayCodeMsg.getExitId()) && !"2".equals(trayCodeMsg.getExitId())) { + return AjaxResult.error("请选择有效的出库出口(出口1或出口2)。"); + } InStockinDetail inStockinDetail = inStockinDetailService.selectInStockinDetailByStockinDetailId(trayCodeMsg.getStockinDetailId()); -// InStockin inStockin = inStockinService.selectInStockinByStockinId(inStockinDetail.getStockinId()); -// inStockinDetail.setSpare4(inStockin.getDepartmentNo()); + if (inStockinDetail == null) { + return AjaxResult.error("未查询到入库明细,请核对条码后重试。"); + } + if (mapper.selectBySpare4OrCargo(inStockinDetail) > 0) { + return AjaxResult.error("该项目或匹配编码已有半盘出库作业,请完成后再操作。"); + } List hasHalfTray = basStockMapper.selectHasHalfTray(inStockinDetail); + if (CollUtil.isEmpty(hasHalfTray)) { + return AjaxResult.error("未找到可出库的未满托盘,请刷新后重试。"); + } HashMap map = new HashMap<>(); map.put("v_tray_code", hasHalfTray.get(0).getTrayCode()); map.put("v_exit_id", trayCodeMsg.getExitId()); diff --git a/ruoyi-admin/src/main/resources/mapper/job/MydoJobMapper.xml b/ruoyi-admin/src/main/resources/mapper/job/MydoJobMapper.xml index d78cd6f..82d20fb 100644 --- a/ruoyi-admin/src/main/resources/mapper/job/MydoJobMapper.xml +++ b/ruoyi-admin/src/main/resources/mapper/job/MydoJobMapper.xml @@ -298,7 +298,17 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" diff --git a/ruoyi-admin/src/test/java/com/ruoyi/pda/service/HalfTrayEligibilityTest.java b/ruoyi-admin/src/test/java/com/ruoyi/pda/service/HalfTrayEligibilityTest.java new file mode 100644 index 0000000..dca7501 --- /dev/null +++ b/ruoyi-admin/src/test/java/com/ruoyi/pda/service/HalfTrayEligibilityTest.java @@ -0,0 +1,136 @@ +package com.ruoyi.pda.service; + +import com.ruoyi.in.stockindetail.domain.InStockinDetail; +import com.ruoyi.in.stockindetail.service.IInStockinDetailService; +import com.ruoyi.job.mydojob.domain.MydoJob; +import com.ruoyi.job.mydojob.mapper.MydoJobMapper; +import com.ruoyi.stock.stock.domain.BasStock; +import com.ruoyi.stock.stock.mapper.BasStockMapper; +import org.apache.ibatis.builder.xml.XMLMapperBuilder; +import org.apache.ibatis.mapping.Environment; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.SqlSessionFactory; +import org.apache.ibatis.session.SqlSessionFactoryBuilder; +import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; +import org.h2.jdbcx.JdbcDataSource; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class HalfTrayEligibilityTest { + private JdbcTemplate jdbc; + private SqlSessionFactory sessions; + + @BeforeEach + void setUp() throws Exception { + JdbcDataSource dataSource = new JdbcDataSource(); + dataSource.setURL("jdbc:h2:mem:" + UUID.randomUUID() + ";MODE=MSSQLServer;DB_CLOSE_DELAY=-1"); + jdbc = new JdbcTemplate(dataSource); + jdbc.execute("create table mydo_job(job_id varchar(80), in_or_out varchar(20))"); + jdbc.execute("create table mydo_job_detail(job_id varchar(80), spare4 varchar(80), exit_id varchar(80))"); + Configuration configuration = new Configuration(new Environment("test", new JdbcTransactionFactory(), dataSource)); + configuration.getTypeAliasRegistry().registerAlias("MydoJob", MydoJob.class); + configuration.getTypeAliasRegistry().registerAlias("InStockinDetail", InStockinDetail.class); + try (var stream = getClass().getClassLoader().getResourceAsStream("mapper/job/MydoJobMapper.xml")) { + new XMLMapperBuilder(stream, configuration, "mapper/job/MydoJobMapper.xml", configuration.getSqlFragments()).parse(); + } + sessions = new SqlSessionFactoryBuilder().build(configuration); + } + + @AfterEach + void close() { + jdbc.execute("shutdown"); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" "}) + void blankCodeDoesNotHideCandidatesForAnUnrelatedProject(String cargoNo) { + job("HPC202609070029", "26", "AD.A-00002.01.26", cargoNo); + InStockinDetail detail = incoming("AD.A-00002.01.32", cargoNo); + IInStockinDetailService inbound = mock(IInStockinDetailService.class); + BasStockMapper stocks = mock(BasStockMapper.class); + when(inbound.selectInStockinDetailByStockinDetailId("5673093")).thenReturn(detail); + BasStock tray333 = new BasStock(); + tray333.setTrayCode("T0000333"); + BasStock tray343 = new BasStock(); + tray343.setTrayCode("T0000343"); + when(stocks.selectHasHalfTray(detail)).thenReturn(List.of(tray333, tray343)); + + try (SqlSession session = sessions.openSession()) { + PdaServiceImp service = new PdaServiceImp(); + ReflectionTestUtils.setField(service, "inStockinDetailService", inbound); + ReflectionTestUtils.setField(service, "basStockMapper", stocks); + ReflectionTestUtils.setField(service, "mapper", session.getMapper(MydoJobMapper.class)); + + assertThat(service.selectHasHalfTray("5673093")) + .extracting(BasStock::getTrayCode).containsExactly("T0000333", "T0000343"); + } + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" "}) + void sameProjectRemainsBlockedEvenWhenCodeIsBlank(String cargoNo) { + job("SAME-PROJECT", "26", "PROJECT", "OTHER-CODE"); + assertThat(count(incoming("PROJECT", cargoNo))).isEqualTo(1); + } + + @Test + void sameNonemptyCodeStillBlocksAcrossProjectsAndCountsAJobOnlyOnce() { + job("SAME-CODE", "26", "OTHER-PROJECT", "020210100000249"); + jdbc.update("insert into mydo_job_detail values(?,?,?)", "SAME-CODE", "OTHER-PROJECT", "020210100000249"); + assertThat(count(incoming("PROJECT", "020210100000249"))).isEqualTo(1); + } + + @Test + void differentNonemptyCodeDoesNotBlock() { + job("UNRELATED", "26", "OTHER-PROJECT", "OTHER-CODE"); + assertThat(count(incoming("PROJECT", "CODE"))).isZero(); + } + + @Test + void nonHalfTrayJobsAndOrphanDetailsDoNotBlock() { + job("NORMAL-OUT", "21", "PROJECT", "CODE"); + jdbc.update("insert into mydo_job_detail values(?,?,?)", "COMPLETED-JOB", "PROJECT", "CODE"); + assertThat(count(incoming("PROJECT", "CODE"))).isZero(); + } + + @Test + void blankProjectDoesNotCreateAnotherEmptyIdentityMatch() { + job("NO-PROJECT", "26", "", "OTHER-CODE"); + assertThat(count(incoming("", "CODE"))).isZero(); + assertThat(count(incoming("", ""))).isZero(); + } + + private int count(InStockinDetail detail) { + try (SqlSession session = sessions.openSession()) { + return session.getMapper(MydoJobMapper.class).selectBySpare4OrCargo(detail); + } + } + + private void job(String id, String type, String project, String code) { + jdbc.update("insert into mydo_job values(?,?)", id, type); + jdbc.update("insert into mydo_job_detail values(?,?,?)", id, project, code); + } + + private InStockinDetail incoming(String project, String code) { + InStockinDetail detail = new InStockinDetail(); + detail.setSpare4(project); + detail.setCargoNo(code); + return detail; + } +} diff --git a/ruoyi-admin/src/test/java/com/ruoyi/pda/service/PdaHalfTrayOutTest.java b/ruoyi-admin/src/test/java/com/ruoyi/pda/service/PdaHalfTrayOutTest.java new file mode 100644 index 0000000..7812910 --- /dev/null +++ b/ruoyi-admin/src/test/java/com/ruoyi/pda/service/PdaHalfTrayOutTest.java @@ -0,0 +1,113 @@ +package com.ruoyi.pda.service; + +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.in.stockindetail.domain.InStockinDetail; +import com.ruoyi.in.stockindetail.service.IInStockinDetailService; +import com.ruoyi.job.mydojob.mapper.MydoJobMapper; +import com.ruoyi.pda.domain.TrayCodeMsg; +import com.ruoyi.pda.mapper.PdaMapper; +import com.ruoyi.stock.stock.domain.BasStock; +import com.ruoyi.stock.stock.mapper.BasStockMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +class PdaHalfTrayOutTest { + private final PdaServiceImp service = new PdaServiceImp(); + private final IInStockinDetailService inbound = mock(IInStockinDetailService.class); + private final BasStockMapper stocks = mock(BasStockMapper.class); + private final MydoJobMapper jobs = mock(MydoJobMapper.class); + private final PdaMapper procedures = mock(PdaMapper.class); + private final InStockinDetail detail = new InStockinDetail(); + + @BeforeEach + void setUp() { + ReflectionTestUtils.setField(service, "inStockinDetailService", inbound); + ReflectionTestUtils.setField(service, "basStockMapper", stocks); + ReflectionTestUtils.setField(service, "mapper", jobs); + ReflectionTestUtils.setField(service, "pdaMapper", procedures); + when(inbound.selectInStockinDetailByStockinDetailId("5673093")).thenReturn(detail); + BasStock tray = new BasStock(); + tray.setTrayCode("T0000343"); + when(stocks.selectHasHalfTray(detail)).thenReturn(List.of(tray)); + } + + @Test + void candidatesDisappearingBetweenLookupAndSubmitReturnBusinessError() { + when(stocks.selectHasHalfTray(detail)).thenReturn(Collections.emptyList()); + AjaxResult result = service.halfTrayOut(request()); + assertThat(result.isError()).isTrue(); + assertThat(result.get("msg").toString()).contains("未找到", "刷新"); + verifyNoInteractions(procedures); + } + + @Test + void newlyCreatedMatchingJobBlocksSubmissionWithAnExplanation() { + when(jobs.selectBySpare4OrCargo(detail)).thenReturn(1); + AjaxResult result = service.halfTrayOut(request()); + assertThat(result.isError()).isTrue(); + assertThat(result.get("msg").toString()).contains("已有", "半盘出库作业"); + verifyNoInteractions(procedures); + } + + @Test + void missingInboundDetailReturnsBusinessError() { + when(inbound.selectInStockinDetailByStockinDetailId("5673093")).thenReturn(null); + AjaxResult result = service.halfTrayOut(request()); + assertThat(result.isError()).isTrue(); + assertThat(result.get("msg").toString()).contains("入库明细", "条码"); + verifyNoInteractions(procedures); + } + + @Test + void invalidExitIsRejectedBeforeCreatingAJob() { + TrayCodeMsg request = request(); + request.setExitId("TD1008"); + AjaxResult result = service.halfTrayOut(request); + assertThat(result.isError()).isTrue(); + assertThat(result.get("msg").toString()).contains("出口"); + verifyNoInteractions(procedures); + } + + @Test + void successfulSubmissionKeepsExistingTrayAndExitArguments() { + doAnswer(invocation -> { + HashMap args = invocation.getArgument(0); + assertThat(args).containsEntry("v_tray_code", "T0000343").containsEntry("v_exit_id", "2"); + args.put("v_result", 10); + return null; + }).when(procedures).halfTrayOut(any()); + assertThat(service.halfTrayOut(request()).isSuccess()).isTrue(); + } + + @Test + void procedureRejectionKeepsItsBusinessMessage() { + doAnswer(invocation -> { + HashMap args = invocation.getArgument(0); + args.put("v_result", 4); + args.put("v_resultmsg", "该库存已被分配生成作业"); + return null; + }).when(procedures).halfTrayOut(any()); + AjaxResult result = service.halfTrayOut(request()); + assertThat(result.isError()).isTrue(); + assertThat(result.get("msg")).isEqualTo("该库存已被分配生成作业"); + } + + private TrayCodeMsg request() { + TrayCodeMsg request = new TrayCodeMsg(); + request.setStockinDetailId("5673093"); + request.setExitId("2"); + return request; + } +}