From bf8218791d4355fad96d94ce585cae8ecd4c7e6b Mon Sep 17 00:00:00 2001 From: yangk Date: Tue, 9 Jun 2026 09:15:23 +0800 Subject: [PATCH] =?UTF-8?q?feat(asset):=20=E6=B7=BB=E5=8A=A0=E8=B5=84?= =?UTF-8?q?=E4=BA=A7=E7=94=9F=E5=91=BD=E5=91=A8=E6=9C=9F=E5=B1=A5=E5=8E=86?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 AmsAssetLifecycleLog 实体类用于记录资产履历信息 - 新增 AmsAssetLifecycleLogMapper 接口和对应 XML 映射文件 - 在 AmsAssetController 中注入 IAssetLifecycleService 并传递履历数据到前端 - 重构 RFID 标签解绑逻辑,将履历记录委托给统一的资产生命周期服务 - 新增 AssetLifecycleBusinessType 常量类定义资产履历业务类型 - 实现 IAssetLifecycleService 接口提供履历查询和记录功能 - 添加资产生命周期履历服务的单元测试 - 移除原来在 RFID 标签映射器中的履历插入方法和相关常量 --- .../constant/AssetLifecycleBusinessType.java | 36 ++ .../asset/controller/AmsAssetController.java | 6 + .../asset/domain/AmsAssetLifecycleLog.java | 419 ++++++++++++++++++ .../mapper/AmsAssetLifecycleLogMapper.java | 28 ++ .../ruoyi/asset/mapper/AmsRfidTagMapper.java | 21 - .../asset/service/IAssetLifecycleService.java | 31 ++ .../service/impl/AmsRfidTagServiceImpl.java | 27 +- .../impl/AssetLifecycleServiceImpl.java | 184 ++++++++ .../asset/AmsAssetLifecycleLogMapper.xml | 93 ++++ .../mapper/asset/AmsRfidTagMapper.xml | 33 -- .../resources/templates/asset/asset/view.html | 37 ++ .../impl/AmsWarehouseServiceImplTest.java | 103 +++++ .../impl/AssetLifecycleServiceImplTest.java | 125 ++++++ 13 files changed, 1085 insertions(+), 58 deletions(-) create mode 100644 ruoyi-asset/src/main/java/com/ruoyi/asset/constant/AssetLifecycleBusinessType.java create mode 100644 ruoyi-asset/src/main/java/com/ruoyi/asset/domain/AmsAssetLifecycleLog.java create mode 100644 ruoyi-asset/src/main/java/com/ruoyi/asset/mapper/AmsAssetLifecycleLogMapper.java create mode 100644 ruoyi-asset/src/main/java/com/ruoyi/asset/service/IAssetLifecycleService.java create mode 100644 ruoyi-asset/src/main/java/com/ruoyi/asset/service/impl/AssetLifecycleServiceImpl.java create mode 100644 ruoyi-asset/src/main/resources/mapper/asset/AmsAssetLifecycleLogMapper.xml create mode 100644 ruoyi-asset/src/test/java/com/ruoyi/asset/service/impl/AmsWarehouseServiceImplTest.java create mode 100644 ruoyi-asset/src/test/java/com/ruoyi/asset/service/impl/AssetLifecycleServiceImplTest.java diff --git a/ruoyi-asset/src/main/java/com/ruoyi/asset/constant/AssetLifecycleBusinessType.java b/ruoyi-asset/src/main/java/com/ruoyi/asset/constant/AssetLifecycleBusinessType.java new file mode 100644 index 0000000..18da04e --- /dev/null +++ b/ruoyi-asset/src/main/java/com/ruoyi/asset/constant/AssetLifecycleBusinessType.java @@ -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() + { + } +} diff --git a/ruoyi-asset/src/main/java/com/ruoyi/asset/controller/AmsAssetController.java b/ruoyi-asset/src/main/java/com/ruoyi/asset/controller/AmsAssetController.java index 9eb5014..b6a9bf2 100644 --- a/ruoyi-asset/src/main/java/com/ruoyi/asset/controller/AmsAssetController.java +++ b/ruoyi-asset/src/main/java/com/ruoyi/asset/controller/AmsAssetController.java @@ -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"; } diff --git a/ruoyi-asset/src/main/java/com/ruoyi/asset/domain/AmsAssetLifecycleLog.java b/ruoyi-asset/src/main/java/com/ruoyi/asset/domain/AmsAssetLifecycleLog.java new file mode 100644 index 0000000..3bda131 --- /dev/null +++ b/ruoyi-asset/src/main/java/com/ruoyi/asset/domain/AmsAssetLifecycleLog.java @@ -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; + } +} diff --git a/ruoyi-asset/src/main/java/com/ruoyi/asset/mapper/AmsAssetLifecycleLogMapper.java b/ruoyi-asset/src/main/java/com/ruoyi/asset/mapper/AmsAssetLifecycleLogMapper.java new file mode 100644 index 0000000..8c63af7 --- /dev/null +++ b/ruoyi-asset/src/main/java/com/ruoyi/asset/mapper/AmsAssetLifecycleLogMapper.java @@ -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 selectAmsAssetLifecycleLogByAssetId(Long assetId); + + /** + * 新增资产履历 + * + * @param lifecycleLog 资产履历 + * @return 结果 + */ + public int insertAmsAssetLifecycleLog(AmsAssetLifecycleLog lifecycleLog); +} 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 7a3ce93..c12ed0c 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 @@ -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标签 * diff --git a/ruoyi-asset/src/main/java/com/ruoyi/asset/service/IAssetLifecycleService.java b/ruoyi-asset/src/main/java/com/ruoyi/asset/service/IAssetLifecycleService.java new file mode 100644 index 0000000..ab9518d --- /dev/null +++ b/ruoyi-asset/src/main/java/com/ruoyi/asset/service/IAssetLifecycleService.java @@ -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 selectByAssetId(Long assetId); + + /** + * 记录资产业务变化履历 + * + * @param beforeAsset 变更前资产快照 + * @param afterAsset 变更后资产快照 + * @param lifecycleLog 业务来源、操作人和变更摘要 + * @return 结果 + */ + public int recordLifecycle(AmsAsset beforeAsset, AmsAsset afterAsset, AmsAssetLifecycleLog lifecycleLog); +} 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 303265d..5d8a970 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 @@ -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; + } } diff --git a/ruoyi-asset/src/main/java/com/ruoyi/asset/service/impl/AssetLifecycleServiceImpl.java b/ruoyi-asset/src/main/java/com/ruoyi/asset/service/impl/AssetLifecycleServiceImpl.java new file mode 100644 index 0000000..76cfc7d --- /dev/null +++ b/ruoyi-asset/src/main/java/com/ruoyi/asset/service/impl/AssetLifecycleServiceImpl.java @@ -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 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 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()); + } +} diff --git a/ruoyi-asset/src/main/resources/mapper/asset/AmsAssetLifecycleLogMapper.xml b/ruoyi-asset/src/main/resources/mapper/asset/AmsAssetLifecycleLogMapper.xml new file mode 100644 index 0000000..b1acd82 --- /dev/null +++ b/ruoyi-asset/src/main/resources/mapper/asset/AmsAssetLifecycleLogMapper.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + + + + 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} + ) + + + diff --git a/ruoyi-asset/src/main/resources/mapper/asset/AmsRfidTagMapper.xml b/ruoyi-asset/src/main/resources/mapper/asset/AmsRfidTagMapper.xml index e87c248..07b7693 100644 --- a/ruoyi-asset/src/main/resources/mapper/asset/AmsRfidTagMapper.xml +++ b/ruoyi-asset/src/main/resources/mapper/asset/AmsRfidTagMapper.xml @@ -142,39 +142,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" where tag_id = #{tagId} and del_flag = '0' - - 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' - ) - - update ams_rfid_tag set del_flag = '1' where tag_id = #{tagId} diff --git a/ruoyi-asset/src/main/resources/templates/asset/asset/view.html b/ruoyi-asset/src/main/resources/templates/asset/asset/view.html index 5f107e0..272d63e 100644 --- a/ruoyi-asset/src/main/resources/templates/asset/asset/view.html +++ b/ruoyi-asset/src/main/resources/templates/asset/asset/view.html @@ -197,6 +197,43 @@ + +

资产履历

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
操作时间业务类型来源单号状态变化仓库变化位置变化使用归属变化变更摘要操作人
暂无资产履历
+
+
diff --git a/ruoyi-asset/src/test/java/com/ruoyi/asset/service/impl/AmsWarehouseServiceImplTest.java b/ruoyi-asset/src/test/java/com/ruoyi/asset/service/impl/AmsWarehouseServiceImplTest.java new file mode 100644 index 0000000..47ffef8 --- /dev/null +++ b/ruoyi-asset/src/test/java/com/ruoyi/asset/service/impl/AmsWarehouseServiceImplTest.java @@ -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 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)); + } +} diff --git a/ruoyi-asset/src/test/java/com/ruoyi/asset/service/impl/AssetLifecycleServiceImplTest.java b/ruoyi-asset/src/test/java/com/ruoyi/asset/service/impl/AssetLifecycleServiceImplTest.java new file mode 100644 index 0000000..5b082dc --- /dev/null +++ b/ruoyi-asset/src/test/java/com/ruoyi/asset/service/impl/AssetLifecycleServiceImplTest.java @@ -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 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; + } +}