Forráskód Böngészése

✨ feat(qc): 增加到货通知单状态校验及文件 URL 格式验证

新增 validateArrivalNoticeReadyForReceipt 方法,确保到货通知单状态为待入库,并校验所有需检行已完成 IQC。同时,增强了文件 URL 格式验证,确保文件值为有效的 http/https URL。
YunaiV 2 hónapja
szülő
commit
80183a18a6

+ 19 - 2
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/qc/indicatorresult/MesQcIndicatorResultServiceImpl.java

@@ -29,6 +29,7 @@ import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
 
 import java.math.BigDecimal;
+import java.net.URI;
 import java.util.*;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -181,7 +182,7 @@ public class MesQcIndicatorResultServiceImpl implements MesQcIndicatorResultServ
      * 按检测项的 resultType 校验明细值格式
      *
      * <p>FLOAT → 必须可解析为 BigDecimal;INTEGER → 必须可解析为整数;
-     * TEXT / DICT / FILE → 放行
+     * DICT → 必须属于字典值域;FILE → 必须为 http/https URL;TEXT → 放行
      */
     private void validateDetailValues(List<MesQcIndicatorResultSaveReqVO.Item> items,
                                       Map<Long, MesQcIndicatorDO> indicatorMap) {
@@ -219,7 +220,23 @@ public class MesQcIndicatorResultServiceImpl implements MesQcIndicatorResultServ
                     dictDataApi.validateDictDataList(dictType, Collections.singleton(item.getValue()));
                 }
             }
-            // FILE / TEXT 不做格式校验
+            if (Objects.equals(resultType, MesQcResultValueTypeEnum.FILE.getType())
+                    && !isHttpUrl(item.getValue())) {
+                throw exception(QC_RESULT_VALUE_FORMAT_INVALID,
+                        "检测项[" + indicator.getName() + "]要求文件 URL,实际值=" + item.getValue());
+            }
+            // TEXT 不做格式校验
+        }
+    }
+
+    private boolean isHttpUrl(String value) {
+        try {
+            URI uri = URI.create(value);
+            String scheme = uri.getScheme();
+            return StrUtil.isNotBlank(uri.getHost())
+                    && ("http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme));
+        } catch (IllegalArgumentException e) {
+            return false;
         }
     }
 

+ 9 - 0
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/arrivalnotice/MesWmArrivalNoticeService.java

@@ -116,6 +116,15 @@ public interface MesWmArrivalNoticeService {
      */
     void validateArrivalNoticeAndLineExists(Long noticeId, Long lineId);
 
+    /**
+     * 校验到货通知单已就绪可被采购入库引用
+     * <p>校验状态为待入库 + 所有需检行都已完成 IQC</p>
+     *
+     * @param id 到货通知单编号
+     * @return 到货通知单
+     */
+    MesWmArrivalNoticeDO validateArrivalNoticeReadyForReceipt(Long id);
+
     /**
      * 查询指定供应商的到货通知单数量
      *

+ 17 - 0
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/arrivalnotice/MesWmArrivalNoticeServiceImpl.java

@@ -210,6 +210,23 @@ public class MesWmArrivalNoticeServiceImpl implements MesWmArrivalNoticeService
         return notice;
     }
 
+    @Override
+    public MesWmArrivalNoticeDO validateArrivalNoticeReadyForReceipt(Long id) {
+        // 1. 校验到货通知单存在且状态为待入库
+        MesWmArrivalNoticeDO notice = validateArrivalNoticeExists(id);
+        if (ObjUtil.notEqual(MesWmArrivalNoticeStatusEnum.PENDING_RECEIPT.getStatus(), notice.getStatus())) {
+            throw exception(WM_ARRIVAL_NOTICE_STATUS_NOT_PENDING_RECEIPT);
+        }
+        // 2. 行级防御校验:确保所有需检行都已完成 IQC
+        List<MesWmArrivalNoticeLineDO> lines = arrivalNoticeLineService.getArrivalNoticeLineListByNoticeId(id);
+        boolean hasUnchecked = CollectionUtils.anyMatch(lines,
+                line -> Boolean.TRUE.equals(line.getIqcCheckFlag()) && line.getIqcId() == null);
+        if (hasUnchecked) {
+            throw exception(WM_ARRIVAL_NOTICE_IQC_PENDING);
+        }
+        return notice;
+    }
+
     private void validateCodeUnique(Long id, String code) {
         MesWmArrivalNoticeDO notice = arrivalNoticeMapper.selectByCode(code);
         if (notice == null) {

+ 1 - 5
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/itemreceipt/MesWmItemReceiptServiceImpl.java

@@ -14,7 +14,6 @@ import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemreceipt.MesWmItemReceip
 import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemreceipt.MesWmItemReceiptLineDO;
 import cn.iocoder.yudao.module.mes.dal.mysql.wm.itemreceipt.MesWmItemReceiptMapper;
 import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
-import cn.iocoder.yudao.module.mes.enums.wm.MesWmArrivalNoticeStatusEnum;
 import cn.iocoder.yudao.module.mes.enums.wm.MesWmItemReceiptStatusEnum;
 import cn.iocoder.yudao.module.mes.enums.wm.MesWmTransactionTypeEnum;
 import cn.iocoder.yudao.module.mes.service.md.vendor.MesMdVendorService;
@@ -92,10 +91,7 @@ public class MesWmItemReceiptServiceImpl implements MesWmItemReceiptService {
         vendorService.validateVendorExistsAndEnable(reqVO.getVendorId());
         // 校验到货通知单存在
         if (reqVO.getNoticeId() != null) {
-            MesWmArrivalNoticeDO notice = arrivalNoticeService.validateArrivalNoticeExists(reqVO.getNoticeId());
-            if (ObjUtil.notEqual(notice.getStatus(), MesWmArrivalNoticeStatusEnum.PENDING_RECEIPT.getStatus())) {
-                throw exception(WM_ARRIVAL_NOTICE_STATUS_NOT_PENDING_RECEIPT);
-            }
+            MesWmArrivalNoticeDO notice = arrivalNoticeService.validateArrivalNoticeReadyForReceipt(reqVO.getNoticeId());
             if (ObjUtil.notEqual(notice.getVendorId(), reqVO.getVendorId())) {
                 throw exception(WM_ARRIVAL_NOTICE_VENDOR_MISMATCH);
             }

+ 288 - 0
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/indicatorresult/MesQcIndicatorResultServiceImplTest.java

@@ -0,0 +1,288 @@
+package cn.iocoder.yudao.module.mes.service.qc.indicatorresult;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+import cn.iocoder.yudao.module.mes.controller.admin.qc.indicatorresult.vo.MesQcIndicatorResultPageReqVO;
+import cn.iocoder.yudao.module.mes.controller.admin.qc.indicatorresult.vo.MesQcIndicatorResultSaveReqVO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicator.MesQcIndicatorDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.qc.indicatorresult.MesQcIndicatorResultDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.qc.iqc.MesQcIqcDO;
+import cn.iocoder.yudao.module.mes.dal.mysql.qc.indicatorresult.MesQcIndicatorResultMapper;
+import cn.iocoder.yudao.module.mes.enums.qc.MesQcResultValueTypeEnum;
+import cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum;
+import cn.iocoder.yudao.module.mes.service.qc.indicator.MesQcIndicatorService;
+import cn.iocoder.yudao.module.mes.service.qc.ipqc.MesQcIpqcService;
+import cn.iocoder.yudao.module.mes.service.qc.iqc.MesQcIqcService;
+import cn.iocoder.yudao.module.mes.service.qc.oqc.MesQcOqcService;
+import cn.iocoder.yudao.module.mes.service.qc.rqc.MesQcRqcService;
+import cn.iocoder.yudao.module.system.api.dict.DictDataApi;
+import org.junit.jupiter.api.Test;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+
+import jakarta.annotation.Resource;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
+import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * {@link MesQcIndicatorResultServiceImpl} 的单元测试
+ *
+ * @author 芋道源码
+ */
+@Import(MesQcIndicatorResultServiceImpl.class)
+public class MesQcIndicatorResultServiceImplTest extends BaseDbUnitTest {
+
+    @Resource
+    private MesQcIndicatorResultServiceImpl indicatorResultService;
+
+    @Resource
+    private MesQcIndicatorResultMapper resultMapper;
+
+    @MockitoBean
+    private MesQcIndicatorResultDetailService resultDetailService;
+    @MockitoBean
+    private MesQcIndicatorService indicatorService;
+    @MockitoBean
+    private MesQcIqcService iqcService;
+    @MockitoBean
+    private MesQcIpqcService ipqcService;
+    @MockitoBean
+    private MesQcOqcService oqcService;
+    @MockitoBean
+    private MesQcRqcService rqcService;
+    @MockitoBean
+    private DictDataApi dictDataApi;
+
+    @Test
+    public void testCreateIndicatorResult_success() {
+        // 准备参数
+        MesQcIndicatorResultSaveReqVO reqVO = buildReqVO("plain text value", MesQcResultValueTypeEnum.TEXT.getType());
+        // mock 方法
+        mockIqcAndIndicator(reqVO, MesQcResultValueTypeEnum.TEXT.getType());
+
+        // 调用
+        Long resultId = indicatorResultService.createIndicatorResult(reqVO);
+
+        // 断言
+        assertNotNull(resultId);
+        MesQcIndicatorResultDO dbResult = resultMapper.selectById(resultId);
+        assertNotNull(dbResult);
+        assertEquals(reqVO.getCode(), dbResult.getCode());
+        assertEquals(reqVO.getQcId(), dbResult.getQcId());
+        assertEquals(reqVO.getQcType(), dbResult.getQcType());
+        assertEquals(100L, dbResult.getItemId()); // 从 mock IQC 获取
+        verify(resultDetailService).createDetailList(anyList());
+    }
+
+    @Test
+    public void testCreateIndicatorResult_fileValueInvalid() {
+        // 准备参数
+        MesQcIndicatorResultSaveReqVO reqVO = buildReqVO("not-a-url", MesQcResultValueTypeEnum.FILE.getType());
+        // mock 方法
+        mockIqcAndIndicator(reqVO, MesQcResultValueTypeEnum.FILE.getType());
+
+        // 调用,并断言异常
+        assertServiceException(() -> indicatorResultService.createIndicatorResult(reqVO),
+                QC_RESULT_VALUE_FORMAT_INVALID, "检测项[检测项A]要求文件 URL,实际值=not-a-url");
+
+        // 断言:未入库
+        assertEquals(0, resultMapper.selectCount());
+        verify(resultDetailService, never()).createDetailList(anyList());
+    }
+
+    @Test
+    public void testUpdateIndicatorResult_success() {
+        // mock 数据
+        MesQcIndicatorResultDO dbResult = randomPojo(MesQcIndicatorResultDO.class, o -> {
+            o.setQcType(MesQcTypeEnum.IQC.getType());
+        });
+        resultMapper.insert(dbResult);
+        // 准备参数
+        MesQcIndicatorResultSaveReqVO reqVO = buildReqVO("updated text", MesQcResultValueTypeEnum.TEXT.getType());
+        reqVO.setId(dbResult.getId());
+        // mock 方法
+        MesQcIndicatorDO indicator = new MesQcIndicatorDO();
+        indicator.setId(reqVO.getItems().get(0).getIndicatorId());
+        indicator.setName("检测项A");
+        indicator.setResultType(MesQcResultValueTypeEnum.TEXT.getType());
+        when(indicatorService.validateIndicatorListExists(anySet()))
+                .thenReturn(Map.of(indicator.getId(), indicator));
+
+        // 调用
+        indicatorResultService.updateIndicatorResult(reqVO);
+
+        // 断言
+        MesQcIndicatorResultDO updatedResult = resultMapper.selectById(dbResult.getId());
+        assertEquals(reqVO.getCode(), updatedResult.getCode());
+        // qcId/qcType/itemId 不允许改挂,应保持原值
+        assertEquals(dbResult.getQcId(), updatedResult.getQcId());
+        assertEquals(dbResult.getQcType(), updatedResult.getQcType());
+        assertEquals(dbResult.getItemId(), updatedResult.getItemId());
+        verify(resultDetailService).createOrUpdateDetailList(anyList());
+    }
+
+    @Test
+    public void testUpdateIndicatorResult_notExists() {
+        // 准备参数
+        MesQcIndicatorResultSaveReqVO reqVO = buildReqVO("text", MesQcResultValueTypeEnum.TEXT.getType());
+        reqVO.setId(randomLongId());
+
+        // 调用,并断言异常
+        assertServiceException(() -> indicatorResultService.updateIndicatorResult(reqVO),
+                QC_RESULT_NOT_EXISTS);
+    }
+
+    @Test
+    public void testDeleteIndicatorResult_success() {
+        // mock 数据
+        MesQcIndicatorResultDO dbResult = randomPojo(MesQcIndicatorResultDO.class);
+        resultMapper.insert(dbResult);
+
+        // 调用
+        indicatorResultService.deleteIndicatorResult(dbResult.getId());
+
+        // 断言
+        assertNull(resultMapper.selectById(dbResult.getId()));
+        verify(resultDetailService).deleteDetailByResultId(dbResult.getId());
+    }
+
+    @Test
+    public void testDeleteIndicatorResult_notExists() {
+        // 调用,并断言异常
+        assertServiceException(() -> indicatorResultService.deleteIndicatorResult(randomLongId()),
+                QC_RESULT_NOT_EXISTS);
+    }
+
+    @Test
+    public void testGetIndicatorResult() {
+        // mock 数据
+        MesQcIndicatorResultDO dbResult = randomPojo(MesQcIndicatorResultDO.class);
+        resultMapper.insert(dbResult);
+
+        // 调用
+        MesQcIndicatorResultDO result = indicatorResultService.getIndicatorResult(dbResult.getId());
+
+        // 断言
+        assertPojoEquals(dbResult, result);
+    }
+
+    @Test
+    public void testGetIndicatorResultPage() {
+        // mock 数据
+        MesQcIndicatorResultDO dbResult = randomPojo(MesQcIndicatorResultDO.class, o -> {
+            o.setQcId(1L);
+            o.setQcType(MesQcTypeEnum.IQC.getType());
+            o.setCode("SPL-001");
+            o.setItemId(100L);
+        });
+        resultMapper.insert(dbResult);
+        // 测试 qcId 不匹配
+        resultMapper.insert(cloneIgnoreId(dbResult, o -> o.setQcId(2L)));
+        // 测试 qcType 不匹配
+        resultMapper.insert(cloneIgnoreId(dbResult, o -> o.setQcType(MesQcTypeEnum.IPQC.getType())));
+        // 测试 code 不匹配
+        resultMapper.insert(cloneIgnoreId(dbResult, o -> o.setCode("SPL-999")));
+        // 测试 itemId 不匹配
+        resultMapper.insert(cloneIgnoreId(dbResult, o -> o.setItemId(999L)));
+        // 准备参数
+        MesQcIndicatorResultPageReqVO pageReqVO = new MesQcIndicatorResultPageReqVO();
+        pageReqVO.setQcId(1L);
+        pageReqVO.setQcType(MesQcTypeEnum.IQC.getType());
+        pageReqVO.setCode("SPL-001");
+        pageReqVO.setItemId(100L);
+
+        // 调用
+        PageResult<MesQcIndicatorResultDO> pageResult = indicatorResultService.getIndicatorResultPage(pageReqVO);
+
+        // 断言
+        assertEquals(1, pageResult.getTotal());
+        assertEquals(1, pageResult.getList().size());
+        assertPojoEquals(dbResult, pageResult.getList().get(0));
+    }
+
+    @Test
+    public void testGetIndicatorResultCountByQcIdAndType() {
+        // mock 数据
+        MesQcIndicatorResultDO result1 = randomPojo(MesQcIndicatorResultDO.class, o -> {
+            o.setQcId(1L);
+            o.setQcType(MesQcTypeEnum.IQC.getType());
+        });
+        resultMapper.insert(result1);
+        MesQcIndicatorResultDO result2 = randomPojo(MesQcIndicatorResultDO.class, o -> {
+            o.setQcId(1L);
+            o.setQcType(MesQcTypeEnum.IQC.getType());
+        });
+        resultMapper.insert(result2);
+        // 不匹配
+        resultMapper.insert(randomPojo(MesQcIndicatorResultDO.class, o -> {
+            o.setQcId(2L);
+            o.setQcType(MesQcTypeEnum.IPQC.getType());
+        }));
+
+        // 调用
+        Long count = indicatorResultService.getIndicatorResultCountByQcIdAndType(1L, MesQcTypeEnum.IQC.getType());
+
+        // 断言
+        assertEquals(2L, count);
+    }
+
+    @Test
+    public void testValidateIndicatorResultExistsByQcIdAndType_success() {
+        // mock 数据
+        resultMapper.insert(randomPojo(MesQcIndicatorResultDO.class, o -> {
+            o.setQcId(1L);
+            o.setQcType(MesQcTypeEnum.IQC.getType());
+        }));
+
+        // 调用(不抛异常即通过)
+        indicatorResultService.validateIndicatorResultExistsByQcIdAndType(1L, MesQcTypeEnum.IQC.getType());
+    }
+
+    @Test
+    public void testValidateIndicatorResultExistsByQcIdAndType_notExists() {
+        // 调用,并断言异常
+        assertServiceException(
+                () -> indicatorResultService.validateIndicatorResultExistsByQcIdAndType(randomLongId(), MesQcTypeEnum.IQC.getType()),
+                QC_FINISH_INDICATOR_RESULT_REQUIRED);
+    }
+
+    // ==================== 辅助方法 ====================
+
+    private void mockIqcAndIndicator(MesQcIndicatorResultSaveReqVO reqVO, Integer resultType) {
+        MesQcIqcDO iqc = new MesQcIqcDO();
+        iqc.setId(reqVO.getQcId());
+        iqc.setItemId(100L);
+        when(iqcService.validateIqcExists(reqVO.getQcId())).thenReturn(iqc);
+
+        MesQcIndicatorDO indicator = new MesQcIndicatorDO();
+        indicator.setId(reqVO.getItems().get(0).getIndicatorId());
+        indicator.setName("检测项A");
+        indicator.setResultType(resultType);
+        when(indicatorService.validateIndicatorListExists(anySet()))
+                .thenReturn(Map.of(indicator.getId(), indicator));
+    }
+
+    private MesQcIndicatorResultSaveReqVO buildReqVO(String value, Integer resultType) {
+        MesQcIndicatorResultSaveReqVO.Item item = new MesQcIndicatorResultSaveReqVO.Item();
+        item.setIndicatorId(10L);
+        item.setValue(value);
+
+        MesQcIndicatorResultSaveReqVO reqVO = new MesQcIndicatorResultSaveReqVO();
+        reqVO.setCode("SPL-001");
+        reqVO.setQcId(1L);
+        reqVO.setQcType(MesQcTypeEnum.IQC.getType());
+        reqVO.setItems(List.of(item));
+        return reqVO;
+    }
+
+}

+ 1 - 0
yudao-module-mes/src/test/resources/sql/clean.sql

@@ -20,3 +20,4 @@ DELETE FROM "mes_wm_batch";
 DELETE FROM "mes_wm_material_stock";
 DELETE FROM "mes_pro_task";
 DELETE FROM "mes_pro_route_process";
+DELETE FROM "mes_qc_indicator_result";

+ 20 - 0
yudao-module-mes/src/test/resources/sql/create_tables.sql

@@ -810,3 +810,23 @@ CREATE TABLE IF NOT EXISTS "mes_wm_product_sales_detail" (
     "tenant_id" bigint NOT NULL DEFAULT 0,
     PRIMARY KEY ("id")
 );
+
+-- ----------------------------
+-- MES 检验结果记录
+-- ----------------------------
+CREATE TABLE IF NOT EXISTS "mes_qc_indicator_result" (
+    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "code" varchar(64) DEFAULT NULL,
+    "qc_id" bigint DEFAULT NULL,
+    "qc_type" int DEFAULT NULL,
+    "item_id" bigint DEFAULT NULL,
+    "sn" varchar(128) DEFAULT NULL,
+    "remark" varchar(500) DEFAULT NULL,
+    "creator" varchar(64) DEFAULT '',
+    "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updater" varchar(64) DEFAULT '',
+    "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "deleted" bit NOT NULL DEFAULT FALSE,
+    "tenant_id" bigint NOT NULL DEFAULT 0,
+    PRIMARY KEY ("id")
+);