From 5c3a7f6ce3e3f0f16765794b64a4ce7ca10bf5fd Mon Sep 17 00:00:00 2001 From: yangk Date: Fri, 3 Jul 2026 08:34:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(asset):=20=E6=B7=BB=E5=8A=A0RFID=E6=A0=87?= =?UTF-8?q?=E7=AD=BE=E6=89=B9=E9=87=8F=E5=AF=BC=E5=85=A5=E5=AF=BC=E5=87=BA?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=8F=8A=E6=A8=A1=E6=9D=BF=E4=B8=8B=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除标签编码手工输入,改为系统统一生成 - 实现EPC编码重复校验和批量查询 - 优化标签状态和绑定状态的Excel导出配置 - 更新新增和编辑页面的标签编码显示方式 - 添加导入数据验证和批量处理逻辑 - 增加相关单元测试确保功能正确性 --- .../controller/AmsRfidTagController.java | 48 ++++- .../com/ruoyi/asset/domain/AmsRfidTag.java | 14 +- .../ruoyi/asset/mapper/AmsRfidTagMapper.java | 8 + .../asset/service/IAmsRfidTagService.java | 9 + .../service/impl/AmsRfidTagServiceImpl.java | 171 +++++++++++++++++- .../mapper/asset/AmsRfidTagMapper.xml | 9 + .../resources/templates/asset/tag/add.html | 8 - .../resources/templates/asset/tag/edit.html | 4 +- .../resources/templates/asset/tag/tag.html | 20 ++ .../controller/AmsRfidTagControllerTest.java | 77 ++++++++ .../impl/AmsRfidTagServiceImplTest.java | 121 +++++++++++++ 11 files changed, 455 insertions(+), 34 deletions(-) diff --git a/ruoyi-asset/src/main/java/com/ruoyi/asset/controller/AmsRfidTagController.java b/ruoyi-asset/src/main/java/com/ruoyi/asset/controller/AmsRfidTagController.java index 9e5765d..e5aa23a 100644 --- a/ruoyi-asset/src/main/java/com/ruoyi/asset/controller/AmsRfidTagController.java +++ b/ruoyi-asset/src/main/java/com/ruoyi/asset/controller/AmsRfidTagController.java @@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.multipart.MultipartFile; import com.ruoyi.common.annotation.Log; import com.ruoyi.common.enums.BusinessType; import com.ruoyi.asset.domain.AmsAsset; @@ -85,6 +86,45 @@ public class AmsRfidTagController extends BaseController return util.exportExcel(list, "RFID标签数据"); } + /** + * 导入RFID标签列表 + */ + @RequiresPermissions("asset:tag:import") + @Log(title = "RFID标签", businessType = BusinessType.IMPORT) + @PostMapping("/importData") + @ResponseBody + public AjaxResult importData(MultipartFile file) + { + if (StringUtils.isNull(file) || file.isEmpty()) + { + throw new ServiceException("导入文件不能为空"); + } + ExcelUtil util = new ExcelUtil(AmsRfidTag.class); + List tagList; + try + { + tagList = util.importExcel(file.getInputStream()); + } + catch (Exception e) + { + throw new ServiceException("Excel文件解析失败,请检查文件格式和模板列名"); + } + String message = amsRfidTagService.importAmsRfidTag(tagList, getLoginName()); + return AjaxResult.success(message); + } + + /** + * 下载RFID标签导入模板 + */ + @RequiresPermissions("asset:tag:view") + @GetMapping("/importTemplate") + @ResponseBody + public AjaxResult importTemplate() + { + ExcelUtil util = new ExcelUtil(AmsRfidTag.class); + return util.importTemplateExcel("RFID标签导入模板"); + } + /** * 查看RFID标签详情 */ @@ -116,10 +156,6 @@ public class AmsRfidTagController extends BaseController @ResponseBody public AjaxResult addSave(AmsRfidTag amsRfidTag) { - if (!amsRfidTagService.checkTagCodeUnique(amsRfidTag)) - { - return error("新增RFID标签失败,标签编码已存在"); - } if (!amsRfidTagService.checkEpcCodeUnique(amsRfidTag)) { return error("新增RFID标签失败,EPC编码已存在"); @@ -149,10 +185,6 @@ public class AmsRfidTagController extends BaseController @ResponseBody public AjaxResult editSave(AmsRfidTag amsRfidTag) { - if (!amsRfidTagService.checkTagCodeUnique(amsRfidTag)) - { - return error("修改RFID标签失败,标签编码已存在"); - } if (!amsRfidTagService.checkEpcCodeUnique(amsRfidTag)) { return error("修改RFID标签失败,EPC编码已存在"); diff --git a/ruoyi-asset/src/main/java/com/ruoyi/asset/domain/AmsRfidTag.java b/ruoyi-asset/src/main/java/com/ruoyi/asset/domain/AmsRfidTag.java index 12894fc..26271be 100644 --- a/ruoyi-asset/src/main/java/com/ruoyi/asset/domain/AmsRfidTag.java +++ b/ruoyi-asset/src/main/java/com/ruoyi/asset/domain/AmsRfidTag.java @@ -21,7 +21,7 @@ public class AmsRfidTag extends BaseEntity private Long tagId; /** 标签编码 */ - @Excel(name = "标签编码") + @Excel(name = "标签编码", type = Excel.Type.EXPORT) private String tagCode; /** EPC编码 */ @@ -29,11 +29,11 @@ public class AmsRfidTag extends BaseEntity private String epcCode; /** 标签状态 */ - @Excel(name = "标签状态", dictType = "ams_tag_status", comboReadDict = true) + @Excel(name = "标签状态", dictType = "ams_tag_status", comboReadDict = true, type = Excel.Type.EXPORT) private String tagStatus; /** 绑定状态 */ - @Excel(name = "绑定状态", dictType = "ams_tag_bind_status", comboReadDict = true) + @Excel(name = "绑定状态", dictType = "ams_tag_bind_status", comboReadDict = true, type = Excel.Type.EXPORT) private String bindStatus; /** 绑定资产ID */ @@ -41,16 +41,16 @@ public class AmsRfidTag extends BaseEntity private Long assetId; /** 绑定资产编码快照 */ - @Excel(name = "绑定资产编码") + @Excel(name = "绑定资产编码", type = Excel.Type.EXPORT) private String assetCode; /** 绑定资产名称快照 */ - @Excel(name = "绑定资产名称") + @Excel(name = "绑定资产名称", type = Excel.Type.EXPORT) private String assetName; /** 绑定时间 */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") - @Excel(name = "绑定时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "绑定时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss", type = Excel.Type.EXPORT) private Date bindTime; /** 绑定人ID */ @@ -58,7 +58,7 @@ public class AmsRfidTag extends BaseEntity private Long bindUserId; /** 绑定人名称快照 */ - @Excel(name = "绑定人") + @Excel(name = "绑定人", type = Excel.Type.EXPORT) private String bindUserName; /** 删除标志:0存在,1删除 */ diff --git a/ruoyi-asset/src/main/java/com/ruoyi/asset/mapper/AmsRfidTagMapper.java b/ruoyi-asset/src/main/java/com/ruoyi/asset/mapper/AmsRfidTagMapper.java index 1d11568..b1a6d6c 100644 --- a/ruoyi-asset/src/main/java/com/ruoyi/asset/mapper/AmsRfidTagMapper.java +++ b/ruoyi-asset/src/main/java/com/ruoyi/asset/mapper/AmsRfidTagMapper.java @@ -45,6 +45,14 @@ public interface AmsRfidTagMapper */ public List selectAmsRfidTagList(AmsRfidTag amsRfidTag); + /** + * 按EPC编码批量查询RFID标签 + * + * @param epcCodes EPC编码集合 + * @return RFID标签集合 + */ + public List selectAmsRfidTagByEpcCodes(@Param("epcCodes") List epcCodes); + /** * 校验标签编码是否唯一 * diff --git a/ruoyi-asset/src/main/java/com/ruoyi/asset/service/IAmsRfidTagService.java b/ruoyi-asset/src/main/java/com/ruoyi/asset/service/IAmsRfidTagService.java index 82a4ab8..c8b4dec 100644 --- a/ruoyi-asset/src/main/java/com/ruoyi/asset/service/IAmsRfidTagService.java +++ b/ruoyi-asset/src/main/java/com/ruoyi/asset/service/IAmsRfidTagService.java @@ -43,6 +43,15 @@ public interface IAmsRfidTagService */ public boolean checkEpcCodeUnique(AmsRfidTag amsRfidTag); + /** + * 导入RFID标签数据 + * + * @param tagList RFID标签数据列表 + * @param operName 操作用户 + * @return 导入结果消息 + */ + public String importAmsRfidTag(List tagList, String operName); + /** * 新增RFID标签 * diff --git a/ruoyi-asset/src/main/java/com/ruoyi/asset/service/impl/AmsRfidTagServiceImpl.java b/ruoyi-asset/src/main/java/com/ruoyi/asset/service/impl/AmsRfidTagServiceImpl.java index 1a9008a..62f075a 100644 --- a/ruoyi-asset/src/main/java/com/ruoyi/asset/service/impl/AmsRfidTagServiceImpl.java +++ b/ruoyi-asset/src/main/java/com/ruoyi/asset/service/impl/AmsRfidTagServiceImpl.java @@ -1,7 +1,13 @@ package com.ruoyi.asset.service.impl; +import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import com.ruoyi.asset.constant.RfidBindStatus; import com.ruoyi.asset.constant.RfidTagStatus; import com.ruoyi.asset.domain.RfidBindingContext; @@ -18,6 +24,7 @@ import com.ruoyi.asset.mapper.AmsRfidTagMapper; import com.ruoyi.asset.domain.AmsRfidTag; import com.ruoyi.asset.service.IAmsRfidTagService; import com.ruoyi.common.core.text.Convert; +import com.ruoyi.system.service.ISysCodeRuleService; /** * RFID标签Service业务层处理 @@ -30,6 +37,14 @@ public class AmsRfidTagServiceImpl implements IAmsRfidTagService { private static final String DEL_FLAG_NORMAL = "0"; + private static final String RFID_TAG_RULE = "RFID_TAG"; + + private static final int EPC_CODE_MAX_LENGTH = 128; + + private static final int IMPORT_QUERY_BATCH_SIZE = 500; + + private static final int IMPORT_MAX_ROWS = 5000; + @Autowired private AmsRfidTagMapper amsRfidTagMapper; @@ -39,6 +54,9 @@ public class AmsRfidTagServiceImpl implements IAmsRfidTagService @Autowired private IRfidBindingService rfidBindingService; + @Autowired + private ISysCodeRuleService sysCodeRuleService; + /** * 查询RFID标签 * @@ -107,6 +125,68 @@ public class AmsRfidTagServiceImpl implements IAmsRfidTagService return UserConstants.UNIQUE; } + /** + * 导入RFID标签数据 + * + * @param tagList RFID标签数据列表 + * @param operName 操作用户 + * @return 导入结果消息 + */ + @Override + public String importAmsRfidTag(List tagList, String operName) + { + if (tagList == null || tagList.isEmpty()) + { + throw new ServiceException("导入RFID标签数据不能为空!"); + } + if (tagList.size() > IMPORT_MAX_ROWS) + { + throw new ServiceException("单次最多导入" + IMPORT_MAX_ROWS + "条RFID标签,请拆分文件后再导入"); + } + int successNum = 0; + int failureNum = 0; + StringBuilder successMsg = new StringBuilder(); + StringBuilder failureMsg = new StringBuilder(); + Map firstRowByEpc = new LinkedHashMap<>(); + Set existingEpcCodes = selectExistingEpcCodes(tagList); + + // 故意不包整批事务:有效行入库、无效行汇总提示;编码规则内部事务负责锁定流水规则行。 + for (int i = 0; i < tagList.size(); i++) + { + AmsRfidTag importTag = tagList.get(i); + int rowNumber = i + 1; + String rowName = "第" + rowNumber + "条"; + try + { + // 批量收货场景要求合法行先建档,行级异常只汇总提示,不回滚其它有效标签。 + AmsRfidTag tag = buildImportTag(importTag, rowNumber, firstRowByEpc, existingEpcCodes, operName); + insertAmsRfidTag(tag); + successNum++; + successMsg.append("
" + successNum + "、EPC " + tag.getEpcCode() + + " 导入成功,标签编码:" + tag.getTagCode()); + } + catch (Exception e) + { + failureNum++; + failureMsg.append("
" + failureNum + "、" + rowName + " 导入失败:" + e.getMessage()); + } + } + + if (failureNum > 0) + { + failureMsg.insert(0, "共 " + failureNum + " 条数据不符合RFID标签规则,错误如下:"); + if (successNum == 0) + { + throw new ServiceException("很抱歉,RFID标签导入失败!" + failureMsg); + } + successMsg.insert(0, "RFID标签部分导入完成!成功 " + successNum + " 条,失败 " + + failureNum + " 条。成功数据如下:"); + return successMsg.append("
").append(failureMsg).toString(); + } + successMsg.insert(0, "恭喜您,RFID标签数据已全部导入成功!共 " + successNum + " 条,数据如下:"); + return successMsg.toString(); + } + /** * 新增RFID标签 * @@ -117,6 +197,8 @@ public class AmsRfidTagServiceImpl implements IAmsRfidTagService public int insertAmsRfidTag(AmsRfidTag amsRfidTag) { validateBasicFields(amsRfidTag); + // 标签编码属于系统台账标识,所有建档入口统一走编码规则,避免页面或导入文件绕过流水规则。 + amsRfidTag.setTagCode(sysCodeRuleService.nextCode(RFID_TAG_RULE)); if (!checkTagCodeUnique(amsRfidTag)) { throw new ServiceException("标签编码已存在"); @@ -146,11 +228,9 @@ public class AmsRfidTagServiceImpl implements IAmsRfidTagService { validateBasicFields(amsRfidTag); AmsRfidTag current = selectExistingTagForUpdate(amsRfidTag.getTagId()); + // 修改页面只维护 EPC 和备注,标签编码作为台账身份必须沿用数据库现值,防止伪造表单改写历史标识。 + amsRfidTag.setTagCode(current.getTagCode()); validateIdentityChange(current, amsRfidTag); - if (!checkTagCodeUnique(amsRfidTag)) - { - throw new ServiceException("标签编码已存在"); - } if (!checkEpcCodeUnique(amsRfidTag)) { throw new ServiceException("EPC编码已存在"); @@ -275,16 +355,89 @@ public class AmsRfidTagServiceImpl implements IAmsRfidTagService { throw new ServiceException("RFID标签不能为空"); } - amsRfidTag.setTagCode(StringUtils.trim(amsRfidTag.getTagCode())); amsRfidTag.setEpcCode(StringUtils.trim(amsRfidTag.getEpcCode())); - if (StringUtils.isEmpty(amsRfidTag.getTagCode())) - { - throw new ServiceException("标签编码不能为空"); - } if (StringUtils.isEmpty(amsRfidTag.getEpcCode())) { throw new ServiceException("EPC编码不能为空"); } + if (amsRfidTag.getEpcCode().length() > EPC_CODE_MAX_LENGTH) + { + throw new ServiceException("EPC编码长度不能超过128个字符"); + } + } + + private AmsRfidTag buildImportTag(AmsRfidTag importTag, int rowNumber, + Map firstRowByEpc, + Set existingEpcCodes, String operName) + { + if (StringUtils.isNull(importTag)) + { + throw new ServiceException("RFID标签不能为空"); + } + String epcCode = StringUtils.trim(importTag.getEpcCode()); + if (StringUtils.isEmpty(epcCode)) + { + throw new ServiceException("EPC编码不能为空"); + } + if (epcCode.length() > EPC_CODE_MAX_LENGTH) + { + throw new ServiceException("EPC编码长度不能超过128个字符"); + } + Integer firstRow = firstRowByEpc.putIfAbsent(epcCode, rowNumber); + if (StringUtils.isNotNull(firstRow)) + { + throw new ServiceException("EPC编码在导入文件中重复,首次出现于第" + firstRow + "条"); + } + if (existingEpcCodes.contains(epcCode)) + { + throw new ServiceException("EPC编码已存在"); + } + + AmsRfidTag tag = new AmsRfidTag(); + tag.setEpcCode(epcCode); + tag.setCreateBy(operName); + return tag; + } + + private Set selectExistingEpcCodes(List tagList) + { + List epcCodes = collectCandidateEpcCodes(tagList); + if (epcCodes.isEmpty()) + { + return new HashSet<>(); + } + Set existingEpcCodes = new HashSet<>(); + for (int fromIndex = 0; fromIndex < epcCodes.size(); fromIndex += IMPORT_QUERY_BATCH_SIZE) + { + int toIndex = Math.min(fromIndex + IMPORT_QUERY_BATCH_SIZE, epcCodes.size()); + List existingTags = amsRfidTagMapper.selectAmsRfidTagByEpcCodes( + epcCodes.subList(fromIndex, toIndex)); + for (AmsRfidTag tag : existingTags) + { + if (StringUtils.isNotEmpty(tag.getEpcCode())) + { + existingEpcCodes.add(tag.getEpcCode()); + } + } + } + return existingEpcCodes; + } + + private List collectCandidateEpcCodes(List tagList) + { + Set epcCodes = new LinkedHashSet<>(); + for (AmsRfidTag tag : tagList) + { + if (StringUtils.isNotNull(tag)) + { + String epcCode = StringUtils.trim(tag.getEpcCode()); + if (StringUtils.isNotEmpty(epcCode)) + { + epcCodes.add(epcCode); + } + } + } + return new ArrayList<>(epcCodes); } private void validateIdentityChange(AmsRfidTag current, AmsRfidTag incoming) diff --git a/ruoyi-asset/src/main/resources/mapper/asset/AmsRfidTagMapper.xml b/ruoyi-asset/src/main/resources/mapper/asset/AmsRfidTagMapper.xml index 1ed8501..68df626 100644 --- a/ruoyi-asset/src/main/resources/mapper/asset/AmsRfidTagMapper.xml +++ b/ruoyi-asset/src/main/resources/mapper/asset/AmsRfidTagMapper.xml @@ -62,6 +62,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" for update + + - - -
diff --git a/ruoyi-asset/src/main/resources/templates/asset/tag/edit.html b/ruoyi-asset/src/main/resources/templates/asset/tag/edit.html index f9becb5..06a1c3f 100644 --- a/ruoyi-asset/src/main/resources/templates/asset/tag/edit.html +++ b/ruoyi-asset/src/main/resources/templates/asset/tag/edit.html @@ -9,9 +9,9 @@
- +
- +

diff --git a/ruoyi-asset/src/main/resources/templates/asset/tag/tag.html b/ruoyi-asset/src/main/resources/templates/asset/tag/tag.html index 062b629..2f121a7 100644 --- a/ruoyi-asset/src/main/resources/templates/asset/tag/tag.html +++ b/ruoyi-asset/src/main/resources/templates/asset/tag/tag.html @@ -64,6 +64,9 @@ 删除 + + 导入 + 导出 @@ -90,6 +93,8 @@ updateUrl: prefix + "/edit/{id}", removeUrl: prefix + "/remove", exportUrl: prefix + "/export", + importUrl: prefix + "/importData", + importTemplateUrl: prefix + "/importTemplate", modalName: "RFID标签", columns: [{ checkbox: true @@ -207,5 +212,20 @@ }); } + + diff --git a/ruoyi-asset/src/test/java/com/ruoyi/asset/controller/AmsRfidTagControllerTest.java b/ruoyi-asset/src/test/java/com/ruoyi/asset/controller/AmsRfidTagControllerTest.java index fff9e3f..8c38f7b 100644 --- a/ruoyi-asset/src/test/java/com/ruoyi/asset/controller/AmsRfidTagControllerTest.java +++ b/ruoyi-asset/src/test/java/com/ruoyi/asset/controller/AmsRfidTagControllerTest.java @@ -2,9 +2,15 @@ package com.ruoyi.asset.controller; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.io.ByteArrayOutputStream; +import java.util.List; import com.ruoyi.asset.domain.AmsRfidTag; import com.ruoyi.asset.domain.RfidBindingContext; import com.ruoyi.asset.service.IAmsAssetService; @@ -13,12 +19,16 @@ import com.ruoyi.asset.service.IRfidBindingService; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.domain.entity.SysUser; import com.ruoyi.common.exception.ServiceException; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.util.ReflectionTestUtils; @ExtendWith(MockitoExtension.class) @@ -44,6 +54,41 @@ class AmsRfidTagControllerTest ReflectionTestUtils.setField(controller, "rfidBindingService", rfidBindingService); } + /** Web新增不再接收标签编码,标签编码由Service调用编码规则统一生成 */ + @Test + void addSaveShouldNotRequireManualTagCode() + { + AmsRfidTag tag = new AmsRfidTag(); + tag.setEpcCode("EPC-001"); + when(amsRfidTagService.checkEpcCodeUnique(tag)).thenReturn(true); + when(amsRfidTagService.insertAmsRfidTag(tag)).thenReturn(1); + + AjaxResult result = controller.addSave(tag); + + verify(amsRfidTagService, never()).checkTagCodeUnique(any(AmsRfidTag.class)); + verify(amsRfidTagService).insertAmsRfidTag(tag); + assertEquals("admin", tag.getCreateBy()); + assertEquals(AjaxResult.Type.SUCCESS.value(), result.get(AjaxResult.CODE_TAG)); + } + + /** Web修改不再提交标签编码,避免页面绕过Service改写台账身份 */ + @Test + void editSaveShouldNotCheckManualTagCode() + { + AmsRfidTag tag = new AmsRfidTag(); + tag.setTagId(10L); + tag.setEpcCode("EPC-001"); + when(amsRfidTagService.checkEpcCodeUnique(tag)).thenReturn(true); + when(amsRfidTagService.updateAmsRfidTag(tag)).thenReturn(1); + + AjaxResult result = controller.editSave(tag); + + verify(amsRfidTagService, never()).checkTagCodeUnique(any(AmsRfidTag.class)); + verify(amsRfidTagService).updateAmsRfidTag(tag); + assertEquals("admin", tag.getUpdateBy()); + assertEquals(AjaxResult.Type.SUCCESS.value(), result.get(AjaxResult.CODE_TAG)); + } + /** Web绑定必须使用数据库中的标签EPC,并把当前登录人传给公共绑定服务 */ @Test void bindSaveShouldDelegateToRfidBindingService() @@ -75,6 +120,38 @@ class AmsRfidTagControllerTest assertEquals("RFID标签不存在或已删除", exception.getMessage()); } + /** 导入接口应解析Excel并把标签列表交给Service,由Service统一生成标签编码 */ + @Test + @SuppressWarnings("unchecked") + void importDataShouldParseExcelAndDelegateToService() throws Exception + { + MockMultipartFile file = new MockMultipartFile("file", "rfid-tags.xlsx", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + buildImportWorkbook("EPC-001")); + when(amsRfidTagService.importAmsRfidTag(anyList(), eq("admin"))).thenReturn("导入成功"); + + AjaxResult result = controller.importData(file); + + ArgumentCaptor> tagListCaptor = ArgumentCaptor.forClass(List.class); + verify(amsRfidTagService).importAmsRfidTag(tagListCaptor.capture(), eq("admin")); + assertEquals("EPC-001", tagListCaptor.getValue().get(0).getEpcCode()); + assertEquals(AjaxResult.Type.SUCCESS.value(), result.get(AjaxResult.CODE_TAG)); + } + + private byte[] buildImportWorkbook(String epcCode) throws Exception + { + try (XSSFWorkbook workbook = new XSSFWorkbook(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) + { + Sheet sheet = workbook.createSheet("RFID标签导入模板"); + Row header = sheet.createRow(0); + header.createCell(0).setCellValue("EPC编码"); + Row dataRow = sheet.createRow(1); + dataRow.createCell(0).setCellValue(epcCode); + workbook.write(outputStream); + return outputStream.toByteArray(); + } + } + private static class TestAmsRfidTagController extends AmsRfidTagController { private final SysUser user; diff --git a/ruoyi-asset/src/test/java/com/ruoyi/asset/service/impl/AmsRfidTagServiceImplTest.java b/ruoyi-asset/src/test/java/com/ruoyi/asset/service/impl/AmsRfidTagServiceImplTest.java index 61ccab4..07c325d 100644 --- a/ruoyi-asset/src/test/java/com/ruoyi/asset/service/impl/AmsRfidTagServiceImplTest.java +++ b/ruoyi-asset/src/test/java/com/ruoyi/asset/service/impl/AmsRfidTagServiceImplTest.java @@ -5,10 +5,15 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import com.ruoyi.asset.constant.RfidBindStatus; import com.ruoyi.asset.constant.RfidTagStatus; import com.ruoyi.asset.domain.AmsRfidTag; @@ -17,6 +22,7 @@ import com.ruoyi.asset.mapper.AmsRfidTagMapper; import com.ruoyi.asset.service.IAssetLifecycleService; import com.ruoyi.asset.service.IRfidBindingService; import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.system.service.ISysCodeRuleService; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; @@ -36,6 +42,9 @@ class AmsRfidTagServiceImplTest @Mock private IRfidBindingService rfidBindingService; + @Mock + private ISysCodeRuleService sysCodeRuleService; + @InjectMocks private AmsRfidTagServiceImpl service; @@ -81,6 +90,25 @@ class AmsRfidTagServiceImplTest verify(amsRfidTagMapper).selectAmsRfidTagByTagIdForUpdate(10L); } + /** 新增建档必须统一使用编码规则生成标签编码,不能信任页面或接口传入的手工编码 */ + @Test + void insertShouldGenerateTagCodeAndIgnoreIncomingTagCode() + { + AmsRfidTag tag = buildUnboundTag(); + tag.setTagId(null); + tag.setTagCode("MANUAL-001"); + when(sysCodeRuleService.nextCode("RFID_TAG")).thenReturn("BQ20260702000031"); + when(amsRfidTagMapper.insertAmsRfidTag(any(AmsRfidTag.class))).thenReturn(1); + + assertEquals(1, service.insertAmsRfidTag(tag)); + + ArgumentCaptor tagCaptor = ArgumentCaptor.forClass(AmsRfidTag.class); + verify(amsRfidTagMapper).insertAmsRfidTag(tagCaptor.capture()); + assertEquals("BQ20260702000031", tagCaptor.getValue().getTagCode()); + assertEquals(RfidTagStatus.NORMAL, tagCaptor.getValue().getTagStatus()); + assertEquals(RfidBindStatus.UNBOUND, tagCaptor.getValue().getBindStatus()); + } + /** 标签修改应先锁定记录并保留当前绑定状态 */ @Test void updateShouldLockTagAndPreserveControlledFields() @@ -88,6 +116,7 @@ class AmsRfidTagServiceImplTest AmsRfidTag current = buildUnboundTag(); current.setTagStatus(RfidTagStatus.VOID); AmsRfidTag incoming = buildUnboundTag(); + incoming.setTagCode("MANUAL-EDIT"); incoming.setTagStatus(RfidTagStatus.NORMAL); incoming.setBindStatus(RfidBindStatus.BOUND); incoming.setAssetId(1L); @@ -98,6 +127,7 @@ class AmsRfidTagServiceImplTest ArgumentCaptor tagCaptor = ArgumentCaptor.forClass(AmsRfidTag.class); verify(amsRfidTagMapper).updateAmsRfidTag(tagCaptor.capture()); + assertEquals("TAG-001", tagCaptor.getValue().getTagCode()); assertEquals(RfidTagStatus.VOID, tagCaptor.getValue().getTagStatus()); assertEquals(RfidBindStatus.UNBOUND, tagCaptor.getValue().getBindStatus()); assertNull(tagCaptor.getValue().getAssetId()); @@ -117,6 +147,90 @@ class AmsRfidTagServiceImplTest verify(amsRfidTagMapper, never()).voidAmsRfidTagByTagId(any(), any(), any()); } + /** 空导入文件直接失败,不触发数据库查询 */ + @Test + void importShouldRejectEmptyList() + { + ServiceException exception = assertThrows(ServiceException.class, + () -> service.importAmsRfidTag(Collections.emptyList(), "admin")); + + assertEquals("导入RFID标签数据不能为空!", exception.getMessage()); + verify(amsRfidTagMapper, never()).selectAmsRfidTagByEpcCodes(anyList()); + } + + /** 导入允许有效行先入库,并把空EPC、文件内重复和库内重复汇总提示 */ + @Test + void importShouldPersistValidRowsAndReportInvalidRows() + { + AmsRfidTag existingTag = new AmsRfidTag(); + existingTag.setEpcCode("EPC-OLD"); + when(amsRfidTagMapper.selectAmsRfidTagByEpcCodes(anyList())).thenReturn(List.of(existingTag)); + when(sysCodeRuleService.nextCode("RFID_TAG")).thenReturn("BQ20260702000001"); + when(amsRfidTagMapper.insertAmsRfidTag(any(AmsRfidTag.class))).thenReturn(1); + + String message = service.importAmsRfidTag(List.of( + buildImportTag(" EPC-001 "), + buildImportTag("EPC-001"), + buildImportTag(" "), + buildImportTag("EPC-OLD") + ), "admin"); + + assertTrue(message.contains("RFID标签部分导入完成")); + assertTrue(message.contains("成功 1 条,失败 3 条")); + assertTrue(message.contains("首次出现于第1条")); + assertTrue(message.contains("EPC编码不能为空")); + assertTrue(message.contains("EPC编码已存在")); + verify(sysCodeRuleService, times(1)).nextCode("RFID_TAG"); + + ArgumentCaptor tagCaptor = ArgumentCaptor.forClass(AmsRfidTag.class); + verify(amsRfidTagMapper).insertAmsRfidTag(tagCaptor.capture()); + assertEquals("BQ20260702000001", tagCaptor.getValue().getTagCode()); + assertEquals("EPC-001", tagCaptor.getValue().getEpcCode()); + assertEquals(RfidTagStatus.NORMAL, tagCaptor.getValue().getTagStatus()); + assertEquals(RfidBindStatus.UNBOUND, tagCaptor.getValue().getBindStatus()); + assertNull(tagCaptor.getValue().getAssetId()); + } + + /** 单次导入设置上限,避免超大Excel长时间占用请求和编码规则流水锁 */ + @Test + void importShouldRejectTooManyRows() + { + List tagList = new ArrayList<>(); + for (int i = 0; i < 5001; i++) + { + tagList.add(buildImportTag("EPC-" + i)); + } + + ServiceException exception = assertThrows(ServiceException.class, + () -> service.importAmsRfidTag(tagList, "admin")); + + assertTrue(exception.getMessage().contains("单次最多导入5000条")); + verify(amsRfidTagMapper, never()).selectAmsRfidTagByEpcCodes(anyList()); + } + + /** EPC库内预查按固定批次拆分,避免超长IN条件影响生产环境稳定性 */ + @Test + @SuppressWarnings("unchecked") + void importShouldBatchExistingEpcLookup() + { + List tagList = new ArrayList<>(); + for (int i = 0; i < 501; i++) + { + tagList.add(buildImportTag("EPC-" + i)); + } + int[] serial = {1}; + when(amsRfidTagMapper.selectAmsRfidTagByEpcCodes(anyList())).thenReturn(Collections.emptyList()); + when(sysCodeRuleService.nextCode("RFID_TAG")).thenAnswer(invocation -> "BQ" + serial[0]++); + when(amsRfidTagMapper.insertAmsRfidTag(any(AmsRfidTag.class))).thenReturn(1); + + service.importAmsRfidTag(tagList, "admin"); + + ArgumentCaptor> epcCodesCaptor = ArgumentCaptor.forClass(List.class); + verify(amsRfidTagMapper, times(2)).selectAmsRfidTagByEpcCodes(epcCodesCaptor.capture()); + assertEquals(500, epcCodesCaptor.getAllValues().get(0).size()); + assertEquals(1, epcCodesCaptor.getAllValues().get(1).size()); + } + private AmsRfidTag buildUnboundTag() { AmsRfidTag tag = new AmsRfidTag(); @@ -127,4 +241,11 @@ class AmsRfidTagServiceImplTest tag.setBindStatus(RfidBindStatus.UNBOUND); return tag; } + + private AmsRfidTag buildImportTag(String epcCode) + { + AmsRfidTag tag = new AmsRfidTag(); + tag.setEpcCode(epcCode); + return tag; + } }