From 9d1cb8f029fcb1157b24b3c59305df790af57fef Mon Sep 17 00:00:00 2001 From: yangk Date: Fri, 12 Jun 2026 10:37:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(asset):=20=E4=BC=98=E5=8C=96=E8=B5=84?= =?UTF-8?q?=E4=BA=A7=E5=85=A5=E5=BA=93=E6=B5=81=E7=A8=8B=E5=92=8C=E8=A1=A8?= =?UTF-8?q?=E5=8D=95=E8=AE=BE=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除资产新增表单中的仓库位置和使用人部门字段,由业务流转服务维护 - 更新资产状态默认值为待入库状态,建档时不设置仓位和使用归属 - 重构入库单明细选择逻辑,改为弹窗选择待入库资产而非直接添加行 - 实现资产归属字段保护机制,防止通过普通编辑绕过业务服务修改 - 优化Excel导入导出配置,区分可编辑字段和业务流转维护字段 - 添加资产状态和仓位的并发占用检测,确保数据一致性 --- .../com/ruoyi/asset/constant/AssetStatus.java | 2 + .../asset/controller/AmsAssetController.java | 11 +- .../controller/AmsInboundOrderController.java | 38 +++- .../java/com/ruoyi/asset/domain/AmsAsset.java | 20 +- .../asset/mapper/AmsInboundOrderMapper.java | 19 ++ .../service/IAmsInboundOrderService.java | 12 ++ .../IAssetStatusTransitionService.java | 4 +- .../service/impl/AmsAssetServiceImpl.java | 181 ++---------------- .../impl/AmsInboundOrderServiceImpl.java | 51 +++-- .../AssetStatusTransitionServiceImpl.java | 4 +- .../mapper/asset/AmsInboundOrderMapper.xml | 60 ++++++ .../resources/templates/asset/asset/add.html | 113 ----------- .../templates/asset/asset/asset.html | 2 +- .../resources/templates/asset/asset/edit.html | 113 ----------- .../templates/asset/inbound/add.html | 91 ++++++--- .../templates/asset/inbound/edit.html | 93 ++++++--- .../templates/asset/inbound/selectAsset.html | 98 ++++++++++ .../service/impl/AmsAssetServiceImplTest.java | 69 +++---- .../impl/AmsInboundOrderServiceImplTest.java | 47 ++++- .../AssetStatusTransitionServiceImplTest.java | 21 +- .../impl/RfidBindingServiceImplTest.java | 2 +- 21 files changed, 509 insertions(+), 542 deletions(-) create mode 100644 ruoyi-asset/src/main/resources/templates/asset/inbound/selectAsset.html diff --git a/ruoyi-asset/src/main/java/com/ruoyi/asset/constant/AssetStatus.java b/ruoyi-asset/src/main/java/com/ruoyi/asset/constant/AssetStatus.java index 61212c4..4af2032 100644 --- a/ruoyi-asset/src/main/java/com/ruoyi/asset/constant/AssetStatus.java +++ b/ruoyi-asset/src/main/java/com/ruoyi/asset/constant/AssetStatus.java @@ -7,6 +7,8 @@ package com.ruoyi.asset.constant; */ public final class AssetStatus { + /** 待入库 */ + public static final String PENDING_INBOUND = "PENDING_INBOUND"; /** 在库 */ public static final String IN_STOCK = "IN_STOCK"; /** 在用 */ 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 b6a9bf2..77895c0 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 @@ -150,7 +150,7 @@ public class AmsAssetController extends BaseController @GetMapping("/add") public String add(ModelMap mmap) { - putAssetOptions(mmap); + putAssetFormOptions(mmap); return prefix + "/add"; } @@ -180,7 +180,7 @@ public class AmsAssetController extends BaseController { AmsAsset amsAsset = amsAssetService.selectAmsAssetByAssetId(assetId); mmap.put("amsAsset", amsAsset); - putAssetOptions(mmap); + putAssetFormOptions(mmap); return prefix + "/edit"; } @@ -222,6 +222,13 @@ public class AmsAssetController extends BaseController mmap.put("userList", selectNormalUserList()); } + /** 新增/编辑表单只需类别和保管人选项,仓库位置和使用归属由业务流转服务维护 */ + private void putAssetFormOptions(ModelMap mmap) + { + mmap.put("categoryList", selectEnabledCategoryList()); + mmap.put("userList", selectNormalUserList()); + } + private List selectEnabledCategoryList() { AmsAssetCategory category = new AmsAssetCategory(); diff --git a/ruoyi-asset/src/main/java/com/ruoyi/asset/controller/AmsInboundOrderController.java b/ruoyi-asset/src/main/java/com/ruoyi/asset/controller/AmsInboundOrderController.java index 6c085b7..486b2d2 100644 --- a/ruoyi-asset/src/main/java/com/ruoyi/asset/controller/AmsInboundOrderController.java +++ b/ruoyi-asset/src/main/java/com/ruoyi/asset/controller/AmsInboundOrderController.java @@ -1,6 +1,7 @@ package com.ruoyi.asset.controller; import java.util.List; +import org.apache.shiro.authz.annotation.Logical; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; @@ -9,6 +10,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import com.ruoyi.common.annotation.Log; import com.ruoyi.common.enums.BusinessType; @@ -16,9 +18,7 @@ import com.ruoyi.asset.domain.AmsInboundOrder; import com.ruoyi.asset.domain.AmsAsset; import com.ruoyi.asset.domain.AmsAssetLocation; import com.ruoyi.asset.domain.AmsWarehouse; -import com.ruoyi.asset.constant.AssetStatus; import com.ruoyi.asset.service.IAmsAssetLocationService; -import com.ruoyi.asset.service.IAmsAssetService; import com.ruoyi.asset.service.IAmsInboundOrderService; import com.ruoyi.asset.service.IAmsWarehouseService; import com.ruoyi.common.core.controller.BaseController; @@ -49,9 +49,6 @@ public class AmsInboundOrderController extends BaseController @Autowired private IAmsAssetLocationService amsAssetLocationService; - @Autowired - private IAmsAssetService amsAssetService; - @RequiresPermissions("asset:inbound:view") @GetMapping() public String inbound(ModelMap mmap) @@ -73,6 +70,33 @@ public class AmsInboundOrderController extends BaseController return getDataTable(list); } + /** + * 打开待入库资产分页选择页(入库单新增/编辑的明细弹窗选择器) + *

编辑场景会携带 orderId 参数,用于排除当前单据自身占用的资产。

+ */ + @RequiresPermissions(value = { "asset:inbound:add", "asset:inbound:edit" }, logical = Logical.OR) + @GetMapping("/selectAsset") + public String selectAsset(@RequestParam(value = "orderId", required = false) Long orderId, ModelMap mmap) + { + mmap.put("orderId", orderId); + return prefix + "/selectAsset"; + } + + /** + * 分页查询未被其他有效草稿占用的待入库资产 + *

仅返回 PENDING_INBOUND 且无其他草稿入库单引用的资产, + * 编辑场景传入 orderId 排除当前单据自身的占用。

+ */ + @RequiresPermissions(value = { "asset:inbound:add", "asset:inbound:edit" }, logical = Logical.OR) + @PostMapping("/availableAssetList") + @ResponseBody + public TableDataInfo availableAssetList(AmsAsset amsAsset, + @RequestParam(value = "orderId", required = false) Long orderId) + { + startPage(); + return getDataTable(amsInboundOrderService.selectAvailableInboundAssetList(amsAsset, orderId)); + } + /** * 导出入库管理列表 */ @@ -181,10 +205,6 @@ public class AmsInboundOrderController extends BaseController AmsAssetLocation location = new AmsAssetLocation(); location.setEnabled(ENABLED_YES); mmap.put("locationList", amsAssetLocationService.selectAmsAssetLocationList(location)); - - AmsAsset asset = new AmsAsset(); - asset.setAssetStatus(AssetStatus.IN_STOCK); - mmap.put("assetList", amsAssetService.selectAmsAssetList(asset)); } private List selectEnabledWarehouseList() diff --git a/ruoyi-asset/src/main/java/com/ruoyi/asset/domain/AmsAsset.java b/ruoyi-asset/src/main/java/com/ruoyi/asset/domain/AmsAsset.java index c64f9be..5e91536 100644 --- a/ruoyi-asset/src/main/java/com/ruoyi/asset/domain/AmsAsset.java +++ b/ruoyi-asset/src/main/java/com/ruoyi/asset/domain/AmsAsset.java @@ -49,40 +49,36 @@ public class AmsAsset extends BaseEntity @Excel(name = "品牌") private String brand; - /** 当前所属仓库ID */ - @Excel(name = "仓库ID", type = Excel.Type.IMPORT) + /** 当前所属仓库ID(由入库/调拨业务维护,不参与Excel导入导出) */ private Long warehouseId; /** 当前仓库编码快照 */ - @Excel(name = "仓库编码") + @Excel(name = "仓库编码", type = Excel.Type.EXPORT) private String warehouseCode; /** 当前仓库名称快照 */ @Excel(name = "仓库名称", type = Excel.Type.EXPORT) private String warehouseName; - /** 当前存放位置ID */ - @Excel(name = "位置ID", type = Excel.Type.IMPORT) + /** 当前存放位置ID(由入库/调拨业务维护,不参与Excel导入导出) */ private Long locationId; /** 当前位置编码快照 */ - @Excel(name = "位置编码") + @Excel(name = "位置编码", type = Excel.Type.EXPORT) private String locationCode; /** 当前位置名称快照 */ @Excel(name = "位置名称", type = Excel.Type.EXPORT) private String locationName; - /** 使用部门ID */ - @Excel(name = "使用部门ID", type = Excel.Type.IMPORT) + /** 使用部门ID(由领用/归还业务维护,不参与Excel导入导出) */ private Long useDeptId; /** 使用部门名称快照 */ @Excel(name = "使用部门", type = Excel.Type.EXPORT) private String useDeptName; - /** 使用人ID */ - @Excel(name = "使用人ID", type = Excel.Type.IMPORT) + /** 使用人ID(由领用/归还业务维护,不参与Excel导入导出) */ private Long useUserId; /** 使用人名称快照 */ @@ -124,8 +120,8 @@ public class AmsAsset extends BaseEntity @Excel(name = "下次维保日期", width = 30, dateFormat = "yyyy-MM-dd") private Date nextMaintenanceDate; - /** 当前资产状态 */ - @Excel(name = "资产状态", dictType = "ams_asset_status", comboReadDict = true) + /** 当前资产状态(仅导出展示,导入时统一设为待入库,状态变更由流转服务驱动) */ + @Excel(name = "资产状态", dictType = "ams_asset_status", type = Excel.Type.EXPORT) private String assetStatus; /** 当前绑定标签ID */ diff --git a/ruoyi-asset/src/main/java/com/ruoyi/asset/mapper/AmsInboundOrderMapper.java b/ruoyi-asset/src/main/java/com/ruoyi/asset/mapper/AmsInboundOrderMapper.java index 0c53a7d..c07cf1e 100644 --- a/ruoyi-asset/src/main/java/com/ruoyi/asset/mapper/AmsInboundOrderMapper.java +++ b/ruoyi-asset/src/main/java/com/ruoyi/asset/mapper/AmsInboundOrderMapper.java @@ -1,6 +1,8 @@ package com.ruoyi.asset.mapper; import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.ruoyi.asset.domain.AmsAsset; import com.ruoyi.asset.domain.AmsInboundOrder; import com.ruoyi.asset.domain.AmsInboundOrderItem; @@ -36,6 +38,23 @@ public interface AmsInboundOrderMapper */ public List selectAmsInboundOrderList(AmsInboundOrder amsInboundOrder); + /** + * 查询未被其他有效草稿占用的待入库资产(入库单明细弹窗选择器使用) + *

只筛选 PENDING_INBOUND 状态的资产,并排除已被其他草稿入库单引用的行项, + * 若传入 currentOrderId 则排除自身占用以支持编辑场景。

+ */ + public List selectAvailableInboundAssetList(@Param("asset") AmsAsset amsAsset, + @Param("currentOrderId") Long currentOrderId, @Param("pendingStatus") String pendingStatus, + @Param("draftStatus") String draftStatus); + + /** + * 统计资产被其他有效草稿占用的数量(保存草稿时防重复占用校验) + *

配合 selectAmsAssetByAssetIdForUpdate 行锁使用, + * 确保同一资产不会被两个并发草稿同时引用。

+ */ + public int countOtherDraftInboundOrderByAssetId(@Param("assetId") Long assetId, + @Param("currentOrderId") Long currentOrderId, @Param("draftStatus") String draftStatus); + /** * 新增入库管理 * diff --git a/ruoyi-asset/src/main/java/com/ruoyi/asset/service/IAmsInboundOrderService.java b/ruoyi-asset/src/main/java/com/ruoyi/asset/service/IAmsInboundOrderService.java index 2756ff2..4fd4fdc 100644 --- a/ruoyi-asset/src/main/java/com/ruoyi/asset/service/IAmsInboundOrderService.java +++ b/ruoyi-asset/src/main/java/com/ruoyi/asset/service/IAmsInboundOrderService.java @@ -1,6 +1,7 @@ package com.ruoyi.asset.service; import java.util.List; +import com.ruoyi.asset.domain.AmsAsset; import com.ruoyi.asset.domain.AmsInboundOrder; /** @@ -27,6 +28,17 @@ public interface IAmsInboundOrderService */ public List selectAmsInboundOrderList(AmsInboundOrder amsInboundOrder); + /** + * 分页查询当前可加入入库草稿的待入库资产 + *

只返回状态为 PENDING_INBOUND 且未被其他草稿入库单占用的资产, + * 供入库单新增/编辑页的资产弹窗选择器使用。

+ * + * @param amsAsset 查询条件(编码、名称、类别等模糊搜索) + * @param currentOrderId 当前编辑入库单ID,新增时为空,编辑时传入以排除自身占用 + * @return 可入库资产集合 + */ + public List selectAvailableInboundAssetList(AmsAsset amsAsset, Long currentOrderId); + /** * 新增入库管理 * diff --git a/ruoyi-asset/src/main/java/com/ruoyi/asset/service/IAssetStatusTransitionService.java b/ruoyi-asset/src/main/java/com/ruoyi/asset/service/IAssetStatusTransitionService.java index fdc8e8e..7b0b5f7 100644 --- a/ruoyi-asset/src/main/java/com/ruoyi/asset/service/IAssetStatusTransitionService.java +++ b/ruoyi-asset/src/main/java/com/ruoyi/asset/service/IAssetStatusTransitionService.java @@ -11,8 +11,8 @@ import com.ruoyi.asset.domain.AssetTransitionResult; public interface IAssetStatusTransitionService { /** - * 确认资产入库:IN_STOCK → IN_STOCK(更新仓位,清除使用归属) - *

允许在库资产重新入库以实现调仓换位。

+ * 确认资产首次入库:PENDING_INBOUND → IN_STOCK(写入仓位,清除使用归属) + *

在库资产换仓必须使用调拨业务。

* * @param assetId 资产ID * @param warehouseId 目标仓库ID diff --git a/ruoyi-asset/src/main/java/com/ruoyi/asset/service/impl/AmsAssetServiceImpl.java b/ruoyi-asset/src/main/java/com/ruoyi/asset/service/impl/AmsAssetServiceImpl.java index 920a1e2..96b04d1 100644 --- a/ruoyi-asset/src/main/java/com/ruoyi/asset/service/impl/AmsAssetServiceImpl.java +++ b/ruoyi-asset/src/main/java/com/ruoyi/asset/service/impl/AmsAssetServiceImpl.java @@ -3,16 +3,10 @@ package com.ruoyi.asset.service.impl; import java.math.BigDecimal; import java.util.Arrays; import java.util.List; -import java.util.Objects; import com.ruoyi.asset.constant.AssetStatus; import com.ruoyi.asset.domain.AmsAssetCategory; -import com.ruoyi.asset.domain.AmsAssetLocation; -import com.ruoyi.asset.domain.AmsWarehouse; import com.ruoyi.asset.service.IAmsAssetCategoryService; -import com.ruoyi.asset.service.IAmsAssetLocationService; -import com.ruoyi.asset.service.IAmsWarehouseService; import com.ruoyi.common.constant.UserConstants; -import com.ruoyi.common.core.domain.entity.SysDept; import com.ruoyi.common.core.domain.entity.SysDictData; import com.ruoyi.common.core.domain.entity.SysUser; import com.ruoyi.common.exception.ServiceException; @@ -24,7 +18,6 @@ import com.ruoyi.asset.mapper.AmsAssetMapper; import com.ruoyi.asset.domain.AmsAsset; import com.ruoyi.asset.service.IAmsAssetService; import com.ruoyi.common.core.text.Convert; -import com.ruoyi.system.service.ISysDeptService; import com.ruoyi.system.service.ISysDictDataService; import com.ruoyi.system.service.ISysUserService; import org.springframework.transaction.annotation.Transactional; @@ -50,15 +43,6 @@ public class AmsAssetServiceImpl implements IAmsAssetService @Autowired private IAmsAssetCategoryService amsAssetCategoryService; - @Autowired - private IAmsWarehouseService amsWarehouseService; - - @Autowired - private IAmsAssetLocationService amsAssetLocationService; - - @Autowired - private ISysDeptService sysDeptService; - @Autowired private ISysUserService sysUserService; @@ -181,11 +165,12 @@ public class AmsAssetServiceImpl implements IAmsAssetService { throw new ServiceException("资产编码已存在"); } - // 建档只产生在库资产,使用归属必须由后续领用业务写入。 - amsAsset.setAssetStatus(AssetStatus.IN_STOCK); + // 普通建档只表示实物已登记,仓位和使用归属必须由首次入库及后续流转业务写入。 + amsAsset.setAssetStatus(AssetStatus.PENDING_INBOUND); + clearWarehouseLocation(amsAsset); clearUseOwnership(amsAsset); clearRfidBinding(amsAsset); - fillAssetForSave(amsAsset); + fillBasicAssetForSave(amsAsset); amsAsset.setDelFlag(DEL_FLAG_NORMAL); amsAsset.setCreateTime(DateUtils.getNowDate()); return amsAssetMapper.insertAmsAsset(amsAsset); @@ -209,29 +194,10 @@ public class AmsAssetServiceImpl implements IAmsAssetService { throw new ServiceException("资产编码已存在"); } - // 检查并拒绝有业务引用的资产修改归属字段,归属只能通过流转服务变更 - int referenceCount = amsAssetMapper.countAssetReferences(current.getAssetId()); - if (referenceCount > 0 && ownershipIdentityChanged(current, amsAsset)) - { - throw new ServiceException("资产已有业务引用或履历,仓库、位置、使用人和部门只能通过业务流转修改"); - } - - // 普通编辑不能改变状态或 RFID 当前关系,避免绕过公共业务服务。 + // 普通编辑不能改变状态、当前归属或 RFID 当前关系,避免绕过公共业务服务。 preserveControlledFields(amsAsset, current); - if (referenceCount > 0) - { - // 有业务引用的资产:保留当前归属快照,仅允许修改基础信息和类别 - fillReferencedAssetForSave(amsAsset, current); - } - else - { - // 未进入业务流程的资产:在库状态下清除使用归属,然后执行完整快照回填 - if (StringUtils.equals(AssetStatus.IN_STOCK, current.getAssetStatus())) - { - clearUseOwnership(amsAsset); - } - fillAssetForSave(amsAsset); - } + preserveOwnershipFields(amsAsset, current); + fillBasicAssetForSave(amsAsset); amsAsset.setUpdateTime(DateUtils.getNowDate()); return amsAssetMapper.updateAmsAsset(amsAsset); } @@ -286,27 +252,10 @@ public class AmsAssetServiceImpl implements IAmsAssetService /** * 页面和导入只传业务选择值,快照字段统一在服务层回填,避免前端伪造名称造成台账不一致。 */ - private void fillAssetForSave(AmsAsset amsAsset) + private void fillBasicAssetForSave(AmsAsset amsAsset) { validateBasicFields(amsAsset); fillCategorySnapshot(amsAsset); - fillWarehouseSnapshot(amsAsset); - fillLocationSnapshot(amsAsset); - fillUseDeptSnapshot(amsAsset); - fillUserSnapshot(amsAsset); - fillCustodianSnapshot(amsAsset); - validateAssetStatus(amsAsset); - validateNumberFields(amsAsset); - } - - /** - * 已进入业务流程的资产保留当前归属快照,避免历史主数据停用后阻断基础信息维护。 - */ - private void fillReferencedAssetForSave(AmsAsset amsAsset, AmsAsset current) - { - validateBasicFields(amsAsset); - fillCategorySnapshot(amsAsset); - preserveOwnershipFields(amsAsset, current); fillCustodianSnapshot(amsAsset); validateAssetStatus(amsAsset); validateNumberFields(amsAsset); @@ -360,96 +309,6 @@ public class AmsAssetServiceImpl implements IAmsAssetService } } - private void fillWarehouseSnapshot(AmsAsset amsAsset) - { - AmsWarehouse warehouse = null; - if (StringUtils.isNotNull(amsAsset.getWarehouseId())) - { - warehouse = amsWarehouseService.selectAmsWarehouseByWarehouseId(amsAsset.getWarehouseId()); - } - else if (StringUtils.isNotEmpty(amsAsset.getWarehouseCode())) - { - AmsWarehouse query = new AmsWarehouse(); - query.setWarehouseCode(amsAsset.getWarehouseCode()); - List warehouseList = amsWarehouseService.selectAmsWarehouseList(query); - warehouse = warehouseList == null || warehouseList.isEmpty() ? null : warehouseList.get(0); - } - - if (StringUtils.isNull(warehouse)) - { - throw new ServiceException("所属仓库不能为空或不存在"); - } - if (!StringUtils.equals(ENABLED_YES, warehouse.getEnabled())) - { - throw new ServiceException("所属仓库已停用"); - } - amsAsset.setWarehouseId(warehouse.getWarehouseId()); - amsAsset.setWarehouseCode(warehouse.getWarehouseCode()); - amsAsset.setWarehouseName(warehouse.getWarehouseName()); - } - - private void fillLocationSnapshot(AmsAsset amsAsset) - { - AmsAssetLocation location = null; - if (StringUtils.isNotNull(amsAsset.getLocationId())) - { - location = amsAssetLocationService.selectAmsAssetLocationByLocationId(amsAsset.getLocationId()); - } - else if (StringUtils.isNotEmpty(amsAsset.getLocationCode())) - { - AmsAssetLocation query = new AmsAssetLocation(); - query.setLocationCode(amsAsset.getLocationCode()); - List locationList = amsAssetLocationService.selectAmsAssetLocationList(query); - location = locationList == null || locationList.isEmpty() ? null : locationList.get(0); - } - - if (StringUtils.isNull(location)) - { - throw new ServiceException("存放位置不能为空或不存在"); - } - if (!StringUtils.equals(ENABLED_YES, location.getEnabled())) - { - throw new ServiceException("存放位置已停用"); - } - if (StringUtils.isNotNull(amsAsset.getWarehouseId()) - && !amsAsset.getWarehouseId().equals(location.getWarehouseId())) - { - throw new ServiceException("存放位置不属于所选仓库"); - } - amsAsset.setLocationId(location.getLocationId()); - amsAsset.setLocationCode(location.getLocationCode()); - amsAsset.setLocationName(location.getLocationName()); - } - - private void fillUseDeptSnapshot(AmsAsset amsAsset) - { - if (StringUtils.isNull(amsAsset.getUseDeptId())) - { - amsAsset.setUseDeptName(null); - return; - } - SysDept query = new SysDept(); - query.setDeptId(amsAsset.getUseDeptId()); - query.setStatus(UserConstants.DEPT_NORMAL); - List deptList = sysDeptService.selectDeptList(query); - if (deptList == null || deptList.isEmpty()) - { - throw new ServiceException("使用部门不存在或已停用"); - } - amsAsset.setUseDeptName(deptList.get(0).getDeptName()); - } - - private void fillUserSnapshot(AmsAsset amsAsset) - { - if (StringUtils.isNull(amsAsset.getUseUserId())) - { - amsAsset.setUseUserName(null); - return; - } - SysUser user = selectNormalUser(amsAsset.getUseUserId(), "使用人"); - amsAsset.setUseUserName(user.getUserName()); - } - private void fillCustodianSnapshot(AmsAsset amsAsset) { if (StringUtils.isNull(amsAsset.getCustodianId())) @@ -507,15 +366,6 @@ public class AmsAssetServiceImpl implements IAmsAssetService } } - /** 判断编辑提交的归属字段是否与当前数据库值不同(任一不同即视为变更) */ - private boolean ownershipIdentityChanged(AmsAsset current, AmsAsset incoming) - { - return !Objects.equals(current.getWarehouseId(), incoming.getWarehouseId()) - || !Objects.equals(current.getLocationId(), incoming.getLocationId()) - || !Objects.equals(current.getUseDeptId(), incoming.getUseDeptId()) - || !Objects.equals(current.getUseUserId(), incoming.getUseUserId()); - } - /** 防篡改:将状态和 RFID 关系强制恢复为数据库当前值,阻止前端直接修改这些受控字段 */ private void preserveControlledFields(AmsAsset incoming, AmsAsset current) { @@ -525,7 +375,7 @@ public class AmsAssetServiceImpl implements IAmsAssetService incoming.setEpcCode(current.getEpcCode()); } - /** 有业务引用时保留当前归属快照不变,避免历史主数据停用后阻断基础信息的日常维护 */ + /** 普通编辑始终保留当前归属快照,归属只能通过业务流转服务修改。 */ private void preserveOwnershipFields(AmsAsset incoming, AmsAsset current) { incoming.setWarehouseId(current.getWarehouseId()); @@ -540,7 +390,18 @@ public class AmsAssetServiceImpl implements IAmsAssetService incoming.setUseUserName(current.getUseUserName()); } - /** 清除使用归属信息(建档和在库编辑时调用,在库资产不应有使用人信息) */ + /** 清除仓库和位置,待入库资产在首次确认入库前不能拥有仓位。 */ + private void clearWarehouseLocation(AmsAsset asset) + { + asset.setWarehouseId(null); + asset.setWarehouseCode(null); + asset.setWarehouseName(null); + asset.setLocationId(null); + asset.setLocationCode(null); + asset.setLocationName(null); + } + + /** 清除使用归属信息,待入库和在库资产均不应有使用归属。 */ private void clearUseOwnership(AmsAsset asset) { asset.setUseDeptId(null); diff --git a/ruoyi-asset/src/main/java/com/ruoyi/asset/service/impl/AmsInboundOrderServiceImpl.java b/ruoyi-asset/src/main/java/com/ruoyi/asset/service/impl/AmsInboundOrderServiceImpl.java index 29cb735..207cfc1 100644 --- a/ruoyi-asset/src/main/java/com/ruoyi/asset/service/impl/AmsInboundOrderServiceImpl.java +++ b/ruoyi-asset/src/main/java/com/ruoyi/asset/service/impl/AmsInboundOrderServiceImpl.java @@ -2,6 +2,7 @@ package com.ruoyi.asset.service.impl; import java.util.ArrayList; import java.util.Arrays; +import java.util.Comparator; import java.util.Date; import java.util.HashSet; import java.util.List; @@ -14,9 +15,9 @@ import com.ruoyi.asset.domain.AmsInboundOrder; import com.ruoyi.asset.domain.AmsInboundOrderItem; import com.ruoyi.asset.domain.AmsWarehouse; import com.ruoyi.asset.domain.AssetTransitionContext; +import com.ruoyi.asset.mapper.AmsAssetMapper; import com.ruoyi.asset.mapper.AmsInboundOrderMapper; import com.ruoyi.asset.service.IAmsAssetLocationService; -import com.ruoyi.asset.service.IAmsAssetService; import com.ruoyi.asset.service.IAmsInboundOrderService; import com.ruoyi.asset.service.IAmsWarehouseService; import com.ruoyi.asset.service.IAssetStatusTransitionService; @@ -46,6 +47,9 @@ public class AmsInboundOrderServiceImpl implements IAmsInboundOrderService @Autowired private AmsInboundOrderMapper amsInboundOrderMapper; + @Autowired + private AmsAssetMapper amsAssetMapper; + @Autowired private ISysCodeRuleService sysCodeRuleService; @@ -55,9 +59,6 @@ public class AmsInboundOrderServiceImpl implements IAmsInboundOrderService @Autowired private IAmsAssetLocationService amsAssetLocationService; - @Autowired - private IAmsAssetService amsAssetService; - @Autowired private IAssetStatusTransitionService assetStatusTransitionService; @@ -73,6 +74,16 @@ public class AmsInboundOrderServiceImpl implements IAmsInboundOrderService return amsInboundOrderMapper.selectAmsInboundOrderList(amsInboundOrder); } + /** + * 查询可入库资产候选列表,状态和占用条件由服务层固定传入,不开放给前端控制。 + */ + @Override + public List selectAvailableInboundAssetList(AmsAsset amsAsset, Long currentOrderId) + { + return amsInboundOrderMapper.selectAvailableInboundAssetList(amsAsset, currentOrderId, + AssetStatus.PENDING_INBOUND, InboundOrderStatus.DRAFT); + } + @Override @Transactional(rollbackFor = Exception.class) public int insertAmsInboundOrder(AmsInboundOrder amsInboundOrder) @@ -85,7 +96,7 @@ public class AmsInboundOrderServiceImpl implements IAmsInboundOrderService amsInboundOrder.setInboundTime(null); amsInboundOrder.setDelFlag(DEL_FLAG_NORMAL); amsInboundOrder.setCreateTime(DateUtils.getNowDate()); - fillOrderSnapshots(amsInboundOrder); + fillOrderSnapshots(amsInboundOrder, null); int rows = amsInboundOrderMapper.insertAmsInboundOrder(amsInboundOrder); if (rows != 1 || StringUtils.isNull(amsInboundOrder.getOrderId())) @@ -114,7 +125,7 @@ public class AmsInboundOrderServiceImpl implements IAmsInboundOrderService amsInboundOrder.setInboundUserName(current.getInboundUserName()); amsInboundOrder.setInboundTime(current.getInboundTime()); amsInboundOrder.setUpdateTime(DateUtils.getNowDate()); - fillOrderSnapshots(amsInboundOrder); + fillOrderSnapshots(amsInboundOrder, amsInboundOrder.getOrderId()); amsInboundOrderMapper.deleteAmsInboundOrderItemByOrderId(amsInboundOrder.getOrderId()); insertAmsInboundOrderItem(amsInboundOrder); @@ -135,7 +146,10 @@ public class AmsInboundOrderServiceImpl implements IAmsInboundOrderService { throw new ServiceException("入库单明细不能为空"); } - for (AmsInboundOrderItem item : itemList) + // 所有批量流转统一按资产ID升序执行,保证与草稿保存使用相同锁序。 + List sortedItems = new ArrayList<>(itemList); + sortedItems.sort(Comparator.comparing(AmsInboundOrderItem::getAssetId)); + for (AmsInboundOrderItem item : sortedItems) { AssetTransitionContext context = new AssetTransitionContext(); context.setSourceOrderId(order.getOrderId()); @@ -240,8 +254,13 @@ public class AmsInboundOrderServiceImpl implements IAmsInboundOrderService /** * 仓库、资产和位置快照全部从主数据回填,页面只提交ID,避免伪造快照。 + *

并发控制要点:明细按 assetId 升序排序后逐行加行锁(FOR UPDATE), + * 与 confirmInbound 使用相同锁序,消除死锁风险。

+ * + * @param order 当前入库单 + * @param currentOrderId 当前单据ID,新增时为 null,编辑时传入以排除自身的草稿占用记录 */ - private void fillOrderSnapshots(AmsInboundOrder order) + private void fillOrderSnapshots(AmsInboundOrder order, Long currentOrderId) { AmsWarehouse warehouse = amsWarehouseService.selectAmsWarehouseByWarehouseId(order.getWarehouseId()); if (StringUtils.isNull(warehouse) || !StringUtils.equals(ENABLED_YES, warehouse.getEnabled())) @@ -252,7 +271,11 @@ public class AmsInboundOrderServiceImpl implements IAmsInboundOrderService order.setWarehouseName(warehouse.getWarehouseName()); Set assetIds = new HashSet<>(); - for (AmsInboundOrderItem item : order.getAmsInboundOrderItemList()) + List sortedItems = new ArrayList<>(order.getAmsInboundOrderItemList()); + sortedItems.sort(Comparator.comparing(AmsInboundOrderItem::getAssetId, + Comparator.nullsFirst(Long::compareTo))); + order.setAmsInboundOrderItemList(sortedItems); + for (AmsInboundOrderItem item : sortedItems) { if (StringUtils.isNull(item.getAssetId())) { @@ -262,15 +285,21 @@ public class AmsInboundOrderServiceImpl implements IAmsInboundOrderService { throw new ServiceException("同一入库单不能重复选择资产"); } - AmsAsset asset = amsAssetService.selectAmsAssetByAssetId(item.getAssetId()); + // 保存草稿时锁定资产,避免两个并发草稿同时通过占用校验。 + AmsAsset asset = amsAssetMapper.selectAmsAssetByAssetIdForUpdate(item.getAssetId()); if (StringUtils.isNull(asset)) { throw new ServiceException("入库资产不存在或已删除"); } - if (!StringUtils.equals(AssetStatus.IN_STOCK, asset.getAssetStatus())) + if (!StringUtils.equals(AssetStatus.PENDING_INBOUND, asset.getAssetStatus())) { throw new ServiceException(StringUtils.format("资产【{}】当前状态不允许入库", asset.getAssetCode())); } + if (amsInboundOrderMapper.countOtherDraftInboundOrderByAssetId(asset.getAssetId(), currentOrderId, + InboundOrderStatus.DRAFT) > 0) + { + throw new ServiceException(StringUtils.format("资产【{}】已被其他草稿入库单占用", asset.getAssetCode())); + } if (StringUtils.isNull(item.getLocationId())) { throw new ServiceException(StringUtils.format("资产【{}】的入库位置不能为空", asset.getAssetCode())); diff --git a/ruoyi-asset/src/main/java/com/ruoyi/asset/service/impl/AssetStatusTransitionServiceImpl.java b/ruoyi-asset/src/main/java/com/ruoyi/asset/service/impl/AssetStatusTransitionServiceImpl.java index 99e6cd2..3cafde5 100644 --- a/ruoyi-asset/src/main/java/com/ruoyi/asset/service/impl/AssetStatusTransitionServiceImpl.java +++ b/ruoyi-asset/src/main/java/com/ruoyi/asset/service/impl/AssetStatusTransitionServiceImpl.java @@ -73,8 +73,8 @@ public class AssetStatusTransitionServiceImpl implements IAssetStatusTransitionS public AssetTransitionResult confirmInbound(Long assetId, Long warehouseId, Long locationId, AssetTransitionContext context) { - // 入库允许 IN_STOCK→IN_STOCK(调仓换位场景),更新仓位并清除使用归属 - return executeTransition(assetId, Set.of(AssetStatus.IN_STOCK), asset -> AssetStatus.IN_STOCK, + // 首次入库是待入库资产进入仓库的唯一入口,在库资产换仓必须走调拨业务。 + return executeTransition(assetId, Set.of(AssetStatus.PENDING_INBOUND), asset -> AssetStatus.IN_STOCK, context, AssetLifecycleBusinessType.INBOUND, "确认资产入库", asset -> { fillWarehouseLocation(asset, warehouseId, locationId); diff --git a/ruoyi-asset/src/main/resources/mapper/asset/AmsInboundOrderMapper.xml b/ruoyi-asset/src/main/resources/mapper/asset/AmsInboundOrderMapper.xml index e91b497..d83db2f 100644 --- a/ruoyi-asset/src/main/resources/mapper/asset/AmsInboundOrderMapper.xml +++ b/ruoyi-asset/src/main/resources/mapper/asset/AmsInboundOrderMapper.xml @@ -51,6 +51,20 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + + + + + + + + + + + + + select order_id, inbound_no, warehouse_id, warehouse_code, warehouse_name, inbound_user_id, inbound_user_name, inbound_time, order_status, @@ -98,6 +112,52 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" for update + + + + + + - - - - - - -
-
- -
- -
-
-
-
-
- -
- -
-
-
-
-
- -
- -
-
-
@@ -178,24 +134,13 @@ + + diff --git a/ruoyi-asset/src/test/java/com/ruoyi/asset/service/impl/AmsAssetServiceImplTest.java b/ruoyi-asset/src/test/java/com/ruoyi/asset/service/impl/AmsAssetServiceImplTest.java index 9f466ef..4cb56c4 100644 --- a/ruoyi-asset/src/test/java/com/ruoyi/asset/service/impl/AmsAssetServiceImplTest.java +++ b/ruoyi-asset/src/test/java/com/ruoyi/asset/service/impl/AmsAssetServiceImplTest.java @@ -13,15 +13,10 @@ import java.util.List; import com.ruoyi.asset.constant.AssetStatus; import com.ruoyi.asset.domain.AmsAsset; import com.ruoyi.asset.domain.AmsAssetCategory; -import com.ruoyi.asset.domain.AmsAssetLocation; -import com.ruoyi.asset.domain.AmsWarehouse; import com.ruoyi.asset.mapper.AmsAssetMapper; import com.ruoyi.asset.service.IAmsAssetCategoryService; -import com.ruoyi.asset.service.IAmsAssetLocationService; -import com.ruoyi.asset.service.IAmsWarehouseService; import com.ruoyi.common.core.domain.entity.SysDictData; import com.ruoyi.common.exception.ServiceException; -import com.ruoyi.system.service.ISysDeptService; import com.ruoyi.system.service.ISysDictDataService; import com.ruoyi.system.service.ISysUserService; import org.junit.jupiter.api.Test; @@ -40,15 +35,6 @@ class AmsAssetServiceImplTest @Mock private IAmsAssetCategoryService amsAssetCategoryService; - @Mock - private IAmsWarehouseService amsWarehouseService; - - @Mock - private IAmsAssetLocationService amsAssetLocationService; - - @Mock - private ISysDeptService sysDeptService; - @Mock private ISysUserService sysUserService; @@ -72,9 +58,9 @@ class AmsAssetServiceImplTest verify(amsAssetMapper).selectBindableAmsAssetList(query, AssetStatus.DISPOSED); } - /** 新建资产时,无论前端传入什么状态,必须强制为在库并清除使用归属 */ + /** 新建资产时,无论前端传入什么状态和归属,必须强制创建为待入库资产 */ @Test - void insertAmsAssetShouldForceStockAndClearUseOwnership() + void insertAmsAssetShouldForcePendingInboundAndClearOwnership() { AmsAsset incoming = buildAsset(); incoming.setAssetStatus(AssetStatus.DISPOSED); @@ -90,7 +76,9 @@ class AmsAssetServiceImplTest ArgumentCaptor captor = ArgumentCaptor.forClass(AmsAsset.class); verify(amsAssetMapper).insertAmsAsset(captor.capture()); - assertEquals(AssetStatus.IN_STOCK, captor.getValue().getAssetStatus()); + assertEquals(AssetStatus.PENDING_INBOUND, captor.getValue().getAssetStatus()); + assertNull(captor.getValue().getWarehouseId()); + assertNull(captor.getValue().getLocationId()); assertNull(captor.getValue().getUseDeptId()); assertNull(captor.getValue().getUseUserId()); assertNull(captor.getValue().getTagId()); @@ -98,9 +86,9 @@ class AmsAssetServiceImplTest assertNull(captor.getValue().getEpcCode()); } - /** 有业务引用的资产,编辑时修改归属字段应被拒绝 */ + /** 普通编辑提交归属字段时,应忽略前端值并保留数据库当前归属 */ @Test - void updateAmsAssetShouldRejectOwnershipChangeAfterBusinessReference() + void updateAmsAssetShouldPreserveOwnership() { AmsAsset current = buildAsset(); current.setUseDeptId(3L); @@ -108,13 +96,16 @@ class AmsAssetServiceImplTest AmsAsset incoming = buildAsset(); incoming.setWarehouseId(2L); when(amsAssetMapper.selectAmsAssetByAssetId(1L)).thenReturn(current); - when(amsAssetMapper.countAssetReferences(1L)).thenReturn(1); + when(amsAssetMapper.updateAmsAsset(any(AmsAsset.class))).thenReturn(1); + stubMasterData(); - ServiceException exception = assertThrows(ServiceException.class, - () -> service.updateAmsAsset(incoming)); + service.updateAmsAsset(incoming); - assertTrue(exception.getMessage().contains("业务流转")); - verify(amsAssetMapper, never()).updateAmsAsset(any(AmsAsset.class)); + ArgumentCaptor captor = ArgumentCaptor.forClass(AmsAsset.class); + verify(amsAssetMapper).updateAmsAsset(captor.capture()); + assertEquals(1L, captor.getValue().getWarehouseId()); + assertEquals(3L, captor.getValue().getUseDeptId()); + assertEquals(4L, captor.getValue().getUseUserId()); } /** 未进入业务流程的在库资产编辑时,应保持状态为在库并清除使用归属 */ @@ -128,7 +119,6 @@ class AmsAssetServiceImplTest incoming.setUseDeptId(3L); incoming.setUseUserId(4L); when(amsAssetMapper.selectAmsAssetByAssetId(1L)).thenReturn(current); - when(amsAssetMapper.countAssetReferences(1L)).thenReturn(0); when(amsAssetMapper.updateAmsAsset(any(AmsAsset.class))).thenReturn(1); stubMasterData(); @@ -164,7 +154,6 @@ class AmsAssetServiceImplTest incoming.setTagCode("TAG-OTHER"); incoming.setEpcCode("EPC-OTHER"); when(amsAssetMapper.selectAmsAssetByAssetId(1L)).thenReturn(current); - when(amsAssetMapper.countAssetReferences(1L)).thenReturn(1); when(amsAssetMapper.updateAmsAsset(any(AmsAsset.class))).thenReturn(1); AmsAssetCategory category = new AmsAssetCategory(); category.setCategoryId(1L); @@ -187,9 +176,6 @@ class AmsAssetServiceImplTest assertEquals(9L, captor.getValue().getTagId()); assertEquals("TAG-001", captor.getValue().getTagCode()); assertEquals("EPC-001", captor.getValue().getEpcCode()); - verify(amsWarehouseService, never()).selectAmsWarehouseByWarehouseId(any()); - verify(amsAssetLocationService, never()).selectAmsAssetLocationByLocationId(any()); - verify(sysDeptService, never()).selectDeptList(any()); verify(sysUserService, never()).selectUserById(any()); } @@ -224,7 +210,7 @@ class AmsAssetServiceImplTest verify(amsAssetMapper).selectAmsAssetByAssetIdForUpdate(1L); } - /** 构建常用主数据的 Mock 返回值(类别、仓库、位置、状态字典) */ + /** 构建资产基础信息保存所需的类别和状态字典 */ private void stubMasterData() { AmsAssetCategory category = new AmsAssetCategory(); @@ -232,26 +218,15 @@ class AmsAssetServiceImplTest category.setCategoryCode("CAT-001"); category.setCategoryName("设备"); category.setEnabled("Y"); - AmsWarehouse warehouse = new AmsWarehouse(); - warehouse.setWarehouseId(1L); - warehouse.setWarehouseCode("WH-001"); - warehouse.setWarehouseName("一号仓"); - warehouse.setEnabled("Y"); - AmsAssetLocation location = new AmsAssetLocation(); - location.setLocationId(10L); - location.setLocationCode("LOC-001"); - location.setLocationName("A区"); - location.setWarehouseId(1L); - location.setEnabled("Y"); - SysDictData status = new SysDictData(); - status.setDictValue(AssetStatus.IN_STOCK); + SysDictData pendingStatus = new SysDictData(); + pendingStatus.setDictValue(AssetStatus.PENDING_INBOUND); + SysDictData stockStatus = new SysDictData(); + stockStatus.setDictValue(AssetStatus.IN_STOCK); when(amsAssetCategoryService.selectAmsAssetCategoryByCategoryId(1L)).thenReturn(category); - when(amsWarehouseService.selectAmsWarehouseByWarehouseId(1L)).thenReturn(warehouse); - when(amsAssetLocationService.selectAmsAssetLocationByLocationId(10L)).thenReturn(location); - when(sysDictDataService.selectDictDataList(any(SysDictData.class))).thenReturn(List.of(status)); + when(sysDictDataService.selectDictDataList(any(SysDictData.class))).thenReturn(List.of(pendingStatus, stockStatus)); } - /** 构建基础的测试资产对象,默认在库状态 */ + /** 构建基础的测试资产对象,默认在库状态,具体场景可覆盖为待入库。 */ private AmsAsset buildAsset() { AmsAsset asset = new AmsAsset(); diff --git a/ruoyi-asset/src/test/java/com/ruoyi/asset/service/impl/AmsInboundOrderServiceImplTest.java b/ruoyi-asset/src/test/java/com/ruoyi/asset/service/impl/AmsInboundOrderServiceImplTest.java index 4668fb7..8a60308 100644 --- a/ruoyi-asset/src/test/java/com/ruoyi/asset/service/impl/AmsInboundOrderServiceImplTest.java +++ b/ruoyi-asset/src/test/java/com/ruoyi/asset/service/impl/AmsInboundOrderServiceImplTest.java @@ -7,6 +7,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -21,9 +22,9 @@ import com.ruoyi.asset.domain.AmsInboundOrder; import com.ruoyi.asset.domain.AmsInboundOrderItem; import com.ruoyi.asset.domain.AmsWarehouse; import com.ruoyi.asset.domain.AssetTransitionContext; +import com.ruoyi.asset.mapper.AmsAssetMapper; import com.ruoyi.asset.mapper.AmsInboundOrderMapper; import com.ruoyi.asset.service.IAmsAssetLocationService; -import com.ruoyi.asset.service.IAmsAssetService; import com.ruoyi.asset.service.IAmsWarehouseService; import com.ruoyi.asset.service.IAssetStatusTransitionService; import com.ruoyi.common.exception.ServiceException; @@ -41,6 +42,9 @@ class AmsInboundOrderServiceImplTest @Mock private AmsInboundOrderMapper amsInboundOrderMapper; + @Mock + private AmsAssetMapper amsAssetMapper; + @Mock private ISysCodeRuleService sysCodeRuleService; @@ -50,9 +54,6 @@ class AmsInboundOrderServiceImplTest @Mock private IAmsAssetLocationService amsAssetLocationService; - @Mock - private IAmsAssetService amsAssetService; - @Mock private IAssetStatusTransitionService assetStatusTransitionService; @@ -66,6 +67,8 @@ class AmsInboundOrderServiceImplTest AmsInboundOrder order = buildDraftRequest(); when(sysCodeRuleService.nextCode("INBOUND_ORDER")).thenReturn("RK202606100001"); stubMasterData(); + when(amsInboundOrderMapper.countOtherDraftInboundOrderByAssetId(1L, null, InboundOrderStatus.DRAFT)) + .thenReturn(0); doAnswer(invocation -> { AmsInboundOrder inserted = invocation.getArgument(0); inserted.setOrderId(100L); @@ -92,6 +95,35 @@ class AmsInboundOrderServiceImplTest assertNotNull(item.getCreateTime()); } + /** 同一待入库资产被其他有效草稿占用时必须拒绝保存。 */ + @Test + void insertShouldRejectAssetReservedByOtherDraft() + { + AmsInboundOrder order = buildDraftRequest(); + when(sysCodeRuleService.nextCode("INBOUND_ORDER")).thenReturn("RK202606100001"); + stubMasterData(); + when(amsInboundOrderMapper.countOtherDraftInboundOrderByAssetId(1L, null, InboundOrderStatus.DRAFT)) + .thenReturn(1); + + ServiceException exception = assertThrows(ServiceException.class, + () -> service.insertAmsInboundOrder(order)); + + assertTrue(exception.getMessage().contains("其他草稿")); + verify(amsInboundOrderMapper, never()).insertAmsInboundOrder(any(AmsInboundOrder.class)); + } + + /** 分页候选查询固定筛选待入库资产并排除其他草稿占用。 */ + @Test + void selectAvailableInboundAssetListShouldDelegateFixedStatuses() + { + AmsAsset query = new AmsAsset(); + List expected = List.of(new AmsAsset()); + when(amsInboundOrderMapper.selectAvailableInboundAssetList(query, 100L, + AssetStatus.PENDING_INBOUND, InboundOrderStatus.DRAFT)).thenReturn(expected); + + assertEquals(expected, service.selectAvailableInboundAssetList(query, 100L)); + } + /** 已完成入库单必须拒绝修改,避免绕过页面直接篡改单据。 */ @Test void updateShouldRejectCompletedOrder() @@ -191,8 +223,8 @@ class AmsInboundOrderServiceImplTest asset.setCategoryName("测试类别"); asset.setSpecModel("SPEC-001"); asset.setBrand("测试品牌"); - asset.setAssetStatus(AssetStatus.IN_STOCK); - when(amsAssetService.selectAmsAssetByAssetId(1L)).thenReturn(asset); + asset.setAssetStatus(AssetStatus.PENDING_INBOUND); + when(amsAssetMapper.selectAmsAssetByAssetIdForUpdate(1L)).thenReturn(asset); AmsAssetLocation location = new AmsAssetLocation(); location.setLocationId(20L); @@ -200,6 +232,7 @@ class AmsInboundOrderServiceImplTest location.setLocationCode("LOC-020"); location.setLocationName("二号仓A区"); location.setEnabled("Y"); - when(amsAssetLocationService.selectAmsAssetLocationByLocationId(20L)).thenReturn(location); + // 草稿占用冲突会在位置校验前失败,因此该公共桩允许部分用例不消费。 + lenient().when(amsAssetLocationService.selectAmsAssetLocationByLocationId(20L)).thenReturn(location); } } diff --git a/ruoyi-asset/src/test/java/com/ruoyi/asset/service/impl/AssetStatusTransitionServiceImplTest.java b/ruoyi-asset/src/test/java/com/ruoyi/asset/service/impl/AssetStatusTransitionServiceImplTest.java index b6e9a6e..31576f3 100644 --- a/ruoyi-asset/src/test/java/com/ruoyi/asset/service/impl/AssetStatusTransitionServiceImplTest.java +++ b/ruoyi-asset/src/test/java/com/ruoyi/asset/service/impl/AssetStatusTransitionServiceImplTest.java @@ -61,7 +61,7 @@ class AssetStatusTransitionServiceImplTest @Test void confirmInboundShouldUpdateLocationClearUseOwnershipAndWriteLifecycle() { - AmsAsset asset = buildAsset(AssetStatus.IN_STOCK); + AmsAsset asset = buildAsset(AssetStatus.PENDING_INBOUND); asset.setUseDeptId(9L); asset.setUseDeptName("旧部门"); asset.setUseUserId(8L); @@ -76,7 +76,20 @@ class AssetStatusTransitionServiceImplTest assertEquals(20L, result.getAfterAsset().getLocationId()); assertNull(result.getAfterAsset().getUseDeptId()); assertNull(result.getAfterAsset().getUseUserId()); - assertLifecycle(AssetLifecycleBusinessType.INBOUND, AssetStatus.IN_STOCK, AssetStatus.IN_STOCK); + assertLifecycle(AssetLifecycleBusinessType.INBOUND, AssetStatus.PENDING_INBOUND, AssetStatus.IN_STOCK); + } + + /** 已在库资产不得重复入库,换仓必须走调拨业务 */ + @Test + void confirmInboundShouldRejectStockAsset() + { + stubLockedAssetWithoutUpdate(buildAsset(AssetStatus.IN_STOCK)); + + ServiceException exception = assertThrows(ServiceException.class, + () -> service.confirmInbound(1L, 2L, 20L, buildContext())); + + assertTrue(exception.getMessage().contains("不允许")); + verify(amsAssetMapper, never()).updateAssetForTransition(any(AmsAsset.class)); } /** 领用流转:写入使用部门和使用人、状态变为在用 */ @@ -329,7 +342,7 @@ class AssetStatusTransitionServiceImplTest @Test void transitionShouldRejectLocationOutsideWarehouse() { - stubLockedAssetWithoutUpdate(buildAsset(AssetStatus.IN_STOCK)); + stubLockedAssetWithoutUpdate(buildAsset(AssetStatus.PENDING_INBOUND)); AmsWarehouse warehouse = buildWarehouse(2L); AmsAssetLocation location = buildLocation(20L, 3L); when(amsWarehouseService.selectAmsWarehouseByWarehouseId(2L)).thenReturn(warehouse); @@ -346,7 +359,7 @@ class AssetStatusTransitionServiceImplTest @Test void transitionShouldRejectInactiveWarehouse() { - stubLockedAssetWithoutUpdate(buildAsset(AssetStatus.IN_STOCK)); + stubLockedAssetWithoutUpdate(buildAsset(AssetStatus.PENDING_INBOUND)); AmsWarehouse warehouse = buildWarehouse(2L); warehouse.setEnabled("N"); when(amsWarehouseService.selectAmsWarehouseByWarehouseId(2L)).thenReturn(warehouse); diff --git a/ruoyi-asset/src/test/java/com/ruoyi/asset/service/impl/RfidBindingServiceImplTest.java b/ruoyi-asset/src/test/java/com/ruoyi/asset/service/impl/RfidBindingServiceImplTest.java index 7132146..dd6d84c 100644 --- a/ruoyi-asset/src/test/java/com/ruoyi/asset/service/impl/RfidBindingServiceImplTest.java +++ b/ruoyi-asset/src/test/java/com/ruoyi/asset/service/impl/RfidBindingServiceImplTest.java @@ -51,7 +51,7 @@ class RfidBindingServiceImplTest void bindShouldUpdateBothSidesAndWriteLifecycle() { AmsRfidTag tag = buildUnboundTag(); - AmsAsset asset = buildAsset(AssetStatus.IN_STOCK); + AmsAsset asset = buildAsset(AssetStatus.PENDING_INBOUND); stubBind(tag, asset); RfidBindingResult result = service.bind(" ASSET-001 ", " EPC-001 ", buildContext());