feat: 办公用品(日常用品),领用归还。领用归还记录 完成
ps:如果pro使用直接引入该commit
已添加17个文件
已修改9个文件
2629 ■■■■■ 文件已修改
doc/20260526_product_borrow_tables.sql 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/产品模块与库存模块分析文档.md 714 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/产品领用归还模块-前端对接文档.md 696 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/service/impl/ProductServiceImpl.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/enums/StockInQualifiedRecordTypeEnum.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/enums/StockOutQualifiedRecordTypeEnum.java 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/controller/ProductBorrowController.java 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/controller/ProductBorrowReturnController.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/controller/StockInventoryController.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/dto/ProductBorrowDto.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/dto/ProductBorrowReturnDto.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/dto/StockInventoryDto.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/mapper/ProductBorrowMapper.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/mapper/ProductBorrowReturnMapper.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/mapper/StockInventoryMapper.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/pojo/ProductBorrow.java 96 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/pojo/ProductBorrowReturn.java 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/ProductBorrowReturnService.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/ProductBorrowService.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/StockInventoryService.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/impl/ProductBorrowReturnServiceImpl.java 109 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/impl/ProductBorrowServiceImpl.java 214 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/impl/StockInventoryServiceImpl.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/stock/ProductBorrowMapper.xml 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/stock/ProductBorrowReturnMapper.xml 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/stock/StockInventoryMapper.xml 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260526_product_borrow_tables.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,49 @@
-- äº§å“é¢†ç”¨è¡¨
CREATE TABLE IF NOT EXISTS `product_borrow` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  `borrow_no` varchar(50) DEFAULT NULL COMMENT '领用单号',
  `product_model_id` bigint NOT NULL COMMENT '产品规格ID',
  `batch_no` varchar(100) DEFAULT NULL COMMENT '批号',
  `borrow_quantity` decimal(18,4) NOT NULL COMMENT '领用数量',
  `returned_quantity` decimal(18,4) DEFAULT 0 COMMENT '已归还数量',
  `borrower_id` bigint DEFAULT NULL COMMENT '领用人ID',
  `borrower_name` varchar(100) DEFAULT NULL COMMENT '领用人姓名',
  `borrow_time` datetime NOT NULL COMMENT '领用时间',
  `expected_return_time` datetime DEFAULT NULL COMMENT '预计归还时间',
  `approval_status` tinyint DEFAULT 0 COMMENT '审批状态(0-待审批,1-已通过,2-已驳回)',
  `status` tinyint DEFAULT 0 COMMENT '归还状态(0-未归还,1-部分归还,2-已全部归还)',
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  `tenant_id` bigint DEFAULT NULL COMMENT '租户ID',
  `dept_id` bigint DEFAULT NULL COMMENT '部门ID',
  `create_user` int DEFAULT NULL COMMENT '创建人',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_user` int DEFAULT NULL COMMENT '更新人',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`),
  KEY `idx_borrow_no` (`borrow_no`),
  KEY `idx_product_model_id` (`product_model_id`),
  KEY `idx_borrower_id` (`borrower_id`),
  KEY `idx_approval_status` (`approval_status`),
  KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='产品领用表';
-- äº§å“å½’还记录表
CREATE TABLE IF NOT EXISTS `product_borrow_return` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  `borrow_id` bigint NOT NULL COMMENT '领用记录ID',
  `product_model_id` bigint NOT NULL COMMENT '产品规格ID',
  `batch_no` varchar(100) DEFAULT NULL COMMENT '批号',
  `return_quantity` decimal(18,4) NOT NULL COMMENT '归还数量',
  `returner_id` bigint DEFAULT NULL COMMENT '归还人ID',
  `returner_name` varchar(100) DEFAULT NULL COMMENT '归还人姓名',
  `return_time` datetime NOT NULL COMMENT '归还时间',
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  `tenant_id` bigint DEFAULT NULL COMMENT '租户ID',
  `dept_id` bigint DEFAULT NULL COMMENT '部门ID',
  `create_user` int DEFAULT NULL COMMENT '创建人',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`),
  KEY `idx_borrow_id` (`borrow_id`),
  KEY `idx_product_model_id` (`product_model_id`),
  KEY `idx_returner_id` (`returner_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='产品归还记录表';
doc/²úÆ·Ä£¿éÓë¿â´æÄ£¿é·ÖÎöÎĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,714 @@
# äº§å“æ¨¡å—与库存模块详细分析文档
## ä¸€ã€äº§å“æ¨¡å— (Product Module)
### 1. æ¨¡å—概述
产品模块负责管理产品的基础信息,采用树形结构组织产品分类,支持产品规格型号的管理。
### 2. æ ¸å¿ƒå®žä½“
#### 2.1 Product (产品)
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| id | Long | ä¸»é”®ID,自增 |
| parentId | Long | çˆ¶äº§å“ID(支持树形结构) |
| productName | String | äº§å“åç§° |
| tenantId | Long | ç§Ÿæˆ·ID(自动填充) |
| createUser | Integer | åˆ›å»ºç”¨æˆ·ï¼ˆè‡ªåŠ¨å¡«å……ï¼‰ |
| deptId | Long | éƒ¨é—¨ID(自动填充) |
| deptIds | Long[] | éƒ¨é—¨ID数组(非数据库字段) |
**数据表**: `product`
#### 2.2 ProductModel (产品规格型号)
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| id | Long | ä¸»é”®ID,自增 |
| productId | Long | å…³è”产品ID |
| model | String | è§„格型号 |
| productCode | String | äº§å“ç¼–码 |
| unit | String | å•位 |
| tenantId | Long | ç§Ÿæˆ·ID |
| createUser | Integer | åˆ›å»ºç”¨æˆ· |
| deptId | Long | éƒ¨é—¨ID |
| inboundNum | BigDecimal | å…¥åº“数量(非数据库字段) |
| outboundNum | BigDecimal | å‡ºåº“数量(非数据库字段) |
| stockQuantity | BigDecimal | å‰©ä½™åº“存(非数据库字段) |
**数据表**: `product_model`
### 3. DTO æ•°æ®ä¼ è¾“对象
#### 3.1 ProductDto
继承自 `Product`,扩展字段:
- `productModelList`: List<ProductModel> - äº§å“è§„格型号列表
#### 3.2 ProductModelDto
继承自 `ProductModel`,扩展字段:
- `productStructureList`: List<ProductStructureDto> - äº§å“ç»“构列表(用于BOM)
#### 3.3 ProductTreeDto
用于树形结构展示:
- `id`, `parentId`, `productName`, `label`
- `children`: List<ProductTreeDto> - å­èŠ‚ç‚¹åˆ—è¡¨
#### 3.4 ProductModelVo
继承自 `ProductModel`,扩展字段:
- `batchNoList`: List<String> - æ‰¹å·åˆ—表
### 4. Controller API æŽ¥å£
**ProductController** (`/basic/product`)
| æ–¹æ³• | HTTP | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|------|
| selectProductList | GET | /list | æŸ¥è¯¢äº§å“åˆ—表(树形结构) |
| selectModelList | GET | /modelList | æ ¹æ®äº§å“ID查询规格型号列表 |
| addOrEditProduct | POST | /addOrEditProduct | æ–°å¢ž/更新产品 |
| addOrEditProductModel | POST | /addOrEditProductModel | æ–°å¢ž/更新产品规格型号 |
| remove | DELETE | /delProduct | åˆ é™¤äº§å“ï¼ˆæ‰¹é‡ï¼‰ |
| delProductModel | DELETE | /delProductModel | åˆ é™¤äº§å“è§„格型号(批量) |
| selectModelListPage | GET | /modelListPage | äº§å“è§„格分页查询 |
| listPageProductModel | GET | /pageModel | åˆ†é¡µæŸ¥è¯¢æ‰€æœ‰äº§å“åž‹å· |
| importProductModel | POST | /import | å¯¼å…¥äº§å“è§„格型号 |
| importProduct | GET | /export | ä¸‹è½½äº§å“å¯¼å…¥æ¨¡æ¿ |
### 5. Service ä¸šåŠ¡é€»è¾‘
#### 5.1 IProductService
```java
// æ–°å¢žæˆ–编辑产品
int addOrEditProduct(ProductDto productDto)
    - éªŒè¯çˆ¶èŠ‚ç‚¹æ˜¯å¦å­˜åœ¨
    - éªŒè¯äº§å“åç§°åœ¨åŒä¸€çˆ¶èŠ‚ç‚¹ä¸‹å”¯ä¸€
    - æ–°å¢ž/更新产品
// åˆ é™¤äº§å“ï¼ˆæ‰¹é‡ï¼‰
int delProductByIds(Long[] ids)
    - çº§è”删除关联的产品规格型号
    - åˆ é™¤äº§å“æœ¬èº«
// æŸ¥è¯¢äº§å“åˆ—表(树形)
List<ProductTreeDto> selectProductList(ProductDto productDto)
    - æŸ¥è¯¢æ ¹èŠ‚ç‚¹ï¼ˆparentId为null)
    - é€’归构建子树
// åˆ†é¡µæŸ¥è¯¢äº§å“åž‹å·
IPage<ProductModelVo> listPageProductModel(Page<ProductModelVo> page, ProductModel productModel)
```
#### 5.2 IProductModelService
```java
// æ–°å¢žæˆ–编辑产品规格型号
int addOrEditProductModel(ProductModelDto productModelDto)
    - éªŒè¯è§„格型号+产品编码唯一性
    - æ–°å¢ž/更新规格型号
// åˆ é™¤äº§å“è§„格型号
int delProductModel(Long[] ids)
    - æ£€æŸ¥æ˜¯å¦å­˜åœ¨é”€å”®å°è´¦/采购台账关联
    - æ£€æŸ¥æ˜¯å¦å­˜åœ¨BOM数据关联
    - æ‰§è¡Œåˆ é™¤
// æŸ¥è¯¢è§„格型号列表
List<ProductModel> selectModelList(ProductDto productDto)
// åˆ†é¡µæŸ¥è¯¢
IPage<ProductModel> modelListPage(Page page, ProductDto productDto)
// å¯¼å…¥äº§å“è§„格型号
AjaxResult importProductModel(MultipartFile file, Integer productId)
    - éªŒè¯äº§å“æ˜¯å¦å­˜åœ¨
    - éªŒè¯å¯¼å…¥æ•°æ®ï¼ˆäº§å“ç¼–码、规格型号、单位必填)
    - åŽ»é‡å¤„ç†ï¼ˆè·³è¿‡å·²å­˜åœ¨çš„åž‹å·ï¼‰
    - æ‰¹é‡ä¿å­˜
```
### 6. ä¸šåŠ¡çº¦æŸ
1. **产品删除约束**:
   - å­˜åœ¨é”€å”®å°è´¦/采购台账关联的产品不能删除
2. **产品规格型号删除约束**:
   - å­˜åœ¨é”€å”®å°è´¦/采购台账关联不能删除
   - å­˜åœ¨BOM数据关联不能删除
3. **唯一性约束**:
   - åŒä¸€çˆ¶èŠ‚ç‚¹ä¸‹äº§å“åç§°å”¯ä¸€
   - è§„格型号+产品编码组合唯一
### 7. æ•°æ®æµç¨‹å›¾
```
产品管理流程:
┌─────────────┐     â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”     â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”
│  äº§å“åˆ†ç±»   â”‚────>│   äº§å“      â”‚────>│ äº§å“è§„格型号 â”‚
│  (父节点)   â”‚     â”‚  (子节点)   â”‚     â”‚  (具体型号)  â”‚
└─────────────┘     â””─────────────┘     â””─────────────┘
                           â”‚
                           â–¼
                    â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”
                    â”‚  é”€å”®å°è´¦   â”‚
                    â”‚  é‡‡è´­å°è´¦   â”‚
                    â”‚  BOM数据    â”‚
                    â””─────────────┘
```
---
## äºŒã€åº“存模块 (Stock Module)
### 1. æ¨¡å—概述
库存模块管理产品的库存信息,支持入库、出库、库存查询、冻结/解冻等功能,采用批号管理库存。
### 2. æ ¸å¿ƒå®žä½“
#### 2.1 StockInventory (库存表)
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| id | Long | ä¸»é”®ID |
| productModelId | Long | äº§å“è§„æ ¼ID(必填) |
| batchNo | String | æ‰¹å· |
| qualitity | BigDecimal | åº“存数量 |
| lockedQuantity | BigDecimal | å†»ç»“/锁定数量 |
| warnNum | BigDecimal | é¢„警数量 |
| version | Integer | ç‰ˆæœ¬å· |
| remark | String | å¤‡æ³¨ |
| createTime | LocalDateTime | åˆ›å»ºæ—¶é—´ |
| updateTime | LocalDateTime | æ›´æ–°æ—¶é—´ |
| createUser | Integer | åˆ›å»ºç”¨æˆ· |
| deptId | Long | éƒ¨é—¨ID |
**数据表**: `stock_inventory`
#### 2.2 StockInRecord (入库记录)
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| id | Long | ä¸»é”®ID |
| inboundBatches | String | å…¥åº“批次 |
| stockInNum | BigDecimal | å…¥åº“数量 |
| batchNo | String | æ‰¹å· |
| recordType | String | è®°å½•类型(枚举) |
| recordId | Long | å…³è”记录ID |
| productModelId | Long | äº§å“è§„æ ¼ID |
| type | String | ç±»åž‹ï¼ˆ0合格/1不合格) |
| warnNum | BigDecimal | é¢„警数量 |
| approvalStatus | Integer | å®¡æ‰¹çŠ¶æ€ï¼ˆ0待审批/1通过/2驳回) |
| remark | String | å¤‡æ³¨ |
**数据表**: `stock_in_record`
#### 2.3 StockOutRecord (出库记录)
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| id | Long | ä¸»é”®ID |
| outboundBatches | String | å‡ºåº“批次 |
| stockOutNum | BigDecimal | å‡ºåº“数量 |
| batchNo | String | æ‰¹å· |
| recordType | String | è®°å½•类型(枚举) |
| recordId | Long | å…³è”记录ID |
| productModelId | Long | äº§å“è§„æ ¼ID |
| type | String | ç±»åž‹ï¼ˆ0合格/1不合格) |
| approvalStatus | Integer | å®¡æ‰¹çŠ¶æ€ï¼ˆ0待审批/1通过/2驳回/3销售出库待确认) |
| remark | String | å¤‡æ³¨ |
**数据表**: `stock_out_record`
### 3. DTO æ•°æ®ä¼ è¾“对象
#### 3.1 StockInventoryDto
继承自 `StockInventory`,扩展字段:
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| productName | String | äº§å“åç§° |
| model | String | è§„格型号 |
| unit | String | å•位 |
| recordType | String | å…¥åº“类型 |
| recordId | Long | å…¥åº“类型对应ID |
| reportDate | LocalDate | æŠ¥è¡¨æ—¥æœŸ |
| startMonth | endMonth | åº“存月报查询字段 |
| totalStockIn | BigDecimal | æ€»å…¥åº“量 |
| totalStockOut | BigDecimal | æ€»å‡ºåº“量 |
| currentStock | BigDecimal | å½“前库存 |
| stockType | String | åº“存类型(qualified/unqualified) |
| qualifiedQuantity | BigDecimal | åˆæ ¼åº“存数量 |
| unQualifiedQuantity | BigDecimal | ä¸åˆæ ¼åº“存数量 |
| qualifiedLockedQuantity | BigDecimal | åˆæ ¼åº“存冻结数量 |
| unQualifiedLockedQuantity | BigDecimal | ä¸åˆæ ¼åº“存冻结数量 |
| qualifiedPendingOutQuantity | BigDecimal | åˆæ ¼åº“存待审核出库数量 |
| productId | Long | äº§å“ID |
| topParentProductId | Long | é¡¶éƒ¨çˆ¶äº§å“ID |
### 4. Controller API æŽ¥å£
**StockInventoryController** (`/stockInventory`)
| æ–¹æ³• | HTTP | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|------|
| pagestockInventory | GET | /pagestockInventory | åˆ†é¡µæŸ¥è¯¢åº“å­˜ |
| pageListCombinedStockInventory | GET | /pageListCombinedStockInventory | åˆ†é¡µæŸ¥è¯¢è”合库存列表 |
| getBatchNoQty | GET | /getBatchNoQty | æŸ¥è¯¢å¯¹åº”批号和数量 |
| addstockInventory | POST | /addstockInventory | æ–°å¢žåº“存(入库) |
| subtractStockInventory | POST | /subtractStockInventory | æ‰£å‡åº“存(出库) |
| addStockInRecordOnly | POST | /addStockInRecordOnly | æ–°å¢žå…¥åº“记录(不调整库存) |
| addStockOutRecordOnly | POST | /addStockOutRecordOnly | æ–°å¢žå‡ºåº“记录(不调整库存) |
| importStockInventory | POST | /importStockInventory | å¯¼å…¥åº“å­˜ |
| downloadStockInventory | POST | /downloadStockInventory | ä¸‹è½½åº“存导入模板 |
| exportStockInventory | POST | /exportStockInventory | å¯¼å‡ºåº“å­˜ |
| stockInventoryPage | GET | /stockInventoryPage | åº“存报表查询 |
| stockInAndOutRecord | GET | /stockInAndOutRecord | ç»Ÿè®¡å…¥åº“出库记录 |
| frozenStock | POST | /frozenStock | å†»ç»“库存 |
| thawStock | POST | /thawStock | è§£å†»åº“å­˜ |
| getByModelId | GET | /getByModelId | æ ¹æ®äº§å“è§„æ ¼ID获取入库记录 |
**StockInRecordController** (`/stockInRecord`)
| æ–¹æ³• | HTTP | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|------|
| listPage | GET | /listPage | å…¥åº“管理列表 |
| delete | DELETE | / | åˆ é™¤å…¥åº“记录 |
| deletePending | DELETE | /pending | åˆ é™¤å¾…审批入库记录 |
| exportStockInRecord | POST | /exportStockInRecord | å¯¼å‡ºå…¥åº“记录 |
| approve | POST | /approve | æ‰¹é‡å®¡æ‰¹å…¥åº“记录 |
| reAudit | POST | /reAudit | æ‰¹é‡åå®¡å…¥åº“记录 |
**StockOutRecordController** (`/stockOutRecord`)
| æ–¹æ³• | HTTP | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|------|
| listPage | GET | /listPage | å‡ºåº“管理列表 |
| add | POST | / | æ–°å¢žå‡ºåº“记录 |
| update | PUT | /{id} | æ›´æ–°å‡ºåº“记录 |
| delete | DELETE | / | åˆ é™¤å‡ºåº“记录 |
| deletePending | DELETE | /pending | åˆ é™¤å¾…审批出库记录 |
| exportStockOutRecord | POST | /exportStockOutRecord | å¯¼å‡ºå‡ºåº“记录 |
| approve | POST | /approve | æ‰¹é‡å®¡æ‰¹å‡ºåº“记录 |
### 5. Service æ ¸å¿ƒä¸šåŠ¡é€»è¾‘
#### 5.1 StockInventoryService
```java
// å…¥åº“操作(记录+库存调整)
Boolean addstockInventory(StockInventoryDto dto)
    1. ç”Ÿæˆæˆ–验证批号
    2. åˆ›å»ºå…¥åº“记录(StockInRecord)
    3. æ£€æŸ¥åº“存是否存在
       - ä¸å­˜åœ¨ï¼šæ–°å¢žåº“存记录
       - å­˜åœ¨ï¼šæ›´æ–°åº“存数量
// å‡ºåº“操作(记录+库存调整)
Boolean subtractStockInventory(StockInventoryDto dto)
    1. æŸ¥è¯¢åº“存记录
    2. éªŒè¯åº“存充足(扣除冻结数量)
    3. åˆ›å»ºå‡ºåº“记录(StockOutRecord)
    4. æ‰£å‡åº“存数量
// ä»…创建入库记录(不调整库存)
Boolean addStockInRecordOnly(StockInventoryDto dto)
    - ç”¨äºŽæ‰‹å·¥å½•入历史数据
// ä»…创建出库记录(不调整库存)
Boolean addStockOutRecordOnly(StockInventoryDto dto)
    - éªŒè¯å¯ç”¨åº“存(库存-冻结-待审核出库)
    - åˆ›å»ºå‡ºåº“记录
// æ‰¹å·è‡ªåŠ¨ç”Ÿæˆè§„åˆ™
// æ ¼å¼: æ—¥æœŸ-产品编码-序号
// ç¤ºä¾‹: 20260526-ABC001-001
String generateAutoBatchNo(Long productModelId)
// å†»ç»“库存
Boolean frozenStock(StockInventoryDto dto)
    - éªŒè¯å†»ç»“数量不超过库存数量
    - ç´¯åŠ å†»ç»“æ•°é‡
// è§£å†»åº“å­˜
Boolean thawStock(StockInventoryDto dto)
    - éªŒè¯è§£å†»æ•°é‡ä¸è¶…过冻结数量
    - æ‰£å‡å†»ç»“数量
```
### 6. æžšä¸¾ç±»åž‹
#### 6.1 å…¥åº“记录类型 (StockInQualifiedRecordTypeEnum)
| ä»£ç  | è¯´æ˜Ž |
|------|------|
| CUSTOMIZATION_STOCK_IN | è‡ªå®šä¹‰å…¥åº“(手工入库) |
#### 6.2 å‡ºåº“记录类型 (StockOutQualifiedRecordTypeEnum)
| ä»£ç  | è¯´æ˜Ž |
|------|------|
| CUSTOMIZATION_STOCK_OUT | è‡ªå®šä¹‰å‡ºåº“(手工出库) |
#### 6.3 å®¡æ‰¹çŠ¶æ€ (ReviewStatusEnum)
| å€¼ | è¯´æ˜Ž |
|------|------|
| 0 | å¾…审批 |
| 1 | é€šè¿‡ |
| 2 | é©³å›ž |
| 3 | é”€å”®å‡ºåº“待确认 |
### 7. ä¸šåŠ¡æµç¨‹å›¾
```
入库流程:
┌──────────┐     â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”     â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”
│ å…¥åº“申请 â”‚────>│ åˆ›å»ºå…¥åº“ â”‚────>│ æ›´æ–°åº“å­˜ â”‚
│ (来源)   â”‚     â”‚ è®°å½•     â”‚     â”‚ æ•°é‡     â”‚
└──────────┘     â””──────────┘     â””──────────┘
                      â”‚
                      â–¼
               â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”
               â”‚ ç”Ÿæˆæ‰¹å· â”‚
               â”‚ (自动/手工)│
               â””──────────┘
出库流程:
┌──────────┐     â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”     â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”
│ å‡ºåº“申请 â”‚────>│ éªŒè¯åº“å­˜ â”‚────>│ åˆ›å»ºå‡ºåº“ â”‚
│          â”‚     â”‚ æ˜¯å¦å……è¶³ â”‚     â”‚ è®°å½•     â”‚
└──────────┘     â””──────────┘     â””──────────┘
                      â”‚
                      â–¼
               â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”
               â”‚ æ‰£å‡åº“å­˜ â”‚
               â”‚ æ•°é‡     â”‚
               â””──────────┘
库存冻结/解冻:
┌──────────┐     â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”
│ å†»ç»“申请 â”‚────>│ ç´¯åŠ å†»ç»“ â”‚
│          â”‚     â”‚ æ•°é‡     â”‚
└──────────┘     â””──────────┘
┌──────────┐     â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”
│ è§£å†»ç”³è¯· â”‚────>│ æ‰£å‡å†»ç»“ â”‚
│          â”‚     â”‚ æ•°é‡     â”‚
└──────────┘     â””──────────┘
```
### 8. åº“存计算公式
```
可用库存 = åº“存数量 - å†»ç»“数量 - å¾…审核出库数量
出库验证条件:
申请出库数量 <= å¯ç”¨åº“å­˜
冻结验证条件:
冻结数量 <= åº“存数量
解冻验证条件:
解冻数量 <= å·²å†»ç»“数量
```
---
## ä¸‰ã€æ¨¡å—关联关系
### 1. æ•°æ®å…³è”
```
Product (产品)
    â”‚
    â”œâ”€â”€ productId â”€â”€> ProductModel (产品规格型号)
    â”‚                        â”‚
    â”‚                        â”œâ”€â”€ productModelId â”€â”€> StockInventory (合格库存)
    â”‚                        â”‚
    â”‚                        â”œâ”€â”€ productModelId â”€â”€> StockUninventory (不合格库存)
    â”‚                        â”‚
    â”‚                        â”œâ”€â”€ productModelId â”€â”€> StockInRecord (入库记录)
    â”‚                        â”‚
    â”‚                        â””── productModelId â”€â”€> StockOutRecord (出库记录)
    â”‚
    â””── è¢«åˆ é™¤æ—¶çº§è”删除 ProductModel
```
### 2. åˆ é™¤çº¦æŸ
| æ¨¡å— | åˆ é™¤å‰æ£€æŸ¥ |
|------|-----------|
| Product | æ£€æŸ¥é”€å”®å°è´¦å…³è” |
| ProductModel | æ£€æŸ¥é”€å”®å°è´¦ã€BOM数据关联 |
| StockInventory | ä¸å…è®¸ç›´æŽ¥åˆ é™¤ï¼Œéœ€é€šè¿‡å‡ºåº“扣减 |
### 3. å…³é”®ä¸šåŠ¡åœºæ™¯
#### 3.1 äº§å“å…¥åº“场景
1. é‡‡è´­å…¥åº“ â†’ ç”Ÿæˆå…¥åº“记录 â†’ å¢žåŠ åº“å­˜
2. ç”Ÿäº§å…¥åº“ â†’ ç”Ÿæˆå…¥åº“记录 â†’ å¢žåŠ åº“å­˜
3. æ‰‹å·¥å…¥åº“ â†’ è‡ªå®šä¹‰å…¥åº“类型
#### 3.2 äº§å“å‡ºåº“场景
1. é”€å”®å‡ºåº“ â†’ æ£€æŸ¥åº“å­˜ â†’ åˆ›å»ºå‡ºåº“记录 â†’ æ‰£å‡åº“å­˜
2. ç”Ÿäº§é¢†æ–™ â†’ æ£€æŸ¥åº“å­˜ â†’ åˆ›å»ºå‡ºåº“记录 â†’ æ‰£å‡åº“å­˜
3. æ‰‹å·¥å‡ºåº“ â†’ è‡ªå®šä¹‰å‡ºåº“类型
#### 3.3 åº“存冻结场景
1. è®¢å•锁定 â†’ å†»ç»“库存
2. è´¨é‡é—®é¢˜ â†’ å†»ç»“库存
3. é—®é¢˜è§£å†³ â†’ è§£å†»åº“å­˜
---
## å››ã€æ–‡ä»¶ç»“æž„
### äº§å“æ¨¡å—文件
```
com.ruoyi.basic/
├── controller/
│   â””── ProductController.java
├── service/
│   â”œâ”€â”€ IProductService.java
│   â”œâ”€â”€ IProductModelService.java
│   â””── impl/
│       â”œâ”€â”€ ProductServiceImpl.java
│       â””── ProductModelServiceImpl.java
├── mapper/
│   â”œâ”€â”€ ProductMapper.java
│   â””── ProductModelMapper.java
├── pojo/
│   â”œâ”€â”€ Product.java
│   â””── ProductModel.java
├── dto/
│   â”œâ”€â”€ ProductDto.java
│   â”œâ”€â”€ ProductModelDto.java
│   â””── ProductTreeDto.java
├── vo/
│   â””── ProductModelVo.java
└── excel/
    â””── (导入导出相关)
```
### åº“存模块文件
```
com.ruoyi.stock/
├── controller/
│   â”œâ”€â”€ StockInventoryController.java
│   â”œâ”€â”€ StockInRecordController.java
│   â”œâ”€â”€ StockOutRecordController.java
│   â””── StockUninventoryController.java
├── service/
│   â”œâ”€â”€ StockInventoryService.java
│   â”œâ”€â”€ StockInRecordService.java
│   â”œâ”€â”€ StockOutRecordService.java
│   â””── impl/
│       â”œâ”€â”€ StockInventoryServiceImpl.java
│       â”œâ”€â”€ StockInRecordServiceImpl.java
│       â”œâ”€â”€ StockOutRecordServiceImpl.java
│       â””── StockUninventoryServiceImpl.java
├── mapper/
│   â”œâ”€â”€ StockInventoryMapper.java
│   â”œâ”€â”€ StockInRecordMapper.java
│   â””── StockOutRecordMapper.java
├── pojo/
│   â”œâ”€â”€ StockInventory.java
│   â”œâ”€â”€ StockInRecord.java
│   â””── StockOutRecord.java
├── dto/
│   â”œâ”€â”€ StockInventoryDto.java
│   â”œâ”€â”€ StockInRecordDto.java
│   â””── StockOutRecordDto.java
└── excel/
    â”œâ”€â”€ StockInventoryExportData.java
    â”œâ”€â”€ StockInRecordExportData.java
    â””── StockOutRecordExportData.java
```
---
## äº”、注意事项
1. **事务管理**: å…¥åº“/出库操作使用 `@Transactional(rollbackFor = Exception.class)` ä¿è¯æ•°æ®ä¸€è‡´æ€§
2. **批号管理**:
   - è‡ªåŠ¨ç”Ÿæˆæ ¼å¼: `YYYYMMDD-产品编码-序号`
   - æ”¯æŒæ‰‹å·¥æŒ‡å®šæ‰¹å·
   - åŒä¸€æ‰¹å·åŒä¸€äº§å“è§„格合并库存
3. **库存安全**:
   - å‡ºåº“前检查可用库存
   - å†»ç»“数量不可用于出库
   - å¾…审核出库数量计入可用库存计算
4. **多租户支持**: é€šè¿‡ `tenantId` å­—段实现数据隔离
5. **部门隔离**: é€šè¿‡ `deptId` å­—段实现部门数据隔离
---
## å…­ã€äº§å“é¢†ç”¨å½’还模块 (Product Borrow Module)
### 1. æ¨¡å—概述
产品领用归还模块管理产品的领用和归还流程,支持审批流程和库存联动。
### 2. æ ¸å¿ƒå®žä½“
#### 2.1 ProductBorrow (产品领用表)
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| id | Long | ä¸»é”®ID |
| borrowNo | String | é¢†ç”¨å•号(自动生成) |
| productModelId | Long | äº§å“è§„æ ¼ID |
| batchNo | String | æ‰¹å· |
| borrowQuantity | BigDecimal | é¢†ç”¨æ•°é‡ |
| returnedQuantity | BigDecimal | å·²å½’还数量 |
| borrowerId | Long | é¢†ç”¨äººID(系统用户) |
| borrowerName | String | é¢†ç”¨äººå§“名 |
| borrowTime | LocalDateTime | é¢†ç”¨æ—¶é—´ |
| expectedReturnTime | LocalDateTime | é¢„计归还时间 |
| approvalStatus | Integer | å®¡æ‰¹çŠ¶æ€ï¼ˆ0待审批/1已通过/2已驳回) |
| status | Integer | å½’还状态(0未归还/1部分归还/2已全部归还) |
| remark | String | å¤‡æ³¨ |
**数据表**: `product_borrow`
#### 2.2 ProductBorrowReturn (产品归还记录表)
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| id | Long | ä¸»é”®ID |
| borrowId | Long | é¢†ç”¨è®°å½•ID |
| productModelId | Long | äº§å“è§„æ ¼ID |
| batchNo | String | æ‰¹å· |
| returnQuantity | BigDecimal | å½’还数量 |
| returnerId | Long | å½’还人ID(系统用户) |
| returnerName | String | å½’还人姓名 |
| returnTime | LocalDateTime | å½’还时间 |
| remark | String | å¤‡æ³¨ |
**数据表**: `product_borrow_return`
### 3. Controller API æŽ¥å£
**ProductBorrowController** (`/productBorrow`)
| æ–¹æ³• | HTTP | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|------|
| listPage | GET | /listPage | åˆ†é¡µæŸ¥è¯¢é¢†ç”¨è®°å½• |
| getDetail | GET | /{id} | æŸ¥è¯¢é¢†ç”¨è®°å½•详情 |
| add | POST | / | æ–°å¢žé¢†ç”¨è®°å½•(待审批) |
| update | PUT | /{id} | ä¿®æ”¹é¢†ç”¨è®°å½• |
| delete | DELETE | / | åˆ é™¤é¢†ç”¨è®°å½• |
| approve | POST | /approve | æ‰¹é‡å®¡æ‰¹é¢†ç”¨è®°å½• |
| reAudit | POST | /reAudit | æ‰¹é‡åå®¡é¢†ç”¨è®°å½• |
**ProductBorrowReturnController** (`/productBorrowReturn`)
| æ–¹æ³• | HTTP | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|------|
| listPage | GET | /listPage | åˆ†é¡µæŸ¥è¯¢å½’还记录 |
| listByBorrowId | GET | /listByBorrowId/{borrowId} | æŸ¥è¯¢æŸé¢†ç”¨è®°å½•的归还记录 |
| add | POST | / | æ–°å¢žå½’还记录(直接增加库存) |
### 4. ä¸šåŠ¡æµç¨‹
```
领用流程:
┌──────────┐     â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”     â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”
│ æ–°å¢žé¢†ç”¨ â”‚────>│ å¾…审批   â”‚────>│ å®¡æ‰¹é€šè¿‡ â”‚
│ è®°å½•     â”‚     â”‚ çŠ¶æ€     â”‚     â”‚          â”‚
└──────────┘     â””──────────┘     â””──────────┘
                                        â”‚
                                        â–¼
                               â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”
                               â”‚ æ‰£å‡åº“å­˜     â”‚
                               â”‚ (出库记录)   â”‚
                               â””──────────────┘
归还流程:
┌──────────┐     â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”     â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”
│ é€‰æ‹©é¢†ç”¨ â”‚────>│ å¡«å†™å½’还 â”‚────>│ éªŒè¯å½’还 â”‚
│ è®°å½•    â”‚     â”‚ ä¿¡æ¯     â”‚     â”‚ æ•°é‡     â”‚
└──────────┘     â””──────────┘     â””──────────┘
                      â”‚                â”‚
                      â–¼                â–¼
               â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”     â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”
               â”‚ å¢žåŠ åº“å­˜ â”‚     â”‚ æ›´æ–°é¢†ç”¨çŠ¶æ€ â”‚
               â”‚ (入库)   â”‚     â””──────────────┘
               â””──────────┘
```
### 5. ä¸šåŠ¡çº¦æŸ
1. **领用约束**:
   - é¢†ç”¨äººä»Žç³»ç»Ÿç”¨æˆ·è¡¨é€‰æ‹©
   - æ–°å¢žé¢†ç”¨è®°å½•状态为"待审批"
   - åªæœ‰å¾…审批状态才能修改/删除
2. **审批约束**:
   - åªæœ‰å¾…审批状态才能审批
   - å®¡æ‰¹é€šè¿‡åŽè‡ªåŠ¨æ‰£å‡åº“å­˜
   - å®¡æ‰¹é©³å›žä¸æ‰£å‡åº“å­˜
3. **归还约束**:
   - å½’还数量不能超过剩余可归还数量
   - é¢†ç”¨è®°å½•必须审批通过才能归还
   - å½’还直接增加库存,无需审批
   - å½’还人从系统用户表选择
### 6. é¢†ç”¨å•号生成规则
```
格式:LY + å¹´æœˆæ—¥ + 4位序号
示例:LY202605260001
```
### 7. æžšä¸¾ç±»åž‹æ‰©å±•
**入库类型** (StockInQualifiedRecordTypeEnum):
- `PRODUCT_BORROW_RETURN("23", "产品归还入库")`
**出库类型** (StockOutQualifiedRecordTypeEnum):
- `PRODUCT_BORROW("16", "产品领用出库")`
- `PRODUCT_RETURN("17", "产品归还入库")`
### 8. æ–‡ä»¶ç»“æž„
```
com.ruoyi.stock/
├── controller/
│   â”œâ”€â”€ ProductBorrowController.java
│   â””── ProductBorrowReturnController.java
├── service/
│   â”œâ”€â”€ ProductBorrowService.java
│   â”œâ”€â”€ ProductBorrowReturnService.java
│   â””── impl/
│       â”œâ”€â”€ ProductBorrowServiceImpl.java
│       â””── ProductBorrowReturnServiceImpl.java
├── mapper/
│   â”œâ”€â”€ ProductBorrowMapper.java
│   â””── ProductBorrowReturnMapper.java
├── pojo/
│   â”œâ”€â”€ ProductBorrow.java
│   â””── ProductBorrowReturn.java
└── dto/
    â”œâ”€â”€ ProductBorrowDto.java
    â””── ProductBorrowReturnDto.java
resources/mapper/stock/
├── ProductBorrowMapper.xml
└── ProductBorrowReturnMapper.xml
```
### 9. æ•°æ®åº“表
SQL脚本位置: `doc/20260526_product_borrow_tables.sql`
doc/²úÆ·ÁìÓù黹ģ¿é-ǰ¶Ë¶Ô½ÓÎĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,696 @@
# äº§å“é¢†ç”¨å½’还模块 - å‰ç«¯å¯¹æŽ¥æ–‡æ¡£
## ä¸€ã€æ¨¡å—概述
产品领用归还模块用于管理产品的领用和归还流程:
- **领用**:新增领用时自动完成出库操作,自动审批通过,取消审批流程
- **归还**:新增归还时自动完成入库操作,自动审批通过
**注意**:所有接口统一使用 POST æ–¹å¼
---
## äºŒã€API æŽ¥å£è¯´æ˜Ž
### 1. äº§å“é¢†ç”¨æŽ¥å£
#### 1.1 åˆ†é¡µæŸ¥è¯¢é¢†ç”¨è®°å½•
**请求**
```
POST /productBorrow/listPage
Content-Type: application/json
```
**请求体**
```json
{
  "current": 1,                   // å½“前页码,默认1
  "size": 10,                     // æ¯é¡µæ¡æ•°ï¼Œé»˜è®¤10
  "borrowNo": "LY202605260001",   // é€‰å¡«ï¼Œé¢†ç”¨å•号,模糊查询
  "productModelId": 100,          // é€‰å¡«ï¼Œäº§å“è§„æ ¼ID
  "topParentProductId": 277,      // é€‰å¡«ï¼Œé¡¶éƒ¨çˆ¶äº§å“ID(产品分类ID),用于筛选产品分类树
  "model": "M6",                  // é€‰å¡«ï¼Œè§„格型号,模糊查询
  "borrowerId": 1,                // é€‰å¡«ï¼Œé¢†ç”¨äººID
  "borrowerName": "张三",          // é€‰å¡«ï¼Œé¢†ç”¨äººå§“名,模糊查询
  "approvalStatus": 0,            // é€‰å¡«ï¼Œå®¡æ‰¹çŠ¶æ€ï¼š0待审批/1已通过/2已驳回
  "status": 0                     // é€‰å¡«ï¼Œå½’还状态:0未归还/1部分归还/2已全部归还
}
```
**响应示例**
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "records": [
      {
        "id": 1,
        "borrowNo": "LY202605260001",
        "productModelId": 100,
        "batchNo": "20260526-ABC001-001",
        "borrowQuantity": 10.0000,
        "returnedQuantity": 5.0000,
        "borrowerId": 1,
        "borrowerName": "张三",
        "borrowTime": "2026-05-26 10:00:00",
        "expectedReturnTime": "2026-06-26 10:00:00",
        "approvalStatus": 1,
        "approvalStatusName": "已通过",
        "status": 1,
        "statusName": "部分归还",
        "remark": "项目使用",
        "productName": "螺丝刀",
        "model": "M6",
        "productCode": "LSD-M6",
        "unit": "把",
        "remainingQuantity": 5.0000,
        "createTime": "2026-05-26 09:00:00"
      }
    ],
    "total": 100,
    "size": 10,
    "current": 1,
    "pages": 10
  }
}
```
#### 1.2 æŸ¥è¯¢é¢†ç”¨è®°å½•详情
**请求**
```
POST /productBorrow/getDetail
Content-Type: application/json
```
**请求体**
```json
{
  "id": 1    // å¿…填,领用记录ID
}
```
**响应示例**
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "id": 1,
    "borrowNo": "LY202605260001",
    "productModelId": 100,
    "batchNo": "20260526-ABC001-001",
    "borrowQuantity": 10.0000,
    "returnedQuantity": 5.0000,
    "remainingQuantity": 5.0000,
    "borrowerId": 1,
    "borrowerName": "张三",
    "borrowTime": "2026-05-26 10:00:00",
    "expectedReturnTime": "2026-06-26 10:00:00",
    "approvalStatus": 1,
    "approvalStatusName": "已通过",
    "status": 1,
    "statusName": "部分归还",
    "remark": "项目使用",
    "productName": "螺丝刀",
    "model": "M6",
    "productCode": "LSD-M6",
    "unit": "把"
  }
}
```
#### 1.3 æ–°å¢žé¢†ç”¨è®°å½•
**请求**
```
POST /productBorrow/add
Content-Type: application/json
```
**请求体**
```json
{
  "productModelId": 100,          // å¿…填,产品规格ID
  "batchNo": "20260526-ABC001-001", // é€‰å¡«ï¼Œæ‰¹å·ï¼Œä¸å¡«åˆ™ä½¿ç”¨é»˜è®¤æ‰¹å·
  "borrowQuantity": 10.0000,      // å¿…填,领用数量
  "borrowerId": 1,                // å¿…填,领用人ID(系统用户ID)
  "borrowerName": "张三",          // å¿…填,领用人姓名
  "expectedReturnTime": "2026-06-26 10:00:00", // é€‰å¡«ï¼Œé¢„计归还时间
  "remark": "项目使用"             // é€‰å¡«ï¼Œå¤‡æ³¨
}
```
**响应示例**
```json
{
  "code": 200,
  "msg": "操作成功"
}
```
**自动执行的操作**:
- è‡ªåŠ¨ç”Ÿæˆé¢†ç”¨å•å·ï¼ˆæ ¼å¼ï¼šLY + å¹´æœˆæ—¥ + 4位序号)
- è‡ªåŠ¨åˆ›å»ºå‡ºåº“è®°å½•
- è‡ªåŠ¨å®¡æ ¸é€šè¿‡
- è‡ªåŠ¨æ‰£å‡åº“å­˜
- çŠ¶æ€ç›´æŽ¥è®¾ä¸º"已通过"
#### 1.4 ä¿®æ”¹é¢†ç”¨è®°å½•
**请求**
```
POST /productBorrow/update
Content-Type: application/json
```
**请求体**
```json
{
  "id": 1,                        // å¿…填,领用记录ID
  "productModelId": 100,
  "batchNo": "20260526-ABC001-001",
  "borrowQuantity": 15.0000,
  "borrowerId": 1,
  "borrowerName": "张三",
  "expectedReturnTime": "2026-06-26 10:00:00",
  "remark": "项目使用"
}
```
**注意**:只有待审批状态(approvalStatus=0)的记录才能修改
#### 1.5 åˆ é™¤é¢†ç”¨è®°å½•
**请求**
```
POST /productBorrow/delete
Content-Type: application/json
```
**请求体**
```json
[1, 2, 3]  // è¦åˆ é™¤çš„记录ID数组
```
**注意**:只有待审批状态的记录才能删除
#### 1.6 æ‰¹é‡å®¡æ‰¹é¢†ç”¨è®°å½•
**请求**
```
POST /productBorrow/approve
Content-Type: application/json
```
**请求体**
```json
{
  "ids": [1, 2, 3],           // å¿…填,要审批的记录ID数组
  "approvalStatusParam": 1    // å¿…填,审批状态:1通过/2驳回
}
```
**响应示例**
```json
{
  "code": 200,
  "msg": "操作成功"
}
```
---
### 2. äº§å“å½’还接口
#### 2.1 åˆ†é¡µæŸ¥è¯¢å½’还记录
**请求**
```
POST /productBorrowReturn/listPage
Content-Type: application/json
```
**请求体**
```json
{
  "current": 1,                   // å½“前页码
  "size": 10,                     // æ¯é¡µæ¡æ•°
  "borrowId": 1,                  // é€‰å¡«ï¼Œé¢†ç”¨è®°å½•ID
  "productModelId": 100,          // é€‰å¡«ï¼Œäº§å“è§„æ ¼ID
  "returnerId": 2,                // é€‰å¡«ï¼Œå½’还人ID
  "returnerName": "李四"          // é€‰å¡«ï¼Œå½’还人姓名
}
```
**响应示例**
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "records": [
      {
        "id": 1,
        "borrowId": 1,
        "borrowNo": "LY202605260001",
        "productModelId": 100,
        "batchNo": "20260526-ABC001-001",
        "returnQuantity": 5.0000,
        "returnerId": 2,
        "returnerName": "李四",
        "returnTime": "2026-05-28 14:00:00",
        "remark": "项目结束归还",
        "productName": "螺丝刀",
        "model": "M6",
        "productCode": "LSD-M6",
        "unit": "把",
        "borrowQuantity": 10.0000,
        "borrowerName": "张三"
      }
    ],
    "total": 50,
    "size": 10,
    "current": 1,
    "pages": 5
  }
}
```
#### 2.2 æŸ¥è¯¢æŸä¸ªé¢†ç”¨è®°å½•的归还记录
**请求**
```
POST /productBorrowReturn/listByBorrowId
Content-Type: application/json
```
**请求体**
```json
{
  "current": 1,       // å½“前页码
  "size": 10,         // æ¯é¡µæ¡æ•°
  "borrowId": 1       // å¿…填,领用记录ID
}
```
**响应示例**
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "records": [...],
    "total": 5,
    "size": 10,
    "current": 1,
    "pages": 1
  }
}
```
#### 2.3 æ–°å¢žå½’还记录
**请求**
```
POST /productBorrowReturn/add
Content-Type: application/json
```
**请求体**
```json
{
  "borrowId": 1,                 // å¿…填,领用记录ID
  "returnQuantity": 5.0000,      // å¿…填,归还数量
  "returnerId": 2,               // å¿…填,归还人ID(系统用户ID)
  "returnerName": "李四",         // å¿…填,归还人姓名
  "remark": "项目结束归还"         // é€‰å¡«ï¼Œå¤‡æ³¨
}
```
**响应示例**
```json
{
  "code": 200,
  "msg": "操作成功"
}
```
**自动执行的操作**:
- è‡ªåŠ¨åˆ›å»ºå…¥åº“è®°å½•
- è‡ªåŠ¨å®¡æ ¸é€šè¿‡
- è‡ªåŠ¨å¢žåŠ åº“å­˜
- è‡ªåŠ¨æ›´æ–°é¢†ç”¨è®°å½•çš„å·²å½’è¿˜æ•°é‡å’ŒçŠ¶æ€
**错误情况**:
```json
{
  "code": 500,
  "msg": "归还数量不能大于剩余可归还数量:5.0000"
}
```
---
### 3. åº“存和领用量查询接口
#### 3.1 åˆ†é¡µæŸ¥è¯¢äº§å“åº“存和领用量
**接口说明**:
- ä»¥"产品规格 + æ‰¹å·"为一条记录
- æ˜¾ç¤ºæ¯ä¸ªæ‰¹å·çš„库存数量和被领用量
- ç”¨äºŽé¢†ç”¨æ—¶é€‰æ‹©äº§å“å’Œæ‰¹å·ï¼ŒæŸ¥çœ‹å¯é¢†ç”¨æ•°é‡
**请求**
```
POST /stockInventory/pageStockAndBorrow
Content-Type: application/json
```
**请求体**
```json
{
  "current": 1,                    // å½“前页码
  "size": 10,                      // æ¯é¡µæ¡æ•°
  "topParentProductId": 277,       // å¿…填,顶部父产品ID(产品分类ID)
  "productName": "螺丝刀",          // é€‰å¡«ï¼Œäº§å“åç§°ï¼Œæ¨¡ç³ŠæŸ¥è¯¢
  "model": "M6",                   // é€‰å¡«ï¼Œè§„格型号,模糊查询
  "batchNo": "20260526"            // é€‰å¡«ï¼Œæ‰¹å·ï¼Œæ¨¡ç³ŠæŸ¥è¯¢
}
```
**参数说明**:
| å‚数名 | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|--------|------|------|------|
| current | Integer | å¦ | å½“前页码,默认1 |
| size | Integer | å¦ | æ¯é¡µæ¡æ•°ï¼Œé»˜è®¤10 |
| topParentProductId | Long | æ˜¯ | é¡¶éƒ¨çˆ¶äº§å“ID(产品分类树根节点ID),用于筛选产品分类 |
| productName | String | å¦ | äº§å“åç§°ï¼Œæ¨¡ç³ŠæŸ¥è¯¢ |
| model | String | å¦ | è§„格型号,模糊查询 |
| batchNo | String | å¦ | æ‰¹å·ï¼Œæ¨¡ç³ŠæŸ¥è¯¢ |
**响应示例**
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "records": [
      {
        "productModelId": 100,
        "model": "M6",
        "productCode": "LSD-M6",
        "unit": "把",
        "productName": "螺丝刀",
        "productId": 50,
        "batchNo": "20260526-ABC001-001",
        "qualitity": 100.0000,
        "lockedQuantity": 5.0000,
        "borrowedQuantity": 10.0000,
        "availableQuantity": 90.0000
      },
      {
        "productModelId": 100,
        "model": "M6",
        "productCode": "LSD-M6",
        "unit": "把",
        "productName": "螺丝刀",
        "productId": 50,
        "batchNo": "20260527-ABC001-001",
        "qualitity": 50.0000,
        "lockedQuantity": 0,
        "borrowedQuantity": 0,
        "availableQuantity": 50.0000
      }
    ],
    "total": 20,
    "size": 10,
    "current": 1,
    "pages": 2
  }
}
```
**响应字段说明**:
| å­—段名 | ç±»åž‹ | è¯´æ˜Ž |
|--------|------|------|
| productModelId | Long | äº§å“è§„æ ¼ID(用于领用时传递) |
| model | String | è§„格型号 |
| productCode | String | äº§å“ç¼–码 |
| unit | String | å•位 |
| productName | String | äº§å“åç§° |
| productId | Long | äº§å“ID |
| batchNo | String | æ‰¹å·ï¼ˆç”¨äºŽé¢†ç”¨æ—¶ä¼ é€’,可为null) |
| qualitity | BigDecimal | å½“前库存数量 |
| lockedQuantity | BigDecimal | å†»ç»“/锁定数量 |
| borrowedQuantity | BigDecimal | è¢«é¢†ç”¨ä¸”未归还的数量 |
| availableQuantity | BigDecimal | å¯é¢†ç”¨æ•°é‡ = åº“存数量 - è¢«é¢†ç”¨æ•°é‡ |
**计算公式**:
```
可领用数量(availableQuantity) = åº“存数量(qualitity) - è¢«é¢†ç”¨é‡(borrowedQuantity)
```
**被领用量计算逻辑**:
- åªç»Ÿè®¡å®¡æ‰¹é€šè¿‡çš„领用记录(approval_status = 1)
- åªç»Ÿè®¡æœªå…¨éƒ¨å½’还的记录(status != 2)
- è®¡ç®—公式:领用数量 - å·²å½’还数量
- æŒ‰äº§å“è§„æ ¼ID + æ‰¹å·åˆ†ç»„统计
**使用场景**:
1. **领用选择产品**:
   - è°ƒç”¨æ­¤æŽ¥å£æŸ¥è¯¢åº“å­˜
   - ç”¨æˆ·é€‰æ‹©äº§å“å’Œæ‰¹å·
   - æ˜¾ç¤ºå¯é¢†ç”¨æ•°é‡ä¾›å‚考
   - é¢†ç”¨æ—¶ä¼ é€’ `productModelId` å’Œ `batchNo`
2. **前端显示建议**:
   - åˆ—表显示:产品名称、规格型号、批号、库存数量、被领用量、可领用量
   - å¯é¢†ç”¨æ•°é‡ä¸º0或负数的记录可以标红或禁用选择
   - æ”¯æŒæŒ‰äº§å“åç§°ã€è§„格型号、批号搜索
**前端页面示例**:
```
┌──────────────────────────────────────────────────────────────────────────┐
│  äº§å“åº“存和领用量查询                                                     â”‚
├──────────────────────────────────────────────────────────────────────────┤
│  äº§å“åˆ†ç±»: [选择分类 â–¼]  äº§å“åç§°: [____]  è§„格型号: [____]  [查询]       â”‚
├──────────────────────────────────────────────────────────────────────────┤
│                                                                          â”‚
│  â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”¬â”€â”€â”€â”€â”€â”€â”€â”€â”¬â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”¬â”€â”€â”€â”€â”€â”€â”€â”€â”¬â”€â”€â”€â”€â”€â”€â”€â”€â”¬â”€â”€â”€â”€â”€â”€â”€â”€â”¬â”€â”€â”€â”€â”€â”€â”€â”€â”   â”‚
│  â”‚产品名称│规格型号│  æ‰¹å·    â”‚库存数量│被领用量│可领用量│  æ“ä½œ  â”‚   â”‚
│  â”œâ”€â”€â”€â”€â”€â”€â”€â”€â”¼â”€â”€â”€â”€â”€â”€â”€â”€â”¼â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”¼â”€â”€â”€â”€â”€â”€â”€â”€â”¼â”€â”€â”€â”€â”€â”€â”€â”€â”¼â”€â”€â”€â”€â”€â”€â”€â”€â”¼â”€â”€â”€â”€â”€â”€â”€â”€â”¤   â”‚
│  â”‚螺丝刀  â”‚  M6    â”‚20260526- â”‚  100   â”‚   10   â”‚   90   â”‚[领用] â”‚   â”‚
│  â”‚        â”‚        â”‚ABC001-001│        â”‚        â”‚        â”‚        â”‚   â”‚
│  â”œâ”€â”€â”€â”€â”€â”€â”€â”€â”¼â”€â”€â”€â”€â”€â”€â”€â”€â”¼â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”¼â”€â”€â”€â”€â”€â”€â”€â”€â”¼â”€â”€â”€â”€â”€â”€â”€â”€â”¼â”€â”€â”€â”€â”€â”€â”€â”€â”¼â”€â”€â”€â”€â”€â”€â”€â”€â”¤   â”‚
│  â”‚螺丝刀  â”‚  M6    â”‚20260527- â”‚   50   â”‚    0   â”‚   50   â”‚[领用] â”‚   â”‚
│  â”‚        â”‚        â”‚ABC001-001│        â”‚        â”‚        â”‚        â”‚   â”‚
│  â”œâ”€â”€â”€â”€â”€â”€â”€â”€â”¼â”€â”€â”€â”€â”€â”€â”€â”€â”¼â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”¼â”€â”€â”€â”€â”€â”€â”€â”€â”¼â”€â”€â”€â”€â”€â”€â”€â”€â”¼â”€â”€â”€â”€â”€â”€â”€â”€â”¼â”€â”€â”€â”€â”€â”€â”€â”€â”¤   â”‚
│  â”‚扳手    â”‚  10寸  â”‚ NULL     â”‚   30   â”‚    5   â”‚   25   â”‚[领用] â”‚   â”‚
│  â””────────┴────────┴──────────┴────────┴────────┴────────┴────────┘   â”‚
│                                                                          â”‚
└──────────────────────────────────────────────────────────────────────────┘
```
---
## ä¸‰ã€æŽ¥å£æ±‡æ€»è¡¨
### äº§å“é¢†ç”¨æŽ¥å£
| æŽ¥å£åç§° | è¯·æ±‚路径 | è¯·æ±‚方式 | è¯´æ˜Ž |
|----------|----------|----------|------|
| åˆ†é¡µæŸ¥è¯¢ | /productBorrow/listPage | POST | æŸ¥è¯¢é¢†ç”¨è®°å½•列表 |
| æŸ¥è¯¢è¯¦æƒ… | /productBorrow/getDetail | POST | æ ¹æ®ID查询详情 |
| æ–°å¢žé¢†ç”¨ | /productBorrow/add | POST | æ–°å¢žé¢†ç”¨ï¼ˆè‡ªåŠ¨å‡ºåº“ï¼‰ |
| ä¿®æ”¹é¢†ç”¨ | /productBorrow/update | POST | ä¿®æ”¹é¢†ç”¨è®°å½• |
| åˆ é™¤é¢†ç”¨ | /productBorrow/delete | POST | æ‰¹é‡åˆ é™¤é¢†ç”¨è®°å½• |
| æ‰¹é‡å®¡æ‰¹ | /productBorrow/approve | POST | æ‰¹é‡å®¡æ‰¹ï¼ˆé€šè¿‡/驳回) |
### äº§å“å½’还接口
| æŽ¥å£åç§° | è¯·æ±‚路径 | è¯·æ±‚方式 | è¯´æ˜Ž |
|----------|----------|----------|------|
| åˆ†é¡µæŸ¥è¯¢ | /productBorrowReturn/listPage | POST | æŸ¥è¯¢å½’还记录列表 |
| æŒ‰é¢†ç”¨æŸ¥è¯¢ | /productBorrowReturn/listByBorrowId | POST | æŸ¥è¯¢æŸé¢†ç”¨çš„归还记录 |
| æ–°å¢žå½’还 | /productBorrowReturn/add | POST | æ–°å¢žå½’还(自动入库) |
### åº“存查询接口
| æŽ¥å£åç§° | è¯·æ±‚路径 | è¯·æ±‚方式 | è¯´æ˜Ž |
|----------|----------|----------|------|
| åº“存和领用量 | /stockInventory/pageStockAndBorrow | POST | æŸ¥è¯¢åº“存和被领用量 |
---
## å››ã€çŠ¶æ€è¯´æ˜Ž
### 1. å®¡æ‰¹çŠ¶æ€ (approvalStatus)
| å€¼ | åç§° | è¯´æ˜Ž |
|----|------|------|
| 0 | å¾…审批 | æ–°å¢žåŽçš„默认状态(仅批量审批场景使用) |
| 1 | å·²é€šè¿‡ | å®¡æ‰¹é€šè¿‡ï¼Œå·²æ‰£å‡åº“存,可进行归还操作 |
| 2 | å·²é©³å›ž | å®¡æ‰¹æœªé€šè¿‡ï¼Œä¸æ‰£å‡åº“å­˜ |
**注意**:新增领用时直接自动审批通过,状态为1
### 2. å½’还状态 (status)
| å€¼ | åç§° | è¯´æ˜Ž |
|----|------|------|
| 0 | æœªå½’还 | å°šæœªå½’还任何数量 |
| 1 | éƒ¨åˆ†å½’还 | å·²å½’还部分数量 |
| 2 | å·²å…¨éƒ¨å½’还 | å·²å½’还全部数量,不能再归还 |
---
## äº”、业务流程图
```
┌─────────────────────────────────────────────────────────────┐
│                      é¢†ç”¨æµç¨‹ï¼ˆè‡ªåŠ¨å‡ºåº“ï¼‰                     â”‚
├─────────────────────────────────────────────────────────────┤
│                                                             â”‚
│   â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”    â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”    â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”       â”‚
│   â”‚ æ–°å¢žé¢†ç”¨   â”‚───>│ è‡ªåŠ¨å‡ºåº“   â”‚───>│ çŠ¶æ€è®¾ä¸º   â”‚       â”‚
│   â”‚ è®°å½•       â”‚    â”‚ æ‰£å‡åº“å­˜   â”‚    â”‚ å·²é€šè¿‡     â”‚       â”‚
│   â””────────────┘    â””────────────┘    â””────────────┘       â”‚
│                           â”‚                                 â”‚
│                           â–¼                                 â”‚
│                    â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”                          â”‚
│                    â”‚ åˆ›å»ºå‡ºåº“   â”‚                          â”‚
│                    â”‚ è®°å½•       â”‚                          â”‚
│                    â””────────────┘                          â”‚
│                                                             â”‚
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│                      å½’还流程(自动入库)                     â”‚
├─────────────────────────────────────────────────────────────┤
│                                                             â”‚
│   â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”    â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”    â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”       â”‚
│   â”‚ é€‰æ‹©é¢†ç”¨   â”‚───>│ å¡«å†™å½’还   â”‚───>│ è‡ªåŠ¨å…¥åº“   â”‚       â”‚
│   â”‚ è®°å½•       â”‚    â”‚ ä¿¡æ¯       â”‚    â”‚ å¢žåŠ åº“å­˜   â”‚       â”‚
│   â””────────────┘    â””────────────┘    â””────────────┘       â”‚
│                                              â”‚              â”‚
│                                              â–¼              â”‚
│                                        â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”       â”‚
│                                        â”‚ æ›´æ–°é¢†ç”¨   â”‚       â”‚
│                                        â”‚ çŠ¶æ€       â”‚       â”‚
│                                        â””────────────┘       â”‚
│                                                             â”‚
└─────────────────────────────────────────────────────────────┘
```
---
## å…­ã€æ³¨æ„äº‹é¡¹
### 1. æ•°æ®æ ¡éªŒ
**前端校验**:
- é¢†ç”¨æ•°é‡å¿…须大于0
- å½’还数量不能超过剩余可归还数量
- é¢†ç”¨äººã€å½’还人必须选择系统用户
**后端校验**:
- é¢†ç”¨æ—¶æ£€æŸ¥åº“存是否充足
- å½’还数量不能超过剩余数量
- å·²å…¨éƒ¨å½’还的记录不能再归还
### 2. ç”¨æˆ·é€‰æ‹©
领用人和归还人需要从系统用户表选择,调用系统用户接口:
```
POST /system/user/list
```
### 3. äº§å“è§„格选择
选择产品规格时,可以调用产品接口:
```
POST /basic/product/pageModel
```
### 4. åº“存联动
- **领用**:新增时自动出库(调用库存扣减接口)
- **归还**:新增时自动入库(调用库存增加接口)
---
## ä¸ƒã€é”™è¯¯ç è¯´æ˜Ž
| é”™è¯¯ä¿¡æ¯ | åŽŸå›  | è§£å†³æ–¹æ¡ˆ |
|----------|------|----------|
| é¢†ç”¨è®°å½•不存在 | ID无效 | æ£€æŸ¥è®°å½•ID |
| äº§å“åº“存不存在 | åº“存为空 | æ£€æŸ¥åº“存数据 |
| åº“存不足无法出库 | åº“存不足 | æ£€æŸ¥åº“存数量 |
| è¯¥é¢†ç”¨è®°å½•已全部归还 | å½’还完成 | æ— éœ€å†æ¬¡å½’还 |
| å½’还数量不能大于剩余可归还数量 | æ•°é‡è¶…限 | æ£€æŸ¥å‰©ä½™å¯å½’还数量 |
---
## å…«ã€æµ‹è¯•用例
### 1. æ–°å¢žé¢†ç”¨ï¼ˆè‡ªåŠ¨å‡ºåº“ï¼‰
```json
POST /productBorrow/add
{
  "productModelId": 1,
  "borrowQuantity": 10,
  "borrowerId": 1,
  "borrowerName": "管理员"
}
```
### 2. æŸ¥è¯¢åº“存和领用量
```json
POST /stockInventory/pageStockAndBorrow
{
  "current": 1,
  "size": 10,
  "topParentProductId": 277
}
```
### 3. å½’还产品(自动入库)
```json
POST /productBorrowReturn/add
{
  "borrowId": 1,
  "returnQuantity": 5,
  "returnerId": 1,
  "returnerName": "管理员"
}
```
### 4. æŸ¥è¯¢é¢†ç”¨è¯¦æƒ…
```json
POST /productBorrow/getDetail
{
  "id": 1
}
```
### 5. æŸ¥è¯¢å½’还记录
```json
POST /productBorrowReturn/listByBorrowId
{
  "borrowId": 1,
  "current": 1,
  "size": 10
}
```
src/main/java/com/ruoyi/basic/service/impl/ProductServiceImpl.java
@@ -89,9 +89,6 @@
    @Override
    public int addOrEditProduct(ProductDto productDto) {
        if (ObjectUtils.isEmpty(productDto.getParentId())) {
            throw new IllegalArgumentException("请选择父节点");
        }
        String productName = StringUtils.trim(productDto.getProductName());
        if (StringUtils.isEmpty(productName)) {
            throw new IllegalArgumentException("产品名称不能为空");
@@ -100,15 +97,17 @@
        checkProductNameUnique(productDto.getParentId(), productName, productDto.getId());
        if (productDto.getId() == null) {
            // æ–°å¢žäº§å“é€»è¾‘
            // æ£€æŸ¥çˆ¶èŠ‚ç‚¹æ˜¯å¦å­˜åœ¨ï¼ˆå¯é€‰ï¼Œæ ¹æ®ä¸šåŠ¡éœ€æ±‚ï¼‰
            Product parent = productMapper.selectById(productDto.getParentId());
            if (parent == null) {
                throw new IllegalArgumentException("父节点不存在,无法添加子产品");
            // å¦‚果有父节点,检查父节点是否存在
            if (productDto.getParentId() != null) {
                Product parent = productMapper.selectById(productDto.getParentId());
                if (parent == null) {
                    throw new IllegalArgumentException("父节点不存在,无法添加子产品");
                }
            }
            return productMapper.insert(productDto);
        } else {
            // ç¼–辑产品逻辑
            // æ£€æŸ¥äº§å“æ˜¯å¦å­˜åœ¨ï¼ˆå¯é€‰ï¼Œæ ¹æ®ä¸šåŠ¡éœ€æ±‚ï¼‰
            // æ£€æŸ¥äº§å“æ˜¯å¦å­˜åœ¨
            Product existingProduct = productMapper.selectById(productDto.getId());
            if (existingProduct == null) {
                throw new IllegalArgumentException("要编辑的产品不存在");
@@ -119,10 +118,15 @@
    private void checkProductNameUnique(Long parentId, String productName, Long currentId) {
        LambdaQueryWrapper<Product> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Product::getParentId, parentId)
                .eq(Product::getProductName, productName)
                .ne(currentId != null, Product::getId, currentId)
                .last("limit 1");
        queryWrapper.eq(Product::getProductName, productName)
                .ne(currentId != null, Product::getId, currentId);
        // å¤„理 parentId ä¸º null çš„æƒ…况
        if (parentId == null) {
            queryWrapper.isNull(Product::getParentId);
        } else {
            queryWrapper.eq(Product::getParentId, parentId);
        }
        queryWrapper.last("limit 1");
        Product duplicateProduct = productMapper.selectOne(queryWrapper);
        if (duplicateProduct != null) {
            throw new IllegalArgumentException("对应的" + productName + "已经存在");
src/main/java/com/ruoyi/common/enums/StockInQualifiedRecordTypeEnum.java
@@ -19,7 +19,8 @@
    RETURN_HE_IN("14", "销售退货-合格入库"),
    RETURN_UNSTOCK_IN("15", "销售退货-不合格入库"),
    PICK_RETURN_IN("20", "领料退料-合格入库"),
    FEED_RETURN_IN("22", "生产退料-合格入库");
    FEED_RETURN_IN("22", "生产退料-合格入库"),
    PRODUCT_BORROW_RETURN("23", "产品归还入库");
src/main/java/com/ruoyi/common/enums/StockOutQualifiedRecordTypeEnum.java
@@ -12,7 +12,9 @@
    CUSTOMIZATION_UNSTOCK_OUT("10", "不合格自定义出库"),
    SALE_SHIP_STOCK_OUT("13", "销售-发货出库"),
    PICK_STOCK_OUT("14", "生产领料出库"),
    FEED_STOCK_OUT("15", "生产补料出库");
    FEED_STOCK_OUT("15", "生产补料出库"),
    PRODUCT_BORROW("16", "产品领用出库"),
    PRODUCT_RETURN("17", "产品归还入库");
    private final String code;
    private final String value;
src/main/java/com/ruoyi/stock/controller/ProductBorrowController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,96 @@
package com.ruoyi.stock.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.stock.dto.ProductBorrowDto;
import com.ruoyi.stock.service.ProductBorrowService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
 * äº§å“é¢†ç”¨ Controller
 *
 * @author ruoyi
 */
@Tag(name = "产品领用")
@RestController
@RequestMapping("/productBorrow")
@RequiredArgsConstructor
public class ProductBorrowController {
    private final ProductBorrowService productBorrowService;
    @PostMapping("/listPage")
    @Log(title = "产品领用-列表", businessType = BusinessType.OTHER)
    @Operation(summary = "分页查询领用记录")
    public AjaxResult listPage(@RequestBody Page<ProductBorrowDto> page, @RequestBody ProductBorrowDto dto) {
        IPage<ProductBorrowDto> result = productBorrowService.listPage(page, dto);
        return AjaxResult.success(result);
    }
    @PostMapping("/getDetail")
    @Log(title = "产品领用-详情", businessType = BusinessType.OTHER)
    @Operation(summary = "查询领用记录详情")
    public AjaxResult getDetail(@RequestBody ProductBorrowDto dto) {
        ProductBorrowDto result = productBorrowService.getDetailById(dto.getId());
        return AjaxResult.success(result);
    }
    @PostMapping("/add")
    @Log(title = "产品领用-新增", businessType = BusinessType.INSERT)
    @Operation(summary = "新增领用记录")
    public AjaxResult add(@RequestBody ProductBorrowDto dto) {
        productBorrowService.add(dto);
        return AjaxResult.success();
    }
    @PostMapping("/update")
    @Log(title = "产品领用-修改", businessType = BusinessType.UPDATE)
    @Operation(summary = "修改领用记录")
    public AjaxResult update(@RequestBody ProductBorrowDto dto) {
        productBorrowService.update(dto);
        return AjaxResult.success();
    }
    @PostMapping("/delete")
    @Log(title = "产品领用-删除", businessType = BusinessType.DELETE)
    @Operation(summary = "删除领用记录")
    public AjaxResult delete(@RequestBody List<Long> ids) {
        if (CollectionUtils.isEmpty(ids)) {
            return AjaxResult.error("请选择至少一条数据");
        }
        productBorrowService.delete(ids);
        return AjaxResult.success();
    }
    @PostMapping("/approve")
    @Log(title = "产品领用-审批", businessType = BusinessType.UPDATE)
    @Operation(summary = "批量审批领用记录")
    public AjaxResult approve(@RequestBody ProductBorrowDto dto) {
        if (CollectionUtils.isEmpty(dto.getIds())) {
            return AjaxResult.error("请选择至少一条数据");
        }
        productBorrowService.batchApprove(dto.getIds(), dto.getApprovalStatusParam());
        return AjaxResult.success();
    }
    @PostMapping("/reAudit")
    @Log(title = "产品领用-反审", businessType = BusinessType.UPDATE)
    @Operation(summary = "批量反审领用记录")
    public AjaxResult reAudit(@RequestBody ProductBorrowDto dto) {
        if (CollectionUtils.isEmpty(dto.getIds())) {
            return AjaxResult.error("请选择至少一条数据");
        }
        // åå®¡ï¼šå°†å·²é€šè¿‡çš„记录状态改回待审批,需要考虑是否已经归还
        // æ­¤å¤„暂不实现反审逻辑,如需要可以扩展
        return AjaxResult.error("反审功能暂未实现");
    }
}
src/main/java/com/ruoyi/stock/controller/ProductBorrowReturnController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
package com.ruoyi.stock.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.stock.dto.ProductBorrowReturnDto;
import com.ruoyi.stock.service.ProductBorrowReturnService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
 * äº§å“å½’还 Controller
 *
 * @author ruoyi
 */
@Tag(name = "产品归还")
@RestController
@RequestMapping("/productBorrowReturn")
@RequiredArgsConstructor
public class ProductBorrowReturnController {
    private final ProductBorrowReturnService productBorrowReturnService;
    @PostMapping("/listPage")
    @Log(title = "产品归还-列表", businessType = BusinessType.OTHER)
    @Operation(summary = "分页查询归还记录")
    public AjaxResult listPage(@RequestBody Page<ProductBorrowReturnDto> page, ProductBorrowReturnDto dto) {
        IPage<ProductBorrowReturnDto> result = productBorrowReturnService.listPage(page, dto);
        return AjaxResult.success(result);
    }
    @PostMapping("/listByBorrowId")
    @Log(title = "产品归还-根据领用ID查询", businessType = BusinessType.OTHER)
    @Operation(summary = "查询某个领用记录的归还记录")
    public AjaxResult listByBorrowId(@RequestBody Page<ProductBorrowReturnDto> page, @RequestBody ProductBorrowReturnDto dto) {
        IPage<ProductBorrowReturnDto> result = productBorrowReturnService.listByBorrowId(page, dto.getBorrowId());
        return AjaxResult.success(result);
    }
    @PostMapping("/add")
    @Log(title = "产品归还-新增", businessType = BusinessType.INSERT)
    @Operation(summary = "新增归还记录(直接增加库存,无需审批)")
    public AjaxResult add(@RequestBody ProductBorrowReturnDto dto) {
        productBorrowReturnService.add(dto);
        return AjaxResult.success();
    }
}
src/main/java/com/ruoyi/stock/controller/StockInventoryController.java
@@ -147,4 +147,10 @@
    public R getByModelId(Long productModelId) {
        return R.ok(stockInventoryService.getByModelId(productModelId));
    }
    @PostMapping("/pageStockAndBorrow")
    @Operation(summary = "分页查询产品库存和领用量")
    public R pageStockAndBorrow(@RequestBody Page page, @RequestBody StockInventoryDto stockInventoryDto) {
        return R.ok(stockInventoryService.pageStockAndBorrow(page, stockInventoryDto));
    }
}
src/main/java/com/ruoyi/stock/dto/ProductBorrowDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,49 @@
package com.ruoyi.stock.dto;
import com.ruoyi.stock.pojo.ProductBorrow;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
 * äº§å“é¢†ç”¨DTO
 *
 * @author ruoyi
 */
@EqualsAndHashCode(callSuper = true)
@Data
@Schema(description = "产品领用DTO")
public class ProductBorrowDto extends ProductBorrow {
    @Schema(description = "产品名称")
    private String productName;
    @Schema(description = "规格型号")
    private String model;
    @Schema(description = "产品编码")
    private String productCode;
    @Schema(description = "单位")
    private String unit;
    @Schema(description = "剩余可归还数量")
    private java.math.BigDecimal remainingQuantity;
    @Schema(description = "审批状态名称")
    private String approvalStatusName;
    @Schema(description = "归还状态名称")
    private String statusName;
    @Schema(description = "批量审批ID列表")
    private List<Long> ids;
    @Schema(description = "审批状态(用于审批操作)")
    private Integer approvalStatusParam;
    @Schema(description = "顶部父产品ID(产品分类ID)")
    private Long topParentProductId;
}
src/main/java/com/ruoyi/stock/dto/ProductBorrowReturnDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
package com.ruoyi.stock.dto;
import com.ruoyi.stock.pojo.ProductBorrowReturn;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
 * äº§å“å½’还DTO
 *
 * @author ruoyi
 */
@EqualsAndHashCode(callSuper = true)
@Data
@Schema(description = "产品归还DTO")
public class ProductBorrowReturnDto extends ProductBorrowReturn {
    @Schema(description = "领用单号")
    private String borrowNo;
    @Schema(description = "产品名称")
    private String productName;
    @Schema(description = "规格型号")
    private String model;
    @Schema(description = "产品编码")
    private String productCode;
    @Schema(description = "单位")
    private String unit;
    @Schema(description = "领用数量")
    private java.math.BigDecimal borrowQuantity;
    @Schema(description = "领用人姓名")
    private String borrowerName;
}
src/main/java/com/ruoyi/stock/dto/StockInventoryDto.java
@@ -80,4 +80,16 @@
    @Schema(description = "产品id")
    private Long productId;
    @Schema(description = "被领用数量(未归还)")
    private BigDecimal borrowedQuantity;
    @Schema(description = "可领用数量(库存 - è¢«é¢†ç”¨ï¼‰")
    private BigDecimal availableQuantity;
    @Schema(description = "产品编码")
    private String productCode;
    @Schema(description = "审批状态(用于入库/出库记录)")
    private Integer approvalStatus;
}
src/main/java/com/ruoyi/stock/mapper/ProductBorrowMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package com.ruoyi.stock.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.stock.dto.ProductBorrowDto;
import com.ruoyi.stock.pojo.ProductBorrow;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
 * äº§å“é¢†ç”¨ Mapper æŽ¥å£
 *
 * @author ruoyi
 */
@Mapper
public interface ProductBorrowMapper extends BaseMapper<ProductBorrow> {
    /**
     * åˆ†é¡µæŸ¥è¯¢é¢†ç”¨è®°å½•
     */
    IPage<ProductBorrowDto> listPage(Page<ProductBorrowDto> page, @Param("ew") ProductBorrowDto dto);
    /**
     * æ ¹æ®ID查询详情
     */
    ProductBorrowDto selectDetailById(@Param("id") Long id);
}
src/main/java/com/ruoyi/stock/mapper/ProductBorrowReturnMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package com.ruoyi.stock.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.stock.dto.ProductBorrowReturnDto;
import com.ruoyi.stock.pojo.ProductBorrowReturn;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
 * äº§å“å½’还记录 Mapper æŽ¥å£
 *
 * @author ruoyi
 */
@Mapper
public interface ProductBorrowReturnMapper extends BaseMapper<ProductBorrowReturn> {
    /**
     * åˆ†é¡µæŸ¥è¯¢å½’还记录
     */
    IPage<ProductBorrowReturnDto> listPage(Page<ProductBorrowReturnDto> page, @Param("ew") ProductBorrowReturnDto dto);
    /**
     * æ ¹æ®é¢†ç”¨ID查询归还记录
     */
    IPage<ProductBorrowReturnDto> listByBorrowId(Page<ProductBorrowReturnDto> page, @Param("borrowId") Long borrowId);
}
src/main/java/com/ruoyi/stock/mapper/StockInventoryMapper.java
@@ -58,4 +58,9 @@
    List<StockInventory> getByModelId(@Param("productModelId") Long productModelId);
    IPage<StockInventoryDto> getBatchNoQty(Page page, @Param("ew") StockInventoryDto stockInventoryDto);
    /**
     * åˆ†é¡µæŸ¥è¯¢äº§å“åº“存和领用量
     */
    IPage<StockInventoryDto> pageStockAndBorrow(Page page, @Param("ew") StockInventoryDto stockInventoryDto);
}
src/main/java/com/ruoyi/stock/pojo/ProductBorrow.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,96 @@
package com.ruoyi.stock.pojo;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * äº§å“é¢†ç”¨è¡¨
 *
 * @author ruoyi
 */
@Data
@TableName("product_borrow")
@Schema(name = "ProductBorrow对象", description = "产品领用表")
public class ProductBorrow implements Serializable {
    private static final long serialVersionUID = 1L;
    @Schema(description = "主键")
    @TableId(type = IdType.AUTO)
    private Long id;
    @Schema(description = "领用单号")
    private String borrowNo;
    @Schema(description = "产品规格ID")
    private Long productModelId;
    @Schema(description = "批号")
    private String batchNo;
    @Schema(description = "领用数量")
    private BigDecimal borrowQuantity;
    @Schema(description = "已归还数量")
    private BigDecimal returnedQuantity;
    @Schema(description = "领用人ID")
    private Long borrowerId;
    @Schema(description = "领用人姓名")
    private String borrowerName;
    @Schema(description = "领用时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime borrowTime;
    @Schema(description = "预计归还时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime expectedReturnTime;
    @Schema(description = "审批状态(0-待审批,1-已通过,2-已驳回)")
    private Integer approvalStatus;
    @Schema(description = "归还状态(0-未归还,1-部分归还,2-已全部归还)")
    private Integer status;
    @Schema(description = "备注")
    private String remark;
    @Schema(description = "租户ID")
    @TableField(fill = FieldFill.INSERT)
    private Long tenantId;
    @Schema(description = "部门ID")
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
    @Schema(description = "创建人")
    @TableField(fill = FieldFill.INSERT)
    private Integer createUser;
    @Schema(description = "创建时间")
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    @Schema(description = "更新人")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Integer updateUser;
    @Schema(description = "更新时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;
}
src/main/java/com/ruoyi/stock/pojo/ProductBorrowReturn.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,72 @@
package com.ruoyi.stock.pojo;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * äº§å“å½’还记录表
 *
 * @author ruoyi
 */
@Data
@TableName("product_borrow_return")
@Schema(name = "ProductBorrowReturn对象", description = "产品归还记录表")
public class ProductBorrowReturn implements Serializable {
    private static final long serialVersionUID = 1L;
    @Schema(description = "主键")
    @TableId(type = IdType.AUTO)
    private Long id;
    @Schema(description = "领用记录ID")
    private Long borrowId;
    @Schema(description = "产品规格ID")
    private Long productModelId;
    @Schema(description = "批号")
    private String batchNo;
    @Schema(description = "归还数量")
    private BigDecimal returnQuantity;
    @Schema(description = "归还人ID")
    private Long returnerId;
    @Schema(description = "归还人姓名")
    private String returnerName;
    @Schema(description = "归还时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime returnTime;
    @Schema(description = "备注")
    private String remark;
    @Schema(description = "租户ID")
    @TableField(fill = FieldFill.INSERT)
    private Long tenantId;
    @Schema(description = "部门ID")
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
    @Schema(description = "创建人")
    @TableField(fill = FieldFill.INSERT)
    private Integer createUser;
    @Schema(description = "创建时间")
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
}
src/main/java/com/ruoyi/stock/service/ProductBorrowReturnService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,30 @@
package com.ruoyi.stock.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.stock.dto.ProductBorrowReturnDto;
import com.ruoyi.stock.pojo.ProductBorrowReturn;
/**
 * äº§å“å½’还记录 Service æŽ¥å£
 *
 * @author ruoyi
 */
public interface ProductBorrowReturnService extends IService<ProductBorrowReturn> {
    /**
     * åˆ†é¡µæŸ¥è¯¢å½’还记录
     */
    IPage<ProductBorrowReturnDto> listPage(Page<ProductBorrowReturnDto> page, ProductBorrowReturnDto dto);
    /**
     * æ–°å¢žå½’还记录(直接增加库存,无需审批)
     */
    Boolean add(ProductBorrowReturnDto dto);
    /**
     * æŸ¥è¯¢æŸä¸ªé¢†ç”¨è®°å½•的归还记录
     */
    IPage<ProductBorrowReturnDto> listByBorrowId(Page<ProductBorrowReturnDto> page, Long borrowId);
}
src/main/java/com/ruoyi/stock/service/ProductBorrowService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,52 @@
package com.ruoyi.stock.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.stock.dto.ProductBorrowDto;
import com.ruoyi.stock.pojo.ProductBorrow;
import java.util.List;
/**
 * äº§å“é¢†ç”¨ Service æŽ¥å£
 *
 * @author ruoyi
 */
public interface ProductBorrowService extends IService<ProductBorrow> {
    /**
     * åˆ†é¡µæŸ¥è¯¢é¢†ç”¨è®°å½•
     */
    IPage<ProductBorrowDto> listPage(Page<ProductBorrowDto> page, ProductBorrowDto dto);
    /**
     * æ–°å¢žé¢†ç”¨è®°å½•(待审批)
     */
    Boolean add(ProductBorrowDto dto);
    /**
     * æ›´æ–°é¢†ç”¨è®°å½•
     */
    Boolean update(ProductBorrowDto dto);
    /**
     * åˆ é™¤é¢†ç”¨è®°å½•(仅待审批状态可删除)
     */
    Boolean delete(List<Long> ids);
    /**
     * æ‰¹é‡å®¡æ‰¹
     */
    Boolean batchApprove(List<Long> ids, Integer approvalStatus);
    /**
     * å®¡æ‰¹é€šè¿‡åŽçš„回调(扣减库存)
     */
    void onApprovePass(ProductBorrow borrow);
    /**
     * æ ¹æ®ID查询详情
     */
    ProductBorrowDto getDetailById(Long id);
}
src/main/java/com/ruoyi/stock/service/StockInventoryService.java
@@ -49,4 +49,9 @@
    List<StockInventory> getByModelId(Long modelId);
    IPage<StockInventoryDto> getBatchNoQty(Page page, StockInventoryDto stockInventoryDto);
    /**
     * åˆ†é¡µæŸ¥è¯¢äº§å“åº“存和领用量
     */
    IPage<StockInventoryDto> pageStockAndBorrow(Page page, StockInventoryDto stockInventoryDto);
}
src/main/java/com/ruoyi/stock/service/impl/ProductBorrowReturnServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,109 @@
package com.ruoyi.stock.service.impl;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.stock.dto.ProductBorrowReturnDto;
import com.ruoyi.stock.dto.StockInventoryDto;
import com.ruoyi.stock.mapper.ProductBorrowReturnMapper;
import com.ruoyi.stock.pojo.ProductBorrow;
import com.ruoyi.stock.pojo.ProductBorrowReturn;
import com.ruoyi.stock.service.ProductBorrowReturnService;
import com.ruoyi.stock.service.ProductBorrowService;
import com.ruoyi.stock.service.StockInventoryService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * äº§å“å½’还记录 Service å®žçŽ°ç±»
 *
 * @author ruoyi
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class ProductBorrowReturnServiceImpl extends ServiceImpl<ProductBorrowReturnMapper, ProductBorrowReturn> implements ProductBorrowReturnService {
    private final ProductBorrowReturnMapper productBorrowReturnMapper;
    private final ProductBorrowService productBorrowService;
    private final StockInventoryService stockInventoryService;
    @Override
    public IPage<ProductBorrowReturnDto> listPage(Page<ProductBorrowReturnDto> page, ProductBorrowReturnDto dto) {
        return productBorrowReturnMapper.listPage(page, dto);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean add(ProductBorrowReturnDto dto) {
        // 1. æŸ¥è¯¢é¢†ç”¨è®°å½•
        ProductBorrow borrow = productBorrowService.getById(dto.getBorrowId());
        if (borrow == null) {
            throw new ServiceException("领用记录不存在");
        }
        // 2. æ£€æŸ¥é¢†ç”¨è®°å½•状态(已通过的记录才能归还)
        if (borrow.getApprovalStatus() != 1) {
            throw new ServiceException("领用记录未完成出库,无法归还");
        }
        if (borrow.getStatus() == 2) {
            throw new ServiceException("该领用记录已全部归还");
        }
        // 3. è®¡ç®—剩余可归还数量
        BigDecimal returnedQty = borrow.getReturnedQuantity() != null ? borrow.getReturnedQuantity() : BigDecimal.ZERO;
        BigDecimal remainingQty = borrow.getBorrowQuantity().subtract(returnedQty);
        // 4. éªŒè¯å½’还数量
        if (dto.getReturnQuantity().compareTo(remainingQty) > 0) {
            throw new ServiceException("归还数量不能大于剩余可归还数量:" + remainingQty);
        }
        // 5. è®¾ç½®å¿…要字段
        dto.setProductModelId(borrow.getProductModelId());
        dto.setBatchNo(borrow.getBatchNo());
        dto.setReturnTime(LocalDateTime.now());
        // 6. ä¿å­˜å½’还记录
        save(dto);
        // 7. æ›´æ–°é¢†ç”¨è®°å½•的已归还数量和状态
        BigDecimal newReturnedQty = returnedQty.add(dto.getReturnQuantity());
        borrow.setReturnedQuantity(newReturnedQty);
        // åˆ¤æ–­æ˜¯å¦å…¨éƒ¨å½’还
        if (newReturnedQty.compareTo(borrow.getBorrowQuantity()) >= 0) {
            borrow.setStatus(2); // å·²å…¨éƒ¨å½’还
        } else if (newReturnedQty.compareTo(BigDecimal.ZERO) > 0) {
            borrow.setStatus(1); // éƒ¨åˆ†å½’还
        }
        productBorrowService.updateById(borrow);
        // 8. å¢žåŠ åº“å­˜ï¼ˆå…¥åº“ï¼‰ï¼Œè®¾ç½®å®¡æ‰¹çŠ¶æ€ä¸ºå·²é€šè¿‡ï¼Œè‡ªåŠ¨å®¡æ ¸
        StockInventoryDto stockInDto = new StockInventoryDto();
        stockInDto.setProductModelId(borrow.getProductModelId());
        stockInDto.setBatchNo(borrow.getBatchNo());
        stockInDto.setQualitity(dto.getReturnQuantity());
        stockInDto.setRecordType(String.valueOf(StockInQualifiedRecordTypeEnum.PRODUCT_BORROW_RETURN.getCode()));
        stockInDto.setRecordId(dto.getId());
        stockInDto.setRemark("产品归还入库,领用单号:" + borrow.getBorrowNo());
        // è®¾ç½®å®¡æ‰¹çŠ¶æ€ä¸ºå·²é€šè¿‡ï¼Œè‡ªåŠ¨å®¡æ ¸
        stockInDto.setApprovalStatus(1);
        stockInventoryService.addstockInventory(stockInDto);
        log.info("产品归还成功,领用单号:{},归还数量:{}", borrow.getBorrowNo(), dto.getReturnQuantity());
        return true;
    }
    @Override
    public IPage<ProductBorrowReturnDto> listByBorrowId(Page<ProductBorrowReturnDto> page, Long borrowId) {
        return productBorrowReturnMapper.listByBorrowId(page, borrowId);
    }
}
src/main/java/com/ruoyi/stock/service/impl/ProductBorrowServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,214 @@
package com.ruoyi.stock.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.enums.StockOutQualifiedRecordTypeEnum;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.stock.dto.ProductBorrowDto;
import com.ruoyi.stock.dto.StockInventoryDto;
import com.ruoyi.stock.mapper.ProductBorrowMapper;
import com.ruoyi.stock.pojo.ProductBorrow;
import com.ruoyi.stock.service.ProductBorrowService;
import com.ruoyi.stock.service.StockInventoryService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
/**
 * äº§å“é¢†ç”¨ Service å®žçŽ°ç±»
 *
 * @author ruoyi
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class ProductBorrowServiceImpl extends ServiceImpl<ProductBorrowMapper, ProductBorrow> implements ProductBorrowService {
    private final ProductBorrowMapper productBorrowMapper;
    private final StockInventoryService stockInventoryService;
    @Override
    public IPage<ProductBorrowDto> listPage(Page<ProductBorrowDto> page, ProductBorrowDto dto) {
        IPage<ProductBorrowDto> result = productBorrowMapper.listPage(page, dto);
        // å¡«å……状态名称
        result.getRecords().forEach(this::fillStatusName);
        return result;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean add(ProductBorrowDto dto) {
        // ç”Ÿæˆé¢†ç”¨å•号
        String borrowNo = generateBorrowNo();
        dto.setBorrowNo(borrowNo);
        // è®¾ç½®åˆå§‹çŠ¶æ€ - ç›´æŽ¥è®¾ä¸ºå·²é€šè¿‡ï¼ˆå–消审批流程)
        dto.setApprovalStatus(1); // å·²é€šè¿‡
        dto.setStatus(0); // æœªå½’还
        dto.setReturnedQuantity(BigDecimal.ZERO);
        dto.setBorrowTime(LocalDateTime.now());
        // ä¿å­˜é¢†ç”¨è®°å½•
        save(dto);
        // é¢†ç”¨æ—¶ç›´æŽ¥åšå‡ºåº“操作,自动审核通过
        doBorrowStockOut(dto);
        return true;
    }
    /**
     * æ‰§è¡Œé¢†ç”¨å‡ºåº“操作(自动审核通过)
     */
    private void doBorrowStockOut(ProductBorrow borrow) {
        // æž„建库存扣减参数
        StockInventoryDto stockInventoryDto = new StockInventoryDto();
        stockInventoryDto.setProductModelId(borrow.getProductModelId());
        stockInventoryDto.setBatchNo(borrow.getBatchNo());
        stockInventoryDto.setQualitity(borrow.getBorrowQuantity());
        stockInventoryDto.setRecordType(String.valueOf(StockOutQualifiedRecordTypeEnum.PRODUCT_BORROW.getCode()));
        stockInventoryDto.setRecordId(borrow.getId());
        stockInventoryDto.setRemark("产品领用出库,领用单号:" + borrow.getBorrowNo());
        // è®¾ç½®å®¡æ‰¹çŠ¶æ€ä¸ºå·²é€šè¿‡ï¼Œè‡ªåŠ¨å®¡æ ¸
        stockInventoryDto.setApprovalStatus(1);
        // è°ƒç”¨åº“存服务扣减库存
        stockInventoryService.subtractStockInventory(stockInventoryDto);
        log.info("产品领用出库成功,领用单号:{},数量:{}", borrow.getBorrowNo(), borrow.getBorrowQuantity());
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean update(ProductBorrowDto dto) {
        ProductBorrow existing = getById(dto.getId());
        if (existing == null) {
            throw new ServiceException("领用记录不存在");
        }
        // åªæœ‰æœªé€šè¿‡å®¡æ‰¹çŠ¶æ€æ‰èƒ½ä¿®æ”¹ï¼ˆä½†çŽ°åœ¨é»˜è®¤å·²é€šè¿‡ï¼Œæ‰€ä»¥ä¸å…è®¸ä¿®æ”¹ï¼‰
        if (existing.getApprovalStatus() != 0) {
            throw new ServiceException("领用记录已完成出库,不能修改");
        }
        return updateById(dto);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean delete(List<Long> ids) {
        for (Long id : ids) {
            ProductBorrow borrow = getById(id);
            if (borrow != null && borrow.getApprovalStatus() != 0) {
                throw new ServiceException("领用记录已完成出库,不能删除");
            }
        }
        return removeByIds(ids);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean batchApprove(List<Long> ids, Integer approvalStatus) {
        for (Long id : ids) {
            ProductBorrow borrow = getById(id);
            if (borrow == null) {
                throw new ServiceException("领用记录不存在: " + id);
            }
            if (borrow.getApprovalStatus() != 0) {
                throw new ServiceException("只有待审批状态的记录才能审批");
            }
            borrow.setApprovalStatus(approvalStatus);
            updateById(borrow);
            // å®¡æ‰¹é€šè¿‡åŽæ‰£å‡åº“å­˜
            if (approvalStatus == 1) {
                onApprovePass(borrow);
            }
        }
        return true;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void onApprovePass(ProductBorrow borrow) {
        // æž„建库存扣减参数,设置审批状态为已通过
        StockInventoryDto stockInventoryDto = new StockInventoryDto();
        stockInventoryDto.setProductModelId(borrow.getProductModelId());
        stockInventoryDto.setBatchNo(borrow.getBatchNo());
        stockInventoryDto.setQualitity(borrow.getBorrowQuantity());
        stockInventoryDto.setRecordType(String.valueOf(StockOutQualifiedRecordTypeEnum.PRODUCT_BORROW.getCode()));
        stockInventoryDto.setRecordId(borrow.getId());
        stockInventoryDto.setRemark("产品领用出库,领用单号:" + borrow.getBorrowNo());
        stockInventoryDto.setApprovalStatus(1);
        stockInventoryService.subtractStockInventory(stockInventoryDto);
        log.info("产品领用审批通过,扣减库存成功,领用单号:{},数量:{}", borrow.getBorrowNo(), borrow.getBorrowQuantity());
    }
    @Override
    public ProductBorrowDto getDetailById(Long id) {
        ProductBorrowDto dto = productBorrowMapper.selectDetailById(id);
        if (dto != null) {
            fillStatusName(dto);
            // è®¡ç®—剩余可归还数量
            if (dto.getBorrowQuantity() != null && dto.getReturnedQuantity() != null) {
                dto.setRemainingQuantity(dto.getBorrowQuantity().subtract(dto.getReturnedQuantity()));
            }
        }
        return dto;
    }
    /**
     * ç”Ÿæˆé¢†ç”¨å•号
     * æ ¼å¼ï¼šLY + å¹´æœˆæ—¥ + 4位序号
     */
    private String generateBorrowNo() {
        String dateStr = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        LambdaQueryWrapper<ProductBorrow> wrapper = new LambdaQueryWrapper<>();
        wrapper.likeRight(ProductBorrow::getBorrowNo, "LY" + dateStr)
               .orderByDesc(ProductBorrow::getBorrowNo)
               .last("LIMIT 1");
        ProductBorrow lastBorrow = getOne(wrapper);
        int sequence = 1;
        if (lastBorrow != null && lastBorrow.getBorrowNo() != null) {
            String lastNo = lastBorrow.getBorrowNo();
            if (lastNo.length() >= 14) {
                try {
                    sequence = Integer.parseInt(lastNo.substring(10)) + 1;
                } catch (NumberFormatException ignored) {
                }
            }
        }
        return "LY" + dateStr + String.format("%04d", sequence);
    }
    /**
     * å¡«å……状态名称
     */
    private void fillStatusName(ProductBorrowDto dto) {
        // å®¡æ‰¹çŠ¶æ€åç§°
        if (dto.getApprovalStatus() != null) {
            switch (dto.getApprovalStatus()) {
                case 0: dto.setApprovalStatusName("待审批"); break;
                case 1: dto.setApprovalStatusName("已通过"); break;
                case 2: dto.setApprovalStatusName("已驳回"); break;
            }
        }
        // å½’还状态名称
        if (dto.getStatus() != null) {
            switch (dto.getStatus()) {
                case 0: dto.setStatusName("未归还"); break;
                case 1: dto.setStatusName("部分归还"); break;
                case 2: dto.setStatusName("已全部归还"); break;
            }
        }
    }
}
src/main/java/com/ruoyi/stock/service/impl/StockInventoryServiceImpl.java
@@ -92,6 +92,11 @@
        stockInRecordDto.setBatchNo(stockInventoryDto.getBatchNo());
        stockInRecordDto.setProductModelId(stockInventoryDto.getProductModelId());
        stockInRecordDto.setType("0");
        stockInRecordDto.setRemark(stockInventoryDto.getRemark());
        // å¦‚æžœDTO中指定了审批状态,则使用;否则默认待审批
        if (stockInventoryDto.getApprovalStatus() != null) {
            stockInRecordDto.setApprovalStatus(stockInventoryDto.getApprovalStatus());
        }
        stockInRecordService.add(stockInRecordDto);
        //再进行新增库存数量库存
        //先查询库存表中的产品是否存在,不存在新增,存在更新
@@ -132,6 +137,11 @@
        stockOutRecordDto.setBatchNo(stockInventoryDto.getBatchNo());
        stockOutRecordDto.setProductModelId(stockInventoryDto.getProductModelId());
        stockOutRecordDto.setType("0");
        stockOutRecordDto.setRemark(stockInventoryDto.getRemark());
        // å¦‚æžœDTO中指定了审批状态,则使用;否则默认待审批
        if (stockInventoryDto.getApprovalStatus() != null) {
            stockOutRecordDto.setApprovalStatus(stockInventoryDto.getApprovalStatus());
        }
        stockOutRecordService.add(stockOutRecordDto);
@@ -440,4 +450,9 @@
    public IPage<StockInventoryDto> getBatchNoQty(Page page, StockInventoryDto stockInventoryDto) {
        return stockInventoryMapper.getBatchNoQty(page, stockInventoryDto);
    }
    @Override
    public IPage<StockInventoryDto> pageStockAndBorrow(Page page, StockInventoryDto stockInventoryDto) {
        return stockInventoryMapper.pageStockAndBorrow(page, stockInventoryDto);
    }
}
src/main/resources/mapper/stock/ProductBorrowMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,95 @@
<?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.stock.mapper.ProductBorrowMapper">
    <resultMap id="ProductBorrowDtoMap" type="com.ruoyi.stock.dto.ProductBorrowDto">
        <id column="id" property="id"/>
        <result column="borrow_no" property="borrowNo"/>
        <result column="product_model_id" property="productModelId"/>
        <result column="batch_no" property="batchNo"/>
        <result column="borrow_quantity" property="borrowQuantity"/>
        <result column="returned_quantity" property="returnedQuantity"/>
        <result column="borrower_id" property="borrowerId"/>
        <result column="borrower_name" property="borrowerName"/>
        <result column="borrow_time" property="borrowTime"/>
        <result column="expected_return_time" property="expectedReturnTime"/>
        <result column="approval_status" property="approvalStatus"/>
        <result column="status" property="status"/>
        <result column="remark" property="remark"/>
        <result column="tenant_id" property="tenantId"/>
        <result column="dept_id" property="deptId"/>
        <result column="create_user" property="createUser"/>
        <result column="create_time" property="createTime"/>
        <result column="update_user" property="updateUser"/>
        <result column="update_time" property="updateTime"/>
        <result column="product_name" property="productName"/>
        <result column="model" property="model"/>
        <result column="product_code" property="productCode"/>
        <result column="unit" property="unit"/>
    </resultMap>
    <select id="listPage" resultMap="ProductBorrowDtoMap">
        WITH RECURSIVE product_tree AS (
            SELECT id
            FROM product
            WHERE id = #{ew.topParentProductId}
            UNION ALL
            SELECT p.id
            FROM product p
            INNER JOIN product_tree pt ON p.parent_id = pt.id
        )
        SELECT
            pb.*,
            p.product_name,
            pm.model,
            pm.product_code,
            pm.unit
        FROM product_borrow pb
        LEFT JOIN product_model pm ON pb.product_model_id = pm.id
        LEFT JOIN product p ON pm.product_id = p.id
        <where>
            <if test="ew.borrowNo != null and ew.borrowNo != ''">
                AND pb.borrow_no LIKE CONCAT('%', #{ew.borrowNo}, '%')
            </if>
            <if test="ew.productModelId != null">
                AND pb.product_model_id = #{ew.productModelId}
            </if>
            <if test="ew.borrowerId != null">
                AND pb.borrower_id = #{ew.borrowerId}
            </if>
            <if test="ew.borrowerName != null and ew.borrowerName != ''">
                AND pb.borrower_name LIKE CONCAT('%', #{ew.borrowerName}, '%')
            </if>
            <if test="ew.approvalStatus != null">
                AND pb.approval_status = #{ew.approvalStatus}
            </if>
            <if test="ew.status != null">
                AND pb.status = #{ew.status}
            </if>
            <if test="ew.deptId != null">
                AND pb.dept_id = #{ew.deptId}
            </if>
            <if test="ew.topParentProductId != null and ew.topParentProductId > 0">
                AND p.id IN (SELECT id FROM product_tree)
            </if>
            <if test="ew.model != null and ew.model != ''">
                AND pm.model LIKE CONCAT('%', #{ew.model}, '%')
            </if>
        </where>
        ORDER BY pb.create_time DESC
    </select>
    <select id="selectDetailById" resultMap="ProductBorrowDtoMap">
        SELECT
            pb.*,
            p.product_name,
            pm.model,
            pm.product_code,
            pm.unit
        FROM product_borrow pb
        LEFT JOIN product_model pm ON pb.product_model_id = pm.id
        LEFT JOIN product p ON pm.product_id = p.id
        WHERE pb.id = #{id}
    </select>
</mapper>
src/main/resources/mapper/stock/ProductBorrowReturnMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,80 @@
<?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.stock.mapper.ProductBorrowReturnMapper">
    <resultMap id="ProductBorrowReturnDtoMap" type="com.ruoyi.stock.dto.ProductBorrowReturnDto">
        <id column="id" property="id"/>
        <result column="borrow_id" property="borrowId"/>
        <result column="product_model_id" property="productModelId"/>
        <result column="batch_no" property="batchNo"/>
        <result column="return_quantity" property="returnQuantity"/>
        <result column="returner_id" property="returnerId"/>
        <result column="returner_name" property="returnerName"/>
        <result column="return_time" property="returnTime"/>
        <result column="remark" property="remark"/>
        <result column="tenant_id" property="tenantId"/>
        <result column="dept_id" property="deptId"/>
        <result column="create_user" property="createUser"/>
        <result column="create_time" property="createTime"/>
        <result column="borrow_no" property="borrowNo"/>
        <result column="product_name" property="productName"/>
        <result column="model" property="model"/>
        <result column="product_code" property="productCode"/>
        <result column="unit" property="unit"/>
        <result column="borrow_quantity" property="borrowQuantity"/>
        <result column="borrower_name" property="borrowerName"/>
    </resultMap>
    <select id="listPage" resultMap="ProductBorrowReturnDtoMap">
        SELECT
            pbr.*,
            pb.borrow_no,
            pb.borrow_quantity,
            pb.borrower_name,
            p.product_name,
            pm.model,
            pm.product_code,
            pm.unit
        FROM product_borrow_return pbr
        LEFT JOIN product_borrow pb ON pbr.borrow_id = pb.id
        LEFT JOIN product_model pm ON pbr.product_model_id = pm.id
        LEFT JOIN product p ON pm.product_id = p.id
        <where>
            <if test="ew.borrowId != null">
                AND pbr.borrow_id = #{ew.borrowId}
            </if>
            <if test="ew.productModelId != null">
                AND pbr.product_model_id = #{ew.productModelId}
            </if>
            <if test="ew.returnerId != null">
                AND pbr.returner_id = #{ew.returnerId}
            </if>
            <if test="ew.returnerName != null and ew.returnerName != ''">
                AND pbr.returner_name LIKE CONCAT('%', #{ew.returnerName}, '%')
            </if>
            <if test="ew.deptId != null">
                AND pbr.dept_id = #{ew.deptId}
            </if>
        </where>
        ORDER BY pbr.create_time DESC
    </select>
    <select id="listByBorrowId" resultMap="ProductBorrowReturnDtoMap">
        SELECT
            pbr.*,
            pb.borrow_no,
            pb.borrow_quantity,
            pb.borrower_name,
            p.product_name,
            pm.model,
            pm.product_code,
            pm.unit
        FROM product_borrow_return pbr
        LEFT JOIN product_borrow pb ON pbr.borrow_id = pb.id
        LEFT JOIN product_model pm ON pbr.product_model_id = pm.id
        LEFT JOIN product p ON pm.product_id = p.id
        WHERE pbr.borrow_id = #{borrowId}
        ORDER BY pbr.create_time DESC
    </select>
</mapper>
src/main/resources/mapper/stock/StockInventoryMapper.xml
@@ -671,4 +671,58 @@
        batch_no
    </select>
    <!-- åˆ†é¡µæŸ¥è¯¢äº§å“åº“存和领用量(按批号区分) -->
    <select id="pageStockAndBorrow" resultType="com.ruoyi.stock.dto.StockInventoryDto">
        WITH RECURSIVE product_tree AS (
            SELECT id
            FROM product
            WHERE id = #{ew.topParentProductId}
            UNION ALL
            SELECT p.id
            FROM product p
            INNER JOIN product_tree pt ON p.parent_id = pt.id
        )
        SELECT
            pm.id as product_model_id,
            pm.model,
            pm.product_code,
            pm.unit,
            p.product_name,
            p.id as product_id,
            si.batch_no,
            IFNULL(si.qualitity, 0) as qualitity,
            IFNULL(si.locked_quantity, 0) as locked_quantity,
            IFNULL(borrowed.borrowed_quantity, 0) as borrowed_quantity,
            IFNULL(si.qualitity, 0) - IFNULL(borrowed.borrowed_quantity, 0) as available_quantity
        FROM product_model pm
        LEFT JOIN product p ON pm.product_id = p.id
        LEFT JOIN stock_inventory si ON pm.id = si.product_model_id
        LEFT JOIN (
            SELECT
                product_model_id,
                batch_no,
                SUM(borrow_quantity - IFNULL(returned_quantity, 0)) as borrowed_quantity
            FROM product_borrow
            WHERE approval_status = 1
              AND status != 2
            GROUP BY product_model_id, batch_no
        ) borrowed ON pm.id = borrowed.product_model_id
            AND (si.batch_no = borrowed.batch_no OR (si.batch_no IS NULL AND borrowed.batch_no IS NULL))
        <where>
            <if test="ew.topParentProductId != null and ew.topParentProductId > 0">
                AND p.id IN (SELECT id FROM product_tree)
            </if>
            <if test="ew.productName != null and ew.productName != ''">
                AND p.product_name LIKE CONCAT('%', #{ew.productName}, '%')
            </if>
            <if test="ew.model != null and ew.model != ''">
                AND pm.model LIKE CONCAT('%', #{ew.model}, '%')
            </if>
            <if test="ew.batchNo != null and ew.batchNo != ''">
                AND si.batch_no LIKE CONCAT('%', #{ew.batchNo}, '%')
            </if>
        </where>
        ORDER BY pm.id DESC, si.batch_no
    </select>
</mapper>