feat(asset): 添加RFID标签绑定功能

- 在AmsAssetMapper中新增selectBindableAmsAssetList方法用于查询可绑定资产
- 在AmsAssetService中实现可绑定资产列表查询功能
- 在AmsRfidTagController中添加绑定页面、资产选择页面和绑定接口
- 创建bind.html和selectAsset.html模板文件
- 添加RFID标签绑定相关的按钮和权限控制
- 实现绑定操作的安全验证和业务逻辑处理
- 添加相应的单元测试确保功能正确性
main
yangk 2 weeks ago
parent f60995ac86
commit 5d17e05f3f

@ -12,10 +12,16 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.asset.domain.AmsAsset;
import com.ruoyi.asset.domain.AmsRfidTag;
import com.ruoyi.asset.domain.RfidBindingContext;
import com.ruoyi.asset.service.IAmsAssetService;
import com.ruoyi.asset.service.IAmsRfidTagService;
import com.ruoyi.asset.service.IRfidBindingService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.common.core.page.TableDataInfo;
@ -34,6 +40,12 @@ public class AmsRfidTagController extends BaseController
@Autowired
private IAmsRfidTagService amsRfidTagService;
@Autowired
private IAmsAssetService amsAssetService;
@Autowired
private IRfidBindingService rfidBindingService;
@RequiresPermissions("asset:tag:view")
@GetMapping()
public String tag()
@ -169,6 +181,61 @@ public class AmsRfidTagController extends BaseController
getSysUser().getUserName(), getLoginName()));
}
/**
* RFID
*/
@RequiresPermissions("asset:tag:bind")
@GetMapping("/bind/{tagId}")
public String bind(@PathVariable("tagId") Long tagId, ModelMap mmap)
{
AmsRfidTag amsRfidTag = requireExistingTag(tagId);
mmap.put("amsRfidTag", amsRfidTag);
return prefix + "/bind";
}
/**
*
*/
@RequiresPermissions("asset:tag:bind")
@GetMapping("/selectAsset")
public String selectAsset()
{
return prefix + "/selectAsset";
}
/**
* RFID
*/
@RequiresPermissions("asset:tag:bind")
@PostMapping("/bindableAssetList")
@ResponseBody
public TableDataInfo bindableAssetList(AmsAsset amsAsset)
{
startPage();
List<AmsAsset> list = amsAssetService.selectBindableAmsAssetList(amsAsset);
return getDataTable(list);
}
/**
* WebRFID
*/
@RequiresPermissions("asset:tag:bind")
@Log(title = "RFID标签绑定", businessType = BusinessType.UPDATE)
@PostMapping("/bind")
@ResponseBody
public AjaxResult bindSave(Long tagId, String assetCode, String remark)
{
AmsRfidTag amsRfidTag = requireExistingTag(tagId);
RfidBindingContext context = new RfidBindingContext();
context.setOperateUserId(getUserId());
context.setOperateUserName(getSysUser().getUserName());
context.setOperateLoginName(getLoginName());
context.setRemark(remark);
// EPC始终从数据库读取避免页面篡改后绑定到其他标签。
rfidBindingService.bind(assetCode, amsRfidTag.getEpcCode(), context);
return success("RFID标签绑定成功");
}
/**
* RFID
*/
@ -180,4 +247,18 @@ public class AmsRfidTagController extends BaseController
{
return toAjax(amsRfidTagService.deleteAmsRfidTagByTagIds(ids));
}
private AmsRfidTag requireExistingTag(Long tagId)
{
if (StringUtils.isNull(tagId))
{
throw new ServiceException("RFID标签ID不能为空");
}
AmsRfidTag amsRfidTag = amsRfidTagService.selectAmsRfidTagByTagId(tagId);
if (StringUtils.isNull(amsRfidTag))
{
throw new ServiceException("RFID标签不存在或已删除");
}
return amsRfidTag;
}
}

@ -45,6 +45,16 @@ public interface AmsAssetMapper
*/
public List<AmsAsset> selectAmsAssetList(AmsAsset amsAsset);
/**
* RFID
*
* @param amsAsset
* @param disposedStatus
* @return
*/
public List<AmsAsset> selectBindableAmsAssetList(@Param("asset") AmsAsset amsAsset,
@Param("disposedStatus") String disposedStatus);
/**
*
*

@ -27,6 +27,14 @@ public interface IAmsAssetService
*/
public List<AmsAsset> selectAmsAssetList(AmsAsset amsAsset);
/**
* RFID
*
* @param amsAsset
* @return
*/
public List<AmsAsset> selectBindableAmsAssetList(AmsAsset amsAsset);
/**
*
*

@ -89,6 +89,15 @@ public class AmsAssetServiceImpl implements IAmsAssetService
return amsAssetMapper.selectAmsAssetList(amsAsset);
}
/**
* RFID
*/
@Override
public List<AmsAsset> selectBindableAmsAssetList(AmsAsset amsAsset)
{
return amsAssetMapper.selectBindableAmsAssetList(amsAsset, AssetStatus.DISPOSED);
}
/**
*
*

@ -84,6 +84,28 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="epcCode != null and epcCode != ''"> and epc_code = #{epcCode}</if>
</where>
</select>
<select id="selectBindableAmsAssetList" resultMap="AmsAssetResult">
<include refid="selectAmsAssetVo"/>
where del_flag = '0'
and asset_status != #{disposedStatus}
and tag_id is null
and tag_code is null
and epc_code is null
<if test="asset.assetCode != null and asset.assetCode != ''">
and asset_code like concat('%', #{asset.assetCode}, '%')
</if>
<if test="asset.assetName != null and asset.assetName != ''">
and asset_name like concat('%', #{asset.assetName}, '%')
</if>
<if test="asset.categoryName != null and asset.categoryName != ''">
and category_name like concat('%', #{asset.categoryName}, '%')
</if>
<if test="asset.assetStatus != null and asset.assetStatus != ''">
and asset_status = #{asset.assetStatus}
</if>
order by asset_id
</select>
<select id="selectAmsAssetByAssetId" parameterType="Long" resultMap="AmsAssetResult">
<include refid="selectAmsAssetVo"/>

@ -0,0 +1,102 @@
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:include="include :: header('绑定RFID标签')" />
</head>
<body class="white-bg">
<div class="wrapper wrapper-content animated fadeInRight ibox-content">
<form class="form-horizontal m" id="form-tag-bind" th:object="${amsRfidTag}">
<input name="tagId" type="hidden" th:field="*{tagId}">
<div class="form-group">
<label class="col-sm-3 control-label">标签编码:</label>
<div class="col-sm-8">
<input class="form-control" type="text" th:value="*{tagCode}" readonly>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">EPC编码</label>
<div class="col-sm-8">
<input class="form-control" type="text" th:value="*{epcCode}" readonly>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label is-required">资产编码:</label>
<div class="col-sm-8">
<div class="input-group">
<input id="assetCode" name="assetCode" class="form-control" type="text"
placeholder="请选择待绑定资产" readonly required>
<span class="input-group-btn">
<button type="button" class="btn btn-primary" onclick="selectAsset()">
<i class="fa fa-search"></i> 选择
</button>
</span>
</div>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">资产名称:</label>
<div class="col-sm-8">
<input id="assetName" class="form-control" type="text" readonly>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">资产状态:</label>
<div class="col-sm-8">
<input id="assetStatusName" class="form-control" type="text" readonly>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">仓库 / 位置:</label>
<div class="col-sm-8">
<input id="assetLocation" class="form-control" type="text" readonly>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">备注:</label>
<div class="col-sm-8">
<textarea name="remark" maxlength="500" class="form-control" rows="3"></textarea>
</div>
</div>
</form>
</div>
<th:block th:include="include :: footer" />
<script th:inline="javascript">
var prefix = ctx + "asset/tag";
var assetStatusDatas = [[${@dict.getType('ams_asset_status')}]];
$("#form-tag-bind").validate({
focusCleanup: true
});
function selectAsset() {
var options = {
title: "选择待绑定资产",
url: prefix + "/selectAsset",
width: "1100",
height: "650",
callBack: setSelectedAsset
};
$.modal.openOptions(options);
}
function setSelectedAsset(index, layero) {
var selectedAsset = layero.find("iframe")[0].contentWindow.getSelectedAsset();
if (!selectedAsset) {
$.modal.alertWarning("请选择一条资产记录");
return;
}
$("#assetCode").val(selectedAsset.assetCode);
$("#assetName").val(selectedAsset.assetName);
$("#assetStatusName").val($.table.selectDictLabel(assetStatusDatas, selectedAsset.assetStatus));
$("#assetLocation").val((selectedAsset.warehouseName || "-") + " / " + (selectedAsset.locationName || "-"));
$.modal.close(index);
}
function submitHandler() {
if ($.validate.form()) {
$.operate.save(prefix + "/bind", $("#form-tag-bind").serialize());
}
}
</script>
</body>
</html>

@ -0,0 +1,102 @@
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:include="include :: header('选择待绑定资产')" />
</head>
<body class="gray-bg">
<div class="container-div">
<div class="row">
<div class="col-sm-12 search-collapse">
<form id="formId">
<div class="select-list">
<ul>
<li>
<label>资产编码:</label>
<input type="text" name="assetCode">
</li>
<li>
<label>资产名称:</label>
<input type="text" name="assetName">
</li>
<li>
<label>资产类别:</label>
<input type="text" name="categoryName">
</li>
<li>
<label>资产状态:</label>
<select name="assetStatus" th:with="type=${@dict.getType('ams_asset_status')}">
<option value="">所有可绑定状态</option>
<option th:each="dict : ${type}" th:text="${dict.dictLabel}"
th:value="${dict.dictValue}"></option>
</select>
</li>
<li>
<a class="btn btn-primary btn-rounded btn-sm" onclick="$.table.search()">
<i class="fa fa-search"></i>&nbsp;搜索
</a>
<a class="btn btn-warning btn-rounded btn-sm" onclick="$.form.reset()">
<i class="fa fa-refresh"></i>&nbsp;重置
</a>
</li>
</ul>
</div>
</form>
</div>
<div class="col-sm-12 select-table table-striped">
<table id="bootstrap-table"></table>
</div>
</div>
</div>
<th:block th:include="include :: footer" />
<script th:inline="javascript">
var prefix = ctx + "asset/tag";
var assetStatusDatas = [[${@dict.getType('ams_asset_status')}]];
$(function() {
var options = {
url: prefix + "/bindableAssetList",
showSearch: false,
showRefresh: true,
showToggle: false,
showColumns: false,
modalName: "待绑定资产",
columns: [{
radio: true
},
{
field: "assetCode",
title: "资产编码"
},
{
field: "assetName",
title: "资产名称"
},
{
field: "categoryName",
title: "资产类别"
},
{
field: "assetStatus",
title: "资产状态",
formatter: function(value) {
return $.table.selectDictLabel(assetStatusDatas, value);
}
},
{
field: "warehouseName",
title: "所属仓库"
},
{
field: "locationName",
title: "存放位置"
}]
};
$.table.init(options);
});
function getSelectedAsset() {
return $("#bootstrap-table").bootstrapTable("getSelections")[0];
}
</script>
</body>
</html>

@ -52,6 +52,9 @@
<a class="btn btn-primary single disabled" onclick="$.operate.edit()" shiro:hasPermission="asset:tag:edit">
<i class="fa fa-edit"></i> 修改
</a>
<a class="btn btn-success single disabled" onclick="bindSelectedTag()" shiro:hasPermission="asset:tag:bind">
<i class="fa fa-link"></i> 绑定
</a>
<a class="btn btn-warning single disabled" onclick="voidSelectedTag()" shiro:hasPermission="asset:tag:edit">
<i class="fa fa-ban"></i> 作废
</a>
@ -73,6 +76,7 @@
<th:block th:include="include :: footer" />
<script th:inline="javascript">
var editFlag = [[${@permission.hasPermi('asset:tag:edit')}]];
var bindFlag = [[${@permission.hasPermi('asset:tag:bind')}]];
var removeFlag = [[${@permission.hasPermi('asset:tag:remove')}]];
var tagStatusDatas = [[${@dict.getType('ams_tag_status')}]];
var bindStatusDatas = [[${@dict.getType('ams_tag_bind_status')}]];
@ -144,6 +148,9 @@
var actions = [];
actions.push('<a class="btn btn-info btn-xs" href="javascript:void(0)" onclick="$.operate.view(\'' + row.tagId + '\')"><i class="fa fa-eye"></i>查看</a> ');
actions.push('<a class="btn btn-success btn-xs ' + editFlag + '" href="javascript:void(0)" onclick="$.operate.edit(\'' + row.tagId + '\')"><i class="fa fa-edit"></i>编辑</a> ');
if (row.tagStatus === 'NORMAL' && row.bindStatus === 'UNBOUND') {
actions.push('<a class="btn btn-success btn-xs ' + bindFlag + '" href="javascript:void(0)" onclick="bindTag(\'' + row.tagId + '\')"><i class="fa fa-link"></i>绑定</a> ');
}
if (row.tagStatus !== 'VOID' && row.bindStatus !== 'BOUND') {
actions.push('<a class="btn btn-warning btn-xs ' + editFlag + '" href="javascript:void(0)" onclick="voidTag(\'' + row.tagId + '\')"><i class="fa fa-ban"></i>作废</a> ');
}
@ -165,6 +172,22 @@
}
}
function bindSelectedTag() {
var row = $("#bootstrap-table").bootstrapTable('getSelections')[0];
if (!row) {
return;
}
if (row.tagStatus !== 'NORMAL' || row.bindStatus !== 'UNBOUND') {
$.modal.alertWarning("只有正常且未绑定的RFID标签可以绑定资产");
return;
}
bindTag(row.tagId);
}
function bindTag(tagId) {
$.modal.open("绑定RFID标签", prefix + "/bind/" + tagId, "800", "620");
}
function unbindSelectedTag() {
var row = $("#bootstrap-table").bootstrapTable('getSelections')[0];
if (row) {

@ -0,0 +1,96 @@
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.Mockito.verify;
import static org.mockito.Mockito.when;
import com.ruoyi.asset.domain.AmsRfidTag;
import com.ruoyi.asset.domain.RfidBindingContext;
import com.ruoyi.asset.service.IAmsAssetService;
import com.ruoyi.asset.service.IAmsRfidTagService;
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.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.test.util.ReflectionTestUtils;
@ExtendWith(MockitoExtension.class)
class AmsRfidTagControllerTest
{
@Mock
private IAmsRfidTagService amsRfidTagService;
@Mock
private IAmsAssetService amsAssetService;
@Mock
private IRfidBindingService rfidBindingService;
private TestAmsRfidTagController controller;
@BeforeEach
void setUp()
{
controller = new TestAmsRfidTagController();
ReflectionTestUtils.setField(controller, "amsRfidTagService", amsRfidTagService);
ReflectionTestUtils.setField(controller, "amsAssetService", amsAssetService);
ReflectionTestUtils.setField(controller, "rfidBindingService", rfidBindingService);
}
/** Web绑定必须使用数据库中的标签EPC并把当前登录人传给公共绑定服务 */
@Test
void bindSaveShouldDelegateToRfidBindingService()
{
AmsRfidTag tag = new AmsRfidTag();
tag.setTagId(10L);
tag.setEpcCode("EPC-001");
when(amsRfidTagService.selectAmsRfidTagByTagId(10L)).thenReturn(tag);
AjaxResult result = controller.bindSave(10L, "ASSET-001", "页面测试");
ArgumentCaptor<RfidBindingContext> contextCaptor = ArgumentCaptor.forClass(RfidBindingContext.class);
verify(rfidBindingService).bind(org.mockito.ArgumentMatchers.eq("ASSET-001"),
org.mockito.ArgumentMatchers.eq("EPC-001"), contextCaptor.capture());
assertEquals(1L, contextCaptor.getValue().getOperateUserId());
assertEquals("管理员", contextCaptor.getValue().getOperateUserName());
assertEquals("admin", contextCaptor.getValue().getOperateLoginName());
assertEquals("页面测试", contextCaptor.getValue().getRemark());
assertEquals(AjaxResult.Type.SUCCESS.value(), result.get(AjaxResult.CODE_TAG));
}
/** 标签不存在时不得调用公共绑定服务 */
@Test
void bindSaveShouldRejectMissingTag()
{
ServiceException exception = assertThrows(ServiceException.class,
() -> controller.bindSave(10L, "ASSET-001", null));
assertEquals("RFID标签不存在或已删除", exception.getMessage());
}
private static class TestAmsRfidTagController extends AmsRfidTagController
{
private final SysUser user;
private TestAmsRfidTagController()
{
user = new SysUser();
user.setUserId(1L);
user.setUserName("管理员");
user.setLoginName("admin");
}
@Override
public SysUser getSysUser()
{
return user;
}
}
}

@ -58,6 +58,20 @@ class AmsAssetServiceImplTest
@InjectMocks
private AmsAssetServiceImpl service;
/** Web绑定资产选择只能通过专用Mapper查询并固定排除已报废资产 */
@Test
void selectBindableAmsAssetListShouldExcludeDisposedAssets()
{
AmsAsset query = new AmsAsset();
query.setAssetCode("ASSET");
List<AmsAsset> expected = List.of(buildAsset());
when(amsAssetMapper.selectBindableAmsAssetList(query, AssetStatus.DISPOSED)).thenReturn(expected);
assertEquals(expected, service.selectBindableAmsAssetList(query));
verify(amsAssetMapper).selectBindableAmsAssetList(query, AssetStatus.DISPOSED);
}
/** 新建资产时,无论前端传入什么状态,必须强制为在库并清除使用归属 */
@Test
void insertAmsAssetShouldForceStockAndClearUseOwnership()

Loading…
Cancel
Save