feat(asset): 在资产报废确认时同步作废当前绑定的RFID标签

- 保留RFID标签与报废资产的绑定快照用于后续追溯
- 新增lockBoundRfidTags方法锁定待作废的RFID标签
- 新增voidBoundRfidTagByAssetId数据库操作防止并发冲突
- 更新前端提示信息明确报废操作对RFID标签的影响
- 修改资产流转服务职责,将标签作废处理移至报废单服务
main
yangk 3 days ago
parent 51cae14152
commit b1f026a5d9

@ -37,6 +37,14 @@ public interface AmsRfidTagMapper
*/
public AmsRfidTag selectAmsRfidTagByEpcForUpdate(String epcCode);
/**
* EPCRFID
*
* @param epcCode EPC
* @return RFID
*/
public AmsRfidTag selectAmsRfidTagByEpc(String epcCode);
/**
* RFID
*
@ -53,6 +61,14 @@ public interface AmsRfidTagMapper
*/
public List<AmsRfidTag> selectAmsRfidTagByEpcCodes(@Param("epcCodes") List<String> epcCodes);
/**
* IDRFID
*
* @param assetIds ID
* @return RFID
*/
public List<AmsRfidTag> selectBoundAmsRfidTagsByAssetIdsForUpdate(@Param("assetIds") List<Long> assetIds);
/**
*
*
@ -105,6 +121,20 @@ public interface AmsRfidTagMapper
@Param("updateBy") String updateBy,
@Param("updateTime") Date updateTime);
/**
* RFID
*
* @param tagId RFID
* @param assetId ID
* @param updateBy
* @param updateTime
* @return
*/
public int voidBoundAmsRfidTagByAssetId(@Param("tagId") Long tagId,
@Param("assetId") Long assetId,
@Param("updateBy") String updateBy,
@Param("updateTime") Date updateTime);
/**
* RFID
*

@ -106,7 +106,7 @@ public interface IAssetStatusTransitionService
/**
* IN_STOCK/IN_USE DISPOSED
* <p> RFID </p>
* <p> RFID </p>
*
* @param assetId ID
* @param context

@ -2,8 +2,10 @@ package com.ruoyi.asset.service.impl;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -13,9 +15,11 @@ import com.ruoyi.asset.domain.AmsAsset;
import com.ruoyi.asset.domain.AmsDisposalOperateContext;
import com.ruoyi.asset.domain.AmsDisposalOrder;
import com.ruoyi.asset.domain.AmsDisposalOrderItem;
import com.ruoyi.asset.domain.AmsRfidTag;
import com.ruoyi.asset.domain.AssetTransitionContext;
import com.ruoyi.asset.mapper.AmsAssetMapper;
import com.ruoyi.asset.mapper.AmsDisposalOrderMapper;
import com.ruoyi.asset.mapper.AmsRfidTagMapper;
import com.ruoyi.asset.service.IAmsDisposalOrderService;
import com.ruoyi.asset.service.IAssetStatusTransitionService;
import com.ruoyi.common.core.text.Convert;
@ -48,6 +52,9 @@ public class AmsDisposalOrderServiceImpl implements IAmsDisposalOrderService
@Autowired
private AmsAssetMapper amsAssetMapper;
@Autowired
private AmsRfidTagMapper amsRfidTagMapper;
@Autowired
private ISysCodeRuleService sysCodeRuleService;
@ -264,6 +271,7 @@ public class AmsDisposalOrderServiceImpl implements IAmsDisposalOrderService
AmsDisposalOrder order = requireOrderForUpdate(orderId, DisposalOrderStatus.PENDING_CONFIRM, "仅待确认状态的报废单允许确认报废");
// 2. 升序加锁校验资产状态及跨单占用
List<AmsRfidTag> boundTags = lockBoundRfidTags(order);
validateOrderAssetsReady(order);
Date now = DateUtils.getNowDate();
@ -281,11 +289,14 @@ public class AmsDisposalOrderServiceImpl implements IAmsDisposalOrderService
context.setChangeSummary("确认资产报废");
context.setRemark(item.getDisposalRemark());
// 变更资产状态为 DISPOSED 写入资产履历,报废不清除 RFID 绑定
// 报废只终止标签继续流转,不清空绑定快照,便于后续追溯报废资产原 RFID 标签。
assetStatusTransitionService.confirmDisposal(item.getAssetId(), context);
}
// 4. 回填确认属性,变更为已报废状态
// 4. 同步作废当前绑定标签,保留标签与报废资产的绑定快照用于追溯。
voidBoundRfidTags(boundTags, operateContext, now);
// 5. 回填确认属性,变更为已报废状态
order.setConfirmUserId(operateContext.getOperateUserId());
order.setConfirmUserName(StringUtils.trim(operateContext.getOperateUserName()));
order.setConfirmTime(now);
@ -385,6 +396,52 @@ public class AmsDisposalOrderServiceImpl implements IAmsDisposalOrderService
return 1;
}
private List<AmsRfidTag> lockBoundRfidTags(AmsDisposalOrder order)
{
List<Long> assetIds = collectSortedAssetIds(order);
if (assetIds.isEmpty())
{
return Collections.emptyList();
}
List<AmsRfidTag> tags = amsRfidTagMapper.selectBoundAmsRfidTagsByAssetIdsForUpdate(assetIds);
return tags == null ? Collections.emptyList() : tags;
}
private List<Long> collectSortedAssetIds(AmsDisposalOrder order)
{
if (order == null || order.getAmsDisposalOrderItemList() == null)
{
return Collections.emptyList();
}
return order.getAmsDisposalOrderItemList().stream()
.filter(item -> item != null && item.getAssetId() != null)
.map(AmsDisposalOrderItem::getAssetId)
.distinct()
.sorted()
.collect(Collectors.toList());
}
private void voidBoundRfidTags(List<AmsRfidTag> boundTags,
AmsDisposalOperateContext operateContext, Date operateTime)
{
if (boundTags == null || boundTags.isEmpty())
{
return;
}
for (AmsRfidTag tag : boundTags)
{
if (tag == null || tag.getTagId() == null || tag.getAssetId() == null)
{
throw new ServiceException("RFID标签绑定信息不完整报废确认失败");
}
if (amsRfidTagMapper.voidBoundAmsRfidTagByAssetId(tag.getTagId(), tag.getAssetId(),
operateContext.getOperateLoginName(), operateTime) != 1)
{
throw new ServiceException(StringUtils.format("RFID标签【{}】状态已变化,报废确认失败", tag.getTagCode()));
}
}
}
private void validateOrderRequest(AmsDisposalOrder order)
{
if (order == null)

@ -62,6 +62,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
for update
</select>
<select id="selectAmsRfidTagByEpc" parameterType="String" resultMap="AmsRfidTagResult">
<include refid="selectAmsRfidTagVo"/>
where epc_code = #{epcCode} and del_flag = '0'
</select>
<select id="selectAmsRfidTagByEpcCodes" resultMap="AmsRfidTagResult">
<include refid="selectAmsRfidTagVo"/>
where del_flag = '0'
@ -71,6 +76,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</foreach>
</select>
<select id="selectBoundAmsRfidTagsByAssetIdsForUpdate" resultMap="AmsRfidTagResult">
<include refid="selectAmsRfidTagVo"/>
where del_flag = '0'
and bind_status = 'BOUND'
and asset_id in
<foreach collection="assetIds" item="assetId" open="(" separator="," close=")">
#{assetId}
</foreach>
order by asset_id, tag_id
for update
</select>
<select id="checkTagCodeUnique" parameterType="String" resultMap="AmsRfidTagResult">
<include refid="selectAmsRfidTagVo"/>
where tag_code = #{tagCode} and del_flag = '0' limit 1
@ -156,6 +173,19 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
and asset_id is null
</update>
<!-- 报废联动作废SQL只允许作废仍正常且绑定到当前资产的标签保留绑定快照用于追溯 -->
<update id="voidBoundAmsRfidTagByAssetId">
update ams_rfid_tag
set tag_status = 'VOID',
update_by = #{updateBy},
update_time = #{updateTime}
where tag_id = #{tagId}
and del_flag = '0'
and tag_status = 'NORMAL'
and bind_status = 'BOUND'
and asset_id = #{assetId}
</update>
<!-- 解绑SQLWHERE精确匹配 bind_status+asset_id防止并发或误解绑到错误资产 -->
<update id="unbindAmsRfidTagByTagId">
update ams_rfid_tag

@ -168,7 +168,7 @@
}
function confirmOrder(orderId) {
$.modal.confirm("确认要同意并执行此报废处置吗?此操作将更改对应资产为已报废状态,并且不可回滚!", function() {
$.modal.confirm("确认要同意并执行此报废处置吗?此操作将更改对应资产为已报废状态,并同步作废已绑定的 RFID 标签,且不可回滚!", function() {
$.post(prefix + "/confirm/" + orderId, function(result) {
if (result.code == web_status.SUCCESS) {
$.modal.msgSuccess(result.msg);

@ -4,10 +4,11 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
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.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ -23,13 +24,17 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import com.ruoyi.asset.constant.DisposalOrderStatus;
import com.ruoyi.asset.constant.RfidBindStatus;
import com.ruoyi.asset.constant.RfidTagStatus;
import com.ruoyi.asset.domain.AmsAsset;
import com.ruoyi.asset.domain.AmsDisposalOperateContext;
import com.ruoyi.asset.domain.AmsDisposalOrder;
import com.ruoyi.asset.domain.AmsDisposalOrderItem;
import com.ruoyi.asset.domain.AmsRfidTag;
import com.ruoyi.asset.domain.AssetTransitionContext;
import com.ruoyi.asset.mapper.AmsAssetMapper;
import com.ruoyi.asset.mapper.AmsDisposalOrderMapper;
import com.ruoyi.asset.mapper.AmsRfidTagMapper;
import com.ruoyi.asset.service.IAssetStatusTransitionService;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.system.service.ISysCodeRuleService;
@ -49,6 +54,9 @@ class AmsDisposalOrderServiceImplTest
@Mock
private AmsAssetMapper amsAssetMapper;
@Mock
private AmsRfidTagMapper amsRfidTagMapper;
@Mock
private ISysCodeRuleService sysCodeRuleService;
@ -198,6 +206,7 @@ class AmsDisposalOrderServiceImplTest
item.setItemId(500L);
when(amsDisposalOrderMapper.selectAmsDisposalOrderByOrderIdForUpdate(eq(100L))).thenReturn(current);
when(amsRfidTagMapper.selectBoundAmsRfidTagsByAssetIdsForUpdate(eq(List.of(1L)))).thenReturn(List.of());
stubAsset(1L, "IN_STOCK");
when(amsDisposalOrderMapper.countOtherActiveDisposalOrderByAssetId(eq(1L), eq(100L))).thenReturn(0);
when(amsDisposalOrderMapper.updateAmsDisposalOrder(any(AmsDisposalOrder.class))).thenReturn(1);
@ -218,6 +227,65 @@ class AmsDisposalOrderServiceImplTest
assertEquals(500L, transitionContext.getSourceItemId());
assertEquals(9L, transitionContext.getOperateUserId());
assertEquals("管理员", transitionContext.getOperateUserName());
verify(amsRfidTagMapper).selectBoundAmsRfidTagsByAssetIdsForUpdate(eq(List.of(1L)));
verify(amsRfidTagMapper, never()).voidBoundAmsRfidTagByAssetId(any(), any(), any(), any());
}
/**
* RFID
*/
@Test
void confirmShouldVoidBoundRfidTagAndKeepBindingSnapshot()
{
AmsDisposalOrder current = buildRequest();
current.setOrderId(100L);
current.setDisposalNo("BF202606170001");
current.setOrderStatus(DisposalOrderStatus.PENDING_CONFIRM);
current.getAmsDisposalOrderItemList().get(0).setItemId(500L);
AmsRfidTag tag = buildBoundTag(10L, 1L);
when(amsDisposalOrderMapper.selectAmsDisposalOrderByOrderIdForUpdate(eq(100L))).thenReturn(current);
when(amsRfidTagMapper.selectBoundAmsRfidTagsByAssetIdsForUpdate(eq(List.of(1L)))).thenReturn(List.of(tag));
stubAsset(1L, "IN_STOCK");
when(amsDisposalOrderMapper.countOtherActiveDisposalOrderByAssetId(eq(1L), eq(100L))).thenReturn(0);
when(amsRfidTagMapper.voidBoundAmsRfidTagByAssetId(eq(10L), eq(1L), eq("admin"), any(Date.class))).thenReturn(1);
when(amsDisposalOrderMapper.updateAmsDisposalOrder(any(AmsDisposalOrder.class))).thenReturn(1);
assertEquals(1, service.confirmDisposal(100L, adminContext()));
verify(assetStatusTransitionService).confirmDisposal(eq(1L), any(AssetTransitionContext.class));
verify(amsRfidTagMapper).voidBoundAmsRfidTagByAssetId(eq(10L), eq(1L), eq("admin"), any(Date.class));
assertEquals(RfidBindStatus.BOUND, tag.getBindStatus());
assertEquals(1L, tag.getAssetId());
assertEquals("A001", tag.getAssetCode());
}
/**
*
*/
@Test
void confirmShouldFailWhenBoundRfidTagChangedBeforeVoid()
{
AmsDisposalOrder current = buildRequest();
current.setOrderId(100L);
current.setDisposalNo("BF202606170001");
current.setOrderStatus(DisposalOrderStatus.PENDING_CONFIRM);
AmsRfidTag tag = buildBoundTag(10L, 1L);
when(amsDisposalOrderMapper.selectAmsDisposalOrderByOrderIdForUpdate(eq(100L))).thenReturn(current);
when(amsRfidTagMapper.selectBoundAmsRfidTagsByAssetIdsForUpdate(eq(List.of(1L)))).thenReturn(List.of(tag));
stubAsset(1L, "IN_STOCK");
when(amsDisposalOrderMapper.countOtherActiveDisposalOrderByAssetId(eq(1L), eq(100L))).thenReturn(0);
when(amsRfidTagMapper.voidBoundAmsRfidTagByAssetId(eq(10L), eq(1L), eq("admin"), any(Date.class))).thenReturn(0);
ServiceException exception = assertThrows(ServiceException.class,
() -> service.confirmDisposal(100L, adminContext()));
assertTrue(exception.getMessage().contains("RFID标签"));
verify(assetStatusTransitionService).confirmDisposal(eq(1L), any(AssetTransitionContext.class));
verify(amsDisposalOrderMapper, never()).updateAmsDisposalOrder(any(AmsDisposalOrder.class));
}
/**
@ -265,6 +333,20 @@ class AmsDisposalOrderServiceImplTest
return new AmsDisposalOperateContext(9L, "管理员", "admin", 3L, "资产管理部");
}
private AmsRfidTag buildBoundTag(Long tagId, Long assetId)
{
AmsRfidTag tag = new AmsRfidTag();
tag.setTagId(tagId);
tag.setTagCode("TAG-001");
tag.setEpcCode("EPC-001");
tag.setTagStatus(RfidTagStatus.NORMAL);
tag.setBindStatus(RfidBindStatus.BOUND);
tag.setAssetId(assetId);
tag.setAssetCode("A001");
tag.setAssetName("资产A");
return tag;
}
private void stubAsset(Long assetId, String status)
{
AmsAsset asset = new AmsAsset();

@ -319,7 +319,7 @@ class AssetStatusTransitionServiceImplTest
verify(amsAssetMapper, never()).updateAssetForTransition(any(AmsAsset.class));
}
/** 报废不清除 RFID 绑定,保留标签溯源能力 */
/** 资产流转服务保留 RFID 快照,标签作废由报废单服务负责 */
@Test
void confirmDisposalShouldEnterDisposedAndKeepRfid()
{

Loading…
Cancel
Save