diff --git a/ruoyi-asset/src/main/java/com/ruoyi/asset/constant/DisposalOrderStatus.java b/ruoyi-asset/src/main/java/com/ruoyi/asset/constant/DisposalOrderStatus.java new file mode 100644 index 0000000..042fd49 --- /dev/null +++ b/ruoyi-asset/src/main/java/com/ruoyi/asset/constant/DisposalOrderStatus.java @@ -0,0 +1,25 @@ +package com.ruoyi.asset.constant; + +/** + * 报废单状态常量定义 + * + * @author Yangk + */ +public final class DisposalOrderStatus +{ + /** 草稿 */ + public static final String DRAFT = "DRAFT"; + + /** 待确认 */ + public static final String PENDING_CONFIRM = "PENDING_CONFIRM"; + + /** 已报废 */ + public static final String DISPOSED_DONE = "DISPOSED_DONE"; + + /** 已驳回 */ + public static final String REJECTED = "REJECTED"; + + private DisposalOrderStatus() + { + } +} diff --git a/ruoyi-asset/src/main/java/com/ruoyi/asset/controller/AmsDisposalOrderController.java b/ruoyi-asset/src/main/java/com/ruoyi/asset/controller/AmsDisposalOrderController.java new file mode 100644 index 0000000..6bc7788 --- /dev/null +++ b/ruoyi-asset/src/main/java/com/ruoyi/asset/controller/AmsDisposalOrderController.java @@ -0,0 +1,222 @@ +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; +import org.springframework.ui.ModelMap; +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; +import com.ruoyi.asset.domain.AmsAsset; +import com.ruoyi.asset.domain.AmsDisposalOperateContext; +import com.ruoyi.asset.domain.AmsDisposalOrder; +import com.ruoyi.asset.service.IAmsDisposalOrderService; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.common.core.page.TableDataInfo; + +/** + * 报废管理Controller + * + * @author Yangk + * @date 2026-06-17 + */ +@Controller +@RequestMapping("/asset/disposal") +public class AmsDisposalOrderController extends BaseController +{ + private String prefix = "asset/disposal"; + + @Autowired + private IAmsDisposalOrderService amsDisposalOrderService; + + @RequiresPermissions("asset:disposal:view") + @GetMapping() + public String disposal() + { + return prefix + "/disposal"; + } + + /** + * 查询报废管理列表 + */ + @RequiresPermissions("asset:disposal:list") + @PostMapping("/list") + @ResponseBody + public TableDataInfo list(AmsDisposalOrder amsDisposalOrder) + { + startPage(); + List list = amsDisposalOrderService.selectAmsDisposalOrderList(amsDisposalOrder); + return getDataTable(list); + } + + /** + * 弹出选择资产模态框 + */ + @RequiresPermissions(value = { "asset:disposal:add", "asset:disposal: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"; + } + + /** + * 查询可用于当前报废单选择的资产列表(在库/在用且未被占用) + */ + @RequiresPermissions(value = { "asset:disposal:add", "asset:disposal:edit" }, logical = Logical.OR) + @PostMapping("/availableAssetList") + @ResponseBody + public TableDataInfo availableAssetList(AmsAsset asset, @RequestParam(value = "orderId", required = false) Long orderId) + { + startPage(); + List list = amsDisposalOrderService.selectAvailableDisposalAssetList(asset, orderId); + return getDataTable(list); + } + + /** + * 导出报废管理列表 + */ + @RequiresPermissions("asset:disposal:export") + @Log(title = "报废管理", businessType = BusinessType.EXPORT) + @PostMapping("/export") + @ResponseBody + public AjaxResult export(AmsDisposalOrder amsDisposalOrder) + { + List list = amsDisposalOrderService.selectAmsDisposalOrderList(amsDisposalOrder); + ExcelUtil util = new ExcelUtil(AmsDisposalOrder.class); + return util.exportExcel(list, "报废管理数据"); + } + + /** + * 查看报废管理详情 + */ + @RequiresPermissions("asset:disposal:view") + @GetMapping("/view/{orderId}") + public String view(@PathVariable("orderId") Long orderId, ModelMap mmap) + { + AmsDisposalOrder amsDisposalOrder = amsDisposalOrderService.selectAmsDisposalOrderByOrderId(orderId); + mmap.put("amsDisposalOrder", amsDisposalOrder); + return prefix + "/view"; + } + + /** + * 新增报废管理页面 + */ + @RequiresPermissions("asset:disposal:add") + @GetMapping("/add") + public String add(ModelMap mmap) + { + SysUser currentUser = getSysUser(); + mmap.put("applicantId", currentUser.getUserId()); + mmap.put("applicantName", currentUser.getUserName()); + if (currentUser.getDept() != null) + { + mmap.put("applyDeptId", currentUser.getDeptId()); + mmap.put("applyDeptName", currentUser.getDept().getDeptName()); + } + return prefix + "/add"; + } + + /** + * 新增保存报废管理(保存为草稿) + */ + @RequiresPermissions("asset:disposal:add") + @Log(title = "报废管理", businessType = BusinessType.INSERT) + @PostMapping("/add") + @ResponseBody + public AjaxResult addSave(AmsDisposalOrder amsDisposalOrder) + { + return toAjax(amsDisposalOrderService.insertAmsDisposalOrder(amsDisposalOrder, buildOperateContext())); + } + + /** + * 修改报废管理页面 + */ + @RequiresPermissions("asset:disposal:edit") + @GetMapping("/edit/{orderId}") + public String edit(@PathVariable("orderId") Long orderId, ModelMap mmap) + { + AmsDisposalOrder amsDisposalOrder = amsDisposalOrderService.selectAmsDisposalOrderByOrderId(orderId); + mmap.put("amsDisposalOrder", amsDisposalOrder); + return prefix + "/edit"; + } + + /** + * 修改保存报废管理(保存为草稿) + */ + @RequiresPermissions("asset:disposal:edit") + @Log(title = "报废管理", businessType = BusinessType.UPDATE) + @PostMapping("/edit") + @ResponseBody + public AjaxResult editSave(AmsDisposalOrder amsDisposalOrder) + { + return toAjax(amsDisposalOrderService.updateAmsDisposalOrder(amsDisposalOrder, buildOperateContext())); + } + + /** + * 删除报废管理(物理删除改为逻辑删除) + */ + @RequiresPermissions("asset:disposal:remove") + @Log(title = "报废管理", businessType = BusinessType.DELETE) + @PostMapping( "/remove") + @ResponseBody + public AjaxResult remove(String ids) + { + return toAjax(amsDisposalOrderService.deleteAmsDisposalOrderByOrderIds(ids)); + } + + /** + * 提交报废申请 + */ + @RequiresPermissions("asset:disposal:submit") + @Log(title = "报废管理", businessType = BusinessType.UPDATE) + @PostMapping("/submit/{orderId}") + @ResponseBody + public AjaxResult submit(@PathVariable("orderId") Long orderId) + { + return toAjax(amsDisposalOrderService.submitDisposal(orderId, buildOperateContext())); + } + + /** + * 确认报废处置 + */ + @RequiresPermissions("asset:disposal:confirm") + @Log(title = "报废管理", businessType = BusinessType.UPDATE) + @PostMapping("/confirm/{orderId}") + @ResponseBody + public AjaxResult confirm(@PathVariable("orderId") Long orderId) + { + return toAjax(amsDisposalOrderService.confirmDisposal(orderId, buildOperateContext())); + } + + /** + * 驳回报废申请 + */ + @RequiresPermissions("asset:disposal:reject") + @Log(title = "报废管理", businessType = BusinessType.UPDATE) + @PostMapping("/reject/{orderId}") + @ResponseBody + public AjaxResult reject(@PathVariable("orderId") Long orderId, @RequestParam("rejectReason") String rejectReason) + { + return toAjax(amsDisposalOrderService.rejectDisposal(orderId, rejectReason, buildOperateContext())); + } + + private AmsDisposalOperateContext buildOperateContext() + { + SysUser currentUser = getSysUser(); + Long deptId = currentUser.getDept() == null ? null : currentUser.getDeptId(); + String deptName = currentUser.getDept() == null ? null : currentUser.getDept().getDeptName(); + return new AmsDisposalOperateContext(currentUser.getUserId(), currentUser.getUserName(), getLoginName(), + deptId, deptName); + } +} diff --git a/ruoyi-asset/src/main/java/com/ruoyi/asset/domain/AmsDisposalOperateContext.java b/ruoyi-asset/src/main/java/com/ruoyi/asset/domain/AmsDisposalOperateContext.java new file mode 100644 index 0000000..ed52be6 --- /dev/null +++ b/ruoyi-asset/src/main/java/com/ruoyi/asset/domain/AmsDisposalOperateContext.java @@ -0,0 +1,94 @@ +package com.ruoyi.asset.domain; + +import java.io.Serializable; + +/** + * 报废处置操作上下文。 + * + * 报废申请人、部门和操作人必须来自当前登录会话,避免信任前端隐藏字段。 + * + * @author Yangk + */ +public class AmsDisposalOperateContext implements Serializable +{ + private static final long serialVersionUID = 1L; + + /** 操作人ID */ + private Long operateUserId; + + /** 操作人名称 */ + private String operateUserName; + + /** 操作人登录账号 */ + private String operateLoginName; + + /** 操作人部门ID */ + private Long operateDeptId; + + /** 操作人部门名称 */ + private String operateDeptName; + + public AmsDisposalOperateContext() + { + } + + public AmsDisposalOperateContext(Long operateUserId, String operateUserName, String operateLoginName, + Long operateDeptId, String operateDeptName) + { + this.operateUserId = operateUserId; + this.operateUserName = operateUserName; + this.operateLoginName = operateLoginName; + this.operateDeptId = operateDeptId; + this.operateDeptName = operateDeptName; + } + + 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 String getOperateLoginName() + { + return operateLoginName; + } + + public void setOperateLoginName(String operateLoginName) + { + this.operateLoginName = operateLoginName; + } + + public Long getOperateDeptId() + { + return operateDeptId; + } + + public void setOperateDeptId(Long operateDeptId) + { + this.operateDeptId = operateDeptId; + } + + public String getOperateDeptName() + { + return operateDeptName; + } + + public void setOperateDeptName(String operateDeptName) + { + this.operateDeptName = operateDeptName; + } +} diff --git a/ruoyi-asset/src/main/java/com/ruoyi/asset/domain/AmsDisposalOrder.java b/ruoyi-asset/src/main/java/com/ruoyi/asset/domain/AmsDisposalOrder.java new file mode 100644 index 0000000..20e3f55 --- /dev/null +++ b/ruoyi-asset/src/main/java/com/ruoyi/asset/domain/AmsDisposalOrder.java @@ -0,0 +1,269 @@ +package com.ruoyi.asset.domain; + +import java.util.List; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 报废管理对象 ams_disposal_order + * + * @author Yangk + * @date 2026-06-17 + */ +public class AmsDisposalOrder extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 单据ID */ + private Long orderId; + + /** 报废单号 */ + @Excel(name = "报废单号") + private String disposalNo; + + /** 申请人ID */ + @Excel(name = "申请人ID") + private Long applicantId; + + /** 申请人名称快照 */ + @Excel(name = "申请人名称快照") + private String applicantName; + + /** 申请部门ID */ + @Excel(name = "申请部门ID") + private Long applyDeptId; + + /** 申请部门名称快照 */ + @Excel(name = "申请部门名称快照") + private String applyDeptName; + + /** 确认人ID */ + @Excel(name = "确认人ID") + private Long confirmUserId; + + /** 确认人名称快照 */ + @Excel(name = "确认人名称快照") + private String confirmUserName; + + /** 确认时间 */ + @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8") + @Excel(name = "确认时间", width = 30, dateFormat = "yyyy-MM-dd") + private Date confirmTime; + + /** 驳回原因 */ + @Excel(name = "驳回原因") + private String rejectReason; + + /** 处置方式 */ + @Excel(name = "处置方式") + private String disposalMethod; + + /** 处置时间 */ + @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8") + @Excel(name = "处置时间", width = 30, dateFormat = "yyyy-MM-dd") + private Date disposalTime; + + /** 单据状态 */ + @Excel(name = "单据状态") + private String orderStatus; + + /** 资产编码查询条件,不对应主表字段 */ + private String assetCode; + + /** 删除标志:0存在,1删除 */ + private String delFlag; + + /** 报废单明细信息 */ + private List amsDisposalOrderItemList; + + public void setOrderId(Long orderId) + { + this.orderId = orderId; + } + + public Long getOrderId() + { + return orderId; + } + + public void setDisposalNo(String disposalNo) + { + this.disposalNo = disposalNo; + } + + public String getDisposalNo() + { + return disposalNo; + } + + public void setApplicantId(Long applicantId) + { + this.applicantId = applicantId; + } + + public Long getApplicantId() + { + return applicantId; + } + + public void setApplicantName(String applicantName) + { + this.applicantName = applicantName; + } + + public String getApplicantName() + { + return applicantName; + } + + public void setApplyDeptId(Long applyDeptId) + { + this.applyDeptId = applyDeptId; + } + + public Long getApplyDeptId() + { + return applyDeptId; + } + + public void setApplyDeptName(String applyDeptName) + { + this.applyDeptName = applyDeptName; + } + + public String getApplyDeptName() + { + return applyDeptName; + } + + public void setConfirmUserId(Long confirmUserId) + { + this.confirmUserId = confirmUserId; + } + + public Long getConfirmUserId() + { + return confirmUserId; + } + + public void setConfirmUserName(String confirmUserName) + { + this.confirmUserName = confirmUserName; + } + + public String getConfirmUserName() + { + return confirmUserName; + } + + public void setConfirmTime(Date confirmTime) + { + this.confirmTime = confirmTime; + } + + public Date getConfirmTime() + { + return confirmTime; + } + + public void setRejectReason(String rejectReason) + { + this.rejectReason = rejectReason; + } + + public String getRejectReason() + { + return rejectReason; + } + + public void setDisposalMethod(String disposalMethod) + { + this.disposalMethod = disposalMethod; + } + + public String getDisposalMethod() + { + return disposalMethod; + } + + public void setDisposalTime(Date disposalTime) + { + this.disposalTime = disposalTime; + } + + public Date getDisposalTime() + { + return disposalTime; + } + + public void setOrderStatus(String orderStatus) + { + this.orderStatus = orderStatus; + } + + public String getOrderStatus() + { + return orderStatus; + } + + public void setAssetCode(String assetCode) + { + this.assetCode = assetCode; + } + + public String getAssetCode() + { + return assetCode; + } + + public void setDelFlag(String delFlag) + { + this.delFlag = delFlag; + } + + public String getDelFlag() + { + return delFlag; + } + + public List getAmsDisposalOrderItemList() + { + return amsDisposalOrderItemList; + } + + public void setAmsDisposalOrderItemList(List amsDisposalOrderItemList) + { + this.amsDisposalOrderItemList = amsDisposalOrderItemList; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("orderId", getOrderId()) + .append("disposalNo", getDisposalNo()) + .append("applicantId", getApplicantId()) + .append("applicantName", getApplicantName()) + .append("applyDeptId", getApplyDeptId()) + .append("applyDeptName", getApplyDeptName()) + .append("confirmUserId", getConfirmUserId()) + .append("confirmUserName", getConfirmUserName()) + .append("confirmTime", getConfirmTime()) + .append("rejectReason", getRejectReason()) + .append("disposalMethod", getDisposalMethod()) + .append("disposalTime", getDisposalTime()) + .append("orderStatus", getOrderStatus()) + .append("assetCode", getAssetCode()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .append("delFlag", getDelFlag()) + .append("amsDisposalOrderItemList", getAmsDisposalOrderItemList()) + .toString(); + } +} diff --git a/ruoyi-asset/src/main/java/com/ruoyi/asset/domain/AmsDisposalOrderItem.java b/ruoyi-asset/src/main/java/com/ruoyi/asset/domain/AmsDisposalOrderItem.java new file mode 100644 index 0000000..e2836c3 --- /dev/null +++ b/ruoyi-asset/src/main/java/com/ruoyi/asset/domain/AmsDisposalOrderItem.java @@ -0,0 +1,223 @@ +package com.ruoyi.asset.domain; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 报废单明细对象 ams_disposal_order_item + * + * @author Yangk + * @date 2026-06-17 + */ +public class AmsDisposalOrderItem extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 明细ID */ + private Long itemId; + + /** 报废单ID */ + @Excel(name = "报废单ID") + private Long orderId; + + /** 报废单号快照 */ + @Excel(name = "报废单号快照") + private String disposalNo; + + /** 资产ID */ + @Excel(name = "资产ID") + private Long assetId; + + /** 资产编码快照 */ + @Excel(name = "资产编码快照") + private String assetCode; + + /** 资产名称快照 */ + @Excel(name = "资产名称快照") + private String assetName; + + /** 资产类别ID快照 */ + @Excel(name = "资产类别ID快照") + private Long categoryId; + + /** 类别编码快照 */ + @Excel(name = "类别编码快照") + private String categoryCode; + + /** 类别名称快照 */ + @Excel(name = "类别名称快照") + private String categoryName; + + /** 规格型号快照 */ + @Excel(name = "规格型号快照") + private String specModel; + + /** 品牌快照 */ + @Excel(name = "品牌快照") + private String brand; + + /** 报废原因 */ + @Excel(name = "报废原因") + private String disposalReason; + + /** 处置备注 */ + @Excel(name = "处置备注") + private String disposalRemark; + + /** 删除标志:0存在,1删除 */ + private String delFlag; + + public void setItemId(Long itemId) + { + this.itemId = itemId; + } + + public Long getItemId() + { + return itemId; + } + public void setOrderId(Long orderId) + { + this.orderId = orderId; + } + + public Long getOrderId() + { + return orderId; + } + public void setDisposalNo(String disposalNo) + { + this.disposalNo = disposalNo; + } + + public String getDisposalNo() + { + return disposalNo; + } + public void setAssetId(Long assetId) + { + this.assetId = assetId; + } + + public Long getAssetId() + { + return assetId; + } + public void setAssetCode(String assetCode) + { + this.assetCode = assetCode; + } + + public String getAssetCode() + { + return assetCode; + } + public void setAssetName(String assetName) + { + this.assetName = assetName; + } + + public String getAssetName() + { + return assetName; + } + public void setCategoryId(Long categoryId) + { + this.categoryId = categoryId; + } + + public Long getCategoryId() + { + return categoryId; + } + public void setCategoryCode(String categoryCode) + { + this.categoryCode = categoryCode; + } + + public String getCategoryCode() + { + return categoryCode; + } + public void setCategoryName(String categoryName) + { + this.categoryName = categoryName; + } + + public String getCategoryName() + { + return categoryName; + } + public void setSpecModel(String specModel) + { + this.specModel = specModel; + } + + public String getSpecModel() + { + return specModel; + } + public void setBrand(String brand) + { + this.brand = brand; + } + + public String getBrand() + { + return brand; + } + public void setDisposalReason(String disposalReason) + { + this.disposalReason = disposalReason; + } + + public String getDisposalReason() + { + return disposalReason; + } + public void setDisposalRemark(String disposalRemark) + { + this.disposalRemark = disposalRemark; + } + + public String getDisposalRemark() + { + return disposalRemark; + } + public void setDelFlag(String delFlag) + { + this.delFlag = delFlag; + } + + public String getDelFlag() + { + return delFlag; + } + + @Override + public String toString() { + return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) + .append("itemId", getItemId()) + .append("orderId", getOrderId()) + .append("disposalNo", getDisposalNo()) + .append("assetId", getAssetId()) + .append("assetCode", getAssetCode()) + .append("assetName", getAssetName()) + .append("categoryId", getCategoryId()) + .append("categoryCode", getCategoryCode()) + .append("categoryName", getCategoryName()) + .append("specModel", getSpecModel()) + .append("brand", getBrand()) + .append("disposalReason", getDisposalReason()) + .append("disposalRemark", getDisposalRemark()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .append("delFlag", getDelFlag()) + .toString(); + } +} diff --git a/ruoyi-asset/src/main/java/com/ruoyi/asset/mapper/AmsDisposalOrderMapper.java b/ruoyi-asset/src/main/java/com/ruoyi/asset/mapper/AmsDisposalOrderMapper.java new file mode 100644 index 0000000..d912899 --- /dev/null +++ b/ruoyi-asset/src/main/java/com/ruoyi/asset/mapper/AmsDisposalOrderMapper.java @@ -0,0 +1,114 @@ +package com.ruoyi.asset.mapper; + +import java.util.List; +import com.ruoyi.asset.domain.AmsAsset; +import com.ruoyi.asset.domain.AmsDisposalOrder; +import com.ruoyi.asset.domain.AmsDisposalOrderItem; + +/** + * 报废管理Mapper接口 + * + * @author Yangk + * @date 2026-06-17 + */ +public interface AmsDisposalOrderMapper +{ + /** + * 查询报废管理 + * + * @param orderId 报废管理主键 + * @return 报废管理 + */ + public AmsDisposalOrder selectAmsDisposalOrderByOrderId(Long orderId); + + /** + * 查询报废管理列表 + * + * @param amsDisposalOrder 报废管理 + * @return 报废管理集合 + */ + public List selectAmsDisposalOrderList(AmsDisposalOrder amsDisposalOrder); + + /** + * 新增报废管理 + * + * @param amsDisposalOrder 报废管理 + * @return 结果 + */ + public int insertAmsDisposalOrder(AmsDisposalOrder amsDisposalOrder); + + /** + * 修改报废管理 + * + * @param amsDisposalOrder 报废管理 + * @return 结果 + */ + public int updateAmsDisposalOrder(AmsDisposalOrder amsDisposalOrder); + + /** + * 删除报废管理 + * + * @param orderId 报废管理主键 + * @return 结果 + */ + public int deleteAmsDisposalOrderByOrderId(Long orderId); + + /** + * 批量删除报废管理 + * + * @param orderIds 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteAmsDisposalOrderByOrderIds(String[] orderIds); + + /** + * 批量删除报废单明细 + * + * @param orderIds 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteAmsDisposalOrderItemByOrderIds(String[] orderIds); + + /** + * 批量新增报废单明细 + * + * @param amsDisposalOrderItemList 报废单明细列表 + * @return 结果 + */ + public int batchAmsDisposalOrderItem(List amsDisposalOrderItemList); + + + /** + * 通过报废管理主键删除报废单明细信息 + * + * @param orderId 报废管理ID + * @return 结果 + */ + public int deleteAmsDisposalOrderItemByOrderId(Long orderId); + + /** + * 悲观锁锁定单据 + * + * @param orderId 单据ID + * @return 结果 + */ + public AmsDisposalOrder selectAmsDisposalOrderByOrderIdForUpdate(Long orderId); + + /** + * 查询可用于报废的资产列表 + * + * @param asset 资产查询条件 + * @param currentOrderId 当前报废单ID + * @return 资产列表 + */ + public List selectAvailableDisposalAssetList(@org.apache.ibatis.annotations.Param("asset") AmsAsset asset, @org.apache.ibatis.annotations.Param("currentOrderId") Long currentOrderId); + + /** + * 查询资产被其他活跃报废单占用的次数 + * + * @param assetId 资产ID + * @param currentOrderId 当前报废单ID + * @return 占用单据数量 + */ + public int countOtherActiveDisposalOrderByAssetId(@org.apache.ibatis.annotations.Param("assetId") Long assetId, @org.apache.ibatis.annotations.Param("currentOrderId") Long currentOrderId); +} diff --git a/ruoyi-asset/src/main/java/com/ruoyi/asset/service/IAmsDisposalOrderService.java b/ruoyi-asset/src/main/java/com/ruoyi/asset/service/IAmsDisposalOrderService.java new file mode 100644 index 0000000..7f5bffb --- /dev/null +++ b/ruoyi-asset/src/main/java/com/ruoyi/asset/service/IAmsDisposalOrderService.java @@ -0,0 +1,101 @@ +package com.ruoyi.asset.service; + +import java.util.List; +import com.ruoyi.asset.domain.AmsDisposalOperateContext; +import com.ruoyi.asset.domain.AmsDisposalOrder; + +/** + * 报废管理Service接口 + * + * @author Yangk + * @date 2026-06-17 + */ +public interface IAmsDisposalOrderService +{ + /** + * 查询报废管理 + * + * @param orderId 报废管理主键 + * @return 报废管理 + */ + public AmsDisposalOrder selectAmsDisposalOrderByOrderId(Long orderId); + + /** + * 查询报废管理列表 + * + * @param amsDisposalOrder 报废管理 + * @return 报废管理集合 + */ + public List selectAmsDisposalOrderList(AmsDisposalOrder amsDisposalOrder); + + /** + * 新增报废管理 + * + * @param amsDisposalOrder 报废管理 + * @param operateContext 操作上下文 + * @return 结果 + */ + public int insertAmsDisposalOrder(AmsDisposalOrder amsDisposalOrder, AmsDisposalOperateContext operateContext); + + /** + * 修改报废管理 + * + * @param amsDisposalOrder 报废管理 + * @param operateContext 操作上下文 + * @return 结果 + */ + public int updateAmsDisposalOrder(AmsDisposalOrder amsDisposalOrder, AmsDisposalOperateContext operateContext); + + /** + * 批量删除报废管理 + * + * @param orderIds 需要删除的报废管理主键集合 + * @return 结果 + */ + public int deleteAmsDisposalOrderByOrderIds(String orderIds); + + /** + * 删除报废管理信息 + * + * @param orderId 报废管理主键 + * @return 结果 + */ + public int deleteAmsDisposalOrderByOrderId(Long orderId); + + /** + * 提交报废申请 + * + * @param orderId 报废单ID + * @param operateContext 操作上下文 + * @return 结果 + */ + public int submitDisposal(Long orderId, AmsDisposalOperateContext operateContext); + + /** + * 确认报废处置 + * + * @param orderId 报废单ID + * @param operateContext 操作上下文 + * @return 结果 + */ + public int confirmDisposal(Long orderId, AmsDisposalOperateContext operateContext); + + /** + * 驳回报废申请 + * + * @param orderId 报废单ID + * @param rejectReason 驳回原因 + * @param operateContext 操作上下文 + * @return 结果 + */ + public int rejectDisposal(Long orderId, String rejectReason, AmsDisposalOperateContext operateContext); + + /** + * 查询可用于报废的资产列表 + * + * @param asset 资产查询条件 + * @param currentOrderId 当前报废单ID + * @return 资产列表 + */ + public List selectAvailableDisposalAssetList(com.ruoyi.asset.domain.AmsAsset asset, Long currentOrderId); +} diff --git a/ruoyi-asset/src/main/java/com/ruoyi/asset/service/impl/AmsDisposalOrderServiceImpl.java b/ruoyi-asset/src/main/java/com/ruoyi/asset/service/impl/AmsDisposalOrderServiceImpl.java new file mode 100644 index 0000000..4d30dd9 --- /dev/null +++ b/ruoyi-asset/src/main/java/com/ruoyi/asset/service/impl/AmsDisposalOrderServiceImpl.java @@ -0,0 +1,533 @@ +package com.ruoyi.asset.service.impl; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.ruoyi.asset.constant.DisposalOrderStatus; +import com.ruoyi.asset.domain.AmsAsset; +import com.ruoyi.asset.domain.AmsDisposalOperateContext; +import com.ruoyi.asset.domain.AmsDisposalOrder; +import com.ruoyi.asset.domain.AmsDisposalOrderItem; +import com.ruoyi.asset.domain.AssetTransitionContext; +import com.ruoyi.asset.mapper.AmsAssetMapper; +import com.ruoyi.asset.mapper.AmsDisposalOrderMapper; +import com.ruoyi.asset.service.IAmsDisposalOrderService; +import com.ruoyi.asset.service.IAssetStatusTransitionService; +import com.ruoyi.common.core.text.Convert; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.system.service.ISysCodeRuleService; + +/** + * 报废管理Service业务层处理 + * + * 核心职责:处理在库(IN_STOCK)或在用(IN_USE)的费用化资产进入报废(DISPOSED)的生命周期。 + * 业务设计亮点: + * 1. 【防死锁设计】:所有悲观锁定资产的操作,均在应用层先对资产ID进行升序排列,统一加锁顺序。 + * 2. 【并发占用校验】:无论在草稿保存、提交还是确认报废阶段,都校验资产没有被其他活跃报废单占用。 + * 3. 【逻辑删除改造】:主表及明细表均进行逻辑删除改造(del_flag='1'),且仅草稿状态允许删除。 + * + * @author Yangk + * @date 2026-06-17 + */ +@Service +public class AmsDisposalOrderServiceImpl implements IAmsDisposalOrderService +{ + private static final String DISPOSAL_ORDER_RULE = "DISPOSAL_ORDER"; + private static final String DEL_FLAG_NORMAL = "0"; + + @Autowired + private AmsDisposalOrderMapper amsDisposalOrderMapper; + + @Autowired + private AmsAssetMapper amsAssetMapper; + + @Autowired + private ISysCodeRuleService sysCodeRuleService; + + @Autowired + private IAssetStatusTransitionService assetStatusTransitionService; + + /** + * 查询报废管理 + * + * @param orderId 报废管理主键 + * @return 报废管理 + */ + @Override + public AmsDisposalOrder selectAmsDisposalOrderByOrderId(Long orderId) + { + return amsDisposalOrderMapper.selectAmsDisposalOrderByOrderId(orderId); + } + + /** + * 查询报废管理列表 + * + * @param amsDisposalOrder 报废管理 + * @return 报废管理 + */ + @Override + public List selectAmsDisposalOrderList(AmsDisposalOrder amsDisposalOrder) + { + return amsDisposalOrderMapper.selectAmsDisposalOrderList(amsDisposalOrder); + } + + /** + * 新增报废管理(草稿) + * + * @param amsDisposalOrder 报废管理 + * @return 结果 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public int insertAmsDisposalOrder(AmsDisposalOrder amsDisposalOrder, AmsDisposalOperateContext operateContext) + { + validateOperateContext(operateContext); + // 1. 基础字段与表单校验 + validateOrderRequest(amsDisposalOrder); + + // 2. 申请人和部门必须以后端登录会话为准,避免前端隐藏字段篡改。 + fillApplicantSnapshots(amsDisposalOrder, operateContext); + + // 3. 生成 BF 前缀的唯一报废单号 + amsDisposalOrder.setDisposalNo(sysCodeRuleService.nextCode(DISPOSAL_ORDER_RULE)); + amsDisposalOrder.setOrderStatus(DisposalOrderStatus.DRAFT); + amsDisposalOrder.setDelFlag(DEL_FLAG_NORMAL); + amsDisposalOrder.setCreateBy(operateContext.getOperateLoginName()); + amsDisposalOrder.setCreateTime(DateUtils.getNowDate()); + + // 重置受控的确认及驳回字段,以防越权篡改 + amsDisposalOrder.setConfirmUserId(null); + amsDisposalOrder.setConfirmUserName(null); + amsDisposalOrder.setConfirmTime(null); + amsDisposalOrder.setRejectReason(null); + amsDisposalOrder.setDisposalTime(null); + + // 4. 防死锁升序加锁、校验资产并回填明细快照 + fillDetailSnapshotsAndValidate(amsDisposalOrder, null); + + // 5. 保存主表与子明细 + int rows = amsDisposalOrderMapper.insertAmsDisposalOrder(amsDisposalOrder); + if (rows != 1 || amsDisposalOrder.getOrderId() == null) + { + throw new ServiceException("报废单保存失败"); + } + insertAmsDisposalOrderItem(amsDisposalOrder); + return rows; + } + + /** + * 修改报废管理(草稿) + * + * @param amsDisposalOrder 报废管理 + * @return 结果 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public int updateAmsDisposalOrder(AmsDisposalOrder amsDisposalOrder, AmsDisposalOperateContext operateContext) + { + validateOperateContext(operateContext); + if (amsDisposalOrder == null || amsDisposalOrder.getOrderId() == null) + { + throw new ServiceException("单据ID不能为空"); + } + + // 1. 悲观锁锁定主表记录,强校验状态必须为 DRAFT。非草稿状态严禁修改 + AmsDisposalOrder current = requireOrderForUpdate(amsDisposalOrder.getOrderId(), DisposalOrderStatus.DRAFT, + "仅草稿状态的报废单允许修改"); + + // 基础校验 + validateOrderRequest(amsDisposalOrder); + + // 2. 强行还原单号、申请人及受控属性,强制复归为 DRAFT + amsDisposalOrder.setDisposalNo(current.getDisposalNo()); + amsDisposalOrder.setApplicantId(current.getApplicantId()); + amsDisposalOrder.setApplicantName(current.getApplicantName()); + amsDisposalOrder.setApplyDeptId(current.getApplyDeptId()); + amsDisposalOrder.setApplyDeptName(current.getApplyDeptName()); + amsDisposalOrder.setConfirmUserId(null); + amsDisposalOrder.setConfirmUserName(null); + amsDisposalOrder.setConfirmTime(null); + amsDisposalOrder.setRejectReason(null); + amsDisposalOrder.setOrderStatus(DisposalOrderStatus.DRAFT); + amsDisposalOrder.setCreateBy(current.getCreateBy()); + amsDisposalOrder.setCreateTime(current.getCreateTime()); + amsDisposalOrder.setDelFlag(DEL_FLAG_NORMAL); + amsDisposalOrder.setUpdateBy(operateContext.getOperateLoginName()); + amsDisposalOrder.setUpdateTime(DateUtils.getNowDate()); + + // 3. 锁定资产,校验并重新填充快照 + fillDetailSnapshotsAndValidate(amsDisposalOrder, amsDisposalOrder.getOrderId()); + + // 4. 逻辑删除旧明细,重新插入新明细以防冗余 + amsDisposalOrderMapper.deleteAmsDisposalOrderItemByOrderId(amsDisposalOrder.getOrderId()); + insertAmsDisposalOrderItem(amsDisposalOrder); + + // 5. 更新主表,采用 CAS 状态校验 + return updateOrderOrThrow(amsDisposalOrder, DisposalOrderStatus.DRAFT, "单据状态已发生改变,更新失败"); + } + + /** + * 批量逻辑删除报废管理 + * + * @param orderIds 需要删除的报废管理主键 + * @return 结果 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public int deleteAmsDisposalOrderByOrderIds(String orderIds) + { + Long[] sortedIds = Arrays.stream(Convert.toStrArray(orderIds)) + .map(Long::valueOf).sorted().toArray(Long[]::new); + int rows = 0; + for (Long orderId : sortedIds) + { + // 获取主表悲观锁并核实必须是草稿 + AmsDisposalOrder order = amsDisposalOrderMapper.selectAmsDisposalOrderByOrderIdForUpdate(orderId); + if (order == null) + { + continue; + } + if (!DisposalOrderStatus.DRAFT.equals(order.getOrderStatus())) + { + throw new ServiceException(StringUtils.format("报废单【{}】非草稿状态,不允许删除", order.getDisposalNo())); + } + // 级联打上逻辑删除标志 + amsDisposalOrderMapper.deleteAmsDisposalOrderItemByOrderId(orderId); + rows += amsDisposalOrderMapper.deleteAmsDisposalOrderByOrderId(orderId); + } + return rows; + } + + /** + * 逻辑删除单个报废管理信息 + * + * @param orderId 报废管理主键 + * @return 结果 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public int deleteAmsDisposalOrderByOrderId(Long orderId) + { + AmsDisposalOrder order = amsDisposalOrderMapper.selectAmsDisposalOrderByOrderIdForUpdate(orderId); + if (order == null) + { + return 0; + } + if (!DisposalOrderStatus.DRAFT.equals(order.getOrderStatus())) + { + throw new ServiceException(StringUtils.format("报废单【{}】非草稿状态,不允许删除", order.getDisposalNo())); + } + amsDisposalOrderMapper.deleteAmsDisposalOrderItemByOrderId(orderId); + return amsDisposalOrderMapper.deleteAmsDisposalOrderByOrderId(orderId); + } + + /** + * 提交报废申请 + */ + @Transactional(rollbackFor = Exception.class) + @Override + public int submitDisposal(Long orderId, AmsDisposalOperateContext operateContext) + { + validateOperateContext(operateContext); + + // 1. 悲观锁锁定主表并校验状态为 DRAFT + AmsDisposalOrder order = requireOrderForUpdate(orderId, DisposalOrderStatus.DRAFT, "仅草稿状态的报废单允许提交"); + + // 2. 再次悲观锁锁定资产,校验资产状态与跨单据占用,防并发 + validateOrderAssetsReady(order); + + // 3. CAS 更新单据状态为待确认 + order.setOrderStatus(DisposalOrderStatus.PENDING_CONFIRM); + order.setUpdateBy(operateContext.getOperateLoginName()); + order.setUpdateTime(DateUtils.getNowDate()); + + return updateOrderOrThrow(order, DisposalOrderStatus.DRAFT, "报废单提交失败,请刷新后重试"); + } + + /** + * 确认报废处置:PENDING_CONFIRM -> DISPOSED_DONE + */ + @Transactional(rollbackFor = Exception.class) + @Override + public int confirmDisposal(Long orderId, AmsDisposalOperateContext operateContext) + { + validateOperateContext(operateContext); + + // 1. 锁主表校验状态必须为待确认 + AmsDisposalOrder order = requireOrderForUpdate(orderId, DisposalOrderStatus.PENDING_CONFIRM, "仅待确认状态的报废单允许确认报废"); + + // 2. 升序加锁校验资产状态及跨单占用 + validateOrderAssetsReady(order); + + Date now = DateUtils.getNowDate(); + + // 3. 逐一调用公共状态流转服务 confirmDisposal + for (AmsDisposalOrderItem item : order.getAmsDisposalOrderItemList()) + { + AssetTransitionContext context = new AssetTransitionContext(); + context.setSourceOrderId(order.getOrderId()); + context.setSourceOrderNo(order.getDisposalNo()); + context.setSourceItemId(item.getItemId()); + context.setOperateUserId(operateContext.getOperateUserId()); + context.setOperateUserName(StringUtils.trim(operateContext.getOperateUserName())); + context.setOperateLoginName(operateContext.getOperateLoginName()); + context.setChangeSummary("确认资产报废"); + context.setRemark(item.getDisposalRemark()); + + // 变更资产状态为 DISPOSED 写入资产履历,报废不清除 RFID 绑定 + assetStatusTransitionService.confirmDisposal(item.getAssetId(), context); + } + + // 4. 回填确认属性,变更为已报废状态 + order.setConfirmUserId(operateContext.getOperateUserId()); + order.setConfirmUserName(StringUtils.trim(operateContext.getOperateUserName())); + order.setConfirmTime(now); + order.setDisposalTime(now); + order.setOrderStatus(DisposalOrderStatus.DISPOSED_DONE); + order.setUpdateBy(operateContext.getOperateLoginName()); + order.setUpdateTime(now); + + return updateOrderOrThrow(order, DisposalOrderStatus.PENDING_CONFIRM, "报废单确认失败,请刷新后重试"); + } + + /** + * 驳回报废申请:PENDING_CONFIRM -> REJECTED + */ + @Transactional(rollbackFor = Exception.class) + @Override + public int rejectDisposal(Long orderId, String rejectReason, AmsDisposalOperateContext operateContext) + { + validateOperateContext(operateContext); + if (StringUtils.isEmpty(StringUtils.trim(rejectReason))) + { + throw new ServiceException("驳回原因不能为空"); + } + validateLength(rejectReason, 500, "驳回原因"); + + // 1. 锁主表校验状态 + AmsDisposalOrder order = requireOrderForUpdate(orderId, DisposalOrderStatus.PENDING_CONFIRM, "仅待确认状态的报废单允许驳回"); + + // 2. 更新单据状态为已驳回,并记录原因。不改资产状态,不写履历 + order.setOrderStatus(DisposalOrderStatus.REJECTED); + order.setRejectReason(StringUtils.trim(rejectReason)); + order.setUpdateBy(operateContext.getOperateLoginName()); + order.setUpdateTime(DateUtils.getNowDate()); + + return updateOrderOrThrow(order, DisposalOrderStatus.PENDING_CONFIRM, "报废单驳回失败,请刷新后重试"); + } + + /** + * 查询可用于报废的资产列表 + */ + @Override + public List selectAvailableDisposalAssetList(AmsAsset asset, Long currentOrderId) + { + return amsDisposalOrderMapper.selectAvailableDisposalAssetList(asset, currentOrderId); + } + + /** + * 批量新增报废单明细 + * + * @param amsDisposalOrder 报废管理对象 + */ + public void insertAmsDisposalOrderItem(AmsDisposalOrder amsDisposalOrder) + { + List amsDisposalOrderItemList = amsDisposalOrder.getAmsDisposalOrderItemList(); + Long orderId = amsDisposalOrder.getOrderId(); + String disposalNo = amsDisposalOrder.getDisposalNo(); + if (StringUtils.isNotNull(amsDisposalOrderItemList)) + { + List list = new ArrayList(); + for (AmsDisposalOrderItem amsDisposalOrderItem : amsDisposalOrderItemList) + { + amsDisposalOrderItem.setOrderId(orderId); + amsDisposalOrderItem.setDisposalNo(disposalNo); + amsDisposalOrderItem.setDelFlag(DEL_FLAG_NORMAL); + list.add(amsDisposalOrderItem); + } + if (list.size() > 0) + { + amsDisposalOrderMapper.batchAmsDisposalOrderItem(list); + } + } + } + + // ========================================== 私有辅助方法 ========================================== + + private AmsDisposalOrder requireOrderForUpdate(Long orderId, String expectedStatus, String errMsg) + { + AmsDisposalOrder order = amsDisposalOrderMapper.selectAmsDisposalOrderByOrderIdForUpdate(orderId); + if (order == null) + { + throw new ServiceException("报废单不存在或已删除"); + } + if (!expectedStatus.equals(order.getOrderStatus())) + { + throw new ServiceException(errMsg); + } + return order; + } + + private int updateOrderOrThrow(AmsDisposalOrder order, String expectedStatus, String errorMessage) + { + order.getParams().put("expectedOrderStatus", expectedStatus); + if (amsDisposalOrderMapper.updateAmsDisposalOrder(order) != 1) + { + throw new ServiceException(errorMessage); + } + return 1; + } + + private void validateOrderRequest(AmsDisposalOrder order) + { + if (order == null) + { + throw new ServiceException("单据数据不能为空"); + } + if (order.getAmsDisposalOrderItemList() == null || order.getAmsDisposalOrderItemList().isEmpty()) + { + throw new ServiceException("报废明细资产不能为空"); + } + validateLength(order.getDisposalMethod(), 64, "处置方式"); + validateLength(order.getRemark(), 500, "备注"); + } + + private void fillApplicantSnapshots(AmsDisposalOrder order, AmsDisposalOperateContext operateContext) + { + order.setApplicantId(operateContext.getOperateUserId()); + order.setApplicantName(StringUtils.trim(operateContext.getOperateUserName())); + order.setApplyDeptId(operateContext.getOperateDeptId()); + order.setApplyDeptName(StringUtils.trim(operateContext.getOperateDeptName())); + } + + private void fillDetailSnapshotsAndValidate(AmsDisposalOrder order, Long currentOrderId) + { + List items = order.getAmsDisposalOrderItemList(); + + // 1. 应用层资产ID升序排序,以防并发加锁导致的数据库死锁 + List sortedItems = new ArrayList<>(items); + sortedItems.sort(java.util.Comparator.comparing(AmsDisposalOrderItem::getAssetId)); + + java.util.Set assetIds = new java.util.HashSet<>(); + + for (AmsDisposalOrderItem item : sortedItems) + { + if (item.getAssetId() == null) + { + throw new ServiceException("明细资产ID不能为空"); + } + if (!assetIds.add(item.getAssetId())) + { + throw new ServiceException("不能在一个报废单中选择相同的资产"); + } + + // 2. 悲观锁定资产,校验其前置实物状态 + AmsAsset asset = amsAssetMapper.selectAmsAssetByAssetIdForUpdate(item.getAssetId()); + if (asset == null || "1".equals(asset.getDelFlag())) + { + throw new ServiceException("资产不存在或已删除"); + } + if (!"IN_STOCK".equals(asset.getAssetStatus()) && !"IN_USE".equals(asset.getAssetStatus())) + { + throw new ServiceException(StringUtils.format("资产【{}】当前状态【{}】不允许进行报废", + asset.getAssetCode(), asset.getAssetStatus())); + } + + // 3. 校验跨单据占用 + int occupied = amsDisposalOrderMapper.countOtherActiveDisposalOrderByAssetId(item.getAssetId(), currentOrderId); + if (occupied > 0) + { + throw new ServiceException(StringUtils.format("资产【{}】已被其他未完成报废单占用", asset.getAssetCode())); + } + + // 4. 回填最新的物理状态和属性快照,防止过期脏写 + item.setAssetCode(asset.getAssetCode()); + item.setAssetName(asset.getAssetName()); + item.setCategoryId(asset.getCategoryId()); + item.setCategoryCode(asset.getCategoryCode()); + item.setCategoryName(asset.getCategoryName()); + item.setSpecModel(asset.getSpecModel()); + item.setBrand(asset.getBrand()); + validateLength(item.getDisposalReason(), 500, "报废原因"); + validateLength(item.getDisposalRemark(), 500, "处置备注"); + } + } + + private void validateOrderAssetsReady(AmsDisposalOrder order) + { + List items = order.getAmsDisposalOrderItemList(); + if (items == null || items.isEmpty()) + { + throw new ServiceException("单据明细资产为空"); + } + + // 按 ID 升序锁定资产 + List sortedItems = new ArrayList<>(items); + sortedItems.sort(java.util.Comparator.comparing(AmsDisposalOrderItem::getAssetId)); + + for (AmsDisposalOrderItem item : sortedItems) + { + AmsAsset asset = amsAssetMapper.selectAmsAssetByAssetIdForUpdate(item.getAssetId()); + if (asset == null || "1".equals(asset.getDelFlag())) + { + throw new ServiceException("资产不存在或已删除"); + } + if (!"IN_STOCK".equals(asset.getAssetStatus()) && !"IN_USE".equals(asset.getAssetStatus())) + { + throw new ServiceException(StringUtils.format("资产【{}】状态已发生改变,不能进行该操作", asset.getAssetCode())); + } + int occupied = amsDisposalOrderMapper.countOtherActiveDisposalOrderByAssetId(item.getAssetId(), order.getOrderId()); + if (occupied > 0) + { + throw new ServiceException(StringUtils.format("资产【{}】已被其他未完成报废单占用", asset.getAssetCode())); + } + } + } + + private void validateLength(String value, int maxLength, String fieldName) + { + if (StringUtils.isNotEmpty(value) && value.length() > maxLength) + { + throw new ServiceException(fieldName + "长度不能超过" + maxLength + "个字符"); + } + } + + private void validateLoginName(String operateLoginName) + { + if (StringUtils.isEmpty(operateLoginName)) + { + throw new ServiceException("操作账号不能为空"); + } + validateLength(operateLoginName, 64, "操作账号"); + } + + private void validateOperateContext(AmsDisposalOperateContext operateContext) + { + if (operateContext == null) + { + throw new ServiceException("操作上下文不能为空"); + } + validateOperator(operateContext.getOperateUserId(), operateContext.getOperateUserName(), "操作人"); + validateLoginName(operateContext.getOperateLoginName()); + validateLength(operateContext.getOperateDeptName(), 100, "操作人部门名称"); + } + + private void validateOperator(Long operateUserId, String operateUserName, String fieldPrefix) + { + if (operateUserId == null) + { + throw new ServiceException(fieldPrefix + "ID不能为空"); + } + if (StringUtils.isEmpty(StringUtils.trim(operateUserName))) + { + throw new ServiceException(fieldPrefix + "名称不能为空"); + } + validateLength(operateUserName, 100, fieldPrefix + "名称"); + } +} diff --git a/ruoyi-asset/src/main/resources/mapper/asset/AmsDisposalOrderMapper.xml b/ruoyi-asset/src/main/resources/mapper/asset/AmsDisposalOrderMapper.xml new file mode 100644 index 0000000..4a7e15c --- /dev/null +++ b/ruoyi-asset/src/main/resources/mapper/asset/AmsDisposalOrderMapper.xml @@ -0,0 +1,280 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select order_id, disposal_no, applicant_id, applicant_name, apply_dept_id, apply_dept_name, confirm_user_id, confirm_user_name, confirm_time, reject_reason, disposal_method, disposal_time, order_status, create_by, create_time, update_by, update_time, remark, del_flag from ams_disposal_order + + + + + + + + + + + + insert into ams_disposal_order + + disposal_no, + applicant_id, + applicant_name, + apply_dept_id, + apply_dept_name, + confirm_user_id, + confirm_user_name, + confirm_time, + reject_reason, + disposal_method, + disposal_time, + order_status, + create_by, + create_time, + update_by, + update_time, + remark, + del_flag, + + + #{disposalNo}, + #{applicantId}, + #{applicantName}, + #{applyDeptId}, + #{applyDeptName}, + #{confirmUserId}, + #{confirmUserName}, + #{confirmTime}, + #{rejectReason}, + #{disposalMethod}, + #{disposalTime}, + #{orderStatus}, + #{createBy}, + #{createTime}, + #{updateBy}, + #{updateTime}, + #{remark}, + #{delFlag}, + + + + + update ams_disposal_order + + disposal_no = #{disposalNo}, + applicant_id = #{applicantId}, + applicant_name = #{applicantName}, + apply_dept_id = #{applyDeptId}, + apply_dept_name = #{applyDeptName}, + confirm_user_id = #{confirmUserId}, + confirm_user_name = #{confirmUserName}, + confirm_time = #{confirmTime}, + reject_reason = #{rejectReason}, + disposal_method = #{disposalMethod}, + disposal_time = #{disposalTime}, + order_status = #{orderStatus}, + create_by = #{createBy}, + create_time = #{createTime}, + update_by = #{updateBy}, + update_time = #{updateTime}, + remark = #{remark}, + del_flag = #{delFlag}, + + where order_id = #{orderId} and del_flag = '0' + + and order_status = #{params.expectedOrderStatus} + + + + + update ams_disposal_order set del_flag = '1' where order_id = #{orderId} and del_flag = '0' and order_status = 'DRAFT' + + + + update ams_disposal_order set del_flag = '1' where order_id in + + #{orderId} + + and del_flag = '0' and order_status = 'DRAFT' + + + + update ams_disposal_order_item set del_flag = '1' where order_id in + + #{orderId} + + and del_flag = '0' + + + + update ams_disposal_order_item set del_flag = '1' where order_id = #{orderId} and del_flag = '0' + + + + insert into ams_disposal_order_item( item_id, order_id, disposal_no, asset_id, asset_code, asset_name, category_id, category_code, category_name, spec_model, brand, disposal_reason, disposal_remark, create_by, create_time, update_by, update_time, remark, del_flag) values + + ( #{item.itemId}, #{item.orderId}, #{item.disposalNo}, #{item.assetId}, #{item.assetCode}, #{item.assetName}, #{item.categoryId}, #{item.categoryCode}, #{item.categoryName}, #{item.specModel}, #{item.brand}, #{item.disposalReason}, #{item.disposalRemark}, #{item.createBy}, #{item.createTime}, #{item.updateBy}, #{item.updateTime}, #{item.remark}, '0') + + + + + + + + diff --git a/ruoyi-asset/src/main/resources/templates/asset/disposal/add.html b/ruoyi-asset/src/main/resources/templates/asset/disposal/add.html new file mode 100644 index 0000000..b355749 --- /dev/null +++ b/ruoyi-asset/src/main/resources/templates/asset/disposal/add.html @@ -0,0 +1,228 @@ + + + + + + +
+
+

基本信息

+ + +
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+ +
+ +
+
+
+
+ +

报废处置明细信息

+
+
+
+ + +
+
+
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/ruoyi-asset/src/main/resources/templates/asset/disposal/disposal.html b/ruoyi-asset/src/main/resources/templates/asset/disposal/disposal.html new file mode 100644 index 0000000..1ba8067 --- /dev/null +++ b/ruoyi-asset/src/main/resources/templates/asset/disposal/disposal.html @@ -0,0 +1,207 @@ + + + + + + +
+
+
+
+
+
    +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
  • + + + - + +
  • +
  • + + +
  • +
  • +  搜索 +  重置 +
  • +
+
+
+
+ + +
+
+
+
+
+ + + + diff --git a/ruoyi-asset/src/main/resources/templates/asset/disposal/edit.html b/ruoyi-asset/src/main/resources/templates/asset/disposal/edit.html new file mode 100644 index 0000000..97f89d8 --- /dev/null +++ b/ruoyi-asset/src/main/resources/templates/asset/disposal/edit.html @@ -0,0 +1,250 @@ + + + + + + +
+
+

基本信息

+ + + +
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+ +
+ +
+
+
+
+ +

报废处置明细信息

+
+
+
+ + +
+
+
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/ruoyi-asset/src/main/resources/templates/asset/disposal/selectAsset.html b/ruoyi-asset/src/main/resources/templates/asset/disposal/selectAsset.html new file mode 100644 index 0000000..a84023d --- /dev/null +++ b/ruoyi-asset/src/main/resources/templates/asset/disposal/selectAsset.html @@ -0,0 +1,68 @@ + + + + + + +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ + + + diff --git a/ruoyi-asset/src/main/resources/templates/asset/disposal/view.html b/ruoyi-asset/src/main/resources/templates/asset/disposal/view.html new file mode 100644 index 0000000..8bda1c3 --- /dev/null +++ b/ruoyi-asset/src/main/resources/templates/asset/disposal/view.html @@ -0,0 +1,175 @@ + + + + + + +
+
+

基本信息

+
+
+
+ +
+

+
+
+
+
+
+ +
+

+
+
+
+
+
+
+
+ +
+

+
+
+
+
+
+ +
+

+
+
+
+
+
+
+
+ +
+

+
+
+
+
+
+ +
+

+
+
+
+
+ +

确认信息

+
+
+
+ +
+

+
+
+
+
+
+ +
+

+
+
+
+
+ +

驳回信息

+
+
+
+ +
+

+
+
+
+
+ +
+
+
+ +
+

+
+
+
+
+ +

报废处置明细信息

+
+
+
+
+
+
+
+ + + + diff --git a/ruoyi-asset/src/test/java/com/ruoyi/asset/service/impl/AmsDisposalOrderServiceImplTest.java b/ruoyi-asset/src/test/java/com/ruoyi/asset/service/impl/AmsDisposalOrderServiceImplTest.java new file mode 100644 index 0000000..aa656da --- /dev/null +++ b/ruoyi-asset/src/test/java/com/ruoyi/asset/service/impl/AmsDisposalOrderServiceImplTest.java @@ -0,0 +1,284 @@ +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.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +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; + +import com.ruoyi.asset.constant.DisposalOrderStatus; +import com.ruoyi.asset.domain.AmsAsset; +import com.ruoyi.asset.domain.AmsDisposalOperateContext; +import com.ruoyi.asset.domain.AmsDisposalOrder; +import com.ruoyi.asset.domain.AmsDisposalOrderItem; +import com.ruoyi.asset.domain.AssetTransitionContext; +import com.ruoyi.asset.mapper.AmsAssetMapper; +import com.ruoyi.asset.mapper.AmsDisposalOrderMapper; +import com.ruoyi.asset.service.IAssetStatusTransitionService; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.system.service.ISysCodeRuleService; + +/** + * 报废处置管理服务层单元测试类 + * + * @author Yangk + * @date 2026-06-17 + */ +@ExtendWith(MockitoExtension.class) +class AmsDisposalOrderServiceImplTest +{ + @Mock + private AmsDisposalOrderMapper amsDisposalOrderMapper; + + @Mock + private AmsAssetMapper amsAssetMapper; + + @Mock + private ISysCodeRuleService sysCodeRuleService; + + @Mock + private IAssetStatusTransitionService assetStatusTransitionService; + + @InjectMocks + private AmsDisposalOrderServiceImpl service; + + /** + * 测试:新增报废单草稿成功 + */ + @Test + void insertShouldGenerateCodeAndFillSnapshots() + { + AmsDisposalOrder order = buildRequest(); + order.setApplicantId(999L); + order.setApplicantName("前端篡改申请人"); + order.setApplyDeptId(888L); + order.setApplyDeptName("前端篡改部门"); + stubAsset(1L, "IN_STOCK"); + + when(sysCodeRuleService.nextCode("DISPOSAL_ORDER")).thenReturn("BF202606170001"); + when(amsDisposalOrderMapper.countOtherActiveDisposalOrderByAssetId(eq(1L), eq(null))).thenReturn(0); + + doAnswer(invocation -> { + AmsDisposalOrder inserted = invocation.getArgument(0); + inserted.setOrderId(100L); + return 1; + }).when(amsDisposalOrderMapper).insertAmsDisposalOrder(any(AmsDisposalOrder.class)); + + assertEquals(1, service.insertAmsDisposalOrder(order, adminContext())); + + assertEquals("BF202606170001", order.getDisposalNo()); + assertEquals(DisposalOrderStatus.DRAFT, order.getOrderStatus()); + assertEquals(9L, order.getApplicantId()); + assertEquals("管理员", order.getApplicantName()); + assertEquals(3L, order.getApplyDeptId()); + assertEquals("资产管理部", order.getApplyDeptName()); + assertEquals("admin", order.getCreateBy()); + assertNotNull(order.getCreateTime()); + assertNull(order.getConfirmUserId()); + + // 校验明细快照被回填 + AmsDisposalOrderItem item = order.getAmsDisposalOrderItemList().get(0); + assertEquals("A001", item.getAssetCode()); + assertEquals("资产A", item.getAssetName()); + assertEquals("CAT-A", item.getCategoryCode()); + assertEquals("品牌A", item.getBrand()); + } + + /** + * 测试:新增草稿时资产已被其他报废单占用,应拦截 + */ + @Test + void insertShouldRejectIfAssetOccupied() + { + AmsDisposalOrder order = buildRequest(); + stubAsset(1L, "IN_STOCK"); + + when(amsDisposalOrderMapper.countOtherActiveDisposalOrderByAssetId(eq(1L), eq(null))).thenReturn(1); + + assertThrows(ServiceException.class, () -> service.insertAmsDisposalOrder(order, adminContext())); + } + + /** + * 测试:新增草稿时资产状态不符合要求,应拦截 + */ + @Test + void insertShouldRejectIfAssetStatusInvalid() + { + AmsDisposalOrder order = buildRequest(); + stubAsset(1L, "DISPOSED"); + + assertThrows(ServiceException.class, () -> service.insertAmsDisposalOrder(order, adminContext())); + } + + /** + * 测试:修改草稿成功 + */ + @Test + void updateShouldSuccessWhenStatusIsDraft() + { + AmsDisposalOrder current = buildRequest(); + current.setOrderId(100L); + current.setDisposalNo("BF202606170001"); + current.setOrderStatus(DisposalOrderStatus.DRAFT); + current.setApplicantId(9L); + current.setApplicantName("管理员"); + current.setApplyDeptId(3L); + current.setApplyDeptName("资产管理部"); + current.setCreateBy("admin"); + current.setCreateTime(new Date()); + + when(amsDisposalOrderMapper.selectAmsDisposalOrderByOrderIdForUpdate(eq(100L))).thenReturn(current); + stubAsset(1L, "IN_STOCK"); + when(amsDisposalOrderMapper.countOtherActiveDisposalOrderByAssetId(eq(1L), eq(100L))).thenReturn(0); + when(amsDisposalOrderMapper.updateAmsDisposalOrder(any(AmsDisposalOrder.class))).thenReturn(1); + + AmsDisposalOrder updateReq = buildRequest(); + updateReq.setOrderId(100L); + updateReq.setDisposalMethod("新处置方式"); + updateReq.setApplicantId(999L); + updateReq.setApplicantName("前端篡改申请人"); + + assertEquals(1, service.updateAmsDisposalOrder(updateReq, adminContext())); + assertEquals(9L, updateReq.getApplicantId()); + assertEquals("管理员", updateReq.getApplicantName()); + assertEquals("admin", updateReq.getUpdateBy()); + assertEquals(DisposalOrderStatus.DRAFT, updateReq.getParams().get("expectedOrderStatus")); + verify(amsDisposalOrderMapper).deleteAmsDisposalOrderItemByOrderId(eq(100L)); + } + + /** + * 测试:提交报废申请 + */ + @Test + void submitShouldChangeStatusToPendingConfirm() + { + AmsDisposalOrder current = buildRequest(); + current.setOrderId(100L); + current.setDisposalNo("BF202606170001"); + current.setOrderStatus(DisposalOrderStatus.DRAFT); + + when(amsDisposalOrderMapper.selectAmsDisposalOrderByOrderIdForUpdate(eq(100L))).thenReturn(current); + stubAsset(1L, "IN_STOCK"); + when(amsDisposalOrderMapper.countOtherActiveDisposalOrderByAssetId(eq(1L), eq(100L))).thenReturn(0); + when(amsDisposalOrderMapper.updateAmsDisposalOrder(any(AmsDisposalOrder.class))).thenReturn(1); + + assertEquals(1, service.submitDisposal(100L, adminContext())); + assertEquals(DisposalOrderStatus.PENDING_CONFIRM, current.getOrderStatus()); + assertEquals(DisposalOrderStatus.DRAFT, current.getParams().get("expectedOrderStatus")); + } + + /** + * 测试:确认报废成功,扭转资产状态并记录履历 + */ + @Test + void confirmShouldChangeStatusToDisposedDoneAndCallTransition() + { + AmsDisposalOrder current = buildRequest(); + current.setOrderId(100L); + current.setDisposalNo("BF202606170001"); + current.setOrderStatus(DisposalOrderStatus.PENDING_CONFIRM); + + AmsDisposalOrderItem item = current.getAmsDisposalOrderItemList().get(0); + item.setItemId(500L); + + when(amsDisposalOrderMapper.selectAmsDisposalOrderByOrderIdForUpdate(eq(100L))).thenReturn(current); + stubAsset(1L, "IN_STOCK"); + when(amsDisposalOrderMapper.countOtherActiveDisposalOrderByAssetId(eq(1L), eq(100L))).thenReturn(0); + when(amsDisposalOrderMapper.updateAmsDisposalOrder(any(AmsDisposalOrder.class))).thenReturn(1); + + assertEquals(1, service.confirmDisposal(100L, adminContext())); + + assertEquals(DisposalOrderStatus.DISPOSED_DONE, current.getOrderStatus()); + assertEquals(DisposalOrderStatus.PENDING_CONFIRM, current.getParams().get("expectedOrderStatus")); + assertNotNull(current.getConfirmTime()); + assertNotNull(current.getDisposalTime()); + + ArgumentCaptor contextCaptor = ArgumentCaptor.forClass(AssetTransitionContext.class); + verify(assetStatusTransitionService).confirmDisposal(eq(1L), contextCaptor.capture()); + + AssetTransitionContext transitionContext = contextCaptor.getValue(); + assertEquals(100L, transitionContext.getSourceOrderId()); + assertEquals("BF202606170001", transitionContext.getSourceOrderNo()); + assertEquals(500L, transitionContext.getSourceItemId()); + assertEquals(9L, transitionContext.getOperateUserId()); + assertEquals("管理员", transitionContext.getOperateUserName()); + } + + /** + * 测试:驳回单据成功 + */ + @Test + void rejectShouldChangeStatusToRejected() + { + AmsDisposalOrder current = buildRequest(); + current.setOrderId(100L); + current.setDisposalNo("BF202606170001"); + current.setOrderStatus(DisposalOrderStatus.PENDING_CONFIRM); + + when(amsDisposalOrderMapper.selectAmsDisposalOrderByOrderIdForUpdate(eq(100L))).thenReturn(current); + when(amsDisposalOrderMapper.updateAmsDisposalOrder(any(AmsDisposalOrder.class))).thenReturn(1); + + assertEquals(1, service.rejectDisposal(100L, "资料不全", adminContext())); + assertEquals(DisposalOrderStatus.REJECTED, current.getOrderStatus()); + assertEquals(DisposalOrderStatus.PENDING_CONFIRM, current.getParams().get("expectedOrderStatus")); + assertEquals("资料不全", current.getRejectReason()); + } + + // ========================================== 辅助构造方法 ========================================== + + private AmsDisposalOrder buildRequest() + { + AmsDisposalOrder order = new AmsDisposalOrder(); + order.setDisposalMethod("垃圾分类处置"); + order.setRemark("常规报废"); + + AmsDisposalOrderItem item = new AmsDisposalOrderItem(); + item.setAssetId(1L); + item.setDisposalReason("物理损坏"); + item.setDisposalRemark("不可修复"); + + List items = new ArrayList<>(); + items.add(item); + order.setAmsDisposalOrderItemList(items); + + return order; + } + + private AmsDisposalOperateContext adminContext() + { + return new AmsDisposalOperateContext(9L, "管理员", "admin", 3L, "资产管理部"); + } + + private void stubAsset(Long assetId, String status) + { + AmsAsset asset = new AmsAsset(); + asset.setAssetId(assetId); + asset.setAssetCode("A001"); + asset.setAssetName("资产A"); + asset.setCategoryId(2L); + asset.setCategoryCode("CAT-A"); + asset.setCategoryName("类别A"); + asset.setSpecModel("型号A"); + asset.setBrand("品牌A"); + asset.setAssetStatus(status); + asset.setDelFlag("0"); + + when(amsAssetMapper.selectAmsAssetByAssetIdForUpdate(eq(assetId))).thenReturn(asset); + } +}