From 5fae393ac090d005158574c4c5ff4f31a27b80c8 Mon Sep 17 00:00:00 2001 From: yangk Date: Fri, 26 Jun 2026 17:09:11 +0800 Subject: [PATCH] =?UTF-8?q?feat(asset):=20=E6=96=B0=E5=A2=9E=E8=B5=84?= =?UTF-8?q?=E4=BA=A7=E5=BA=93=E5=AD=98=E6=98=8E=E7=BB=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 创建 AmsAssetStock 实体类用于库存汇总统计 - 实现 AmsAssetStockController 提供库存查询和导出功能 - 开发 AmsAssetStockMapper 和 XML 映射文件进行数据库操作 - 添加 AmsAssetStockService 业务逻辑处理层 - 集成前端 stock.html 页面实现库存汇总和明细查看 - 支持按分类、名称、仓库、状态等条件筛选库存 - 实现点击汇总行查看对应资产明细的功能 - 添加单元测试验证库存查询逻辑的正确性 --- .../controller/AmsAssetStockController.java | 117 +++++++++ .../com/ruoyi/asset/domain/AmsAssetStock.java | 241 ++++++++++++++++++ .../asset/mapper/AmsAssetStockMapper.java | 29 +++ .../asset/service/IAmsAssetStockService.java | 29 +++ .../impl/AmsAssetStockServiceImpl.java | 58 +++++ .../mapper/asset/AmsAssetStockMapper.xml | 127 +++++++++ .../templates/asset/stock/stock.html | 230 +++++++++++++++++ .../impl/AmsAssetStockServiceImplTest.java | 101 ++++++++ 8 files changed, 932 insertions(+) create mode 100644 ruoyi-asset/src/main/java/com/ruoyi/asset/controller/AmsAssetStockController.java create mode 100644 ruoyi-asset/src/main/java/com/ruoyi/asset/domain/AmsAssetStock.java create mode 100644 ruoyi-asset/src/main/java/com/ruoyi/asset/mapper/AmsAssetStockMapper.java create mode 100644 ruoyi-asset/src/main/java/com/ruoyi/asset/service/IAmsAssetStockService.java create mode 100644 ruoyi-asset/src/main/java/com/ruoyi/asset/service/impl/AmsAssetStockServiceImpl.java create mode 100644 ruoyi-asset/src/main/resources/mapper/asset/AmsAssetStockMapper.xml create mode 100644 ruoyi-asset/src/main/resources/templates/asset/stock/stock.html create mode 100644 ruoyi-asset/src/test/java/com/ruoyi/asset/service/impl/AmsAssetStockServiceImplTest.java diff --git a/ruoyi-asset/src/main/java/com/ruoyi/asset/controller/AmsAssetStockController.java b/ruoyi-asset/src/main/java/com/ruoyi/asset/controller/AmsAssetStockController.java new file mode 100644 index 0000000..e43c7e8 --- /dev/null +++ b/ruoyi-asset/src/main/java/com/ruoyi/asset/controller/AmsAssetStockController.java @@ -0,0 +1,117 @@ +package com.ruoyi.asset.controller; + +import java.util.List; +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.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import com.ruoyi.asset.constant.AssetStatus; +import com.ruoyi.asset.domain.AmsAsset; +import com.ruoyi.asset.domain.AmsAssetCategory; +import com.ruoyi.asset.domain.AmsAssetStock; +import com.ruoyi.asset.domain.AmsWarehouse; +import com.ruoyi.asset.service.IAmsAssetCategoryService; +import com.ruoyi.asset.service.IAmsAssetStockService; +import com.ruoyi.asset.service.IAmsWarehouseService; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.poi.ExcelUtil; + +/** + * 资产库存明细Controller + * + * @author Yangk + */ +@Controller +@RequestMapping("/asset/stock") +public class AmsAssetStockController extends BaseController +{ + private static final String ENABLED_YES = "Y"; + + private String prefix = "asset/stock"; + + @Autowired + private IAmsAssetStockService amsAssetStockService; + + @Autowired + private IAmsAssetCategoryService amsAssetCategoryService; + + @Autowired + private IAmsWarehouseService amsWarehouseService; + + @RequiresPermissions("asset:stock:view") + @GetMapping() + public String stock(ModelMap mmap) + { + putStockOptions(mmap); + mmap.put("defaultAssetStatus", AssetStatus.IN_STOCK); + return prefix + "/stock"; + } + + /** + * 查询资产库存汇总列表。 + */ + @RequiresPermissions("asset:stock:list") + @PostMapping("/list") + @ResponseBody + public TableDataInfo list(AmsAssetStock amsAssetStock) + { + startPage(); + List list = amsAssetStockService.selectAmsAssetStockList(amsAssetStock); + return getDataTable(list); + } + + /** + * 查询库存汇总行下的一物一码资产明细。 + */ + @RequiresPermissions("asset:stock:detail") + @PostMapping("/detail") + @ResponseBody + public TableDataInfo detail(AmsAssetStock amsAssetStock) + { + startPage(); + List list = amsAssetStockService.selectAmsAssetStockDetailList(amsAssetStock); + return getDataTable(list); + } + + /** + * 导出资产库存汇总列表。 + */ + @RequiresPermissions("asset:stock:export") + @Log(title = "资产库存明细", businessType = BusinessType.EXPORT) + @PostMapping("/export") + @ResponseBody + public AjaxResult export(AmsAssetStock amsAssetStock) + { + List list = amsAssetStockService.selectAmsAssetStockList(amsAssetStock); + ExcelUtil util = new ExcelUtil(AmsAssetStock.class); + return util.exportExcel(list, "资产库存明细数据"); + } + + private void putStockOptions(ModelMap mmap) + { + mmap.put("categoryList", selectEnabledCategoryList()); + mmap.put("warehouseList", selectEnabledWarehouseList()); + } + + private List selectEnabledCategoryList() + { + AmsAssetCategory category = new AmsAssetCategory(); + category.setEnabled(ENABLED_YES); + return amsAssetCategoryService.selectAmsAssetCategoryList(category); + } + + private List selectEnabledWarehouseList() + { + AmsWarehouse warehouse = new AmsWarehouse(); + warehouse.setEnabled(ENABLED_YES); + return amsWarehouseService.selectAmsWarehouseList(warehouse); + } +} diff --git a/ruoyi-asset/src/main/java/com/ruoyi/asset/domain/AmsAssetStock.java b/ruoyi-asset/src/main/java/com/ruoyi/asset/domain/AmsAssetStock.java new file mode 100644 index 0000000..fa8eca8 --- /dev/null +++ b/ruoyi-asset/src/main/java/com/ruoyi/asset/domain/AmsAssetStock.java @@ -0,0 +1,241 @@ +package com.ruoyi.asset.domain; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.annotation.Excel.ColumnType; +import com.ruoyi.common.core.domain.BaseEntity; + +/** + * 资产库存明细汇总对象。 + * + * @author Yangk + */ +public class AmsAssetStock extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + /** 内部汇总键,用于下钻明细时精确匹配空值维度 */ + private String groupKey; + + /** 资产类别ID */ + private Long categoryId; + + /** 类别编码 */ + private String categoryCode; + + /** 类别名称 */ + @Excel(name = "资产类别") + private String categoryName; + + /** 资产名称 */ + @Excel(name = "资产名称") + private String assetName; + + /** 规格型号 */ + @Excel(name = "规格型号") + private String specModel; + + /** 品牌 */ + @Excel(name = "品牌") + private String brand; + + /** 仓库ID */ + private Long warehouseId; + + /** 仓库编码 */ + private String warehouseCode; + + /** 仓库名称 */ + @Excel(name = "所属仓库") + private String warehouseName; + + /** 位置ID */ + private Long locationId; + + /** 位置编码 */ + private String locationCode; + + /** 位置名称 */ + private String locationName; + + /** 资产状态 */ + @Excel(name = "资产状态", dictType = "ams_asset_status") + private String assetStatus; + + /** 库存数量 */ + @Excel(name = "库存数量", cellType = ColumnType.NUMERIC) + private Long stockQuantity; + + public String getGroupKey() + { + return groupKey; + } + + public void setGroupKey(String groupKey) + { + this.groupKey = groupKey; + } + + public Long getCategoryId() + { + return categoryId; + } + + public void setCategoryId(Long categoryId) + { + this.categoryId = categoryId; + } + + public String getCategoryCode() + { + return categoryCode; + } + + public void setCategoryCode(String categoryCode) + { + this.categoryCode = categoryCode; + } + + public String getCategoryName() + { + return categoryName; + } + + public void setCategoryName(String categoryName) + { + this.categoryName = categoryName; + } + + public String getAssetName() + { + return assetName; + } + + public void setAssetName(String assetName) + { + this.assetName = assetName; + } + + public String getSpecModel() + { + return specModel; + } + + public void setSpecModel(String specModel) + { + this.specModel = specModel; + } + + public String getBrand() + { + return brand; + } + + public void setBrand(String brand) + { + this.brand = brand; + } + + public Long getWarehouseId() + { + return warehouseId; + } + + public void setWarehouseId(Long warehouseId) + { + this.warehouseId = warehouseId; + } + + public String getWarehouseCode() + { + return warehouseCode; + } + + public void setWarehouseCode(String warehouseCode) + { + this.warehouseCode = warehouseCode; + } + + public String getWarehouseName() + { + return warehouseName; + } + + public void setWarehouseName(String warehouseName) + { + this.warehouseName = warehouseName; + } + + public Long getLocationId() + { + return locationId; + } + + public void setLocationId(Long locationId) + { + this.locationId = locationId; + } + + public String getLocationCode() + { + return locationCode; + } + + public void setLocationCode(String locationCode) + { + this.locationCode = locationCode; + } + + public String getLocationName() + { + return locationName; + } + + public void setLocationName(String locationName) + { + this.locationName = locationName; + } + + public String getAssetStatus() + { + return assetStatus; + } + + public void setAssetStatus(String assetStatus) + { + this.assetStatus = assetStatus; + } + + public Long getStockQuantity() + { + return stockQuantity; + } + + public void setStockQuantity(Long stockQuantity) + { + this.stockQuantity = stockQuantity; + } + + @Override + public String toString() + { + return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE) + .append("groupKey", getGroupKey()) + .append("categoryId", getCategoryId()) + .append("categoryCode", getCategoryCode()) + .append("categoryName", getCategoryName()) + .append("assetName", getAssetName()) + .append("specModel", getSpecModel()) + .append("brand", getBrand()) + .append("warehouseId", getWarehouseId()) + .append("warehouseCode", getWarehouseCode()) + .append("warehouseName", getWarehouseName()) + .append("locationId", getLocationId()) + .append("locationCode", getLocationCode()) + .append("locationName", getLocationName()) + .append("assetStatus", getAssetStatus()) + .append("stockQuantity", getStockQuantity()) + .toString(); + } +} diff --git a/ruoyi-asset/src/main/java/com/ruoyi/asset/mapper/AmsAssetStockMapper.java b/ruoyi-asset/src/main/java/com/ruoyi/asset/mapper/AmsAssetStockMapper.java new file mode 100644 index 0000000..bda27c6 --- /dev/null +++ b/ruoyi-asset/src/main/java/com/ruoyi/asset/mapper/AmsAssetStockMapper.java @@ -0,0 +1,29 @@ +package com.ruoyi.asset.mapper; + +import java.util.List; +import com.ruoyi.asset.domain.AmsAsset; +import com.ruoyi.asset.domain.AmsAssetStock; + +/** + * 资产库存明细Mapper接口 + * + * @author Yangk + */ +public interface AmsAssetStockMapper +{ + /** + * 查询资产库存汇总列表。 + * + * @param amsAssetStock 查询条件 + * @return 库存汇总集合 + */ + public List selectAmsAssetStockList(AmsAssetStock amsAssetStock); + + /** + * 查询库存汇总行对应的一物一码资产明细。 + * + * @param amsAssetStock 查询条件 + * @return 资产明细集合 + */ + public List selectAmsAssetStockDetailList(AmsAssetStock amsAssetStock); +} diff --git a/ruoyi-asset/src/main/java/com/ruoyi/asset/service/IAmsAssetStockService.java b/ruoyi-asset/src/main/java/com/ruoyi/asset/service/IAmsAssetStockService.java new file mode 100644 index 0000000..8ca6b4c --- /dev/null +++ b/ruoyi-asset/src/main/java/com/ruoyi/asset/service/IAmsAssetStockService.java @@ -0,0 +1,29 @@ +package com.ruoyi.asset.service; + +import java.util.List; +import com.ruoyi.asset.domain.AmsAsset; +import com.ruoyi.asset.domain.AmsAssetStock; + +/** + * 资产库存明细Service接口 + * + * @author Yangk + */ +public interface IAmsAssetStockService +{ + /** + * 查询资产库存汇总列表。 + * + * @param amsAssetStock 查询条件 + * @return 库存汇总集合 + */ + public List selectAmsAssetStockList(AmsAssetStock amsAssetStock); + + /** + * 查询库存汇总行对应的一物一码资产明细。 + * + * @param amsAssetStock 查询条件 + * @return 资产明细集合 + */ + public List selectAmsAssetStockDetailList(AmsAssetStock amsAssetStock); +} diff --git a/ruoyi-asset/src/main/java/com/ruoyi/asset/service/impl/AmsAssetStockServiceImpl.java b/ruoyi-asset/src/main/java/com/ruoyi/asset/service/impl/AmsAssetStockServiceImpl.java new file mode 100644 index 0000000..9f77e6e --- /dev/null +++ b/ruoyi-asset/src/main/java/com/ruoyi/asset/service/impl/AmsAssetStockServiceImpl.java @@ -0,0 +1,58 @@ +package com.ruoyi.asset.service.impl; + +import java.util.Collections; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.asset.constant.AssetStatus; +import com.ruoyi.asset.domain.AmsAsset; +import com.ruoyi.asset.domain.AmsAssetStock; +import com.ruoyi.asset.mapper.AmsAssetStockMapper; +import com.ruoyi.asset.service.IAmsAssetStockService; +import com.ruoyi.common.utils.StringUtils; + +/** + * 资产库存明细Service业务层处理 + * + * @author Yangk + */ +@Service +public class AmsAssetStockServiceImpl implements IAmsAssetStockService +{ + @Autowired + private AmsAssetStockMapper amsAssetStockMapper; + + /** + * 查询资产库存汇总列表。 + */ + @Override + public List selectAmsAssetStockList(AmsAssetStock amsAssetStock) + { + AmsAssetStock query = defaultStockQuery(amsAssetStock); + return amsAssetStockMapper.selectAmsAssetStockList(query); + } + + /** + * 查询库存汇总行对应的一物一码资产明细。 + */ + @Override + public List selectAmsAssetStockDetailList(AmsAssetStock amsAssetStock) + { + AmsAssetStock query = defaultStockQuery(amsAssetStock); + if (StringUtils.isEmpty(query.getGroupKey())) + { + return Collections.emptyList(); + } + return amsAssetStockMapper.selectAmsAssetStockDetailList(query); + } + + private AmsAssetStock defaultStockQuery(AmsAssetStock amsAssetStock) + { + AmsAssetStock query = amsAssetStock == null ? new AmsAssetStock() : amsAssetStock; + if (query.getAssetStatus() == null) + { + query.setAssetStatus(AssetStatus.IN_STOCK); + } + return query; + } +} diff --git a/ruoyi-asset/src/main/resources/mapper/asset/AmsAssetStockMapper.xml b/ruoyi-asset/src/main/resources/mapper/asset/AmsAssetStockMapper.xml new file mode 100644 index 0000000..49df5d1 --- /dev/null +++ b/ruoyi-asset/src/main/resources/mapper/asset/AmsAssetStockMapper.xml @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + sha2(concat_ws('|', + ifnull(cast(category_id as char), '__NULL__'), + ifnull(category_code, '__NULL__'), + ifnull(category_name, '__NULL__'), + ifnull(asset_name, '__NULL__'), + ifnull(spec_model, '__NULL__'), + ifnull(brand, '__NULL__'), + ifnull(cast(warehouse_id as char), '__NULL__'), + ifnull(warehouse_code, '__NULL__'), + ifnull(warehouse_name, '__NULL__'), + ifnull(asset_status, '__NULL__') + ), 256) + + + + + del_flag = '0' + and category_id = #{categoryId} + and asset_name like concat('%', #{assetName}, '%') + and spec_model like concat('%', #{specModel}, '%') + and brand like concat('%', #{brand}, '%') + and warehouse_id = #{warehouseId} + and asset_status = #{assetStatus} + + + + + + + diff --git a/ruoyi-asset/src/main/resources/templates/asset/stock/stock.html b/ruoyi-asset/src/main/resources/templates/asset/stock/stock.html new file mode 100644 index 0000000..8a75bcd --- /dev/null +++ b/ruoyi-asset/src/main/resources/templates/asset/stock/stock.html @@ -0,0 +1,230 @@ + + + + + + + +
+
+
+
+
+
    +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
  • +  搜索 +  重置 +
  • +
+
+
+
+ + +
+
+
+ +
+

库存构成明细

+
+
+
+
+
+
+ + + + + diff --git a/ruoyi-asset/src/test/java/com/ruoyi/asset/service/impl/AmsAssetStockServiceImplTest.java b/ruoyi-asset/src/test/java/com/ruoyi/asset/service/impl/AmsAssetStockServiceImplTest.java new file mode 100644 index 0000000..9828522 --- /dev/null +++ b/ruoyi-asset/src/test/java/com/ruoyi/asset/service/impl/AmsAssetStockServiceImplTest.java @@ -0,0 +1,101 @@ +package com.ruoyi.asset.service.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.List; +import com.ruoyi.asset.constant.AssetStatus; +import com.ruoyi.asset.domain.AmsAsset; +import com.ruoyi.asset.domain.AmsAssetStock; +import com.ruoyi.asset.mapper.AmsAssetStockMapper; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class AmsAssetStockServiceImplTest +{ + @Mock + private AmsAssetStockMapper amsAssetStockMapper; + + @InjectMocks + private AmsAssetStockServiceImpl service; + + /** 库存页默认看在库资产,避免空状态查询把待入库、报废等非库存数据混入汇总 */ + @Test + void selectAmsAssetStockListShouldDefaultToInStock() + { + AmsAssetStock query = new AmsAssetStock(); + when(amsAssetStockMapper.selectAmsAssetStockList(query)).thenReturn(List.of()); + + service.selectAmsAssetStockList(query); + + ArgumentCaptor captor = ArgumentCaptor.forClass(AmsAssetStock.class); + verify(amsAssetStockMapper).selectAmsAssetStockList(captor.capture()); + assertEquals(AssetStatus.IN_STOCK, captor.getValue().getAssetStatus()); + } + + /** 用户显式选择状态时必须尊重筛选条件,支持查看维修中、借用中等资产分布 */ + @Test + void selectAmsAssetStockListShouldRespectExplicitStatus() + { + AmsAssetStock query = new AmsAssetStock(); + query.setAssetStatus(AssetStatus.REPAIRING); + when(amsAssetStockMapper.selectAmsAssetStockList(query)).thenReturn(List.of()); + + service.selectAmsAssetStockList(query); + + ArgumentCaptor captor = ArgumentCaptor.forClass(AmsAssetStock.class); + verify(amsAssetStockMapper).selectAmsAssetStockList(captor.capture()); + assertEquals(AssetStatus.REPAIRING, captor.getValue().getAssetStatus()); + } + + @Test + void selectAmsAssetStockListShouldReturnStockQuantityFromMapper() + { + AmsAssetStock row = new AmsAssetStock(); + row.setAssetName("手持终端"); + row.setSpecModel("PDA-X1"); + row.setStockQuantity(3L); + when(amsAssetStockMapper.selectAmsAssetStockList(any(AmsAssetStock.class))).thenReturn(List.of(row)); + + List result = service.selectAmsAssetStockList(new AmsAssetStock()); + + assertEquals(1, result.size()); + assertEquals(3L, result.get(0).getStockQuantity()); + } + + /** 没有汇总键时不允许直接查明细,防止空条件误扫整张资产表 */ + @Test + void selectAmsAssetStockDetailListShouldReturnEmptyWhenGroupKeyBlank() + { + AmsAssetStock query = new AmsAssetStock(); + + List result = service.selectAmsAssetStockDetailList(query); + + assertTrue(result.isEmpty()); + verify(amsAssetStockMapper, never()).selectAmsAssetStockDetailList(any(AmsAssetStock.class)); + } + + @Test + void selectAmsAssetStockDetailListShouldUseGroupKeyAndDefaultStatus() + { + AmsAssetStock query = new AmsAssetStock(); + query.setGroupKey("group-1"); + when(amsAssetStockMapper.selectAmsAssetStockDetailList(query)).thenReturn(List.of(new AmsAsset())); + + service.selectAmsAssetStockDetailList(query); + + ArgumentCaptor captor = ArgumentCaptor.forClass(AmsAssetStock.class); + verify(amsAssetStockMapper).selectAmsAssetStockDetailList(captor.capture()); + assertEquals("group-1", captor.getValue().getGroupKey()); + assertEquals(AssetStatus.IN_STOCK, captor.getValue().getAssetStatus()); + } +}