feat(asset): 新增资产借用归还管理功能

- 创建借用单新增页面界面,包含基本信息和借用明细表格
- 实现借用单核心数据模型AmsBorrowOrder和明细项AmsBorrowOrderItem
- 开发借用单控制器AmsBorrowOrderController,提供完整的CRUD和业务流程接口
- 实现资产选择功能,支持从可用资产中筛选并添加到借用明细
- 添加借用流程管理,包括提交申请、确认借出、申请归还、确认归还等操作
- 集成部门和用户选择功能,支持按部门筛选借用人
- 实现借用单状态管理和数据持久化存储
main
yangk 2 weeks ago
parent 0c7cd2b3df
commit f50c0f9aa3

@ -0,0 +1,31 @@
package com.ruoyi.asset.constant;
/**
*
*
* @author Yangk
*/
public final class BorrowOrderStatus
{
/** 草稿 */
public static final String DRAFT = "DRAFT";
/** 待确认 */
public static final String PENDING_CONFIRM = "PENDING_CONFIRM";
/** 已驳回 */
public static final String REJECTED = "REJECTED";
/** 借用中 */
public static final String BORROWING = "BORROWING";
/** 待归还确认 */
public static final String PENDING_RETURN_CONFIRM = "PENDING_RETURN_CONFIRM";
/** 已归还 */
public static final String BORROW_RETURNED = "BORROW_RETURNED";
private BorrowOrderStatus()
{
}
}

@ -0,0 +1,293 @@
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.RequestBody;
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.common.constant.UserConstants;
import com.ruoyi.asset.domain.AmsAsset;
import com.ruoyi.asset.domain.AmsBorrowOrder;
import com.ruoyi.asset.domain.AmsBorrowOrderItem;
import com.ruoyi.asset.domain.AmsAssetLocation;
import com.ruoyi.asset.domain.AmsWarehouse;
import com.ruoyi.asset.service.IAmsAssetLocationService;
import com.ruoyi.asset.service.IAmsBorrowOrderService;
import com.ruoyi.asset.service.IAmsWarehouseService;
import com.ruoyi.system.service.ISysDeptService;
import com.ruoyi.system.service.ISysUserService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.common.core.page.TableDataInfo;
/**
* Controller
*
* 稿API
*
* @author Yangk
* @date 2026-06-16
*/
@Controller
@RequestMapping("/asset/borrow")
public class AmsBorrowOrderController extends BaseController
{
private String prefix = "asset/borrow";
@Autowired
private IAmsBorrowOrderService amsBorrowOrderService;
@Autowired
private ISysDeptService sysDeptService;
@Autowired
private ISysUserService sysUserService;
@Autowired
private IAmsWarehouseService amsWarehouseService;
@Autowired
private IAmsAssetLocationService amsAssetLocationService;
@RequiresPermissions("asset:borrow:view")
@GetMapping()
public String borrow(ModelMap mmap)
{
mmap.put("deptList", selectNormalDeptList());
return prefix + "/borrow";
}
/**
*
*/
@RequiresPermissions("asset:borrow:list")
@PostMapping("/list")
@ResponseBody
public TableDataInfo list(AmsBorrowOrder amsBorrowOrder)
{
startPage();
List<AmsBorrowOrder> list = amsBorrowOrderService.selectAmsBorrowOrderList(amsBorrowOrder);
return getDataTable(list);
}
/**
*
*/
@RequiresPermissions(value = { "asset:borrow:add", "asset:borrow: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:borrow:add", "asset:borrow:edit" }, logical = Logical.OR)
@PostMapping("/availableAssetList")
@ResponseBody
public TableDataInfo availableAssetList(AmsAsset asset, @RequestParam(value = "orderId", required = false) Long orderId)
{
startPage();
List<AmsAsset> list = amsBorrowOrderService.selectAvailableBorrowAssetList(asset, orderId);
return getDataTable(list);
}
/**
*
*/
@RequiresPermissions("asset:borrow:export")
@Log(title = "借用归还管理", businessType = BusinessType.EXPORT)
@PostMapping("/export")
@ResponseBody
public AjaxResult export(AmsBorrowOrder amsBorrowOrder)
{
List<AmsBorrowOrder> list = amsBorrowOrderService.selectAmsBorrowOrderList(amsBorrowOrder);
ExcelUtil<AmsBorrowOrder> util = new ExcelUtil<AmsBorrowOrder>(AmsBorrowOrder.class);
return util.exportExcel(list, "借用归还管理数据");
}
/**
*
*/
@RequiresPermissions("asset:borrow:view")
@GetMapping("/view/{orderId}")
public String view(@PathVariable("orderId") Long orderId, ModelMap mmap)
{
AmsBorrowOrder amsBorrowOrder = amsBorrowOrderService.selectAmsBorrowOrderByOrderId(orderId);
mmap.put("amsBorrowOrder", amsBorrowOrder);
putWarehouseLocationOptions(mmap);
return prefix + "/view";
}
/**
*
*/
@RequiresPermissions("asset:borrow:add")
@GetMapping("/add")
public String add(ModelMap mmap)
{
putBorrowOptions(mmap);
return prefix + "/add";
}
/**
* 稿
*/
@RequiresPermissions("asset:borrow:add")
@Log(title = "借用归还管理", businessType = BusinessType.INSERT)
@PostMapping("/add")
@ResponseBody
public AjaxResult addSave(AmsBorrowOrder amsBorrowOrder)
{
amsBorrowOrder.setCreateBy(getLoginName());
return toAjax(amsBorrowOrderService.insertAmsBorrowOrder(amsBorrowOrder));
}
/**
*
*/
@RequiresPermissions("asset:borrow:edit")
@GetMapping("/edit/{orderId}")
public String edit(@PathVariable("orderId") Long orderId, ModelMap mmap)
{
AmsBorrowOrder amsBorrowOrder = amsBorrowOrderService.selectAmsBorrowOrderByOrderId(orderId);
mmap.put("amsBorrowOrder", amsBorrowOrder);
putBorrowOptions(mmap);
return prefix + "/edit";
}
/**
* 稿
*/
@RequiresPermissions("asset:borrow:edit")
@Log(title = "借用归还管理", businessType = BusinessType.UPDATE)
@PostMapping("/edit")
@ResponseBody
public AjaxResult editSave(AmsBorrowOrder amsBorrowOrder)
{
amsBorrowOrder.setUpdateBy(getLoginName());
return toAjax(amsBorrowOrderService.updateAmsBorrowOrder(amsBorrowOrder));
}
/**
* 稿
*/
@RequiresPermissions("asset:borrow:remove")
@Log(title = "借用归还管理", businessType = BusinessType.DELETE)
@PostMapping("/remove")
@ResponseBody
public AjaxResult remove(String ids)
{
return toAjax(amsBorrowOrderService.deleteAmsBorrowOrderByOrderIds(ids));
}
/**
*
*/
@RequiresPermissions("asset:borrow:submit")
@Log(title = "借用归还管理", businessType = BusinessType.UPDATE)
@PostMapping("/submit/{orderId}")
@ResponseBody
public AjaxResult submit(@PathVariable("orderId") Long orderId)
{
return toAjax(amsBorrowOrderService.submitBorrow(orderId, getLoginName()));
}
/**
*
*/
@RequiresPermissions("asset:borrow:confirm")
@Log(title = "借用归还管理", businessType = BusinessType.UPDATE)
@PostMapping("/confirm/{orderId}")
@ResponseBody
public AjaxResult confirm(@PathVariable("orderId") Long orderId)
{
return toAjax(amsBorrowOrderService.confirmBorrow(orderId, getUserId(),
getSysUser().getUserName(), getLoginName()));
}
/**
*
*/
@RequiresPermissions("asset:borrow:reject")
@Log(title = "借用归还管理", businessType = BusinessType.UPDATE)
@PostMapping("/reject/{orderId}")
@ResponseBody
public AjaxResult reject(@PathVariable("orderId") Long orderId, @RequestParam("rejectReason") String rejectReason)
{
return toAjax(amsBorrowOrderService.rejectBorrow(orderId, rejectReason, getLoginName()));
}
/**
*
*/
@RequiresPermissions("asset:borrow:applyReturn")
@Log(title = "借用归还管理", businessType = BusinessType.UPDATE)
@PostMapping("/applyReturn/{orderId}")
@ResponseBody
public AjaxResult applyReturn(@PathVariable("orderId") Long orderId, @RequestBody List<AmsBorrowOrderItem> items)
{
return toAjax(amsBorrowOrderService.applyReturn(orderId, items, getLoginName()));
}
/**
*
*/
@RequiresPermissions("asset:borrow:confirmReturn")
@Log(title = "借用归还管理", businessType = BusinessType.UPDATE)
@PostMapping("/confirmReturn/{orderId}")
@ResponseBody
public AjaxResult confirmReturn(@PathVariable("orderId") Long orderId, @RequestBody List<AmsBorrowOrderItem> items)
{
return toAjax(amsBorrowOrderService.confirmReturn(orderId, items, getUserId(),
getSysUser().getUserName(), getLoginName()));
}
private void putBorrowOptions(ModelMap mmap)
{
mmap.put("deptList", selectNormalDeptList());
SysUser user = new SysUser();
user.setStatus(UserConstants.NORMAL);
mmap.put("userList", sysUserService.selectUserList(user));
SysUser currentUser = getSysUser();
mmap.put("defaultBorrowDeptId", currentUser.getDeptId());
mmap.put("defaultBorrowUserId", currentUser.getUserId());
}
private List<SysDept> selectNormalDeptList()
{
SysDept dept = new SysDept();
dept.setStatus(UserConstants.DEPT_NORMAL);
return sysDeptService.selectDeptList(dept);
}
private void putWarehouseLocationOptions(ModelMap mmap)
{
mmap.put("warehouseList", selectEnabledWarehouseList());
AmsAssetLocation location = new AmsAssetLocation();
location.setEnabled("Y");
mmap.put("locationList", amsAssetLocationService.selectAmsAssetLocationList(location));
}
private List<AmsWarehouse> selectEnabledWarehouseList()
{
AmsWarehouse warehouse = new AmsWarehouse();
warehouse.setEnabled("Y");
return amsWarehouseService.selectAmsWarehouseList(warehouse);
}
}

@ -0,0 +1,240 @@
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_borrow_order
*
* @author Yangk
* @date 2026-06-16
*/
public class AmsBorrowOrder extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** 单据ID */
private Long orderId;
/** 借用单号 */
@Excel(name = "借用单号")
private String borrowNo;
/** 借用人ID */
@Excel(name = "借用人ID")
private Long borrowUserId;
/** 借用人名称快照 */
@Excel(name = "借用人名称快照")
private String borrowUserName;
/** 借用部门ID */
@Excel(name = "借用部门ID")
private Long borrowDeptId;
/** 借用部门名称快照 */
@Excel(name = "借用部门名称快照")
private String borrowDeptName;
/** 预计归还日期 */
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
@Excel(name = "预计归还日期", width = 30, dateFormat = "yyyy-MM-dd")
private Date expectedReturnDate;
/** 确认人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 orderStatus;
/** 删除标志0存在1删除 */
private String delFlag;
/** 借用单明细信息 */
private List<AmsBorrowOrderItem> amsBorrowOrderItemList;
public void setOrderId(Long orderId)
{
this.orderId = orderId;
}
public Long getOrderId()
{
return orderId;
}
public void setBorrowNo(String borrowNo)
{
this.borrowNo = borrowNo;
}
public String getBorrowNo()
{
return borrowNo;
}
public void setBorrowUserId(Long borrowUserId)
{
this.borrowUserId = borrowUserId;
}
public Long getBorrowUserId()
{
return borrowUserId;
}
public void setBorrowUserName(String borrowUserName)
{
this.borrowUserName = borrowUserName;
}
public String getBorrowUserName()
{
return borrowUserName;
}
public void setBorrowDeptId(Long borrowDeptId)
{
this.borrowDeptId = borrowDeptId;
}
public Long getBorrowDeptId()
{
return borrowDeptId;
}
public void setBorrowDeptName(String borrowDeptName)
{
this.borrowDeptName = borrowDeptName;
}
public String getBorrowDeptName()
{
return borrowDeptName;
}
public void setExpectedReturnDate(Date expectedReturnDate)
{
this.expectedReturnDate = expectedReturnDate;
}
public Date getExpectedReturnDate()
{
return expectedReturnDate;
}
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 setOrderStatus(String orderStatus)
{
this.orderStatus = orderStatus;
}
public String getOrderStatus()
{
return orderStatus;
}
public void setDelFlag(String delFlag)
{
this.delFlag = delFlag;
}
public String getDelFlag()
{
return delFlag;
}
public List<AmsBorrowOrderItem> getAmsBorrowOrderItemList()
{
return amsBorrowOrderItemList;
}
public void setAmsBorrowOrderItemList(List<AmsBorrowOrderItem> amsBorrowOrderItemList)
{
this.amsBorrowOrderItemList = amsBorrowOrderItemList;
}
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
.append("orderId", getOrderId())
.append("borrowNo", getBorrowNo())
.append("borrowUserId", getBorrowUserId())
.append("borrowUserName", getBorrowUserName())
.append("borrowDeptId", getBorrowDeptId())
.append("borrowDeptName", getBorrowDeptName())
.append("expectedReturnDate", getExpectedReturnDate())
.append("confirmUserId", getConfirmUserId())
.append("confirmUserName", getConfirmUserName())
.append("confirmTime", getConfirmTime())
.append("rejectReason", getRejectReason())
.append("orderStatus", getOrderStatus())
.append("createBy", getCreateBy())
.append("createTime", getCreateTime())
.append("updateBy", getUpdateBy())
.append("updateTime", getUpdateTime())
.append("remark", getRemark())
.append("delFlag", getDelFlag())
.append("amsBorrowOrderItemList", getAmsBorrowOrderItemList())
.toString();
}
}

@ -0,0 +1,394 @@
package com.ruoyi.asset.domain;
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_borrow_order_item
*
* @author Yangk
* @date 2026-06-16
*/
public class AmsBorrowOrderItem extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** 明细ID */
private Long itemId;
/** 借用单ID */
@Excel(name = "借用单ID")
private Long orderId;
/** 借用单号快照 */
@Excel(name = "借用单号快照")
private String borrowNo;
/** 资产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;
/** 借用前仓库ID */
@Excel(name = "借用前仓库ID")
private Long beforeWarehouseId;
/** 借用前仓库编码快照 */
@Excel(name = "借用前仓库编码快照")
private String beforeWarehouseCode;
/** 借用前仓库名称快照 */
@Excel(name = "借用前仓库名称快照")
private String beforeWarehouseName;
/** 借用前位置ID */
@Excel(name = "借用前位置ID")
private Long beforeLocationId;
/** 借用前位置编码快照 */
@Excel(name = "借用前位置编码快照")
private String beforeLocationCode;
/** 借用前位置名称快照 */
@Excel(name = "借用前位置名称快照")
private String beforeLocationName;
/** 归还仓库ID */
@Excel(name = "归还仓库ID")
private Long returnWarehouseId;
/** 归还仓库编码快照 */
@Excel(name = "归还仓库编码快照")
private String returnWarehouseCode;
/** 归还仓库名称快照 */
@Excel(name = "归还仓库名称快照")
private String returnWarehouseName;
/** 归还位置ID */
@Excel(name = "归还位置ID")
private Long returnLocationId;
/** 归还位置编码快照 */
@Excel(name = "归还位置编码快照")
private String returnLocationCode;
/** 归还位置名称快照 */
@Excel(name = "归还位置名称快照")
private String returnLocationName;
/** 实际归还日期 */
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
@Excel(name = "实际归还日期", width = 30, dateFormat = "yyyy-MM-dd")
private Date actualReturnDate;
/** 归还状态 */
@Excel(name = "归还状态")
private String returnStatus;
/** 删除标志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 setBorrowNo(String borrowNo)
{
this.borrowNo = borrowNo;
}
public String getBorrowNo()
{
return borrowNo;
}
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 setBeforeWarehouseId(Long beforeWarehouseId)
{
this.beforeWarehouseId = beforeWarehouseId;
}
public Long getBeforeWarehouseId()
{
return beforeWarehouseId;
}
public void setBeforeWarehouseCode(String beforeWarehouseCode)
{
this.beforeWarehouseCode = beforeWarehouseCode;
}
public String getBeforeWarehouseCode()
{
return beforeWarehouseCode;
}
public void setBeforeWarehouseName(String beforeWarehouseName)
{
this.beforeWarehouseName = beforeWarehouseName;
}
public String getBeforeWarehouseName()
{
return beforeWarehouseName;
}
public void setBeforeLocationId(Long beforeLocationId)
{
this.beforeLocationId = beforeLocationId;
}
public Long getBeforeLocationId()
{
return beforeLocationId;
}
public void setBeforeLocationCode(String beforeLocationCode)
{
this.beforeLocationCode = beforeLocationCode;
}
public String getBeforeLocationCode()
{
return beforeLocationCode;
}
public void setBeforeLocationName(String beforeLocationName)
{
this.beforeLocationName = beforeLocationName;
}
public String getBeforeLocationName()
{
return beforeLocationName;
}
public void setReturnWarehouseId(Long returnWarehouseId)
{
this.returnWarehouseId = returnWarehouseId;
}
public Long getReturnWarehouseId()
{
return returnWarehouseId;
}
public void setReturnWarehouseCode(String returnWarehouseCode)
{
this.returnWarehouseCode = returnWarehouseCode;
}
public String getReturnWarehouseCode()
{
return returnWarehouseCode;
}
public void setReturnWarehouseName(String returnWarehouseName)
{
this.returnWarehouseName = returnWarehouseName;
}
public String getReturnWarehouseName()
{
return returnWarehouseName;
}
public void setReturnLocationId(Long returnLocationId)
{
this.returnLocationId = returnLocationId;
}
public Long getReturnLocationId()
{
return returnLocationId;
}
public void setReturnLocationCode(String returnLocationCode)
{
this.returnLocationCode = returnLocationCode;
}
public String getReturnLocationCode()
{
return returnLocationCode;
}
public void setReturnLocationName(String returnLocationName)
{
this.returnLocationName = returnLocationName;
}
public String getReturnLocationName()
{
return returnLocationName;
}
public void setActualReturnDate(Date actualReturnDate)
{
this.actualReturnDate = actualReturnDate;
}
public Date getActualReturnDate()
{
return actualReturnDate;
}
public void setReturnStatus(String returnStatus)
{
this.returnStatus = returnStatus;
}
public String getReturnStatus()
{
return returnStatus;
}
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("borrowNo", getBorrowNo())
.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("beforeWarehouseId", getBeforeWarehouseId())
.append("beforeWarehouseCode", getBeforeWarehouseCode())
.append("beforeWarehouseName", getBeforeWarehouseName())
.append("beforeLocationId", getBeforeLocationId())
.append("beforeLocationCode", getBeforeLocationCode())
.append("beforeLocationName", getBeforeLocationName())
.append("returnWarehouseId", getReturnWarehouseId())
.append("returnWarehouseCode", getReturnWarehouseCode())
.append("returnWarehouseName", getReturnWarehouseName())
.append("returnLocationId", getReturnLocationId())
.append("returnLocationCode", getReturnLocationCode())
.append("returnLocationName", getReturnLocationName())
.append("actualReturnDate", getActualReturnDate())
.append("returnStatus", getReturnStatus())
.append("createBy", getCreateBy())
.append("createTime", getCreateTime())
.append("updateBy", getUpdateBy())
.append("updateTime", getUpdateTime())
.append("remark", getRemark())
.append("delFlag", getDelFlag())
.toString();
}
}

@ -0,0 +1,193 @@
package com.ruoyi.asset.mapper;
import java.util.List;
import java.util.Date;
import com.ruoyi.asset.domain.AmsAsset;
import com.ruoyi.asset.domain.AmsBorrowOrder;
import com.ruoyi.asset.domain.AmsBorrowOrderItem;
import org.apache.ibatis.annotations.Param;
/**
* Mapper
*
* @author Yangk
* @date 2026-06-16
*/
public interface AmsBorrowOrderMapper
{
/**
*
*
* @param orderId
* @return
*/
public AmsBorrowOrder selectAmsBorrowOrderByOrderId(Long orderId);
/**
*
*
* @param orderId
* @return
*/
public AmsBorrowOrder selectAmsBorrowOrderByOrderIdForUpdate(Long orderId);
/**
*
*
* @param amsBorrowOrder
* @return
*/
public List<AmsBorrowOrder> selectAmsBorrowOrderList(AmsBorrowOrder amsBorrowOrder);
/**
*
*
* @param amsBorrowOrder
* @return
*/
public int insertAmsBorrowOrder(AmsBorrowOrder amsBorrowOrder);
/**
*
*
* @param amsBorrowOrder
* @return
*/
public int updateAmsBorrowOrder(AmsBorrowOrder amsBorrowOrder);
/**
*
*
* @param orderId
* @return
*/
public int deleteAmsBorrowOrderByOrderId(Long orderId);
/**
*
*
* @param orderIds
* @return
*/
public int deleteAmsBorrowOrderByOrderIds(String[] orderIds);
/**
*
*
* @param orderIds
* @return
*/
public int deleteAmsBorrowOrderItemByOrderIds(String[] orderIds);
/**
*
*
* @param amsBorrowOrderItemList
* @return
*/
public int batchAmsBorrowOrderItem(List<AmsBorrowOrderItem> amsBorrowOrderItemList);
/**
*
*
* @param orderId ID
* @return
*/
public int deleteAmsBorrowOrderItemByOrderId(Long orderId);
/**
*
*
* @param asset
* @param currentOrderId ID
* @param stockStatus
* @param draftStatus 稿
* @param pendingStatus
* @return
*/
public List<AmsAsset> selectAvailableBorrowAssetList(@Param("asset") AmsAsset asset,
@Param("currentOrderId") Long currentOrderId,
@Param("stockStatus") String stockStatus,
@Param("draftStatus") String draftStatus,
@Param("pendingStatus") String pendingStatus);
/**
*
*
* @param assetId ID
* @param currentOrderId ID
* @param draftStatus 稿
* @param pendingStatus
* @return
*/
public int countOtherActiveBorrowOrderByAssetId(@Param("assetId") Long assetId,
@Param("currentOrderId") Long currentOrderId,
@Param("draftStatus") String draftStatus,
@Param("pendingStatus") String pendingStatus);
/**
*
*
* @param amsBorrowOrder
* @return
*/
public int submitAmsBorrowOrder(AmsBorrowOrder amsBorrowOrder);
/**
*
*
* @param amsBorrowOrder
* @return
*/
public int confirmAmsBorrowOrder(AmsBorrowOrder amsBorrowOrder);
/**
*
*
* @param amsBorrowOrder
* @return
*/
public int rejectAmsBorrowOrder(AmsBorrowOrder amsBorrowOrder);
/**
* ID
*
* @param orderId ID
* @param itemId ID
* @return
*/
public AmsBorrowOrderItem selectAmsBorrowOrderItemByOrderIdAndItemIdForUpdate(
@Param("orderId") Long orderId, @Param("itemId") Long itemId);
/**
*
*
* @param item
* @param expectedStatus
* @return
*/
public int updateAmsBorrowOrderItemReturnStatus(@Param("item") AmsBorrowOrderItem item,
@Param("expectedStatus") String expectedStatus);
/**
*
*
* @param orderId ID
* @return
*/
public List<AmsBorrowOrderItem> selectAmsBorrowOrderItemList(Long orderId);
/**
*
*
* @param orderId ID
* @param orderStatus
* @param updateBy
* @param updateTime
* @return
*/
public int updateAmsBorrowOrderStatus(@Param("orderId") Long orderId,
@Param("orderStatus") String orderStatus,
@Param("updateBy") String updateBy,
@Param("updateTime") Date updateTime);
}

@ -0,0 +1,124 @@
package com.ruoyi.asset.service;
import java.util.List;
import com.ruoyi.asset.domain.AmsAsset;
import com.ruoyi.asset.domain.AmsBorrowOrder;
import com.ruoyi.asset.domain.AmsBorrowOrderItem;
/**
* Service
*
* @author Yangk
* @date 2026-06-16
*/
public interface IAmsBorrowOrderService
{
/**
*
*
* @param orderId
* @return
*/
public AmsBorrowOrder selectAmsBorrowOrderByOrderId(Long orderId);
/**
*
*
* @param amsBorrowOrder
* @return
*/
public List<AmsBorrowOrder> selectAmsBorrowOrderList(AmsBorrowOrder amsBorrowOrder);
/**
*
*
* @param amsBorrowOrder
* @return
*/
public int insertAmsBorrowOrder(AmsBorrowOrder amsBorrowOrder);
/**
*
*
* @param amsBorrowOrder
* @return
*/
public int updateAmsBorrowOrder(AmsBorrowOrder amsBorrowOrder);
/**
*
*
* @param orderIds
* @return
*/
public int deleteAmsBorrowOrderByOrderIds(String orderIds);
/**
*
*
* @param orderId
* @return
*/
public int deleteAmsBorrowOrderByOrderId(Long orderId);
/**
*
*
* @param asset
* @param currentOrderId ID
* @return
*/
public List<AmsAsset> selectAvailableBorrowAssetList(AmsAsset asset, Long currentOrderId);
/**
*
*
* @param orderId ID
* @param operateLoginName
* @return
*/
public int submitBorrow(Long orderId, String operateLoginName);
/**
*
*
* @param orderId ID
* @param operateUserId ID
* @param operateUserName
* @param operateLoginName
* @return
*/
public int confirmBorrow(Long orderId, Long operateUserId, String operateUserName, String operateLoginName);
/**
*
*
* @param orderId ID
* @param rejectReason
* @param operateLoginName
* @return
*/
public int rejectBorrow(Long orderId, String rejectReason, String operateLoginName);
/**
*
*
* @param orderId ID
* @param returnItems /ID
* @param operateLoginName
* @return
*/
public int applyReturn(Long orderId, List<AmsBorrowOrderItem> returnItems, String operateLoginName);
/**
*
*
* @param orderId ID
* @param returnItems
* @param operateUserId ID
* @param operateUserName
* @param operateLoginName
* @return
*/
public int confirmReturn(Long orderId, List<AmsBorrowOrderItem> returnItems, Long operateUserId, String operateUserName, String operateLoginName);
}

@ -0,0 +1,843 @@
package com.ruoyi.asset.service.impl;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.ruoyi.asset.constant.AssetStatus;
import com.ruoyi.asset.constant.BorrowOrderStatus;
import com.ruoyi.asset.domain.AmsAsset;
import com.ruoyi.asset.domain.AmsBorrowOrder;
import com.ruoyi.asset.domain.AmsBorrowOrderItem;
import com.ruoyi.asset.domain.AmsWarehouse;
import com.ruoyi.asset.domain.AmsAssetLocation;
import com.ruoyi.asset.domain.AssetTransitionContext;
import com.ruoyi.asset.mapper.AmsAssetMapper;
import com.ruoyi.asset.mapper.AmsBorrowOrderMapper;
import com.ruoyi.asset.service.IAmsBorrowOrderService;
import com.ruoyi.asset.service.IAssetStatusTransitionService;
import com.ruoyi.asset.service.IAmsWarehouseService;
import com.ruoyi.asset.service.IAmsAssetLocationService;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.common.core.domain.entity.SysUser;
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;
import com.ruoyi.system.service.ISysDeptService;
import com.ruoyi.system.service.ISysUserService;
/**
* Service
*
* IN_STOCKBORROWED
*
* 1. ID
* 2.
* 3. del_flag = '0' 稿
*
* @author Yangk
* @date 2026-06-16
*/
@Service
public class AmsBorrowOrderServiceImpl implements IAmsBorrowOrderService
{
private static final String BORROW_ORDER_RULE = "BORROW_ORDER";
private static final String DEL_FLAG_NORMAL = "0";
@Autowired
private AmsBorrowOrderMapper amsBorrowOrderMapper;
@Autowired
private AmsAssetMapper amsAssetMapper;
@Autowired
private ISysCodeRuleService sysCodeRuleService;
@Autowired
private ISysDeptService sysDeptService;
@Autowired
private ISysUserService sysUserService;
@Autowired
private IAssetStatusTransitionService assetStatusTransitionService;
@Autowired
private IAmsWarehouseService amsWarehouseService;
@Autowired
private IAmsAssetLocationService amsAssetLocationService;
/**
*
*/
@Override
public AmsBorrowOrder selectAmsBorrowOrderByOrderId(Long orderId)
{
return amsBorrowOrderMapper.selectAmsBorrowOrderByOrderId(orderId);
}
/**
*
*/
@Override
public List<AmsBorrowOrder> selectAmsBorrowOrderList(AmsBorrowOrder amsBorrowOrder)
{
return amsBorrowOrderMapper.selectAmsBorrowOrderList(amsBorrowOrder);
}
/**
* 稿
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int insertAmsBorrowOrder(AmsBorrowOrder amsBorrowOrder)
{
// 1. 基础报文校验
validateOrderRequest(amsBorrowOrder);
// 2. 校验借用责任人与部门,回填快照
fillBorrowerSnapshots(amsBorrowOrder);
// 3. 生成唯一单号,初始化单据受控字段
amsBorrowOrder.setBorrowNo(sysCodeRuleService.nextCode(BORROW_ORDER_RULE));
amsBorrowOrder.setConfirmUserId(null);
amsBorrowOrder.setConfirmUserName(null);
amsBorrowOrder.setConfirmTime(null);
amsBorrowOrder.setRejectReason(null);
amsBorrowOrder.setOrderStatus(BorrowOrderStatus.DRAFT);
amsBorrowOrder.setDelFlag(DEL_FLAG_NORMAL);
amsBorrowOrder.setCreateTime(DateUtils.getNowDate());
// 4. 填充明细资产历史仓位快照,执行资产防重和并发占用校验
fillOrderSnapshots(amsBorrowOrder, null);
// 5. 保存主表及子表明细
int rows = amsBorrowOrderMapper.insertAmsBorrowOrder(amsBorrowOrder);
if (rows != 1 || amsBorrowOrder.getOrderId() == null)
{
throw new ServiceException("借用单保存失败");
}
insertAmsBorrowOrderItems(amsBorrowOrder);
return rows;
}
/**
* 稿
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int updateAmsBorrowOrder(AmsBorrowOrder amsBorrowOrder)
{
if (amsBorrowOrder == null || amsBorrowOrder.getOrderId() == null)
{
throw new ServiceException("单据ID不能为空");
}
// 1. 悲观锁锁定主表记录,强校验状态必须为 DRAFT。非草稿状态严禁修改
AmsBorrowOrder current = requireOrderForUpdate(amsBorrowOrder.getOrderId(), BorrowOrderStatus.DRAFT,
"仅草稿状态的借用单允许修改");
validateOrderRequest(amsBorrowOrder);
// 2. 强行还原单号等受控属性,清空审批人及驳回信息,强制复归为 DRAFT
amsBorrowOrder.setBorrowNo(current.getBorrowNo());
amsBorrowOrder.setBorrowUserId(current.getBorrowUserId());
amsBorrowOrder.setBorrowUserName(current.getBorrowUserName());
amsBorrowOrder.setBorrowDeptId(current.getBorrowDeptId());
amsBorrowOrder.setBorrowDeptName(current.getBorrowDeptName());
amsBorrowOrder.setConfirmUserId(null);
amsBorrowOrder.setConfirmUserName(null);
amsBorrowOrder.setConfirmTime(null);
amsBorrowOrder.setRejectReason(null);
amsBorrowOrder.setOrderStatus(BorrowOrderStatus.DRAFT);
amsBorrowOrder.setUpdateTime(DateUtils.getNowDate());
// 3. 校验并重新填充资产快照
fillOrderSnapshots(amsBorrowOrder, amsBorrowOrder.getOrderId());
// 4. 逻辑删除旧明细,再写入新明细以防冗余
amsBorrowOrderMapper.deleteAmsBorrowOrderItemByOrderId(amsBorrowOrder.getOrderId());
insertAmsBorrowOrderItems(amsBorrowOrder);
// 5. 更新主表,采用受控修改
if (amsBorrowOrderMapper.updateAmsBorrowOrder(amsBorrowOrder) != 1)
{
throw new ServiceException("单据状态已发生改变,更新失败");
}
return 1;
}
/**
* 稿
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int deleteAmsBorrowOrderByOrderIds(String orderIds)
{
// 1. 将操作ID排序以彻底防范死锁
Long[] sortedIds = Arrays.stream(Convert.toStrArray(orderIds))
.map(Long::valueOf).sorted().toArray(Long[]::new);
int rows = 0;
for (Long orderId : sortedIds)
{
// 2. 获取主表悲观排他锁,验证是否为 DRAFT 草稿单
AmsBorrowOrder order = amsBorrowOrderMapper.selectAmsBorrowOrderByOrderIdForUpdate(orderId);
if (order == null)
{
continue;
}
if (!BorrowOrderStatus.DRAFT.equals(order.getOrderStatus()))
{
throw new ServiceException(StringUtils.format("借用单【{}】非草稿状态,不允许删除", order.getBorrowNo()));
}
// 3. 子表及主表联级打上逻辑删除标记
amsBorrowOrderMapper.deleteAmsBorrowOrderItemByOrderId(orderId);
rows += amsBorrowOrderMapper.deleteAmsBorrowOrderByOrderId(orderId);
}
return rows;
}
/**
* 稿
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int deleteAmsBorrowOrderByOrderId(Long orderId)
{
AmsBorrowOrder order = amsBorrowOrderMapper.selectAmsBorrowOrderByOrderIdForUpdate(orderId);
if (order == null)
{
return 0;
}
if (!BorrowOrderStatus.DRAFT.equals(order.getOrderStatus()))
{
throw new ServiceException(StringUtils.format("借用单【{}】非草稿状态,不允许删除", order.getBorrowNo()));
}
amsBorrowOrderMapper.deleteAmsBorrowOrderItemByOrderId(orderId);
return amsBorrowOrderMapper.deleteAmsBorrowOrderByOrderId(orderId);
}
/**
*
*/
@Override
public List<AmsAsset> selectAvailableBorrowAssetList(AmsAsset asset, Long currentOrderId)
{
return amsBorrowOrderMapper.selectAvailableBorrowAssetList(asset, currentOrderId,
AssetStatus.IN_STOCK, BorrowOrderStatus.DRAFT, BorrowOrderStatus.PENDING_CONFIRM);
}
/**
*
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int submitBorrow(Long orderId, String operateLoginName)
{
// 1. 获取主单行锁并校验状态必须为 DRAFT
AmsBorrowOrder order = requireOrderForUpdate(orderId, BorrowOrderStatus.DRAFT, "仅草稿借用单允许提交");
validateLoginName(operateLoginName);
// 2. 核心校验:锁定资产、核查跨单据占用以及物理仓位快照一致性
validateOrderReadyForFlow(order);
// 3. 更新主表状态为待确认
order.setOrderStatus(BorrowOrderStatus.PENDING_CONFIRM);
order.setUpdateBy(operateLoginName);
order.setUpdateTime(DateUtils.getNowDate());
if (amsBorrowOrderMapper.submitAmsBorrowOrder(order) != 1)
{
throw new ServiceException("借用单状态已变化,提交失败");
}
return 1;
}
/**
*
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int confirmBorrow(Long orderId, Long operateUserId, String operateUserName, String operateLoginName)
{
// 1. 悲观锁锁定主表记录,强校验必须处于待确认状态
AmsBorrowOrder order = requireOrderForUpdate(orderId, BorrowOrderStatus.PENDING_CONFIRM,
"仅待确认状态的借用单允许确认借出");
validateOperator(operateUserId, operateUserName, "确认人");
validateLoginName(operateLoginName);
// 2. 二次核查防并发:重新升序加锁锁定资产,校验仓位和占用一致性
validateOrderReadyForFlow(order);
// 3. 逐一调用公共状态流转服务执行 confirmBorrow 借出
for (AmsBorrowOrderItem item : sortedItems(order))
{
AssetTransitionContext context = new AssetTransitionContext();
context.setSourceOrderId(order.getOrderId());
context.setSourceOrderNo(order.getBorrowNo());
context.setSourceItemId(item.getItemId());
context.setOperateUserId(operateUserId);
context.setOperateUserName(operateUserName);
context.setOperateLoginName(operateLoginName);
context.setChangeSummary("确认资产借出");
context.setRemark(order.getRemark());
// 公共流转服务会将资产状态置为 BORROWED清空当前仓位与归属写入借出履历
assetStatusTransitionService.confirmBorrow(item.getAssetId(), context);
}
// 4. 更新单据状态为 BORROWING 借用中,保存确认信息快照
Date now = DateUtils.getNowDate();
order.setOrderStatus(BorrowOrderStatus.BORROWING);
order.setConfirmUserId(operateUserId);
order.setConfirmUserName(operateUserName);
order.setConfirmTime(now);
order.setUpdateBy(operateLoginName);
order.setUpdateTime(now);
if (amsBorrowOrderMapper.confirmAmsBorrowOrder(order) != 1)
{
throw new ServiceException("借用单确认失败,状态已被其他线程更改");
}
return 1;
}
/**
*
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int rejectBorrow(Long orderId, String rejectReason, String operateLoginName)
{
// 1. 获取待确认主单锁
AmsBorrowOrder order = requireOrderForUpdate(orderId, BorrowOrderStatus.PENDING_CONFIRM,
"仅待确认状态的借用单允许驳回");
validateLoginName(operateLoginName);
if (StringUtils.isEmpty(StringUtils.trim(rejectReason)))
{
throw new ServiceException("驳回原因不能为空");
}
validateLength(rejectReason, 500, "驳回原因");
// 2. 状态直接扭转为已驳回,不改变实物台账,缩短事务冲突
order.setOrderStatus(BorrowOrderStatus.REJECTED);
order.setRejectReason(StringUtils.trim(rejectReason));
order.setUpdateBy(operateLoginName);
order.setUpdateTime(DateUtils.getNowDate());
if (amsBorrowOrderMapper.rejectAmsBorrowOrder(order) != 1)
{
throw new ServiceException("单据状态已变化,驳回失败");
}
return 1;
}
/**
*
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int applyReturn(Long orderId, List<AmsBorrowOrderItem> returnItems, String operateLoginName)
{
if (returnItems == null || returnItems.isEmpty())
{
throw new ServiceException("申请归还的明细行不能为空");
}
validateLoginName(operateLoginName);
// 1. 锁定主单据。必须是借用中BORROWING或待归还确认PENDING_RETURN_CONFIRM状态
AmsBorrowOrder order = amsBorrowOrderMapper.selectAmsBorrowOrderByOrderIdForUpdate(orderId);
if (order == null)
{
throw new ServiceException("借用单不存在或已删除");
}
if (!BorrowOrderStatus.BORROWING.equals(order.getOrderStatus())
&& !BorrowOrderStatus.PENDING_RETURN_CONFIRM.equals(order.getOrderStatus()))
{
throw new ServiceException("仅借用中或待归还确认的单据允许发起归还申请");
}
// 2. 先按明细ID锁定真实明细再按真实资产ID升序锁资产避免前端篡改 assetId 后跨资产归还
Map<Long, AmsBorrowOrderItem> requestItemMap = buildRequestItemMap(returnItems);
List<AmsBorrowOrderItem> lockedItems = lockReturnItems(orderId, requestItemMap.keySet(),
BorrowOrderStatus.BORROWING, "仅借用中的明细允许申请归还");
Date now = DateUtils.getNowDate();
for (AmsBorrowOrderItem currentItem : lockedItems)
{
AmsBorrowOrderItem paramItem = requestItemMap.get(currentItem.getItemId());
// 锁定资产台账行,验证当前状态为 BORROWED
AmsAsset asset = amsAssetMapper.selectAmsAssetByAssetIdForUpdate(currentItem.getAssetId());
if (asset == null || "1".equals(asset.getDelFlag()))
{
throw new ServiceException("资产不存在或已删除");
}
if (!AssetStatus.BORROWED.equals(asset.getAssetStatus()))
{
throw new ServiceException(StringUtils.format("资产【{}】当前未处于借用状态,无法申请归还", asset.getAssetCode()));
}
// 验证并锁定拟归还的目标仓位及启用状态
if (paramItem.getReturnWarehouseId() == null || paramItem.getReturnLocationId() == null)
{
throw new ServiceException(StringUtils.format("资产【{}】归还的目标仓库或存放位置不能为空", asset.getAssetCode()));
}
AmsWarehouse warehouse = amsWarehouseService.selectAmsWarehouseByWarehouseId(paramItem.getReturnWarehouseId());
if (warehouse == null || !"Y".equals(warehouse.getEnabled()))
{
throw new ServiceException("归还目标仓库不存在或已停用");
}
AmsAssetLocation location = amsAssetLocationService.selectAmsAssetLocationByLocationId(paramItem.getReturnLocationId());
if (location == null || !"Y".equals(location.getEnabled()))
{
throw new ServiceException("归还目标位置不存在或已停用");
}
if (!Objects.equals(location.getWarehouseId(), warehouse.getWarehouseId()))
{
throw new ServiceException("归还存放位置不属于当前选择仓库");
}
// 填充明细行仓库、位置快照
currentItem.setReturnWarehouseId(warehouse.getWarehouseId());
currentItem.setReturnWarehouseCode(warehouse.getWarehouseCode());
currentItem.setReturnWarehouseName(warehouse.getWarehouseName());
currentItem.setReturnLocationId(location.getLocationId());
currentItem.setReturnLocationCode(location.getLocationCode());
currentItem.setReturnLocationName(location.getLocationName());
currentItem.setReturnStatus(BorrowOrderStatus.PENDING_RETURN_CONFIRM);
currentItem.setUpdateBy(operateLoginName);
currentItem.setUpdateTime(now);
// 更新子表该明细状态
if (amsBorrowOrderMapper.updateAmsBorrowOrderItemReturnStatus(currentItem, BorrowOrderStatus.BORROWING) != 1)
{
throw new ServiceException("明细状态已被篡改,申请归还失败");
}
}
// 3. 联动修改主表单据状态为待归还确认
if (amsBorrowOrderMapper.updateAmsBorrowOrderStatus(orderId, BorrowOrderStatus.PENDING_RETURN_CONFIRM,
operateLoginName, now) != 1)
{
throw new ServiceException("借用单主表状态联动失败");
}
return 1;
}
/**
*
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int confirmReturn(Long orderId, List<AmsBorrowOrderItem> returnItems, Long operateUserId,
String operateUserName, String operateLoginName)
{
if (returnItems == null || returnItems.isEmpty())
{
throw new ServiceException("确认归还的明细行不能为空");
}
validateOperator(operateUserId, operateUserName, "确认人");
validateLoginName(operateLoginName);
// 1. 悲观锁锁定主表单据必须为待归还确认PENDING_RETURN_CONFIRM状态
AmsBorrowOrder order = requireOrderForUpdate(orderId, BorrowOrderStatus.PENDING_RETURN_CONFIRM,
"仅待归还确认状态的单据允许归还确认");
// 2. 只确认已经申请归还的明细,确认阶段统一使用明细中已保存的目标仓位
Map<Long, AmsBorrowOrderItem> requestItemMap = buildRequestItemMap(returnItems);
List<AmsBorrowOrderItem> lockedItems = lockReturnItems(orderId, requestItemMap.keySet(),
BorrowOrderStatus.PENDING_RETURN_CONFIRM, "仅待归还确认的明细允许确认归还");
Date now = DateUtils.getNowDate();
for (AmsBorrowOrderItem currentItem : lockedItems)
{
if (currentItem.getReturnWarehouseId() == null || currentItem.getReturnLocationId() == null)
{
throw new ServiceException(StringUtils.format("资产【{}】缺少已申请的归还仓库或位置",
currentItem.getAssetCode()));
}
// 锁定资产行,验证状态仍为 BORROWED
AmsAsset asset = amsAssetMapper.selectAmsAssetByAssetIdForUpdate(currentItem.getAssetId());
if (asset == null || "1".equals(asset.getDelFlag()))
{
throw new ServiceException("资产不存在或已删除");
}
if (!AssetStatus.BORROWED.equals(asset.getAssetStatus()))
{
throw new ServiceException(StringUtils.format("资产【{}】非借用状态,无法确认归还", asset.getAssetCode()));
}
// 调用公共状态流转服务 confirmBorrowReturn 归位资产为在库并记录履历
AssetTransitionContext context = new AssetTransitionContext();
context.setSourceOrderId(order.getOrderId());
context.setSourceOrderNo(order.getBorrowNo());
context.setSourceItemId(currentItem.getItemId());
context.setOperateUserId(operateUserId);
context.setOperateUserName(operateUserName);
context.setOperateLoginName(operateLoginName);
context.setChangeSummary("确认借用归还");
context.setRemark(order.getRemark());
assetStatusTransitionService.confirmBorrowReturn(currentItem.getAssetId(),
currentItem.getReturnWarehouseId(), currentItem.getReturnLocationId(), context);
// 3. 更新该明细状态为已归还
currentItem.setReturnStatus(BorrowOrderStatus.BORROW_RETURNED);
currentItem.setActualReturnDate(now);
currentItem.setUpdateBy(operateLoginName);
currentItem.setUpdateTime(now);
if (amsBorrowOrderMapper.updateAmsBorrowOrderItemReturnStatus(currentItem,
BorrowOrderStatus.PENDING_RETURN_CONFIRM) != 1)
{
throw new ServiceException("明细确认归还更新失败");
}
}
// 4. 判断主表下是否所有明细行的归还状态均为了 BORROW_RETURNED
List<AmsBorrowOrderItem> allItems = amsBorrowOrderMapper.selectAmsBorrowOrderItemList(orderId);
boolean allReturned = true;
boolean hasPending = false;
for (AmsBorrowOrderItem detail : allItems)
{
if (!BorrowOrderStatus.BORROW_RETURNED.equals(detail.getReturnStatus()))
{
allReturned = false;
}
if (BorrowOrderStatus.PENDING_RETURN_CONFIRM.equals(detail.getReturnStatus()))
{
hasPending = true;
}
}
// 5. 联动刷新主单据最新状态
String nextStatus;
if (allReturned)
{
nextStatus = BorrowOrderStatus.BORROW_RETURNED; // 全部归还完毕
}
else if (hasPending)
{
nextStatus = BorrowOrderStatus.PENDING_RETURN_CONFIRM; // 还有其它分批归还申请待确认
}
else
{
nextStatus = BorrowOrderStatus.BORROWING; // 有部分已归还,但还有的处于借用中
}
if (amsBorrowOrderMapper.updateAmsBorrowOrderStatus(orderId, nextStatus, operateLoginName, now) != 1)
{
throw new ServiceException("借用单主单状态联动失败");
}
return 1;
}
private Map<Long, AmsBorrowOrderItem> buildRequestItemMap(List<AmsBorrowOrderItem> requestItems)
{
Map<Long, AmsBorrowOrderItem> requestItemMap = new HashMap<>();
for (AmsBorrowOrderItem requestItem : requestItems)
{
if (requestItem.getItemId() == null)
{
throw new ServiceException("归还明细行ID不能为空");
}
if (requestItemMap.put(requestItem.getItemId(), requestItem) != null)
{
throw new ServiceException("归还明细行不能重复提交");
}
}
return requestItemMap;
}
private List<AmsBorrowOrderItem> lockReturnItems(Long orderId, Set<Long> itemIds, String expectedStatus,
String statusErrorMessage)
{
List<Long> sortedItemIds = new ArrayList<>(itemIds);
Collections.sort(sortedItemIds);
List<AmsBorrowOrderItem> lockedItems = new ArrayList<>();
for (Long itemId : sortedItemIds)
{
AmsBorrowOrderItem item = amsBorrowOrderMapper
.selectAmsBorrowOrderItemByOrderIdAndItemIdForUpdate(orderId, itemId);
if (item == null)
{
throw new ServiceException("归还明细不存在或不属于当前借用单");
}
if (item.getAssetId() == null)
{
throw new ServiceException("归还明细缺少资产ID");
}
if (!expectedStatus.equals(item.getReturnStatus()))
{
throw new ServiceException(statusErrorMessage);
}
lockedItems.add(item);
}
lockedItems.sort(Comparator.comparing(AmsBorrowOrderItem::getAssetId));
return lockedItems;
}
/**
*
*/
private void insertAmsBorrowOrderItems(AmsBorrowOrder amsBorrowOrder)
{
List<AmsBorrowOrderItem> amsBorrowOrderItemList = amsBorrowOrder.getAmsBorrowOrderItemList();
Long orderId = amsBorrowOrder.getOrderId();
String borrowNo = amsBorrowOrder.getBorrowNo();
if (StringUtils.isNotNull(amsBorrowOrderItemList))
{
List<AmsBorrowOrderItem> list = new ArrayList<AmsBorrowOrderItem>();
for (AmsBorrowOrderItem amsBorrowOrderItem : amsBorrowOrderItemList)
{
amsBorrowOrderItem.setOrderId(orderId);
amsBorrowOrderItem.setBorrowNo(borrowNo);
amsBorrowOrderItem.setDelFlag(DEL_FLAG_NORMAL);
amsBorrowOrderItem.setCreateTime(DateUtils.getNowDate());
list.add(amsBorrowOrderItem);
}
if (list.size() > 0)
{
amsBorrowOrderMapper.batchAmsBorrowOrderItem(list);
}
}
}
/**
*
*/
private AmsBorrowOrder requireOrderForUpdate(Long orderId, String expectedStatus, String errMsg)
{
AmsBorrowOrder order = amsBorrowOrderMapper.selectAmsBorrowOrderByOrderIdForUpdate(orderId);
if (order == null)
{
throw new ServiceException("借用单不存在或已删除");
}
if (!expectedStatus.equals(order.getOrderStatus()))
{
throw new ServiceException(errMsg);
}
return order;
}
/**
*
*/
private void validateOrderRequest(AmsBorrowOrder order)
{
if (order == null)
{
throw new ServiceException("借用单不能为空");
}
if (order.getAmsBorrowOrderItemList() == null || order.getAmsBorrowOrderItemList().isEmpty())
{
throw new ServiceException("借用单明细不能为空");
}
if (order.getExpectedReturnDate() == null)
{
throw new ServiceException("预计归还日期不能为空");
}
if (DateUtils.getNowDate().after(order.getExpectedReturnDate()))
{
throw new ServiceException("预计归还日期不能早于当前系统日期");
}
validateLength(order.getRemark(), 500, "备注");
}
/**
*
*/
private void fillBorrowerSnapshots(AmsBorrowOrder order)
{
if (order.getBorrowUserId() == null)
{
throw new ServiceException("借用人不能为空");
}
SysUser user = sysUserService.selectUserById(order.getBorrowUserId());
if (user == null || "1".equals(user.getDelFlag()) || "1".equals(user.getStatus()))
{
throw new ServiceException("借用人不存在或已被停用");
}
order.setBorrowUserName(user.getUserName());
if (order.getBorrowDeptId() == null)
{
throw new ServiceException("借用部门不能为空");
}
SysDept dept = sysDeptService.selectDeptById(order.getBorrowDeptId());
if (dept == null || "1".equals(dept.getDelFlag()) || "1".equals(dept.getStatus()))
{
throw new ServiceException("借用部门不存在或已被停用");
}
order.setBorrowDeptName(dept.getDeptName());
if (!Objects.equals(user.getDeptId(), dept.getDeptId()))
{
throw new ServiceException("所选借用人与借用部门不匹配");
}
}
/**
* /
*/
private void fillOrderSnapshots(AmsBorrowOrder order, Long currentOrderId)
{
Set<Long> assetIds = new HashSet<>();
List<AmsBorrowOrderItem> items = sortedItems(order);
order.setAmsBorrowOrderItemList(items);
for (AmsBorrowOrderItem item : items)
{
if (item.getAssetId() == null)
{
throw new ServiceException("资产ID不能为空");
}
if (!assetIds.add(item.getAssetId()))
{
throw new ServiceException("同一借用单不能选择重复的资产明细");
}
// 悲观锁定该资产,防并发重写
AmsAsset asset = amsAssetMapper.selectAmsAssetByAssetIdForUpdate(item.getAssetId());
if (asset == null || "1".equals(asset.getDelFlag()))
{
throw new ServiceException("资产不存在或已删除");
}
if (!AssetStatus.IN_STOCK.equals(asset.getAssetStatus()))
{
throw new ServiceException(StringUtils.format("资产【{}】当前非在库状态,不允许借出", asset.getAssetCode()));
}
if (asset.getWarehouseId() == null || asset.getLocationId() == null)
{
throw new ServiceException(StringUtils.format("资产【{}】缺少仓库或存放位置", asset.getAssetCode()));
}
// 是否存在其它活跃借用单并发占用
if (amsBorrowOrderMapper.countOtherActiveBorrowOrderByAssetId(asset.getAssetId(), currentOrderId,
BorrowOrderStatus.DRAFT, BorrowOrderStatus.PENDING_CONFIRM) > 0)
{
throw new ServiceException(StringUtils.format("资产【{}】已被其他未完成借用单占用", asset.getAssetCode()));
}
// 回填属性快照
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());
// 借用前仓位快照
item.setBeforeWarehouseId(asset.getWarehouseId());
item.setBeforeWarehouseCode(asset.getWarehouseCode());
item.setBeforeWarehouseName(asset.getWarehouseName());
item.setBeforeLocationId(asset.getLocationId());
item.setBeforeLocationCode(asset.getLocationCode());
item.setBeforeLocationName(asset.getLocationName());
// 回填单号快照与状态
item.setBorrowNo(order.getBorrowNo());
item.setReturnStatus(BorrowOrderStatus.BORROWING);
item.setDelFlag(DEL_FLAG_NORMAL);
validateLength(item.getRemark(), 500, "明细备注");
}
}
/**
*
*/
private void validateOrderReadyForFlow(AmsBorrowOrder order)
{
List<AmsBorrowOrderItem> items = sortedItems(order);
if (items.isEmpty())
{
throw new ServiceException("借用单明细项不能为空");
}
Set<Long> assetIds = new HashSet<>();
for (AmsBorrowOrderItem item : items)
{
// 重新获取排他锁锁定资产
AmsAsset asset = amsAssetMapper.selectAmsAssetByAssetIdForUpdate(item.getAssetId());
if (asset == null || "1".equals(asset.getDelFlag()))
{
throw new ServiceException("资产不存在或已删除");
}
if (!assetIds.add(asset.getAssetId()))
{
throw new ServiceException("同一借用单不能重复选择资产");
}
if (!AssetStatus.IN_STOCK.equals(asset.getAssetStatus()))
{
throw new ServiceException(StringUtils.format("资产【{}】当前状态不允许借出", asset.getAssetCode()));
}
// 核查占用
if (amsBorrowOrderMapper.countOtherActiveBorrowOrderByAssetId(asset.getAssetId(), order.getOrderId(),
BorrowOrderStatus.DRAFT, BorrowOrderStatus.PENDING_CONFIRM) > 0)
{
throw new ServiceException(StringUtils.format("资产【{}】已被其他有效借用单占用", asset.getAssetCode()));
}
// 校验仓位一致性防脏快照
if (!Objects.equals(item.getBeforeWarehouseId(), asset.getWarehouseId())
|| !Objects.equals(item.getBeforeLocationId(), asset.getLocationId()))
{
throw new ServiceException(StringUtils.format("资产【{}】物理存放仓位已发生变更,请撤回单据重新编辑", asset.getAssetCode()));
}
}
}
private List<AmsBorrowOrderItem> sortedItems(AmsBorrowOrder order)
{
List<AmsBorrowOrderItem> items = new ArrayList<>(order.getAmsBorrowOrderItemList());
items.sort(Comparator.comparing(AmsBorrowOrderItem::getAssetId));
return items;
}
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 validateOperator(Long operateUserId, String operateUserName, String fieldPrefix)
{
if (operateUserId == null)
{
throw new ServiceException(fieldPrefix + "用户ID不能为空");
}
if (StringUtils.isEmpty(operateUserName))
{
throw new ServiceException(fieldPrefix + "名称不能为空");
}
validateLength(operateUserName, 100, fieldPrefix + "名称");
}
}

@ -0,0 +1,365 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.asset.mapper.AmsBorrowOrderMapper">
<resultMap type="AmsBorrowOrder" id="AmsBorrowOrderResult">
<result property="orderId" column="order_id" />
<result property="borrowNo" column="borrow_no" />
<result property="borrowUserId" column="borrow_user_id" />
<result property="borrowUserName" column="borrow_user_name" />
<result property="borrowDeptId" column="borrow_dept_id" />
<result property="borrowDeptName" column="borrow_dept_name" />
<result property="expectedReturnDate" column="expected_return_date" />
<result property="confirmUserId" column="confirm_user_id" />
<result property="confirmUserName" column="confirm_user_name" />
<result property="confirmTime" column="confirm_time" />
<result property="rejectReason" column="reject_reason" />
<result property="orderStatus" column="order_status" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
<result property="remark" column="remark" />
<result property="delFlag" column="del_flag" />
</resultMap>
<resultMap id="AmsBorrowOrderAmsBorrowOrderItemResult" type="AmsBorrowOrder" extends="AmsBorrowOrderResult">
<collection property="amsBorrowOrderItemList" ofType="AmsBorrowOrderItem" column="order_id" select="selectAmsBorrowOrderItemList" />
</resultMap>
<resultMap type="AmsBorrowOrderItem" id="AmsBorrowOrderItemResult">
<result property="itemId" column="item_id" />
<result property="orderId" column="order_id" />
<result property="borrowNo" column="borrow_no" />
<result property="assetId" column="asset_id" />
<result property="assetCode" column="asset_code" />
<result property="assetName" column="asset_name" />
<result property="categoryId" column="category_id" />
<result property="categoryCode" column="category_code" />
<result property="categoryName" column="category_name" />
<result property="specModel" column="spec_model" />
<result property="brand" column="brand" />
<result property="beforeWarehouseId" column="before_warehouse_id" />
<result property="beforeWarehouseCode" column="before_warehouse_code" />
<result property="beforeWarehouseName" column="before_warehouse_name" />
<result property="beforeLocationId" column="before_location_id" />
<result property="beforeLocationCode" column="before_location_code" />
<result property="beforeLocationName" column="before_location_name" />
<result property="returnWarehouseId" column="return_warehouse_id" />
<result property="returnWarehouseCode" column="return_warehouse_code" />
<result property="returnWarehouseName" column="return_warehouse_name" />
<result property="returnLocationId" column="return_location_id" />
<result property="returnLocationCode" column="return_location_code" />
<result property="returnLocationName" column="return_location_name" />
<result property="actualReturnDate" column="actual_return_date" />
<result property="returnStatus" column="return_status" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
<result property="remark" column="remark" />
<result property="delFlag" column="del_flag" />
</resultMap>
<resultMap type="AmsAsset" id="AvailableBorrowAssetResult">
<result property="assetId" column="asset_id" />
<result property="assetCode" column="asset_code" />
<result property="assetName" column="asset_name" />
<result property="categoryId" column="category_id" />
<result property="categoryCode" column="category_code" />
<result property="categoryName" column="category_name" />
<result property="specModel" column="spec_model" />
<result property="brand" column="brand" />
<result property="assetStatus" column="asset_status" />
<result property="warehouseId" column="warehouse_id" />
<result property="warehouseCode" column="warehouse_code" />
<result property="warehouseName" column="warehouse_name" />
<result property="locationId" column="location_id" />
<result property="locationCode" column="location_code" />
<result property="locationName" column="location_name" />
<result property="tagCode" column="tag_code" />
</resultMap>
<sql id="selectAmsBorrowOrderVo">
select order_id, borrow_no, borrow_user_id, borrow_user_name, borrow_dept_id, borrow_dept_name, expected_return_date, confirm_user_id, confirm_user_name, confirm_time, reject_reason, order_status, create_by, create_time, update_by, update_time, remark, del_flag from ams_borrow_order
</sql>
<select id="selectAmsBorrowOrderList" parameterType="AmsBorrowOrder" resultMap="AmsBorrowOrderResult">
<include refid="selectAmsBorrowOrderVo"/>
<where>
del_flag = '0'
<if test="borrowNo != null and borrowNo != ''"> and borrow_no like concat(#{borrowNo}, '%')</if>
<if test="borrowUserId != null "> and borrow_user_id = #{borrowUserId}</if>
<if test="borrowUserName != null and borrowUserName != ''"> and borrow_user_name like concat('%', #{borrowUserName}, '%')</if>
<if test="borrowDeptId != null "> and borrow_dept_id = #{borrowDeptId}</if>
<if test="borrowDeptName != null and borrowDeptName != ''"> and borrow_dept_name like concat('%', #{borrowDeptName}, '%')</if>
<if test="expectedReturnDate != null "> and expected_return_date = #{expectedReturnDate}</if>
<if test="confirmUserId != null "> and confirm_user_id = #{confirmUserId}</if>
<if test="confirmUserName != null and confirmUserName != ''"> and confirm_user_name like concat('%', #{confirmUserName}, '%')</if>
<if test="confirmTime != null "> and confirm_time = #{confirmTime}</if>
<if test="rejectReason != null and rejectReason != ''"> and reject_reason = #{rejectReason}</if>
<if test="orderStatus != null and orderStatus != ''"> and order_status = #{orderStatus}</if>
<if test="params.assetCode != null and params.assetCode != ''">
and exists (
select 1 from ams_borrow_order_item item
where item.order_id = ams_borrow_order.order_id
and item.del_flag = '0'
and item.asset_code like concat(#{params.assetCode}, '%')
)
</if>
</where>
order by create_time desc, order_id desc
</select>
<select id="selectAmsBorrowOrderByOrderId" parameterType="Long" resultMap="AmsBorrowOrderAmsBorrowOrderItemResult">
<include refid="selectAmsBorrowOrderVo"/>
where order_id = #{orderId} and del_flag = '0'
</select>
<select id="selectAmsBorrowOrderByOrderIdForUpdate" parameterType="Long" resultMap="AmsBorrowOrderAmsBorrowOrderItemResult">
<include refid="selectAmsBorrowOrderVo"/>
where order_id = #{orderId} and del_flag = '0'
for update
</select>
<select id="selectAmsBorrowOrderItemList" resultMap="AmsBorrowOrderItemResult">
select item_id, order_id, borrow_no, asset_id, asset_code, asset_name, category_id, category_code, category_name, spec_model, brand, before_warehouse_id, before_warehouse_code, before_warehouse_name, before_location_id, before_location_code, before_location_name, return_warehouse_id, return_warehouse_code, return_warehouse_name, return_location_id, return_location_code, return_location_name, actual_return_date, return_status, create_by, create_time, update_by, update_time, remark, del_flag
from ams_borrow_order_item
where order_id = #{order_id} and del_flag = '0'
order by item_id
</select>
<select id="selectAmsBorrowOrderItemByOrderIdAndItemIdForUpdate" resultMap="AmsBorrowOrderItemResult">
select item_id, order_id, borrow_no, asset_id, asset_code, asset_name, category_id, category_code, category_name, spec_model, brand, before_warehouse_id, before_warehouse_code, before_warehouse_name, before_location_id, before_location_code, before_location_name, return_warehouse_id, return_warehouse_code, return_warehouse_name, return_location_id, return_location_code, return_location_name, actual_return_date, return_status, create_by, create_time, update_by, update_time, remark, del_flag
from ams_borrow_order_item
where order_id = #{orderId}
and item_id = #{itemId}
and del_flag = '0'
for update
</select>
<select id="selectAvailableBorrowAssetList" resultMap="AvailableBorrowAssetResult">
select asset.asset_id, asset.asset_code, asset.asset_name, asset.category_id,
asset.category_code, asset.category_name, asset.spec_model, asset.brand,
asset.asset_status, asset.warehouse_id, asset.warehouse_code, asset.warehouse_name,
asset.location_id, asset.location_code, asset.location_name, asset.tag_code
from ams_asset asset
where asset.del_flag = '0'
and asset.asset_status = #{stockStatus}
and not exists (
select 1
from ams_borrow_order_item item
inner join ams_borrow_order borrow_order on borrow_order.order_id = item.order_id
and borrow_order.del_flag = '0'
and borrow_order.order_status in (#{draftStatus}, #{pendingStatus})
where item.asset_id = asset.asset_id
and item.del_flag = '0'
<if test="currentOrderId != null">
and borrow_order.order_id != #{currentOrderId}
</if>
)
<if test="asset.assetCode != null and asset.assetCode != ''">
and asset.asset_code like concat('%', #{asset.assetCode}, '%')
</if>
<if test="asset.assetName != null and asset.assetName != ''">
and asset.asset_name like concat('%', #{asset.assetName}, '%')
</if>
<if test="asset.categoryName != null and asset.categoryName != ''">
and asset.category_name like concat('%', #{asset.categoryName}, '%')
</if>
order by asset.asset_id
</select>
<select id="countOtherActiveBorrowOrderByAssetId" resultType="int">
select count(1)
from ams_borrow_order_item item
inner join ams_borrow_order borrow_order on borrow_order.order_id = item.order_id
and borrow_order.del_flag = '0'
and borrow_order.order_status in (#{draftStatus}, #{pendingStatus})
where item.asset_id = #{assetId}
and item.del_flag = '0'
<if test="currentOrderId != null">
and borrow_order.order_id != #{currentOrderId}
</if>
</select>
<insert id="insertAmsBorrowOrder" parameterType="AmsBorrowOrder" useGeneratedKeys="true" keyProperty="orderId">
insert into ams_borrow_order
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="borrowNo != null and borrowNo != ''">borrow_no,</if>
<if test="borrowUserId != null">borrow_user_id,</if>
<if test="borrowUserName != null">borrow_user_name,</if>
<if test="borrowDeptId != null">borrow_dept_id,</if>
<if test="borrowDeptName != null">borrow_dept_name,</if>
<if test="expectedReturnDate != null">expected_return_date,</if>
<if test="confirmUserId != null">confirm_user_id,</if>
<if test="confirmUserName != null">confirm_user_name,</if>
<if test="confirmTime != null">confirm_time,</if>
<if test="rejectReason != null">reject_reason,</if>
<if test="orderStatus != null and orderStatus != ''">order_status,</if>
<if test="createBy != null">create_by,</if>
<if test="createTime != null">create_time,</if>
<if test="updateBy != null">update_by,</if>
<if test="updateTime != null">update_time,</if>
<if test="remark != null">remark,</if>
<if test="delFlag != null and delFlag != ''">del_flag,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="borrowNo != null and borrowNo != ''">#{borrowNo},</if>
<if test="borrowUserId != null">#{borrowUserId},</if>
<if test="borrowUserName != null">#{borrowUserName},</if>
<if test="borrowDeptId != null">#{borrowDeptId},</if>
<if test="borrowDeptName != null">#{borrowDeptName},</if>
<if test="expectedReturnDate != null">#{expectedReturnDate},</if>
<if test="confirmUserId != null">#{confirmUserId},</if>
<if test="confirmUserName != null">#{confirmUserName},</if>
<if test="confirmTime != null">#{confirmTime},</if>
<if test="rejectReason != null">#{rejectReason},</if>
<if test="orderStatus != null and orderStatus != ''">#{orderStatus},</if>
<if test="createBy != null">#{createBy},</if>
<if test="createTime != null">#{createTime},</if>
<if test="updateBy != null">#{updateBy},</if>
<if test="updateTime != null">#{updateTime},</if>
<if test="remark != null">#{remark},</if>
<if test="delFlag != null and delFlag != ''">#{delFlag},</if>
</trim>
</insert>
<update id="updateAmsBorrowOrder" parameterType="AmsBorrowOrder">
update ams_borrow_order
<trim prefix="SET" suffixOverrides=",">
<if test="borrowNo != null and borrowNo != ''">borrow_no = #{borrowNo},</if>
<if test="borrowUserId != null">borrow_user_id = #{borrowUserId},</if>
<if test="borrowUserName != null">borrow_user_name = #{borrowUserName},</if>
<if test="borrowDeptId != null">borrow_dept_id = #{borrowDeptId},</if>
<if test="borrowDeptName != null">borrow_dept_name = #{borrowDeptName},</if>
<if test="expectedReturnDate != null">expected_return_date = #{expectedReturnDate},</if>
<if test="confirmUserId != null">confirm_user_id = #{confirmUserId},</if>
<if test="confirmUserName != null">confirm_user_name = #{confirmUserName},</if>
<if test="confirmTime != null">confirm_time = #{confirmTime},</if>
<if test="rejectReason != null">reject_reason = #{rejectReason},</if>
<if test="orderStatus != null and orderStatus != ''">order_status = #{orderStatus},</if>
<if test="createBy != null">create_by = #{createBy},</if>
<if test="createTime != null">create_time = #{createTime},</if>
<if test="updateBy != null">update_by = #{updateBy},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
<if test="remark != null">remark = #{remark},</if>
<if test="delFlag != null and delFlag != ''">del_flag = #{delFlag},</if>
</trim>
where order_id = #{orderId} and del_flag = '0' and order_status = 'DRAFT'
</update>
<!-- 逻辑删除,仅限草稿单 -->
<update id="deleteAmsBorrowOrderByOrderId" parameterType="Long">
update ams_borrow_order set del_flag = '1' where order_id = #{orderId} and del_flag = '0' and order_status = 'DRAFT'
</update>
<update id="deleteAmsBorrowOrderByOrderIds" parameterType="String">
update ams_borrow_order set del_flag = '1' where order_id in
<foreach item="orderId" collection="array" open="(" separator="," close=")">
#{orderId}
</foreach>
and del_flag = '0' and order_status = 'DRAFT'
</update>
<update id="deleteAmsBorrowOrderItemByOrderIds" parameterType="String">
update ams_borrow_order_item set del_flag = '1' where order_id in
<foreach item="orderId" collection="array" open="(" separator="," close=")">
#{orderId}
</foreach>
and del_flag = '0'
</update>
<update id="deleteAmsBorrowOrderItemByOrderId" parameterType="Long">
update ams_borrow_order_item set del_flag = '1' where order_id = #{orderId} and del_flag = '0'
</update>
<insert id="batchAmsBorrowOrderItem">
insert into ams_borrow_order_item(
order_id, borrow_no, asset_id, asset_code, asset_name, category_id, category_code, category_name,
spec_model, brand, before_warehouse_id, before_warehouse_code, before_warehouse_name,
before_location_id, before_location_code, before_location_name, return_warehouse_id,
return_warehouse_code, return_warehouse_name, return_location_id, return_location_code,
return_location_name, actual_return_date, return_status, create_by, create_time,
remark, del_flag
) values
<foreach item="item" index="index" collection="list" separator=",">
(
#{item.orderId}, #{item.borrowNo}, #{item.assetId}, #{item.assetCode}, #{item.assetName},
#{item.categoryId}, #{item.categoryCode}, #{item.categoryName}, #{item.specModel}, #{item.brand},
#{item.beforeWarehouseId}, #{item.beforeWarehouseCode}, #{item.beforeWarehouseName},
#{item.beforeLocationId}, #{item.beforeLocationCode}, #{item.beforeLocationName},
#{item.returnWarehouseId}, #{item.returnWarehouseCode}, #{item.returnWarehouseName},
#{item.returnLocationId}, #{item.returnLocationCode}, #{item.returnLocationName},
#{item.actualReturnDate}, #{item.returnStatus}, #{item.createBy}, #{item.createTime},
#{item.remark}, #{item.delFlag}
)
</foreach>
</insert>
<!-- 提交:仅限草稿状态 -->
<update id="submitAmsBorrowOrder" parameterType="AmsBorrowOrder">
update ams_borrow_order
set order_status = #{orderStatus},
reject_reason = null,
update_by = #{updateBy},
update_time = #{updateTime}
where order_id = #{orderId} and del_flag = '0' and order_status = 'DRAFT'
</update>
<!-- 确认借出:仅期待确认状态 -->
<update id="confirmAmsBorrowOrder" parameterType="AmsBorrowOrder">
update ams_borrow_order
set order_status = #{orderStatus},
confirm_user_id = #{confirmUserId},
confirm_user_name = #{confirmUserName},
confirm_time = #{confirmTime},
reject_reason = null,
update_by = #{updateBy},
update_time = #{updateTime}
where order_id = #{orderId} and del_flag = '0' and order_status = 'PENDING_CONFIRM'
</update>
<!-- 驳回借用:仅期待确认状态 -->
<update id="rejectAmsBorrowOrder" parameterType="AmsBorrowOrder">
update ams_borrow_order
set order_status = #{orderStatus},
reject_reason = #{rejectReason},
update_by = #{updateBy},
update_time = #{updateTime}
where order_id = #{orderId} and del_flag = '0' and order_status = 'PENDING_CONFIRM'
</update>
<!-- 更新明细归还状态及信息:必须同时命中主单、明细和期望状态,避免跨单据或并发覆盖 -->
<update id="updateAmsBorrowOrderItemReturnStatus">
update ams_borrow_order_item
<trim prefix="SET" suffixOverrides=",">
<if test="item.returnWarehouseId != null">return_warehouse_id = #{item.returnWarehouseId},</if>
<if test="item.returnWarehouseCode != null">return_warehouse_code = #{item.returnWarehouseCode},</if>
<if test="item.returnWarehouseName != null">return_warehouse_name = #{item.returnWarehouseName},</if>
<if test="item.returnLocationId != null">return_location_id = #{item.returnLocationId},</if>
<if test="item.returnLocationCode != null">return_location_code = #{item.returnLocationCode},</if>
<if test="item.returnLocationName != null">return_location_name = #{item.returnLocationName},</if>
<if test="item.actualReturnDate != null">actual_return_date = #{item.actualReturnDate},</if>
<if test="item.returnStatus != null and item.returnStatus != ''">return_status = #{item.returnStatus},</if>
<if test="item.updateBy != null">update_by = #{item.updateBy},</if>
<if test="item.updateTime != null">update_time = #{item.updateTime},</if>
</trim>
where order_id = #{item.orderId}
and item_id = #{item.itemId}
and del_flag = '0'
and return_status = #{expectedStatus}
</update>
<!-- 自定义修改主单状态 -->
<update id="updateAmsBorrowOrderStatus">
update ams_borrow_order
set order_status = #{orderStatus},
update_by = #{updateBy},
update_time = #{updateTime}
where order_id = #{orderId} and del_flag = '0'
</update>
</mapper>

@ -0,0 +1,241 @@
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:include="include :: header('新增借用单')" />
<th:block th:include="include :: datetimepicker-css" />
<th:block th:include="include :: select2-css" />
<style type="text/css">
table label.error { position: inherit; }
select + label.error { z-index: 1; right: 40px; }
</style>
</head>
<body class="white-bg">
<div class="wrapper wrapper-content animated fadeInRight ibox-content">
<form class="form-horizontal m" id="form-borrow-add">
<h4 class="form-header h4">基本信息</h4>
<div class="row">
<div class="col-sm-4">
<div class="form-group">
<label class="col-sm-4 control-label is-required">借用部门:</label>
<div class="col-sm-8">
<select name="borrowDeptId" class="form-control" required onchange="changeBorrowDept(this.value)">
<option value="">请选择借用部门</option>
<option th:each="dept : ${deptList}" th:value="${dept.deptId}"
th:text="${dept.deptName}"
th:selected="${dept.deptId == defaultBorrowDeptId}"></option>
</select>
</div>
</div>
</div>
<div class="col-sm-4">
<div class="form-group">
<label class="col-sm-4 control-label is-required">借用人:</label>
<div class="col-sm-8">
<select name="borrowUserId" id="borrowUserId" class="form-control select-user" required></select>
</div>
</div>
</div>
<div class="col-sm-4">
<div class="form-group">
<label class="col-sm-4 control-label is-required">预计归还:</label>
<div class="col-sm-8">
<input name="expectedReturnDate" class="form-control time-input" type="text"
placeholder="yyyy-MM-dd" required>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<div class="form-group">
<label class="col-sm-2 control-label">备注:</label>
<div class="col-sm-10">
<textarea name="remark" maxlength="500" class="form-control" rows="3"></textarea>
</div>
</div>
</div>
</div>
<h4 class="form-header h4">借用明细</h4>
<div class="row">
<div class="col-sm-12">
<button type="button" class="btn btn-white btn-sm" onclick="selectAssets()">
<i class="fa fa-plus"> 选择资产</i>
</button>
<button type="button" class="btn btn-white btn-sm" onclick="removeSelectedAssets()">
<i class="fa fa-minus"> 删除</i>
</button>
<div class="col-sm-12 select-table table-striped">
<table id="bootstrap-table"></table>
</div>
</div>
</div>
</form>
</div>
<th:block th:include="include :: footer" />
<th:block th:include="include :: datetimepicker-js" />
<th:block th:include="include :: select2-js" />
<script th:inline="javascript">
var prefix = ctx + "asset/borrow";
var userList = [[${userList}]];
var defaultBorrowUserId = [[${defaultBorrowUserId}]];
$("#form-borrow-add").validate({ focusCleanup: true });
$(function() {
refreshBorrowUsers(defaultBorrowUserId);
initDetailTable([]);
if ($.fn.select2) {
$(".select-user").select2({ width: "100%" });
}
});
function submitHandler() {
if ($("#bootstrap-table").bootstrapTable("getData").length === 0) {
$.modal.alertWarning("请至少添加一条借用明细");
return;
}
if ($.validate.form()) {
syncDetailRows();
$.operate.save(prefix + "/add", $("#form-borrow-add").serialize());
}
}
function initDetailTable(data) {
$.table.init({
data: data,
pagination: false,
showSearch: false,
showRefresh: false,
showToggle: false,
showColumns: false,
sidePagination: "client",
columns: [
{ checkbox: true },
{ field: "assetId", title: "资产", formatter: function(value, row, index) { return buildAssetCell(row, index); } },
{ field: "categoryName", title: "资产类别" },
{ field: "beforeWarehouseName", title: "当前仓位", formatter: function(value, row) {
return $("<span>").text((row.beforeWarehouseName || "-") + " / " + (row.beforeLocationName || "-")).prop("outerHTML");
}},
{ field: "tagCode", title: "RFID标签", formatter: function(value) { return value || "-"; } },
{ field: "remark", title: "明细备注", formatter: function(value, row, index) {
return buildInput("amsBorrowOrderItemList[" + index + "].remark", value, 500);
}},
{ title: "操作", align: "center", formatter: function(value, row) {
return '<a class="btn btn-danger btn-xs" href="javascript:void(0)" onclick="removeAsset(\'' + row.assetId + '\')"><i class="fa fa-remove"></i>删除</a>';
}}
]
});
}
function selectAssets() {
syncDetailRows();
$.modal.openOptions({
title: "选择可借用资产",
url: prefix + "/selectAsset",
width: "1200",
height: "680",
callBack: addSelectedAssets
});
}
function addSelectedAssets(index, layero) {
var selectedAssets = layero.find("iframe")[0].contentWindow.getSelectedAssets();
if (!selectedAssets || selectedAssets.length === 0) {
$.modal.alertWarning("请至少选择一条资产记录");
return;
}
syncDetailRows();
var existing = {};
$.each($("#bootstrap-table").bootstrapTable("getData"), function(i, row) {
existing[String(row.assetId)] = true;
});
$.each(selectedAssets, function(i, asset) {
if (!existing[String(asset.assetId)]) {
$("#bootstrap-table").bootstrapTable("insertRow", {
index: $("#bootstrap-table").bootstrapTable("getData").length,
row: buildBorrowItem(asset)
});
existing[String(asset.assetId)] = true;
}
});
$.modal.close(index);
}
function buildBorrowItem(asset) {
return {
assetId: asset.assetId,
assetCode: asset.assetCode,
assetName: asset.assetName,
categoryName: asset.categoryName,
beforeWarehouseName: asset.warehouseName,
beforeLocationName: asset.locationName,
tagCode: asset.tagCode,
remark: ""
};
}
function removeSelectedAssets() {
var rows = $("#bootstrap-table").bootstrapTable("getSelections");
if (rows.length === 0) {
$.modal.alertWarning("请至少选择一条记录");
return;
}
syncDetailRows();
$("#bootstrap-table").bootstrapTable("remove", {
field: "assetId",
values: $.map(rows, function(row) { return row.assetId; })
});
}
function removeAsset(assetId) {
syncDetailRows();
$("#bootstrap-table").bootstrapTable("remove", { field: "assetId", values: [assetId] });
}
function syncDetailRows() {
var rows = $("#bootstrap-table").bootstrapTable("getData");
$.each(rows, function(index, row) {
var tr = $("#bootstrap-table tbody tr[data-index='" + index + "']");
row.remark = tr.find("[name$='.remark']").val() || "";
});
}
function buildAssetCell(row, index) {
var hidden = $("<input>").attr({
type: "hidden",
name: "amsBorrowOrderItemList[" + index + "].assetId",
value: row.assetId
}).prop("outerHTML");
return hidden + $("<span>").text(row.assetCode + " - " + row.assetName).prop("outerHTML");
}
function buildInput(name, value, maxLength) {
return $("<input>").addClass("form-control").attr({
type: "text",
name: name,
maxlength: maxLength
}).val(value || "").prop("outerHTML");
}
function changeBorrowDept() {
refreshBorrowUsers("");
}
function refreshBorrowUsers(selectedUserId) {
var deptId = $("[name='borrowDeptId']").val();
var userSelect = $("#borrowUserId");
userSelect.empty().append($("<option>").val("").text("请选择借用人"));
$.each(userList, function(i, user) {
if (String(user.deptId) === String(deptId)) {
var option = $("<option>").val(user.userId).text(user.userName + "" + user.loginName + "");
if (String(user.userId) === String(selectedUserId)) {
option.attr("selected", "selected");
}
userSelect.append(option);
}
});
userSelect.trigger("change");
}
</script>
</body>
</html>

@ -0,0 +1,140 @@
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
<th:block th:include="include :: header('借用归还列表')" />
<th:block th:include="include :: datetimepicker-css" />
</head>
<body class="gray-bg">
<div class="container-div">
<div class="row">
<div class="col-sm-12 search-collapse">
<form id="formId">
<div class="select-list">
<ul>
<li><label>借用单号:</label><input type="text" name="borrowNo"/></li>
<li><label>借用人:</label><input type="text" name="borrowUserName"/></li>
<li>
<label>借用部门:</label>
<select name="borrowDeptId">
<option value="">所有</option>
<option th:each="dept : ${deptList}" th:value="${dept.deptId}" th:text="${dept.deptName}"></option>
</select>
</li>
<li class="select-time">
<label>确认时间:</label>
<input type="text" class="time-input" id="startTime" placeholder="开始时间"
name="params[beginConfirmTime]"/>
<span>-</span>
<input type="text" class="time-input" id="endTime" placeholder="结束时间"
name="params[endConfirmTime]"/>
</li>
<li>
<label>单据状态:</label>
<select name="orderStatus" th:with="type=${@dict.getType('ams_borrow_status')}">
<option value="">所有</option>
<option th:each="dict : ${type}" th:text="${dict.dictLabel}"
th:value="${dict.dictValue}"></option>
</select>
</li>
<li><label>资产编码:</label><input type="text" name="params[assetCode]"/></li>
<li>
<a class="btn btn-primary btn-rounded btn-sm" onclick="$.table.search()">
<i class="fa fa-search"></i>&nbsp;搜索
</a>
<a class="btn btn-warning btn-rounded btn-sm" onclick="$.form.reset()">
<i class="fa fa-refresh"></i>&nbsp;重置
</a>
</li>
</ul>
</div>
</form>
</div>
<div class="btn-group-sm" id="toolbar" role="group">
<a class="btn btn-success" onclick="$.operate.add()" shiro:hasPermission="asset:borrow:add">
<i class="fa fa-plus"></i> 添加
</a>
<a class="btn btn-warning" onclick="$.table.exportExcel()" shiro:hasPermission="asset:borrow:export">
<i class="fa fa-download"></i> 导出
</a>
</div>
<div class="col-sm-12 select-table table-striped">
<table id="bootstrap-table"></table>
</div>
</div>
</div>
<th:block th:include="include :: footer" />
<th:block th:include="include :: datetimepicker-js" />
<script th:inline="javascript">
var editFlag = [[${@permission.hasPermi('asset:borrow:edit')}]];
var removeFlag = [[${@permission.hasPermi('asset:borrow:remove')}]];
var submitFlag = [[${@permission.hasPermi('asset:borrow:submit')}]];
var confirmFlag = [[${@permission.hasPermi('asset:borrow:confirm')}]];
var rejectFlag = [[${@permission.hasPermi('asset:borrow:reject')}]];
var applyReturnFlag = [[${@permission.hasPermi('asset:borrow:applyReturn')}]];
var confirmReturnFlag = [[${@permission.hasPermi('asset:borrow:confirmReturn')}]];
var orderStatusDatas = [[${@dict.getType('ams_borrow_status')}]];
var prefix = ctx + "asset/borrow";
$(function() {
$.table.init({
url: prefix + "/list",
viewUrl: prefix + "/view/{id}",
createUrl: prefix + "/add",
updateUrl: prefix + "/edit/{id}",
removeUrl: prefix + "/remove",
exportUrl: prefix + "/export",
modalName: "借用单",
columns: [
{ field: "orderId", title: "单据ID", visible: false },
{ field: "borrowNo", title: "借用单号" },
{ field: "borrowUserName", title: "借用人" },
{ field: "borrowDeptName", title: "借用部门" },
{ field: "expectedReturnDate", title: "预计归还日期" },
{ field: "confirmUserName", title: "确认人" },
{ field: "confirmTime", title: "确认时间" },
{ field: "orderStatus", title: "单据状态", formatter: function(value) {
return $.table.selectDictLabel(orderStatusDatas, value);
}},
{ field: "rejectReason", title: "驳回原因", visible: false },
{ title: "操作", align: "center", formatter: function(value, row) {
var actions = [];
actions.push('<a class="btn btn-info btn-xs" href="javascript:void(0)" onclick="$.operate.view(\'' + row.orderId + '\')"><i class="fa fa-eye"></i>查看</a> ');
if (row.orderStatus === "DRAFT") {
actions.push('<a class="btn btn-success btn-xs ' + editFlag + '" href="javascript:void(0)" onclick="$.operate.edit(\'' + row.orderId + '\')"><i class="fa fa-edit"></i>编辑</a> ');
actions.push('<a class="btn btn-primary btn-xs ' + submitFlag + '" href="javascript:void(0)" onclick="submitBorrow(\'' + row.orderId + '\')"><i class="fa fa-upload"></i>提交</a> ');
actions.push('<a class="btn btn-danger btn-xs ' + removeFlag + '" href="javascript:void(0)" onclick="$.operate.remove(\'' + row.orderId + '\')"><i class="fa fa-remove"></i>删除</a>');
} else if (row.orderStatus === "PENDING_CONFIRM") {
actions.push('<a class="btn btn-primary btn-xs ' + confirmFlag + '" href="javascript:void(0)" onclick="confirmBorrow(\'' + row.orderId + '\')"><i class="fa fa-check"></i>确认借出</a> ');
actions.push('<a class="btn btn-warning btn-xs ' + rejectFlag + '" href="javascript:void(0)" onclick="rejectBorrow(\'' + row.orderId + '\')"><i class="fa fa-reply"></i>驳回</a>');
} else if (row.orderStatus === "BORROWING") {
actions.push('<a class="btn btn-primary btn-xs ' + applyReturnFlag + '" href="javascript:void(0)" onclick="$.operate.view(\'' + row.orderId + '\')"><i class="fa fa-sign-in"></i>归还申请</a>');
} else if (row.orderStatus === "PENDING_RETURN_CONFIRM") {
actions.push('<a class="btn btn-primary btn-xs ' + confirmReturnFlag + '" href="javascript:void(0)" onclick="$.operate.view(\'' + row.orderId + '\')"><i class="fa fa-check-square-o"></i>归还确认</a>');
}
return actions.join("");
}}
]
});
});
function submitBorrow(orderId) {
$.modal.confirm("提交后借用单不可再修改或删除,是否继续?", function() {
$.operate.post(prefix + "/submit/" + orderId, {});
});
}
function confirmBorrow(orderId) {
$.modal.confirm("确认后资产将借出并清空当前仓位,是否继续?", function() {
$.operate.post(prefix + "/confirm/" + orderId, {});
});
}
function rejectBorrow(orderId) {
layer.prompt({ title: "请输入驳回原因", formType: 2, maxlength: 500 }, function(text, index) {
layer.close(index);
$.operate.post(prefix + "/reject/" + orderId, { rejectReason: text });
});
}
</script>
</body>
</html>

@ -0,0 +1,220 @@
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:include="include :: header('修改借用单')" />
<th:block th:include="include :: datetimepicker-css" />
<style type="text/css">
table label.error { position: inherit; }
</style>
</head>
<body class="white-bg">
<div class="wrapper wrapper-content animated fadeInRight ibox-content">
<form class="form-horizontal m" id="form-borrow-edit" th:object="${amsBorrowOrder}">
<input name="orderId" th:field="*{orderId}" type="hidden">
<h4 class="form-header h4">基本信息</h4>
<div class="row">
<div class="col-sm-4">
<div class="form-group">
<label class="col-sm-4 control-label">借用单号:</label>
<div class="col-sm-8">
<input th:value="*{borrowNo}" class="form-control" type="text" readonly>
</div>
</div>
</div>
<div class="col-sm-4">
<div class="form-group">
<label class="col-sm-4 control-label">借用人:</label>
<div class="col-sm-8">
<input th:value="*{borrowUserName}" class="form-control" type="text" readonly>
</div>
</div>
</div>
<div class="col-sm-4">
<div class="form-group">
<label class="col-sm-4 control-label">借用部门:</label>
<div class="col-sm-8">
<input th:value="*{borrowDeptName}" class="form-control" type="text" readonly>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-4">
<div class="form-group">
<label class="col-sm-4 control-label is-required">预计归还:</label>
<div class="col-sm-8">
<input name="expectedReturnDate" th:value="${#dates.format(amsBorrowOrder.expectedReturnDate, 'yyyy-MM-dd')}"
class="form-control time-input" type="text" required>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<div class="form-group">
<label class="col-sm-2 control-label">备注:</label>
<div class="col-sm-10">
<textarea name="remark" maxlength="500" class="form-control" rows="3">[[*{remark}]]</textarea>
</div>
</div>
</div>
</div>
<h4 class="form-header h4">借用明细</h4>
<div class="row">
<div class="col-sm-12">
<button type="button" class="btn btn-white btn-sm" onclick="selectAssets()">
<i class="fa fa-plus"> 选择资产</i>
</button>
<button type="button" class="btn btn-white btn-sm" onclick="removeSelectedAssets()">
<i class="fa fa-minus"> 删除</i>
</button>
<div class="col-sm-12 select-table table-striped">
<table id="bootstrap-table"></table>
</div>
</div>
</div>
</form>
</div>
<th:block th:include="include :: footer" />
<th:block th:include="include :: datetimepicker-js" />
<script th:inline="javascript">
var prefix = ctx + "asset/borrow";
var orderId = [[${amsBorrowOrder.orderId}]];
var detailData = [[${amsBorrowOrder.amsBorrowOrderItemList}]];
$("#form-borrow-edit").validate({ focusCleanup: true });
$(function() {
initDetailTable(detailData || []);
});
function submitHandler() {
if ($("#bootstrap-table").bootstrapTable("getData").length === 0) {
$.modal.alertWarning("请至少保留一条借用明细");
return;
}
if ($.validate.form()) {
syncDetailRows();
$.operate.save(prefix + "/edit", $("#form-borrow-edit").serialize());
}
}
function initDetailTable(data) {
$.table.init({
data: data,
pagination: false,
showSearch: false,
showRefresh: false,
showToggle: false,
showColumns: false,
sidePagination: "client",
columns: [
{ checkbox: true },
{ field: "assetId", title: "资产", formatter: function(value, row, index) { return buildAssetCell(row, index); } },
{ field: "categoryName", title: "资产类别" },
{ field: "beforeWarehouseName", title: "借用前仓位", formatter: function(value, row) {
return $("<span>").text((row.beforeWarehouseName || "-") + " / " + (row.beforeLocationName || "-")).prop("outerHTML");
}},
{ field: "returnStatus", title: "归还状态", formatter: function(value) { return value || "-"; } },
{ field: "remark", title: "明细备注", formatter: function(value, row, index) {
return buildInput("amsBorrowOrderItemList[" + index + "].remark", value, 500);
}},
{ title: "操作", align: "center", formatter: function(value, row) {
return '<a class="btn btn-danger btn-xs" href="javascript:void(0)" onclick="removeAsset(\'' + row.assetId + '\')"><i class="fa fa-remove"></i>删除</a>';
}}
]
});
}
function selectAssets() {
syncDetailRows();
$.modal.openOptions({
title: "选择可借用资产",
url: prefix + "/selectAsset?orderId=" + orderId,
width: "1200",
height: "680",
callBack: addSelectedAssets
});
}
function addSelectedAssets(index, layero) {
var selectedAssets = layero.find("iframe")[0].contentWindow.getSelectedAssets();
if (!selectedAssets || selectedAssets.length === 0) {
$.modal.alertWarning("请至少选择一条资产记录");
return;
}
syncDetailRows();
var existing = {};
$.each($("#bootstrap-table").bootstrapTable("getData"), function(i, row) {
existing[String(row.assetId)] = true;
});
$.each(selectedAssets, function(i, asset) {
if (!existing[String(asset.assetId)]) {
$("#bootstrap-table").bootstrapTable("insertRow", {
index: $("#bootstrap-table").bootstrapTable("getData").length,
row: buildBorrowItem(asset)
});
existing[String(asset.assetId)] = true;
}
});
$.modal.close(index);
}
function buildBorrowItem(asset) {
return {
assetId: asset.assetId,
assetCode: asset.assetCode,
assetName: asset.assetName,
categoryName: asset.categoryName,
beforeWarehouseName: asset.warehouseName,
beforeLocationName: asset.locationName,
returnStatus: "BORROWING",
remark: ""
};
}
function removeSelectedAssets() {
var rows = $("#bootstrap-table").bootstrapTable("getSelections");
if (rows.length === 0) {
$.modal.alertWarning("请至少选择一条记录");
return;
}
syncDetailRows();
$("#bootstrap-table").bootstrapTable("remove", {
field: "assetId",
values: $.map(rows, function(row) { return row.assetId; })
});
}
function removeAsset(assetId) {
syncDetailRows();
$("#bootstrap-table").bootstrapTable("remove", { field: "assetId", values: [assetId] });
}
function syncDetailRows() {
var rows = $("#bootstrap-table").bootstrapTable("getData");
$.each(rows, function(index, row) {
var tr = $("#bootstrap-table tbody tr[data-index='" + index + "']");
row.remark = tr.find("[name$='.remark']").val() || "";
});
}
function buildAssetCell(row, index) {
var hidden = $("<input>").attr({
type: "hidden",
name: "amsBorrowOrderItemList[" + index + "].assetId",
value: row.assetId
}).prop("outerHTML");
return hidden + $("<span>").text(row.assetCode + " - " + row.assetName).prop("outerHTML");
}
function buildInput(name, value, maxLength) {
return $("<input>").addClass("form-control").attr({
type: "text",
name: name,
maxlength: maxLength
}).val(value || "").prop("outerHTML");
}
</script>
</body>
</html>

@ -0,0 +1,67 @@
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:include="include :: header('选择可借用资产')" />
</head>
<body class="gray-bg">
<div class="container-div">
<div class="row">
<div class="col-sm-12 search-collapse">
<form id="formId">
<div class="select-list">
<ul>
<li><label>资产编码:</label><input type="text" name="assetCode"></li>
<li><label>资产名称:</label><input type="text" name="assetName"></li>
<li><label>资产类别:</label><input type="text" name="categoryName"></li>
<li>
<a class="btn btn-primary btn-rounded btn-sm" onclick="$.table.search()">
<i class="fa fa-search"></i>&nbsp;搜索
</a>
<a class="btn btn-warning btn-rounded btn-sm" onclick="$.form.reset()">
<i class="fa fa-refresh"></i>&nbsp;重置
</a>
</li>
</ul>
</div>
</form>
</div>
<div class="col-sm-12 select-table table-striped">
<table id="bootstrap-table"></table>
</div>
</div>
</div>
<th:block th:include="include :: footer" />
<script th:inline="javascript">
var prefix = ctx + "asset/borrow";
var orderId = [[${orderId}]];
$(function() {
var url = prefix + "/availableAssetList";
if (orderId) {
url += "?orderId=" + orderId;
}
$.table.init({
url: url,
showSearch: false,
showRefresh: true,
showToggle: false,
showColumns: false,
modalName: "可借用资产",
columns: [
{ checkbox: true },
{ field: "assetCode", title: "资产编码" },
{ field: "assetName", title: "资产名称" },
{ field: "categoryName", title: "资产类别" },
{ field: "warehouseName", title: "当前仓库" },
{ field: "locationName", title: "当前位置" },
{ field: "tagCode", title: "RFID标签", formatter: function(value) { return value || "-"; } }
]
});
});
function getSelectedAssets() {
return $("#bootstrap-table").bootstrapTable("getSelections");
}
</script>
</body>
</html>

@ -0,0 +1,223 @@
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
<th:block th:include="include :: header('借用单详情')" />
</head>
<body>
<div class="main-content">
<form class="form-horizontal" th:object="${amsBorrowOrder}">
<h4 class="form-header h4">基本信息</h4>
<div class="row">
<div class="col-sm-6">
<div class="form-group">
<label class="col-sm-4 control-label">借用单号:</label>
<div class="col-sm-8"><p class="form-control-plaintext" th:text="*{borrowNo}"></p></div>
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label class="col-sm-4 control-label">借用人:</label>
<div class="col-sm-8"><p class="form-control-plaintext" th:text="*{borrowUserName}"></p></div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="form-group">
<label class="col-sm-4 control-label">借用部门:</label>
<div class="col-sm-8"><p class="form-control-plaintext" th:text="*{borrowDeptName}"></p></div>
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label class="col-sm-4 control-label">预计归还:</label>
<div class="col-sm-8">
<p class="form-control-plaintext" th:text="*{expectedReturnDate == null ? '' : #dates.format(expectedReturnDate, 'yyyy-MM-dd')}"></p>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="form-group">
<label class="col-sm-4 control-label">单据状态:</label>
<div class="col-sm-8"><p class="form-control-plaintext" th:text="*{@dict.getLabel('ams_borrow_status', orderStatus)}"></p></div>
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label class="col-sm-4 control-label">确认人:</label>
<div class="col-sm-8"><p class="form-control-plaintext" th:text="*{confirmUserName}"></p></div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-6">
<div class="form-group">
<label class="col-sm-4 control-label">确认时间:</label>
<div class="col-sm-8">
<p class="form-control-plaintext" th:text="*{confirmTime == null ? '' : #dates.format(confirmTime, 'yyyy-MM-dd HH:mm:ss')}"></p>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label class="col-sm-4 control-label">驳回原因:</label>
<div class="col-sm-8"><p class="form-control-plaintext" th:text="*{rejectReason}"></p></div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<div class="form-group">
<label class="col-sm-2 control-label">备注:</label>
<div class="col-sm-10"><p class="form-control-plaintext" th:text="*{remark}"></p></div>
</div>
</div>
</div>
<h4 class="form-header h4">借用明细</h4>
<div class="row">
<div class="col-sm-12 select-table table-striped">
<table class="table table-bordered">
<thead>
<tr>
<th>序号</th>
<th>资产</th>
<th>资产类别</th>
<th>借用前仓位</th>
<th>归还仓位</th>
<th>归还状态</th>
<th>实际归还</th>
<th>备注</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr th:each="item, stat : *{amsBorrowOrderItemList}">
<td th:text="${stat.count}"></td>
<td th:text="${item.assetCode + ' - ' + item.assetName}"></td>
<td th:text="${item.categoryName}"></td>
<td th:text="${(item.beforeWarehouseName ?: '-') + ' / ' + (item.beforeLocationName ?: '-')}"></td>
<td>
<div th:if="${item.returnStatus == 'BORROWING'}">
<select class="form-control return-warehouse" th:id="${'returnWarehouseId_' + item.itemId}"
th:data-item-id="${item.itemId}" onchange="refreshReturnLocations(this.getAttribute('data-item-id'))">
<option value="">请选择归还仓库</option>
<option th:each="warehouse : ${warehouseList}" th:value="${warehouse.warehouseId}"
th:text="${warehouse.warehouseCode + ' - ' + warehouse.warehouseName}"
th:selected="${warehouse.warehouseId == item.beforeWarehouseId}"></option>
</select>
<select class="form-control return-location" th:id="${'returnLocationId_' + item.itemId}"
th:data-default-location-id="${item.beforeLocationId}">
<option value="">请选择归还位置</option>
</select>
</div>
<span th:if="${item.returnStatus != 'BORROWING'}"
th:text="${(item.returnWarehouseName ?: '-') + ' / ' + (item.returnLocationName ?: '-')}"></span>
</td>
<td th:text="${@dict.getLabel('ams_borrow_status', item.returnStatus)}"></td>
<td th:text="${item.actualReturnDate == null ? '' : #dates.format(item.actualReturnDate, 'yyyy-MM-dd')}"></td>
<td th:text="${item.remark}"></td>
<td>
<a th:if="${item.returnStatus == 'BORROWING'}"
class="btn btn-primary btn-xs" shiro:hasPermission="asset:borrow:applyReturn"
href="javascript:void(0)" th:onclick="'applyReturn(' + ${item.itemId} + ')'">
<i class="fa fa-sign-in"></i>申请归还
</a>
<a th:if="${item.returnStatus == 'PENDING_RETURN_CONFIRM'}"
class="btn btn-primary btn-xs" shiro:hasPermission="asset:borrow:confirmReturn"
href="javascript:void(0)" th:onclick="'confirmReturn(' + ${item.itemId} + ')'">
<i class="fa fa-check"></i>确认归还
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</form>
</div>
<th:block th:include="include :: footer" />
<script th:inline="javascript">
var prefix = ctx + "asset/borrow";
var orderId = [[${amsBorrowOrder.orderId}]];
var locationList = [[${locationList}]];
$(function() {
$(".return-location").each(function() {
var itemId = this.id.replace("returnLocationId_", "");
refreshReturnLocations(itemId);
});
});
function refreshReturnLocations(itemId) {
var warehouseId = $("#returnWarehouseId_" + itemId).val();
var locationSelect = $("#returnLocationId_" + itemId);
var defaultLocationId = locationSelect.attr("data-default-location-id");
locationSelect.empty().append($("<option>").val("").text("请选择归还位置"));
$.each(locationList, function(i, location) {
if (String(location.warehouseId) === String(warehouseId)) {
var option = $("<option>").val(location.locationId).text(location.locationCode + " - " + location.locationName);
if (String(location.locationId) === String(defaultLocationId)) {
option.attr("selected", "selected");
}
locationSelect.append(option);
}
});
}
function applyReturn(itemId) {
var warehouseId = $("#returnWarehouseId_" + itemId).val();
var locationId = $("#returnLocationId_" + itemId).val();
if (!warehouseId || !locationId) {
$.modal.alertWarning("请选择归还仓库和归还位置");
return;
}
$.modal.confirm("提交后该明细将进入待归还确认状态,是否继续?", function() {
postJson(prefix + "/applyReturn/" + orderId, [{
itemId: itemId,
returnWarehouseId: warehouseId,
returnLocationId: locationId
}]);
});
}
function confirmReturn(itemId) {
$.modal.confirm("确认后该资产将重新入库到申请归还时保存的仓位,是否继续?", function() {
postJson(prefix + "/confirmReturn/" + orderId, [{ itemId: itemId }]);
});
}
function postJson(url, payload) {
$.ajax({
url: url,
type: "post",
contentType: "application/json",
dataType: "json",
data: JSON.stringify(payload),
beforeSend: function(xhr) {
var token = $("meta[name='csrf-token']").attr("content");
if (token) {
xhr.setRequestHeader("X-CSRF-Token", token);
}
$.modal.loading("正在处理中,请稍候...");
},
complete: function() {
$.modal.closeLoading();
},
success: function(result) {
if (result.code === web_status.SUCCESS) {
$.modal.msgSuccess(result.msg);
if (parent && parent.$ && parent.$.table) {
parent.$.table.refresh();
}
window.location.reload();
} else {
$.modal.alertError(result.msg);
}
}
});
}
</script>
</body>
</html>

@ -0,0 +1,559 @@
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.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.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import com.ruoyi.asset.constant.AssetStatus;
import com.ruoyi.asset.constant.BorrowOrderStatus;
import com.ruoyi.asset.domain.AmsAsset;
import com.ruoyi.asset.domain.AmsAssetLocation;
import com.ruoyi.asset.domain.AmsBorrowOrder;
import com.ruoyi.asset.domain.AmsBorrowOrderItem;
import com.ruoyi.asset.domain.AmsWarehouse;
import com.ruoyi.asset.domain.AssetTransitionContext;
import com.ruoyi.asset.mapper.AmsAssetMapper;
import com.ruoyi.asset.mapper.AmsBorrowOrderMapper;
import com.ruoyi.asset.service.IAssetStatusTransitionService;
import com.ruoyi.asset.service.IAmsWarehouseService;
import com.ruoyi.asset.service.IAmsAssetLocationService;
import com.ruoyi.common.core.domain.entity.SysDept;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.system.service.ISysCodeRuleService;
import com.ruoyi.system.service.ISysDeptService;
import com.ruoyi.system.service.ISysUserService;
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;
/**
*
*
*
*
* @author Yangk
* @date 2026-06-16
*/
@ExtendWith(MockitoExtension.class)
class AmsBorrowOrderServiceImplTest
{
@Mock
private AmsBorrowOrderMapper amsBorrowOrderMapper;
@Mock
private AmsAssetMapper amsAssetMapper;
@Mock
private ISysCodeRuleService sysCodeRuleService;
@Mock
private ISysDeptService sysDeptService;
@Mock
private ISysUserService sysUserService;
@Mock
private IAssetStatusTransitionService assetStatusTransitionService;
@Mock
private IAmsWarehouseService amsWarehouseService;
@Mock
private IAmsAssetLocationService amsAssetLocationService;
@InjectMocks
private AmsBorrowOrderServiceImpl service;
/**
* 稿
*
* 1. JY
* 2. DRAFT
* 3.
*/
@Test
void insertShouldGenerateCodeAndFillSnapshots()
{
AmsBorrowOrder order = buildRequest();
stubBorrower(); // 模拟借用责任人
stubStockAsset(); // 模拟资产正处于在库状态
when(sysCodeRuleService.nextCode("BORROW_ORDER")).thenReturn("JY202606160001");
doAnswer(invocation -> {
AmsBorrowOrder inserted = invocation.getArgument(0);
inserted.setOrderId(100L);
return 1;
}).when(amsBorrowOrderMapper).insertAmsBorrowOrder(any(AmsBorrowOrder.class));
when(amsBorrowOrderMapper.batchAmsBorrowOrderItem(anyList()))
.thenAnswer(invocation -> ((List<?>) invocation.getArgument(0)).size());
assertEquals(1, service.insertAmsBorrowOrder(order));
AmsBorrowOrderItem item = order.getAmsBorrowOrderItemList().get(0);
assertEquals("JY202606160001", order.getBorrowNo());
assertEquals(BorrowOrderStatus.DRAFT, order.getOrderStatus());
assertEquals("WH-001", item.getBeforeWarehouseCode());
assertEquals("LOC-001", item.getBeforeLocationCode());
assertEquals(BorrowOrderStatus.BORROWING, item.getReturnStatus());
assertNotNull(item.getCreateTime());
assertNull(order.getRejectReason());
}
/**
* 稿
*
*/
@Test
void insertShouldRejectAssetOccupiedByOtherOrder()
{
AmsBorrowOrder order = buildRequest();
stubBorrower();
stubStockAsset();
when(sysCodeRuleService.nextCode("BORROW_ORDER")).thenReturn("JY202606160001");
// 模拟资产已被其他借用单占用
when(amsBorrowOrderMapper.countOtherActiveBorrowOrderByAssetId(1L, null,
BorrowOrderStatus.DRAFT, BorrowOrderStatus.PENDING_CONFIRM)).thenReturn(1);
ServiceException exception = assertThrows(ServiceException.class,
() -> service.insertAmsBorrowOrder(order));
assertTrue(exception.getMessage().contains("已被其他未完成借用单占用"));
verify(amsBorrowOrderMapper, never()).insertAmsBorrowOrder(any(AmsBorrowOrder.class));
}
/**
* 稿
* PENDING_CONFIRM
*/
@Test
void submitShouldMoveDraftToPendingConfirm()
{
AmsBorrowOrder order = buildPersistedOrder(BorrowOrderStatus.DRAFT);
when(amsBorrowOrderMapper.selectAmsBorrowOrderByOrderIdForUpdate(100L)).thenReturn(order);
stubStockAsset();
when(amsBorrowOrderMapper.submitAmsBorrowOrder(any(AmsBorrowOrder.class))).thenReturn(1);
assertEquals(1, service.submitBorrow(100L, "admin"));
assertEquals(BorrowOrderStatus.PENDING_CONFIRM, order.getOrderStatus());
verify(amsBorrowOrderMapper).submitAmsBorrowOrder(order);
}
/**
*
*
*/
@Test
void submitShouldRejectDirtySnapshot()
{
AmsBorrowOrder order = buildPersistedOrder(BorrowOrderStatus.DRAFT);
when(amsBorrowOrderMapper.selectAmsBorrowOrderByOrderIdForUpdate(100L)).thenReturn(order);
// 模拟物理仓位已被移动 (返回的仓库ID与明细的10L不符)
AmsAsset asset = new AmsAsset();
asset.setAssetId(1L);
asset.setAssetCode("A001");
asset.setAssetStatus(AssetStatus.IN_STOCK);
asset.setWarehouseId(99L);
asset.setLocationId(20L);
asset.setDelFlag("0");
when(amsAssetMapper.selectAmsAssetByAssetIdForUpdate(1L)).thenReturn(asset);
ServiceException exception = assertThrows(ServiceException.class,
() -> service.submitBorrow(100L, "admin"));
assertTrue(exception.getMessage().contains("物理存放仓位已发生变更"));
verify(amsBorrowOrderMapper, never()).submitAmsBorrowOrder(any(AmsBorrowOrder.class));
}
/**
*
* confirmBorrow BORROWING
*/
@Test
void confirmShouldDelegateTransitionAndCompleteBorrowing()
{
AmsBorrowOrder order = buildPersistedOrder(BorrowOrderStatus.PENDING_CONFIRM);
when(amsBorrowOrderMapper.selectAmsBorrowOrderByOrderIdForUpdate(100L)).thenReturn(order);
stubStockAsset();
when(amsBorrowOrderMapper.confirmAmsBorrowOrder(any(AmsBorrowOrder.class))).thenReturn(1);
assertEquals(1, service.confirmBorrow(100L, 1L, "确认人", "admin"));
ArgumentCaptor<AssetTransitionContext> contextCaptor = ArgumentCaptor.forClass(AssetTransitionContext.class);
verify(assetStatusTransitionService).confirmBorrow(
org.mockito.ArgumentMatchers.eq(1L),
contextCaptor.capture());
assertEquals(100L, contextCaptor.getValue().getSourceOrderId());
assertEquals(101L, contextCaptor.getValue().getSourceItemId());
assertEquals(BorrowOrderStatus.BORROWING, order.getOrderStatus());
assertEquals("确认人", order.getConfirmUserName());
}
/**
*
* PENDING_RETURN_CONFIRM PENDING_RETURN_CONFIRM
*/
@Test
void applyReturnShouldSetStatusAndWarehouse()
{
AmsBorrowOrder order = buildPersistedOrder(BorrowOrderStatus.BORROWING);
when(amsBorrowOrderMapper.selectAmsBorrowOrderByOrderIdForUpdate(100L)).thenReturn(order);
when(amsBorrowOrderMapper.selectAmsBorrowOrderItemByOrderIdAndItemIdForUpdate(100L, 101L))
.thenReturn(buildBorrowItem(BorrowOrderStatus.BORROWING));
// 模拟处于借出状态下的资产
AmsAsset asset = new AmsAsset();
asset.setAssetId(1L);
asset.setAssetCode("A001");
asset.setAssetStatus(AssetStatus.BORROWED);
asset.setDelFlag("0");
when(amsAssetMapper.selectAmsAssetByAssetIdForUpdate(1L)).thenReturn(asset);
// 模拟要归还的目标仓位
AmsWarehouse wh = new AmsWarehouse();
wh.setWarehouseId(10L);
wh.setWarehouseCode("WH-001");
wh.setWarehouseName("一号仓");
wh.setEnabled("Y");
when(amsWarehouseService.selectAmsWarehouseByWarehouseId(10L)).thenReturn(wh);
AmsAssetLocation loc = new AmsAssetLocation();
loc.setLocationId(20L);
loc.setLocationCode("LOC-001");
loc.setLocationName("货架01");
loc.setWarehouseId(10L);
loc.setEnabled("Y");
when(amsAssetLocationService.selectAmsAssetLocationByLocationId(20L)).thenReturn(loc);
// 模拟更新明细和主单状态
when(amsBorrowOrderMapper.updateAmsBorrowOrderItemReturnStatus(any(AmsBorrowOrderItem.class),
org.mockito.ArgumentMatchers.eq(BorrowOrderStatus.BORROWING))).thenReturn(1);
when(amsBorrowOrderMapper.updateAmsBorrowOrderStatus(
org.mockito.ArgumentMatchers.eq(100L),
org.mockito.ArgumentMatchers.eq(BorrowOrderStatus.PENDING_RETURN_CONFIRM),
any(), any())).thenReturn(1);
// 构造归还申请明细参数
List<AmsBorrowOrderItem> applyList = new ArrayList<>();
AmsBorrowOrderItem applyItem = new AmsBorrowOrderItem();
applyItem.setItemId(101L);
applyItem.setAssetId(999L);
applyItem.setReturnWarehouseId(10L);
applyItem.setReturnLocationId(20L);
applyList.add(applyItem);
assertEquals(1, service.applyReturn(100L, applyList, "admin"));
// 验证快照正确回填
ArgumentCaptor<AmsBorrowOrderItem> itemCaptor = ArgumentCaptor.forClass(AmsBorrowOrderItem.class);
verify(amsBorrowOrderMapper).updateAmsBorrowOrderItemReturnStatus(itemCaptor.capture(),
org.mockito.ArgumentMatchers.eq(BorrowOrderStatus.BORROWING));
assertEquals(1L, itemCaptor.getValue().getAssetId());
assertEquals("WH-001", itemCaptor.getValue().getReturnWarehouseCode());
assertEquals("LOC-001", itemCaptor.getValue().getReturnLocationCode());
assertEquals(BorrowOrderStatus.PENDING_RETURN_CONFIRM, itemCaptor.getValue().getReturnStatus());
verify(amsAssetMapper).selectAmsAssetByAssetIdForUpdate(1L);
verify(amsAssetMapper, never()).selectAmsAssetByAssetIdForUpdate(999L);
}
/**
*
*
*/
@Test
void applyReturnShouldRejectItemNotBelongToOrder()
{
AmsBorrowOrder order = buildPersistedOrder(BorrowOrderStatus.BORROWING);
when(amsBorrowOrderMapper.selectAmsBorrowOrderByOrderIdForUpdate(100L)).thenReturn(order);
when(amsBorrowOrderMapper.selectAmsBorrowOrderItemByOrderIdAndItemIdForUpdate(100L, 999L))
.thenReturn(null);
AmsBorrowOrderItem applyItem = new AmsBorrowOrderItem();
applyItem.setItemId(999L);
applyItem.setReturnWarehouseId(10L);
applyItem.setReturnLocationId(20L);
ServiceException exception = assertThrows(ServiceException.class,
() -> service.applyReturn(100L, Collections.singletonList(applyItem), "admin"));
assertTrue(exception.getMessage().contains("归还明细不存在"));
verify(amsAssetMapper, never()).selectAmsAssetByAssetIdForUpdate(any());
verify(amsBorrowOrderMapper, never()).updateAmsBorrowOrderItemReturnStatus(any(), any());
}
/**
*
* IN_STOCK BORROW_RETURNED
*/
@Test
void confirmReturnShouldCompleteOrderIfAllReturned()
{
AmsBorrowOrder order = buildPersistedOrder(BorrowOrderStatus.PENDING_RETURN_CONFIRM);
when(amsBorrowOrderMapper.selectAmsBorrowOrderByOrderIdForUpdate(100L)).thenReturn(order);
when(amsBorrowOrderMapper.selectAmsBorrowOrderItemByOrderIdAndItemIdForUpdate(100L, 101L))
.thenReturn(buildPendingReturnItem());
AmsAsset asset = new AmsAsset();
asset.setAssetId(1L);
asset.setAssetCode("A001");
asset.setAssetStatus(AssetStatus.BORROWED);
asset.setDelFlag("0");
when(amsAssetMapper.selectAmsAssetByAssetIdForUpdate(1L)).thenReturn(asset);
when(amsBorrowOrderMapper.updateAmsBorrowOrderItemReturnStatus(any(AmsBorrowOrderItem.class),
org.mockito.ArgumentMatchers.eq(BorrowOrderStatus.PENDING_RETURN_CONFIRM))).thenReturn(1);
// 模拟查询明细列表
List<AmsBorrowOrderItem> details = new ArrayList<>();
AmsBorrowOrderItem detailItem = new AmsBorrowOrderItem();
detailItem.setItemId(101L);
detailItem.setAssetId(1L);
detailItem.setReturnWarehouseId(10L);
detailItem.setReturnLocationId(20L);
// 这里模拟这行在确认后变成了已归还
detailItem.setReturnStatus(BorrowOrderStatus.BORROW_RETURNED);
details.add(detailItem);
when(amsBorrowOrderMapper.selectAmsBorrowOrderItemList(100L)).thenReturn(details);
when(amsBorrowOrderMapper.updateAmsBorrowOrderStatus(
org.mockito.ArgumentMatchers.eq(100L),
org.mockito.ArgumentMatchers.eq(BorrowOrderStatus.BORROW_RETURNED),
any(), any())).thenReturn(1);
// 构造确认归还明细参数
List<AmsBorrowOrderItem> confirmList = new ArrayList<>();
AmsBorrowOrderItem confirmItem = new AmsBorrowOrderItem();
confirmItem.setItemId(101L);
confirmItem.setAssetId(999L);
confirmItem.setReturnWarehouseId(99L);
confirmItem.setReturnLocationId(98L);
confirmList.add(confirmItem);
assertEquals(1, service.confirmReturn(100L, confirmList, 1L, "确认人", "admin"));
// 验证执行了公共状态流转服务 confirmBorrowReturn
verify(assetStatusTransitionService).confirmBorrowReturn(
org.mockito.ArgumentMatchers.eq(1L),
org.mockito.ArgumentMatchers.eq(10L),
org.mockito.ArgumentMatchers.eq(20L),
any(AssetTransitionContext.class));
verify(amsAssetMapper).selectAmsAssetByAssetIdForUpdate(1L);
verify(amsAssetMapper, never()).selectAmsAssetByAssetIdForUpdate(999L);
}
/**
*
* 退 BORROWING
*/
@Test
void confirmReturnShouldKeepBorrowingIfPartiallyReturned()
{
AmsBorrowOrder order = buildPersistedOrder(BorrowOrderStatus.PENDING_RETURN_CONFIRM);
when(amsBorrowOrderMapper.selectAmsBorrowOrderByOrderIdForUpdate(100L)).thenReturn(order);
when(amsBorrowOrderMapper.selectAmsBorrowOrderItemByOrderIdAndItemIdForUpdate(100L, 101L))
.thenReturn(buildPendingReturnItem());
AmsAsset asset = new AmsAsset();
asset.setAssetId(1L);
asset.setAssetCode("A001");
asset.setAssetStatus(AssetStatus.BORROWED);
asset.setDelFlag("0");
when(amsAssetMapper.selectAmsAssetByAssetIdForUpdate(1L)).thenReturn(asset);
when(amsBorrowOrderMapper.updateAmsBorrowOrderItemReturnStatus(any(AmsBorrowOrderItem.class),
org.mockito.ArgumentMatchers.eq(BorrowOrderStatus.PENDING_RETURN_CONFIRM))).thenReturn(1);
// 模拟该借用单有两行,一行已归还,另一行仍是 BORROWING借出状态中
List<AmsBorrowOrderItem> details = new ArrayList<>();
AmsBorrowOrderItem detailItem1 = new AmsBorrowOrderItem();
detailItem1.setItemId(101L);
detailItem1.setAssetId(1L);
detailItem1.setReturnStatus(BorrowOrderStatus.BORROW_RETURNED);
details.add(detailItem1);
AmsBorrowOrderItem detailItem2 = new AmsBorrowOrderItem();
detailItem2.setItemId(102L);
detailItem2.setAssetId(2L);
detailItem2.setReturnStatus(BorrowOrderStatus.BORROWING);
details.add(detailItem2);
when(amsBorrowOrderMapper.selectAmsBorrowOrderItemList(100L)).thenReturn(details);
// 此时主表状态联动退回到 BORROWING
when(amsBorrowOrderMapper.updateAmsBorrowOrderStatus(
org.mockito.ArgumentMatchers.eq(100L),
org.mockito.ArgumentMatchers.eq(BorrowOrderStatus.BORROWING),
any(), any())).thenReturn(1);
List<AmsBorrowOrderItem> confirmList = new ArrayList<>();
AmsBorrowOrderItem confirmItem = new AmsBorrowOrderItem();
confirmItem.setItemId(101L);
confirmList.add(confirmItem);
assertEquals(1, service.confirmReturn(100L, confirmList, 1L, "确认人", "admin"));
}
/**
*
*
*/
@Test
void confirmReturnShouldRejectMissingStoredReturnLocation()
{
AmsBorrowOrder order = buildPersistedOrder(BorrowOrderStatus.PENDING_RETURN_CONFIRM);
when(amsBorrowOrderMapper.selectAmsBorrowOrderByOrderIdForUpdate(100L)).thenReturn(order);
AmsBorrowOrderItem lockedItem = buildBorrowItem(BorrowOrderStatus.PENDING_RETURN_CONFIRM);
lockedItem.setReturnWarehouseId(null);
lockedItem.setReturnLocationId(null);
when(amsBorrowOrderMapper.selectAmsBorrowOrderItemByOrderIdAndItemIdForUpdate(100L, 101L))
.thenReturn(lockedItem);
AmsBorrowOrderItem confirmItem = new AmsBorrowOrderItem();
confirmItem.setItemId(101L);
confirmItem.setReturnWarehouseId(10L);
confirmItem.setReturnLocationId(20L);
ServiceException exception = assertThrows(ServiceException.class,
() -> service.confirmReturn(100L, Collections.singletonList(confirmItem),
1L, "确认人", "admin"));
assertTrue(exception.getMessage().contains("缺少已申请的归还仓库或位置"));
verify(assetStatusTransitionService, never()).confirmBorrowReturn(any(), any(), any(), any());
verify(amsBorrowOrderMapper, never()).updateAmsBorrowOrderItemReturnStatus(any(), any());
}
private AmsBorrowOrder buildRequest()
{
AmsBorrowOrder order = new AmsBorrowOrder();
order.setBorrowUserId(2L);
order.setBorrowDeptId(103L);
Calendar cal = Calendar.getInstance();
cal.add(Calendar.DAY_OF_YEAR, 5); // 5天后
order.setExpectedReturnDate(cal.getTime());
order.setRemark("测试借用单");
List<AmsBorrowOrderItem> itemList = new ArrayList<>();
AmsBorrowOrderItem item = new AmsBorrowOrderItem();
item.setAssetId(1L);
itemList.add(item);
order.setAmsBorrowOrderItemList(itemList);
return order;
}
private AmsBorrowOrder buildPersistedOrder(String status)
{
AmsBorrowOrder order = buildRequest();
order.setOrderId(100L);
order.setBorrowNo("JY202606160001");
order.setOrderStatus(status);
order.setBorrowUserName("借用用户");
order.setBorrowDeptName("生产部门");
AmsBorrowOrderItem item = order.getAmsBorrowOrderItemList().get(0);
item.setItemId(101L);
item.setBorrowNo("JY202606160001");
item.setBeforeWarehouseId(10L);
item.setBeforeLocationId(20L);
return order;
}
private AmsBorrowOrderItem buildBorrowItem(String returnStatus)
{
AmsBorrowOrderItem item = new AmsBorrowOrderItem();
item.setItemId(101L);
item.setOrderId(100L);
item.setBorrowNo("JY202606160001");
item.setAssetId(1L);
item.setAssetCode("A001");
item.setAssetName("一号资产");
item.setBeforeWarehouseId(10L);
item.setBeforeWarehouseCode("WH-001");
item.setBeforeWarehouseName("一号仓");
item.setBeforeLocationId(20L);
item.setBeforeLocationCode("LOC-001");
item.setBeforeLocationName("货架01");
item.setReturnStatus(returnStatus);
item.setDelFlag("0");
return item;
}
private AmsBorrowOrderItem buildPendingReturnItem()
{
AmsBorrowOrderItem item = buildBorrowItem(BorrowOrderStatus.PENDING_RETURN_CONFIRM);
item.setReturnWarehouseId(10L);
item.setReturnWarehouseCode("WH-001");
item.setReturnWarehouseName("一号仓");
item.setReturnLocationId(20L);
item.setReturnLocationCode("LOC-001");
item.setReturnLocationName("货架01");
return item;
}
private void stubBorrower()
{
SysUser user = new SysUser();
user.setUserId(2L);
user.setUserName("借用用户");
user.setDeptId(103L);
user.setStatus("0");
user.setDelFlag("0");
when(sysUserService.selectUserById(2L)).thenReturn(user);
SysDept dept = new SysDept();
dept.setDeptId(103L);
dept.setDeptName("生产部门");
dept.setStatus("0");
dept.setDelFlag("0");
when(sysDeptService.selectDeptById(103L)).thenReturn(dept);
}
private void stubStockAsset()
{
AmsAsset asset = new AmsAsset();
asset.setAssetId(1L);
asset.setAssetCode("A001");
asset.setAssetName("一号资产");
asset.setCategoryId(5L);
asset.setCategoryCode("CAT-001");
asset.setCategoryName("类别一");
asset.setWarehouseId(10L);
asset.setWarehouseCode("WH-001");
asset.setWarehouseName("一号仓");
asset.setLocationId(20L);
asset.setLocationCode("LOC-001");
asset.setLocationName("货架01");
asset.setAssetStatus(AssetStatus.IN_STOCK);
asset.setDelFlag("0");
when(amsAssetMapper.selectAmsAssetByAssetIdForUpdate(1L)).thenReturn(asset);
}
}
Loading…
Cancel
Save