feat(asset): 添加RFID标签批量导入导出功能及模板下载

- 移除标签编码手工输入,改为系统统一生成
- 实现EPC编码重复校验和批量查询
- 优化标签状态和绑定状态的Excel导出配置
- 更新新增和编辑页面的标签编码显示方式
- 添加导入数据验证和批量处理逻辑
- 增加相关单元测试确保功能正确性
main
yangk 2 days ago
parent 238c5fb213
commit 5c3a7f6ce3

@ -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<AmsRfidTag> util = new ExcelUtil<AmsRfidTag>(AmsRfidTag.class);
List<AmsRfidTag> 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<AmsRfidTag> util = new ExcelUtil<AmsRfidTag>(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编码已存在");

@ -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删除 */

@ -45,6 +45,14 @@ public interface AmsRfidTagMapper
*/
public List<AmsRfidTag> selectAmsRfidTagList(AmsRfidTag amsRfidTag);
/**
* EPCRFID
*
* @param epcCodes EPC
* @return RFID
*/
public List<AmsRfidTag> selectAmsRfidTagByEpcCodes(@Param("epcCodes") List<String> epcCodes);
/**
*
*

@ -43,6 +43,15 @@ public interface IAmsRfidTagService
*/
public boolean checkEpcCodeUnique(AmsRfidTag amsRfidTag);
/**
* RFID
*
* @param tagList RFID
* @param operName
* @return
*/
public String importAmsRfidTag(List<AmsRfidTag> tagList, String operName);
/**
* RFID
*

@ -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;
/**
* RFIDService
@ -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<AmsRfidTag> 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<String, Integer> firstRowByEpc = new LinkedHashMap<>();
Set<String> 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("<br/>" + successNum + "、EPC " + tag.getEpcCode()
+ " 导入成功,标签编码:" + tag.getTagCode());
}
catch (Exception e)
{
failureNum++;
failureMsg.append("<br/>" + 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("<br/>").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<String, Integer> firstRowByEpc,
Set<String> 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<String> selectExistingEpcCodes(List<AmsRfidTag> tagList)
{
List<String> epcCodes = collectCandidateEpcCodes(tagList);
if (epcCodes.isEmpty())
{
return new HashSet<>();
}
Set<String> 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<AmsRfidTag> existingTags = amsRfidTagMapper.selectAmsRfidTagByEpcCodes(
epcCodes.subList(fromIndex, toIndex));
for (AmsRfidTag tag : existingTags)
{
if (StringUtils.isNotEmpty(tag.getEpcCode()))
{
existingEpcCodes.add(tag.getEpcCode());
}
}
}
return existingEpcCodes;
}
private List<String> collectCandidateEpcCodes(List<AmsRfidTag> tagList)
{
Set<String> 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)

@ -62,6 +62,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
for update
</select>
<select id="selectAmsRfidTagByEpcCodes" resultMap="AmsRfidTagResult">
<include refid="selectAmsRfidTagVo"/>
where del_flag = '0'
and epc_code in
<foreach collection="epcCodes" item="epcCode" open="(" separator="," close=")">
#{epcCode}
</foreach>
</select>
<select id="checkTagCodeUnique" parameterType="String" resultMap="AmsRfidTagResult">
<include refid="selectAmsRfidTagVo"/>
where tag_code = #{tagCode} and del_flag = '0' limit 1

@ -6,14 +6,6 @@
<body class="white-bg">
<div class="wrapper wrapper-content animated fadeInRight ibox-content">
<form class="form-horizontal m" id="form-tag-add">
<div class="col-xs-6">
<div class="form-group">
<label class="col-sm-4 control-label is-required">标签编码:</label>
<div class="col-sm-8">
<input name="tagCode" class="form-control" type="text" required>
</div>
</div>
</div>
<div class="col-xs-6">
<div class="form-group">
<label class="col-sm-4 control-label is-required">EPC编码</label>

@ -9,9 +9,9 @@
<input name="tagId" th:field="*{tagId}" type="hidden">
<div class="col-xs-6">
<div class="form-group">
<label class="col-sm-4 control-label is-required">标签编码:</label>
<label class="col-sm-4 control-label">标签编码:</label>
<div class="col-sm-8">
<input name="tagCode" th:field="*{tagCode}" class="form-control" type="text" required>
<p class="form-control-plaintext" th:text="*{tagCode}"></p>
</div>
</div>
</div>

@ -64,6 +64,9 @@
<a class="btn btn-danger multiple disabled" onclick="$.operate.removeAll()" shiro:hasPermission="asset:tag:remove">
<i class="fa fa-remove"></i> 删除
</a>
<a class="btn btn-info" onclick="$.table.importExcel()" shiro:hasPermission="asset:tag:import">
<i class="fa fa-upload"></i> 导入
</a>
<a class="btn btn-warning" onclick="$.table.exportExcel()" shiro:hasPermission="asset:tag:export">
<i class="fa fa-download"></i> 导出
</a>
@ -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 @@
});
}
</script>
<!-- 导入区域 -->
<script id="importTpl" type="text/template">
<form enctype="multipart/form-data" class="mt20 mb10">
<div class="col-xs-offset-1">
<input type="file" id="file" name="file"/>
<div class="mt10 pt5">
导入仅需填写EPC编码标签编码由系统自动生成导入后统一为正常、未绑定状态重复或无效行会汇总提示。
&nbsp; <a onclick="$.table.importTemplate()" class="btn btn-default btn-xs"><i class="fa fa-file-excel-o"></i> 下载模板</a>
</div>
<font color="red" class="pull-left mt10">
提示仅允许导入“xls”或“xlsx”格式文件
</font>
</div>
</form>
</script>
</body>
</html>

@ -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<List<AmsRfidTag>> 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;

@ -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<AmsRfidTag> 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<AmsRfidTag> 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<AmsRfidTag> 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<AmsRfidTag> 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<AmsRfidTag> 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<List<String>> 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;
}
}

Loading…
Cancel
Save