feat(asset): 添加资产生命周期履历功能

- 新增 AmsAssetLifecycleLog 实体类用于记录资产履历信息
- 新增 AmsAssetLifecycleLogMapper 接口和对应 XML 映射文件
- 在 AmsAssetController 中注入 IAssetLifecycleService 并传递履历数据到前端
- 重构 RFID 标签解绑逻辑,将履历记录委托给统一的资产生命周期服务
- 新增 AssetLifecycleBusinessType 常量类定义资产履历业务类型
- 实现 IAssetLifecycleService 接口提供履历查询和记录功能
- 添加资产生命周期履历服务的单元测试
- 移除原来在 RFID 标签映射器中的履历插入方法和相关常量
main
yangk 2 weeks ago
parent 2d2a5f1c66
commit bf8218791d

@ -0,0 +1,36 @@
package com.ruoyi.asset.constant;
/**
*
*
* @author Yangk
*/
public final class AssetLifecycleBusinessType
{
/** 资产入库 */
public static final String INBOUND = "INBOUND";
/** 资产领用 */
public static final String RECEIVE = "RECEIVE";
/** 资产退库 */
public static final String RETURN = "RETURN";
/** 资产借用 */
public static final String BORROW = "BORROW";
/** 借用归还 */
public static final String BORROW_RETURN = "BORROW_RETURN";
/** 资产调拨 */
public static final String TRANSFER = "TRANSFER";
/** 资产维修 */
public static final String REPAIR = "REPAIR";
/** 资产处置/报废 */
public static final String DISPOSAL = "DISPOSAL";
/** 资产盘点 */
public static final String INVENTORY = "INVENTORY";
/** RFID标签绑定 */
public static final String RFID_BIND = "RFID_BIND";
/** RFID标签解绑 */
public static final String RFID_UNBIND = "RFID_UNBIND";
private AssetLifecycleBusinessType()
{
}
}

@ -19,6 +19,7 @@ import com.ruoyi.asset.service.IAmsAssetCategoryService;
import com.ruoyi.asset.service.IAmsAssetLocationService;
import com.ruoyi.asset.service.IAmsAssetService;
import com.ruoyi.asset.service.IAmsWarehouseService;
import com.ruoyi.asset.service.IAssetLifecycleService;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.controller.BaseController;
@ -48,6 +49,9 @@ public class AmsAssetController extends BaseController
@Autowired
private IAmsAssetService amsAssetService;
@Autowired
private IAssetLifecycleService assetLifecycleService;
@Autowired
private IAmsAssetCategoryService amsAssetCategoryService;
@ -134,6 +138,8 @@ public class AmsAssetController extends BaseController
{
AmsAsset amsAsset = amsAssetService.selectAmsAssetByAssetId(assetId);
mmap.put("amsAsset", amsAsset);
// 获取当前资产在台账中的完整生命周期历史流转履历,传递至前端展示页面进行渲染
mmap.put("lifecycleList", assetLifecycleService.selectByAssetId(assetId));
return prefix + "/view";
}

@ -0,0 +1,419 @@
package com.ruoyi.asset.domain;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.core.domain.BaseEntity;
/**
* ams_asset_lifecycle_log
*
* @author Yangk
*/
public class AmsAssetLifecycleLog extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** 履历ID */
private Long logId;
/** 资产ID */
private Long assetId;
/** 资产编码快照 */
private String assetCode;
/** 资产名称快照 */
private String assetName;
/** 业务类型 */
private String businessType;
/** 来源单据ID */
private Long sourceOrderId;
/** 来源单据号 */
private String sourceOrderNo;
/** 来源明细ID */
private Long sourceItemId;
/** 变更前资产状态 */
private String beforeStatus;
/** 变更后资产状态 */
private String afterStatus;
/** 变更前仓库ID */
private Long beforeWarehouseId;
/** 变更前仓库名称 */
private String beforeWarehouseName;
/** 变更后仓库ID */
private Long afterWarehouseId;
/** 变更后仓库名称 */
private String afterWarehouseName;
/** 变更前位置ID */
private Long beforeLocationId;
/** 变更前位置名称 */
private String beforeLocationName;
/** 变更后位置ID */
private Long afterLocationId;
/** 变更后位置名称 */
private String afterLocationName;
/** 变更前使用人ID */
private Long beforeUserId;
/** 变更前使用人名称 */
private String beforeUserName;
/** 变更后使用人ID */
private Long afterUserId;
/** 变更后使用人名称 */
private String afterUserName;
/** 变更前使用部门ID */
private Long beforeDeptId;
/** 变更前使用部门名称 */
private String beforeDeptName;
/** 变更后使用部门ID */
private Long afterDeptId;
/** 变更后使用部门名称 */
private String afterDeptName;
/** 操作用户ID */
private Long operateUserId;
/** 操作用户名称 */
private String operateUserName;
/** 操作时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date operateTime;
/** 变更摘要 */
private String changeSummary;
/** 删除标志 */
private String delFlag;
public Long getLogId()
{
return logId;
}
public void setLogId(Long logId)
{
this.logId = logId;
}
public Long getAssetId()
{
return assetId;
}
public void setAssetId(Long assetId)
{
this.assetId = assetId;
}
public String getAssetCode()
{
return assetCode;
}
public void setAssetCode(String assetCode)
{
this.assetCode = assetCode;
}
public String getAssetName()
{
return assetName;
}
public void setAssetName(String assetName)
{
this.assetName = assetName;
}
public String getBusinessType()
{
return businessType;
}
public void setBusinessType(String businessType)
{
this.businessType = businessType;
}
public Long getSourceOrderId()
{
return sourceOrderId;
}
public void setSourceOrderId(Long sourceOrderId)
{
this.sourceOrderId = sourceOrderId;
}
public String getSourceOrderNo()
{
return sourceOrderNo;
}
public void setSourceOrderNo(String sourceOrderNo)
{
this.sourceOrderNo = sourceOrderNo;
}
public Long getSourceItemId()
{
return sourceItemId;
}
public void setSourceItemId(Long sourceItemId)
{
this.sourceItemId = sourceItemId;
}
public String getBeforeStatus()
{
return beforeStatus;
}
public void setBeforeStatus(String beforeStatus)
{
this.beforeStatus = beforeStatus;
}
public String getAfterStatus()
{
return afterStatus;
}
public void setAfterStatus(String afterStatus)
{
this.afterStatus = afterStatus;
}
public Long getBeforeWarehouseId()
{
return beforeWarehouseId;
}
public void setBeforeWarehouseId(Long beforeWarehouseId)
{
this.beforeWarehouseId = beforeWarehouseId;
}
public String getBeforeWarehouseName()
{
return beforeWarehouseName;
}
public void setBeforeWarehouseName(String beforeWarehouseName)
{
this.beforeWarehouseName = beforeWarehouseName;
}
public Long getAfterWarehouseId()
{
return afterWarehouseId;
}
public void setAfterWarehouseId(Long afterWarehouseId)
{
this.afterWarehouseId = afterWarehouseId;
}
public String getAfterWarehouseName()
{
return afterWarehouseName;
}
public void setAfterWarehouseName(String afterWarehouseName)
{
this.afterWarehouseName = afterWarehouseName;
}
public Long getBeforeLocationId()
{
return beforeLocationId;
}
public void setBeforeLocationId(Long beforeLocationId)
{
this.beforeLocationId = beforeLocationId;
}
public String getBeforeLocationName()
{
return beforeLocationName;
}
public void setBeforeLocationName(String beforeLocationName)
{
this.beforeLocationName = beforeLocationName;
}
public Long getAfterLocationId()
{
return afterLocationId;
}
public void setAfterLocationId(Long afterLocationId)
{
this.afterLocationId = afterLocationId;
}
public String getAfterLocationName()
{
return afterLocationName;
}
public void setAfterLocationName(String afterLocationName)
{
this.afterLocationName = afterLocationName;
}
public Long getBeforeUserId()
{
return beforeUserId;
}
public void setBeforeUserId(Long beforeUserId)
{
this.beforeUserId = beforeUserId;
}
public String getBeforeUserName()
{
return beforeUserName;
}
public void setBeforeUserName(String beforeUserName)
{
this.beforeUserName = beforeUserName;
}
public Long getAfterUserId()
{
return afterUserId;
}
public void setAfterUserId(Long afterUserId)
{
this.afterUserId = afterUserId;
}
public String getAfterUserName()
{
return afterUserName;
}
public void setAfterUserName(String afterUserName)
{
this.afterUserName = afterUserName;
}
public Long getBeforeDeptId()
{
return beforeDeptId;
}
public void setBeforeDeptId(Long beforeDeptId)
{
this.beforeDeptId = beforeDeptId;
}
public String getBeforeDeptName()
{
return beforeDeptName;
}
public void setBeforeDeptName(String beforeDeptName)
{
this.beforeDeptName = beforeDeptName;
}
public Long getAfterDeptId()
{
return afterDeptId;
}
public void setAfterDeptId(Long afterDeptId)
{
this.afterDeptId = afterDeptId;
}
public String getAfterDeptName()
{
return afterDeptName;
}
public void setAfterDeptName(String afterDeptName)
{
this.afterDeptName = afterDeptName;
}
public Long getOperateUserId()
{
return operateUserId;
}
public void setOperateUserId(Long operateUserId)
{
this.operateUserId = operateUserId;
}
public String getOperateUserName()
{
return operateUserName;
}
public void setOperateUserName(String operateUserName)
{
this.operateUserName = operateUserName;
}
public Date getOperateTime()
{
return operateTime;
}
public void setOperateTime(Date operateTime)
{
this.operateTime = operateTime;
}
public String getChangeSummary()
{
return changeSummary;
}
public void setChangeSummary(String changeSummary)
{
this.changeSummary = changeSummary;
}
public String getDelFlag()
{
return delFlag;
}
public void setDelFlag(String delFlag)
{
this.delFlag = delFlag;
}
}

@ -0,0 +1,28 @@
package com.ruoyi.asset.mapper;
import java.util.List;
import com.ruoyi.asset.domain.AmsAssetLifecycleLog;
/**
* Mapper
*
* @author Yangk
*/
public interface AmsAssetLifecycleLogMapper
{
/**
* ID
*
* @param assetId ID
* @return
*/
public List<AmsAssetLifecycleLog> selectAmsAssetLifecycleLogByAssetId(Long assetId);
/**
*
*
* @param lifecycleLog
* @return
*/
public int insertAmsAssetLifecycleLog(AmsAssetLifecycleLog lifecycleLog);
}

@ -3,7 +3,6 @@ package com.ruoyi.asset.mapper;
import java.util.Date;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.ruoyi.asset.domain.AmsAsset;
import com.ruoyi.asset.domain.AmsRfidTag;
/**
@ -86,26 +85,6 @@ public interface AmsRfidTagMapper
@Param("updateBy") String updateBy,
@Param("updateTime") Date updateTime);
/**
* RFID
*
* @param asset
* @param businessType
* @param operateUserId ID
* @param operateUserName
* @param operateTime
* @param changeSummary
* @param operName
* @return
*/
public int insertRfidLifecycleLog(@Param("asset") AmsAsset asset,
@Param("businessType") String businessType,
@Param("operateUserId") Long operateUserId,
@Param("operateUserName") String operateUserName,
@Param("operateTime") Date operateTime,
@Param("changeSummary") String changeSummary,
@Param("operName") String operName);
/**
* RFID
*

@ -0,0 +1,31 @@
package com.ruoyi.asset.service;
import java.util.List;
import com.ruoyi.asset.domain.AmsAsset;
import com.ruoyi.asset.domain.AmsAssetLifecycleLog;
/**
*
*
* @author Yangk
*/
public interface IAssetLifecycleService
{
/**
* ID
*
* @param assetId ID
* @return
*/
public List<AmsAssetLifecycleLog> selectByAssetId(Long assetId);
/**
*
*
* @param beforeAsset
* @param afterAsset
* @param lifecycleLog
* @return
*/
public int recordLifecycle(AmsAsset beforeAsset, AmsAsset afterAsset, AmsAssetLifecycleLog lifecycleLog);
}

@ -2,8 +2,11 @@ package com.ruoyi.asset.service.impl;
import java.util.Date;
import java.util.List;
import com.ruoyi.asset.constant.AssetLifecycleBusinessType;
import com.ruoyi.asset.domain.AmsAsset;
import com.ruoyi.asset.domain.AmsAssetLifecycleLog;
import com.ruoyi.asset.mapper.AmsAssetMapper;
import com.ruoyi.asset.service.IAssetLifecycleService;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.DateUtils;
@ -33,8 +36,6 @@ public class AmsRfidTagServiceImpl implements IAmsRfidTagService
private static final String BIND_STATUS_BOUND = "BOUND";
private static final String BUSINESS_TYPE_RFID_UNBIND = "RFID_UNBIND";
private static final String DEL_FLAG_NORMAL = "0";
@Autowired
@ -43,6 +44,9 @@ public class AmsRfidTagServiceImpl implements IAmsRfidTagService
@Autowired
private AmsAssetMapper amsAssetMapper;
@Autowired
private IAssetLifecycleService assetLifecycleService;
/**
* RFID
*
@ -231,8 +235,10 @@ public class AmsRfidTagServiceImpl implements IAmsRfidTagService
{
throw new ServiceException("RFID标签当前绑定信息已变化解绑失败");
}
amsRfidTagMapper.insertRfidLifecycleLog(asset, BUSINESS_TYPE_RFID_UNBIND, operUserId,
operUserName, now, buildUnbindSummary(asset, tag), operName);
// 记录资产生命周期履历。注意解绑RFID标签动作并不改变资产本身的物理状态、存放位置及使用部门归属
// 故前后快照均传入当前资产对象(asset),仅在变更摘要中详细体现解绑动作。
assetLifecycleService.recordLifecycle(asset, asset,
buildUnbindLifecycleLog(asset, tag, operUserId, operUserName, operName, now));
return 1;
}
@ -374,4 +380,17 @@ public class AmsRfidTagServiceImpl implements IAmsRfidTagService
return StringUtils.format("RFID标签解绑标签编码【{}】EPC【{}】从资产【{}】解绑",
tag.getTagCode(), tag.getEpcCode(), asset.getAssetCode());
}
private AmsAssetLifecycleLog buildUnbindLifecycleLog(AmsAsset asset, AmsRfidTag tag,
Long operUserId, String operUserName, String operName, Date operateTime)
{
AmsAssetLifecycleLog lifecycleLog = new AmsAssetLifecycleLog();
lifecycleLog.setBusinessType(AssetLifecycleBusinessType.RFID_UNBIND);
lifecycleLog.setOperateUserId(operUserId);
lifecycleLog.setOperateUserName(operUserName);
lifecycleLog.setOperateTime(operateTime);
lifecycleLog.setChangeSummary(buildUnbindSummary(asset, tag));
lifecycleLog.setCreateBy(operName);
return lifecycleLog;
}
}

@ -0,0 +1,184 @@
package com.ruoyi.asset.service.impl;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import com.ruoyi.asset.domain.AmsAsset;
import com.ruoyi.asset.domain.AmsAssetLifecycleLog;
import com.ruoyi.asset.mapper.AmsAssetLifecycleLogMapper;
import com.ruoyi.asset.service.IAssetLifecycleService;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.domain.entity.SysDictData;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.service.ISysDictDataService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
*
*
* @author Yangk
*/
@Service
public class AssetLifecycleServiceImpl implements IAssetLifecycleService
{
private static final String LIFECYCLE_BUSINESS_TYPE_DICT = "ams_lifecycle_business_type";
private static final String DEL_FLAG_NORMAL = "0";
@Autowired
private AmsAssetLifecycleLogMapper lifecycleLogMapper;
@Autowired
private ISysDictDataService sysDictDataService;
@Override
public List<AmsAssetLifecycleLog> selectByAssetId(Long assetId)
{
if (StringUtils.isNull(assetId))
{
throw new ServiceException("资产ID不能为空");
}
return lifecycleLogMapper.selectAmsAssetLifecycleLogByAssetId(assetId);
}
@Override
@Transactional(rollbackFor = Exception.class)
public int recordLifecycle(AmsAsset beforeAsset, AmsAsset afterAsset, AmsAssetLifecycleLog lifecycleLog)
{
// 1. 规范化处理传入的日志数据,统一去除空格
normalizeLifecycleLog(lifecycleLog);
// 2. 强校验执行核心业务边界与入参有效性校验Fail-Fast
validateRequest(beforeAsset, afterAsset, lifecycleLog);
// 3. 确定资产身份标识快照(优先使用变动后快照,若被删除/归空则使用变动前快照)
AmsAsset assetSnapshot = StringUtils.isNotNull(afterAsset) ? afterAsset : beforeAsset;
fillAssetIdentity(lifecycleLog, assetSnapshot);
fillBeforeSnapshot(lifecycleLog, beforeAsset);
fillAfterSnapshot(lifecycleLog, afterAsset);
Date operateTime = StringUtils.nvl(lifecycleLog.getOperateTime(), DateUtils.getNowDate());
lifecycleLog.setOperateTime(operateTime);
lifecycleLog.setCreateTime(operateTime);
lifecycleLog.setDelFlag(DEL_FLAG_NORMAL);
lifecycleLog.setUpdateBy(null);
lifecycleLog.setUpdateTime(null);
int rows = lifecycleLogMapper.insertAmsAssetLifecycleLog(lifecycleLog);
if (rows != 1)
{
throw new ServiceException("资产履历写入失败");
}
return rows;
}
private void validateRequest(AmsAsset beforeAsset, AmsAsset afterAsset, AmsAssetLifecycleLog lifecycleLog)
{
// 落实 Fail Fast 原则:防范核心主表快照为空,确保履历能关联到有效实体
if (StringUtils.isNull(beforeAsset) && StringUtils.isNull(afterAsset))
{
throw new ServiceException("资产变更前后快照不能同时为空");
}
if (StringUtils.isNull(lifecycleLog))
{
throw new ServiceException("资产履历业务信息不能为空");
}
// 强一致性校验:防止因操作失误传入不同资产的快照造成履历数据交叉污染
if (StringUtils.isNotNull(beforeAsset) && StringUtils.isNotNull(afterAsset)
&& !Objects.equals(beforeAsset.getAssetId(), afterAsset.getAssetId()))
{
throw new ServiceException("资产变更前后快照不属于同一资产");
}
AmsAsset assetSnapshot = StringUtils.isNotNull(afterAsset) ? afterAsset : beforeAsset;
if (StringUtils.isNull(assetSnapshot.getAssetId())
|| StringUtils.isEmpty(assetSnapshot.getAssetCode())
|| StringUtils.isEmpty(assetSnapshot.getAssetName()))
{
throw new ServiceException("资产履历缺少资产ID、资产编码或资产名称");
}
validateBusinessType(lifecycleLog.getBusinessType());
validateLength(lifecycleLog.getSourceOrderNo(), 64, "来源单据号");
validateLength(lifecycleLog.getOperateUserName(), 100, "操作用户名称");
validateLength(lifecycleLog.getChangeSummary(), 500, "变更摘要");
validateLength(lifecycleLog.getCreateBy(), 64, "创建账号");
validateLength(lifecycleLog.getRemark(), 500, "备注");
}
private void normalizeLifecycleLog(AmsAssetLifecycleLog lifecycleLog)
{
if (StringUtils.isNull(lifecycleLog))
{
return;
}
lifecycleLog.setBusinessType(StringUtils.trim(lifecycleLog.getBusinessType()));
lifecycleLog.setSourceOrderNo(StringUtils.trim(lifecycleLog.getSourceOrderNo()));
lifecycleLog.setOperateUserName(StringUtils.trim(lifecycleLog.getOperateUserName()));
lifecycleLog.setChangeSummary(StringUtils.trim(lifecycleLog.getChangeSummary()));
lifecycleLog.setCreateBy(StringUtils.trim(lifecycleLog.getCreateBy()));
lifecycleLog.setRemark(StringUtils.trim(lifecycleLog.getRemark()));
}
private void validateBusinessType(String businessType)
{
if (StringUtils.isEmpty(businessType))
{
throw new ServiceException("资产履历业务类型不能为空");
}
// 动态数据字典校验:通过系统字典 ams_lifecycle_business_type 验证,而非硬编码,实现未来灵活扩展
SysDictData query = new SysDictData();
query.setDictType(LIFECYCLE_BUSINESS_TYPE_DICT);
query.setDictValue(businessType);
query.setStatus(UserConstants.DICT_NORMAL);
List<SysDictData> dictDataList = sysDictDataService.selectDictDataList(query);
boolean exists = dictDataList != null && dictDataList.stream()
.anyMatch(data -> StringUtils.equals(businessType, data.getDictValue()));
if (!exists)
{
throw new ServiceException("资产履历业务类型不存在或已停用");
}
}
private void validateLength(String value, int maxLength, String fieldName)
{
if (StringUtils.isNotEmpty(value) && value.length() > maxLength)
{
throw new ServiceException(fieldName + "长度不能超过" + maxLength + "个字符");
}
}
private void fillAssetIdentity(AmsAssetLifecycleLog lifecycleLog, AmsAsset asset)
{
lifecycleLog.setAssetId(asset.getAssetId());
lifecycleLog.setAssetCode(asset.getAssetCode());
lifecycleLog.setAssetName(asset.getAssetName());
}
private void fillBeforeSnapshot(AmsAssetLifecycleLog lifecycleLog, AmsAsset asset)
{
lifecycleLog.setBeforeStatus(StringUtils.isNull(asset) ? null : asset.getAssetStatus());
lifecycleLog.setBeforeWarehouseId(StringUtils.isNull(asset) ? null : asset.getWarehouseId());
lifecycleLog.setBeforeWarehouseName(StringUtils.isNull(asset) ? null : asset.getWarehouseName());
lifecycleLog.setBeforeLocationId(StringUtils.isNull(asset) ? null : asset.getLocationId());
lifecycleLog.setBeforeLocationName(StringUtils.isNull(asset) ? null : asset.getLocationName());
lifecycleLog.setBeforeUserId(StringUtils.isNull(asset) ? null : asset.getUseUserId());
lifecycleLog.setBeforeUserName(StringUtils.isNull(asset) ? null : asset.getUseUserName());
lifecycleLog.setBeforeDeptId(StringUtils.isNull(asset) ? null : asset.getUseDeptId());
lifecycleLog.setBeforeDeptName(StringUtils.isNull(asset) ? null : asset.getUseDeptName());
}
private void fillAfterSnapshot(AmsAssetLifecycleLog lifecycleLog, AmsAsset asset)
{
lifecycleLog.setAfterStatus(StringUtils.isNull(asset) ? null : asset.getAssetStatus());
lifecycleLog.setAfterWarehouseId(StringUtils.isNull(asset) ? null : asset.getWarehouseId());
lifecycleLog.setAfterWarehouseName(StringUtils.isNull(asset) ? null : asset.getWarehouseName());
lifecycleLog.setAfterLocationId(StringUtils.isNull(asset) ? null : asset.getLocationId());
lifecycleLog.setAfterLocationName(StringUtils.isNull(asset) ? null : asset.getLocationName());
lifecycleLog.setAfterUserId(StringUtils.isNull(asset) ? null : asset.getUseUserId());
lifecycleLog.setAfterUserName(StringUtils.isNull(asset) ? null : asset.getUseUserName());
lifecycleLog.setAfterDeptId(StringUtils.isNull(asset) ? null : asset.getUseDeptId());
lifecycleLog.setAfterDeptName(StringUtils.isNull(asset) ? null : asset.getUseDeptName());
}
}

@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.asset.mapper.AmsAssetLifecycleLogMapper">
<resultMap type="AmsAssetLifecycleLog" id="AmsAssetLifecycleLogResult">
<result property="logId" column="log_id"/>
<result property="assetId" column="asset_id"/>
<result property="assetCode" column="asset_code"/>
<result property="assetName" column="asset_name"/>
<result property="businessType" column="business_type"/>
<result property="sourceOrderId" column="source_order_id"/>
<result property="sourceOrderNo" column="source_order_no"/>
<result property="sourceItemId" column="source_item_id"/>
<result property="beforeStatus" column="before_status"/>
<result property="afterStatus" column="after_status"/>
<result property="beforeWarehouseId" column="before_warehouse_id"/>
<result property="beforeWarehouseName" column="before_warehouse_name"/>
<result property="afterWarehouseId" column="after_warehouse_id"/>
<result property="afterWarehouseName" column="after_warehouse_name"/>
<result property="beforeLocationId" column="before_location_id"/>
<result property="beforeLocationName" column="before_location_name"/>
<result property="afterLocationId" column="after_location_id"/>
<result property="afterLocationName" column="after_location_name"/>
<result property="beforeUserId" column="before_user_id"/>
<result property="beforeUserName" column="before_user_name"/>
<result property="afterUserId" column="after_user_id"/>
<result property="afterUserName" column="after_user_name"/>
<result property="beforeDeptId" column="before_dept_id"/>
<result property="beforeDeptName" column="before_dept_name"/>
<result property="afterDeptId" column="after_dept_id"/>
<result property="afterDeptName" column="after_dept_name"/>
<result property="operateUserId" column="operate_user_id"/>
<result property="operateUserName" column="operate_user_name"/>
<result property="operateTime" column="operate_time"/>
<result property="changeSummary" column="change_summary"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
<result property="updateTime" column="update_time"/>
<result property="remark" column="remark"/>
<result property="delFlag" column="del_flag"/>
</resultMap>
<sql id="selectAmsAssetLifecycleLogVo">
select log_id, asset_id, asset_code, asset_name, business_type,
source_order_id, source_order_no, source_item_id,
before_status, after_status,
before_warehouse_id, before_warehouse_name, after_warehouse_id, after_warehouse_name,
before_location_id, before_location_name, after_location_id, after_location_name,
before_user_id, before_user_name, after_user_id, after_user_name,
before_dept_id, before_dept_name, after_dept_id, after_dept_name,
operate_user_id, operate_user_name, operate_time, change_summary,
create_by, create_time, update_by, update_time, remark, del_flag
from ams_asset_lifecycle_log
</sql>
<select id="selectAmsAssetLifecycleLogByAssetId" parameterType="Long" resultMap="AmsAssetLifecycleLogResult">
<include refid="selectAmsAssetLifecycleLogVo"/>
where asset_id = #{assetId} and del_flag = '0'
order by operate_time desc, log_id desc
</select>
<insert id="insertAmsAssetLifecycleLog" parameterType="AmsAssetLifecycleLog"
useGeneratedKeys="true" keyProperty="logId">
insert into ams_asset_lifecycle_log
(
asset_id, asset_code, asset_name, business_type,
source_order_id, source_order_no, source_item_id,
before_status, after_status,
before_warehouse_id, before_warehouse_name, after_warehouse_id, after_warehouse_name,
before_location_id, before_location_name, after_location_id, after_location_name,
before_user_id, before_user_name, after_user_id, after_user_name,
before_dept_id, before_dept_name, after_dept_id, after_dept_name,
operate_user_id, operate_user_name, operate_time, change_summary,
create_by, create_time, remark, del_flag
)
values
(
#{assetId}, #{assetCode}, #{assetName}, #{businessType},
#{sourceOrderId}, #{sourceOrderNo}, #{sourceItemId},
#{beforeStatus}, #{afterStatus},
#{beforeWarehouseId}, #{beforeWarehouseName}, #{afterWarehouseId}, #{afterWarehouseName},
#{beforeLocationId}, #{beforeLocationName}, #{afterLocationId}, #{afterLocationName},
#{beforeUserId}, #{beforeUserName}, #{afterUserId}, #{afterUserName},
#{beforeDeptId}, #{beforeDeptName}, #{afterDeptId}, #{afterDeptName},
#{operateUserId}, #{operateUserName}, #{operateTime}, #{changeSummary},
#{createBy}, #{createTime}, #{remark}, #{delFlag}
)
</insert>
</mapper>

@ -142,39 +142,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
where tag_id = #{tagId} and del_flag = '0'
</update>
<insert id="insertRfidLifecycleLog">
insert into ams_asset_lifecycle_log
(
asset_id, asset_code, asset_name, business_type,
before_status, after_status,
before_warehouse_id, before_warehouse_name,
after_warehouse_id, after_warehouse_name,
before_location_id, before_location_name,
after_location_id, after_location_name,
before_user_id, before_user_name,
after_user_id, after_user_name,
before_dept_id, before_dept_name,
after_dept_id, after_dept_name,
operate_user_id, operate_user_name, operate_time,
change_summary, create_by, create_time, del_flag
)
values
(
#{asset.assetId}, #{asset.assetCode}, #{asset.assetName}, #{businessType},
#{asset.assetStatus}, #{asset.assetStatus},
#{asset.warehouseId}, #{asset.warehouseName},
#{asset.warehouseId}, #{asset.warehouseName},
#{asset.locationId}, #{asset.locationName},
#{asset.locationId}, #{asset.locationName},
#{asset.useUserId}, #{asset.useUserName},
#{asset.useUserId}, #{asset.useUserName},
#{asset.useDeptId}, #{asset.useDeptName},
#{asset.useDeptId}, #{asset.useDeptName},
#{operateUserId}, #{operateUserName}, #{operateTime},
#{changeSummary}, #{operName}, #{operateTime}, '0'
)
</insert>
<update id="deleteAmsRfidTagByTagId" parameterType="Long">
update ams_rfid_tag set del_flag = '1' where tag_id = #{tagId}
</update>

@ -197,6 +197,43 @@
</div>
</div>
</div>
<h4 class="form-header h4">资产履历</h4>
<div class="row">
<div class="col-sm-12 table-responsive">
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>操作时间</th>
<th>业务类型</th>
<th>来源单号</th>
<th>状态变化</th>
<th>仓库变化</th>
<th>位置变化</th>
<th>使用归属变化</th>
<th>变更摘要</th>
<th>操作人</th>
</tr>
</thead>
<tbody>
<tr th:if="${#lists.isEmpty(lifecycleList)}">
<td colspan="9" class="text-center">暂无资产履历</td>
</tr>
<tr th:each="lifecycle : ${lifecycleList}">
<td th:text="${#dates.format(lifecycle.operateTime, 'yyyy-MM-dd HH:mm:ss')}"></td>
<td th:text="${@dict.getLabel('ams_lifecycle_business_type', lifecycle.businessType) ?: lifecycle.businessType}"></td>
<td th:text="${lifecycle.sourceOrderNo ?: '-'}"></td>
<td th:text="${(lifecycle.beforeStatus == null ? '-' : (@dict.getLabel('ams_asset_status', lifecycle.beforeStatus) ?: lifecycle.beforeStatus)) + ' → ' + (lifecycle.afterStatus == null ? '-' : (@dict.getLabel('ams_asset_status', lifecycle.afterStatus) ?: lifecycle.afterStatus))}"></td>
<td th:text="${(lifecycle.beforeWarehouseName ?: '-') + ' → ' + (lifecycle.afterWarehouseName ?: '-')}"></td>
<td th:text="${(lifecycle.beforeLocationName ?: '-') + ' → ' + (lifecycle.afterLocationName ?: '-')}"></td>
<td th:text="${(lifecycle.beforeDeptName ?: '-') + '/' + (lifecycle.beforeUserName ?: '-') + ' → ' + (lifecycle.afterDeptName ?: '-') + '/' + (lifecycle.afterUserName ?: '-')}"></td>
<td th:text="${lifecycle.changeSummary ?: '-'}"></td>
<td th:text="${lifecycle.operateUserName ?: '-'}"></td>
</tr>
</tbody>
</table>
</div>
</div>
</form>
</div>
<th:block th:include="include :: footer" />

@ -0,0 +1,103 @@
package com.ruoyi.asset.service.impl;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
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.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.ruoyi.asset.domain.AmsWarehouse;
import com.ruoyi.asset.mapper.AmsWarehouseMapper;
import com.ruoyi.common.exception.ServiceException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class AmsWarehouseServiceImplTest
{
@Mock
private AmsWarehouseMapper amsWarehouseMapper;
@InjectMocks
private AmsWarehouseServiceImpl service;
@Test
void insertAmsWarehouseShouldSetAssetDefaults()
{
AmsWarehouse warehouse = new AmsWarehouse();
warehouse.setWarehouseCode("WH001");
warehouse.setWarehouseName("主仓");
when(amsWarehouseMapper.insertAmsWarehouse(any(AmsWarehouse.class))).thenReturn(1);
int rows = service.insertAmsWarehouse(warehouse);
ArgumentCaptor<AmsWarehouse> captor = ArgumentCaptor.forClass(AmsWarehouse.class);
verify(amsWarehouseMapper).insertAmsWarehouse(captor.capture());
assertEquals(1, rows);
assertEquals("Y", captor.getValue().getEnabled());
assertEquals("0", captor.getValue().getDelFlag());
assertNotNull(captor.getValue().getCreateTime());
}
@Test
void deleteAmsWarehouseByWarehouseIdsShouldRejectReferencedWarehouse()
{
AmsWarehouse warehouse = new AmsWarehouse();
warehouse.setWarehouseId(10L);
warehouse.setWarehouseName("主仓");
when(amsWarehouseMapper.selectAmsWarehouseByWarehouseId(10L)).thenReturn(warehouse);
when(amsWarehouseMapper.countWarehouseReferences(10L)).thenReturn(1);
ServiceException exception = assertThrows(ServiceException.class,
() -> service.deleteAmsWarehouseByWarehouseIds("10"));
assertTrue(exception.getMessage().contains("主仓"));
assertTrue(exception.getMessage().contains("不允许删除"));
verify(amsWarehouseMapper, never()).deleteAmsWarehouseByWarehouseIds(any());
}
@Test
void deleteAmsWarehouseByWarehouseIdShouldRejectReferencedWarehouse()
{
AmsWarehouse warehouse = new AmsWarehouse();
warehouse.setWarehouseId(10L);
warehouse.setWarehouseName("主仓");
when(amsWarehouseMapper.selectAmsWarehouseByWarehouseId(10L)).thenReturn(warehouse);
when(amsWarehouseMapper.countWarehouseReferences(10L)).thenReturn(1);
ServiceException exception = assertThrows(ServiceException.class,
() -> service.deleteAmsWarehouseByWarehouseId(10L));
assertTrue(exception.getMessage().contains("主仓"));
assertTrue(exception.getMessage().contains("不允许删除"));
verify(amsWarehouseMapper, never()).deleteAmsWarehouseByWarehouseId(10L);
}
@Test
void checkWarehouseCodeUniqueShouldIgnoreCurrentWarehouse()
{
AmsWarehouse existing = new AmsWarehouse();
existing.setWarehouseId(10L);
existing.setWarehouseCode("WH001");
when(amsWarehouseMapper.checkWarehouseCodeUnique("WH001")).thenReturn(existing);
AmsWarehouse current = new AmsWarehouse();
current.setWarehouseId(10L);
current.setWarehouseCode("WH001");
AmsWarehouse other = new AmsWarehouse();
other.setWarehouseId(11L);
other.setWarehouseCode("WH001");
assertTrue(service.checkWarehouseCodeUnique(current));
assertFalse(service.checkWarehouseCodeUnique(other));
}
}

@ -0,0 +1,125 @@
package com.ruoyi.asset.service.impl;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.List;
import com.ruoyi.asset.constant.AssetLifecycleBusinessType;
import com.ruoyi.asset.domain.AmsAsset;
import com.ruoyi.asset.domain.AmsAssetLifecycleLog;
import com.ruoyi.asset.mapper.AmsAssetLifecycleLogMapper;
import com.ruoyi.common.core.domain.entity.SysDictData;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.system.service.ISysDictDataService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class AssetLifecycleServiceImplTest
{
@Mock
private AmsAssetLifecycleLogMapper lifecycleLogMapper;
@Mock
private ISysDictDataService sysDictDataService;
@InjectMocks
private AssetLifecycleServiceImpl service;
@Test
void recordLifecycleShouldFillSnapshotsAndDefaults()
{
AmsAsset beforeAsset = buildAsset(10L, "IN_STOCK", 1L, "一号仓", 11L, "A区", null, null, null, null);
AmsAsset afterAsset = buildAsset(10L, "IN_USE", 2L, "二号仓", 22L, "B区", 3L, "张三", 4L, "生产部");
AmsAssetLifecycleLog request = new AmsAssetLifecycleLog();
request.setBusinessType(" " + AssetLifecycleBusinessType.RECEIVE + " ");
request.setOperateUserId(100L);
request.setOperateUserName(" 管理员 ");
request.setCreateBy(" admin ");
request.setChangeSummary(" 领用确认 ");
SysDictData businessType = new SysDictData();
businessType.setDictValue(AssetLifecycleBusinessType.RECEIVE);
when(sysDictDataService.selectDictDataList(any(SysDictData.class))).thenReturn(List.of(businessType));
when(lifecycleLogMapper.insertAmsAssetLifecycleLog(any(AmsAssetLifecycleLog.class))).thenReturn(1);
int rows = service.recordLifecycle(beforeAsset, afterAsset, request);
ArgumentCaptor<AmsAssetLifecycleLog> captor = ArgumentCaptor.forClass(AmsAssetLifecycleLog.class);
verify(lifecycleLogMapper).insertAmsAssetLifecycleLog(captor.capture());
AmsAssetLifecycleLog actual = captor.getValue();
assertEquals(1, rows);
assertEquals(10L, actual.getAssetId());
assertEquals("ASSET-001", actual.getAssetCode());
assertEquals(AssetLifecycleBusinessType.RECEIVE, actual.getBusinessType());
assertEquals("IN_STOCK", actual.getBeforeStatus());
assertEquals("IN_USE", actual.getAfterStatus());
assertEquals("一号仓", actual.getBeforeWarehouseName());
assertEquals("二号仓", actual.getAfterWarehouseName());
assertEquals("管理员", actual.getOperateUserName());
assertEquals("领用确认", actual.getChangeSummary());
assertEquals("admin", actual.getCreateBy());
assertEquals("0", actual.getDelFlag());
assertNotNull(actual.getOperateTime());
assertNotNull(actual.getCreateTime());
}
@Test
void recordLifecycleShouldRejectDifferentAssets()
{
AmsAsset beforeAsset = buildAsset(10L, "IN_STOCK", null, null, null, null, null, null, null, null);
AmsAsset afterAsset = buildAsset(11L, "IN_USE", null, null, null, null, null, null, null, null);
AmsAssetLifecycleLog request = new AmsAssetLifecycleLog();
request.setBusinessType(AssetLifecycleBusinessType.RECEIVE);
assertThrows(ServiceException.class, () -> service.recordLifecycle(beforeAsset, afterAsset, request));
verify(sysDictDataService, never()).selectDictDataList(any(SysDictData.class));
verify(lifecycleLogMapper, never()).insertAmsAssetLifecycleLog(any(AmsAssetLifecycleLog.class));
}
@Test
void recordLifecycleShouldRejectUnknownBusinessType()
{
AmsAsset asset = buildAsset(10L, "IN_STOCK", null, null, null, null, null, null, null, null);
AmsAssetLifecycleLog request = new AmsAssetLifecycleLog();
request.setBusinessType("UNKNOWN");
SysDictData otherBusinessType = new SysDictData();
otherBusinessType.setDictValue(AssetLifecycleBusinessType.INBOUND);
when(sysDictDataService.selectDictDataList(any(SysDictData.class)))
.thenReturn(List.of(otherBusinessType));
assertThrows(ServiceException.class, () -> service.recordLifecycle(asset, asset, request));
verify(lifecycleLogMapper, never()).insertAmsAssetLifecycleLog(any(AmsAssetLifecycleLog.class));
}
private AmsAsset buildAsset(Long assetId, String assetStatus,
Long warehouseId, String warehouseName, Long locationId, String locationName,
Long userId, String userName, Long deptId, String deptName)
{
AmsAsset asset = new AmsAsset();
asset.setAssetId(assetId);
asset.setAssetCode("ASSET-001");
asset.setAssetName("测试资产");
asset.setAssetStatus(assetStatus);
asset.setWarehouseId(warehouseId);
asset.setWarehouseName(warehouseName);
asset.setLocationId(locationId);
asset.setLocationName(locationName);
asset.setUseUserId(userId);
asset.setUseUserName(userName);
asset.setUseDeptId(deptId);
asset.setUseDeptName(deptName);
return asset;
}
}
Loading…
Cancel
Save