7 天以前 2f80b7085c4eabce06d3491306b75eecc275275f
Merge remote-tracking branch 'origin/dev_New_pro' into dev_New_pro

# Conflicts:
# src/main/java/com/ruoyi/approve/service/impl/ApproveProcessServiceImpl.java
已添加5个文件
已修改56个文件
已删除3个文件
3613 ■■■■■ 文件已修改
FILE_UPLOAD_README.md 734 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApproveProcessServiceImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/controller/StorageAttachmentController.java 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/mapper/StorageBlobMapper.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/pojo/ProductModel.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/task/StorageBlobCleanupTask.java 184 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/enums/StockInQualifiedRecordTypeEnum.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/measuringinstrumentledger/service/MeasuringInstrumentLedgerRecordService.java 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/measuringinstrumentledger/service/impl/MeasuringInstrumentLedgerRecordServiceImpl.java 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/measuringinstrumentledger/service/impl/MeasuringInstrumentLedgerServiceImpl.java 105 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/other/controller/TempFileController.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/other/service/TempFileService.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/other/service/impl/TempFileServiceImpl.java 184 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/procurementrecord/utils/StockUtils.java 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/dto/ProductionAccountDto.java 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/dto/ProductionOperationTaskDto.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/dto/ProductionProductMainDto.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/vo/ProductionAccountVo.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/vo/ProductionOperationTaskVo.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/vo/ProductionOrderVo.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/vo/ProductionOrderWorkOrderDetailVo.java 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/controller/ProductionAccountController.java 29 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/controller/ProductionOperationTaskController.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/controller/ProductionOrderController.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/controller/ProductionProductMainController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/mapper/ProductionAccountMapper.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/mapper/ProductionOperationTaskMapper.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/mapper/ProductionProductMainMapper.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/pojo/ProductionOrder.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/pojo/ProductionOrderRoutingOperationParam.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/pojo/ProductionProductOutput.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/ProductionAccountService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/ProductionOperationTaskService.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/ProductionOrderService.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionAccountServiceImpl.java 69 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOperationTaskServiceImpl.java 293 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickServiceImpl.java 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderRoutingOperationParamServiceImpl.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java 208 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionProductMainServiceImpl.java 209 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/common/CommonController.java 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/controller/InvoicePurchaseController.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/controller/TicketRegistrationController.java 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/dto/PurchaseLedgerDto.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/impl/InvoicePurchaseServiceImpl.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/impl/PurchaseLedgerServiceImpl.java 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/impl/TicketRegistrationServiceImpl.java 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java 15 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/safe/controller/SafeTrainingController.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/ICommonFileService.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/CommonFileServiceImpl.java 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/ShippingInfoServiceImpl.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/stock/service/impl/StockInventoryServiceImpl.java 107 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/controller/TechnologyRoutingController.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/technology/service/impl/TechnologyRoutingServiceImpl.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-dev-pro.yml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/basic/StorageAttachmentMapper.xml 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/basic/StorageBlobMapper.xml 61 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionAccountMapper.xml 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionOperationTaskMapper.xml 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionOrderMapper.xml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionProductMainMapper.xml 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/system/SysUserMapper.xml 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
FILE_UPLOAD_README.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,734 @@
# æ–‡ä»¶ä¸Šä¼ åŠŸèƒ½è¯´æ˜Ž
本文档基于以下代码整理:
- `src/main/java/com/ruoyi/basic/utils/FileUtil.java`
- `src/main/java/com/ruoyi/basic/enums/ApplicationTypeEnum.java`
- `src/main/java/com/ruoyi/basic/enums/RecordTypeEnum.java`
- `src/main/java/com/ruoyi/project/common/CommonController.java`
- `src/main/java/com/ruoyi/basic/controller/StorageAttachmentController.java`
用于说明本项目中文件上传、附件绑定、文件预览/下载的整体设计,以及 `FileUtil` ä¸­æ¯ä¸ªæ–¹æ³•的作用。
## 1. æ•´ä½“设计
本项目的文件体系分成两层:
- `storage_blob`:存文件实体信息
  - åŽŸå§‹æ–‡ä»¶å
  - å”¯ä¸€æ–‡ä»¶å `uidFilename`
  - æ–‡ä»¶è·¯å¾„ `path`
  - æ–‡ä»¶å¤§å° `byteSize`
  - æ–‡ä»¶ç±»åž‹ `contentType`
  - å…¬å…±è®¿é—®æ ‡è¯† `resourceKey`
- `storage_attachment`:存文件和业务记录的关联关系
  - `application`:文件用途
  - `recordType`:业务记录类型
  - `recordId`:业务记录主键
  - `storageBlobId`:关联的文件主表 id
可以理解为:
- `storage_blob` è´Ÿè´£â€œæ–‡ä»¶æœ¬èº«â€
- `storage_attachment` è´Ÿè´£â€œæ–‡ä»¶æŒ‚在哪条业务数据上”
## 2. ä¸Šä¼ æµç¨‹
### 2.1 æ™®é€šä¸Šä¼ 
接口:
- `POST /common/upload`
控制器位置:
- `src/main/java/com/ruoyi/project/common/CommonController.java`
入参:
- è¡¨å•字段名:`files`
- ç±»åž‹ï¼š`List<MultipartFile>`
代码逻辑:
1. å‰ç«¯å…ˆè°ƒç”¨ `/common/upload`
2. `CommonController.upload()` è°ƒç”¨ `storageBlobService.upload(files, false)`
3. æœåŠ¡å±‚ä¿å­˜æ–‡ä»¶å…ƒæ•°æ®åˆ° `storage_blob`
4. è¿”回 `StorageBlobVO` åˆ—表,里面通常会带:
   - æ–‡ä»¶ id
   - åŽŸå§‹æ–‡ä»¶å
   - å”¯ä¸€æ–‡ä»¶å
   - é¢„览地址 `previewURL`
   - ä¸‹è½½åœ°å€ `downloadURL`
说明:
- æ­¤æ—¶åªæ˜¯â€œä¸Šä¼ äº†æ–‡ä»¶â€
- è¿˜æ²¡æœ‰å’Œå…·ä½“业务单据建立关系
### 2.2 å…¬å…±ä¸Šä¼ 
接口:
- `POST /common/public/upload`
代码逻辑:
- `CommonController.publicUpload()` è°ƒç”¨ `storageBlobService.upload(files, true)`
说明:
- è¯¥æŽ¥å£ä¸Šä¼ çš„æ–‡ä»¶èµ°â€œå…¬å…±æ–‡ä»¶â€æ¨¡å¼
- æŽ§åˆ¶å™¨æ³¨é‡Šå·²æ˜Žç¡®è¯´æ˜Žï¼šæ°¸ä¹…有效,慎用
- å¯¹åº” URL æž„建时,可能走 `publicKey` å‚数,而不是临时 `token`
## 3. é™„件绑定流程
上传完成后,如果需要把文件绑定到某条业务记录,需要再调用附件接口。
接口:
- `POST /storageAttachment/add`
控制器位置:
- `src/main/java/com/ruoyi/basic/controller/StorageAttachmentController.java`
核心请求对象:
- `StorageAttachmentDTO`
其中继承了 `StorageAttachment`,并额外包含:
- `storageBlobDTOs`:待绑定的文件列表
常用字段含义:
- `application`:文件用途
- `recordType`:业务类型
- `recordId`:业务主键
- `storageBlobDTOs[].id`:上传成功后返回的文件 id
示例请求体:
```json
{
  "application": "file",
  "recordType": "common_file",
  "recordId": 1001,
  "storageBlobDTOs": [
    {
      "id": 12,
      "application": "file"
    },
    {
      "id": 13,
      "application": "file"
    }
  ]
}
```
绑定逻辑说明:
1. å…ˆä¸Šä¼ æ–‡ä»¶ï¼Œæ‹¿åˆ° `storage_blob.id`
2. å†è°ƒç”¨ `/storageAttachment/add`
3. æœåŠ¡å±‚æœ€ç»ˆä¼šé€šè¿‡ `FileUtil` ä¿å­˜ `storage_attachment`
4. åŽç»­å³å¯æŒ‰ä¸šåŠ¡è®°å½•æŸ¥è¯¢å‡ºè¯¥è®°å½•ä¸‹çš„é™„ä»¶
## 4. æŸ¥è¯¢ä¸Žåˆ é™¤é™„ä»¶
### 4.1 æŸ¥è¯¢é™„件列表
接口:
- `GET /storageAttachment/list`
说明:
- æŒ‰ `StorageAttachmentDTO` ä¸­çš„æ¡ä»¶æŸ¥è¯¢
- å¸¸è§æ¡ä»¶æ˜¯ `application`、`recordType`、`recordId`
- è¿”回结果本质上是和业务记录关联后的文件列表
### 4.2 åˆ é™¤é™„ä»¶
接口:
- `DELETE /storageAttachment/delete`
请求体:
- `List<Long> ids`
说明:
- è¿™é‡Œçš„ `ids` æ˜¯é™„件关联表 id,一般是 `storage_attachment.id`
- åˆ é™¤æ—¶é€šå¸¸ä¸ä»…会删关联关系,也会进一步删除对应文件记录
## 5. é¢„览与下载流程
### 5.1 ä¸‹è½½æŽ¥å£
接口:
- `GET /common/download/{fileName}`
支持两种访问方式:
- ä¸´æ—¶é“¾æŽ¥ï¼š`token`
- å…¬å…±é“¾æŽ¥ï¼š`publicKey`
代码逻辑:
1. å¦‚果请求里有 `publicKey`,走 `storageBlobService.getPublicFile(fileName, publicKey)`
2. å¦åˆ™èµ° `storageBlobService.getFileByToken(fileName, token)`
3. å–到实际文件后,调用 `fileUtil.compressFile(file)` åšå›¾ç‰‡åŽ‹ç¼©å¤„ç†
4. è®¾ç½®ä¸‹è½½å“åº”头,输出文件流
### 5.2 é¢„览接口
接口:
- `GET /common/preview/{fileName}`
支持两种访问方式:
- ä¸´æ—¶é“¾æŽ¥ï¼š`token`
- å…¬å…±é“¾æŽ¥ï¼š`publicKey`
代码逻辑:
1. æ ¡éªŒ `token` æˆ– `publicKey`
2. èŽ·å–æ–‡ä»¶
3. è°ƒç”¨ `fileUtil.compressFile(file)`
4. æ ¹æ®æ–‡ä»¶å†…容类型返回 inline é¢„览
## 6. æžšä¸¾å«ä¹‰
### 6.1 `ApplicationTypeEnum`
位置:
- `src/main/java/com/ruoyi/basic/enums/ApplicationTypeEnum.java`
当前定义值:
| æžšä¸¾ | type | è¯´æ˜Ž |
|---|---|---|
| `IMAGE` | `image` | å›¾ç‰‡ç±»æ–‡ä»¶ |
| `FILE` | `file` | æ™®é€šæ–‡ä»¶ |
| `AFTER_FILE` | `after_file` | å”®åŽç›¸å…³æ–‡ä»¶ |
| `BEFORE_FILE` | `before_file` | å”®å‰/前置相关文件 |
| `APK` | `apk` | å®‰è£…包文件 |
作用:
- ç”¨äºŽåŒºåˆ†åŒä¸€æ¡ä¸šåŠ¡è®°å½•ä¸‹ï¼Œä¸åŒç”¨é€”çš„æ–‡ä»¶
- `FileUtil` çš„很多查询、删除、保存方法都会用到该字段
### 6.2 `RecordTypeEnum`
位置:
- `src/main/java/com/ruoyi/basic/enums/RecordTypeEnum.java`
作用:
- ç”¨äºŽæ ‡è®°æ–‡ä»¶å±žäºŽå“ªç±»ä¸šåŠ¡è®°å½•
- ä¾‹å¦‚质检、采购、客户、售后、台账、通知、设备等模块
- ä¸Šä¼ å®ŒæˆåŽï¼Œé™„件最终通过 `recordType + recordId` å’Œä¸šåŠ¡æ•°æ®å…³è”
说明:
- è¯¥æžšä¸¾å€¼å¾ˆå¤šï¼Œæ–‡æ¡£ä¸é€ä¸ªå±•å¼€
- å®žé™…使用时必须传代码中已定义的 `type` å€¼
- å¦‚:
  - `common_file`
  - `after_sales_service`
  - `quality_inspect`
  - `product`
  - `notice`
## 7. `FileUtil` æ–¹æ³•说明
`FileUtil` æ˜¯æœ¬å¥—文件上传体系的核心工具类,主要负责:
- æ–‡ä»¶ä¸Žä¸šåŠ¡è®°å½•ç»‘å®š
- æ–‡ä»¶ä¸Žé™„件删除
- é™„件查询
- é¢„览/下载地址生成
- token ä½¿ç”¨æ¬¡æ•°æŽ§åˆ¶
- å›¾ç‰‡åŽ‹ç¼©
下面按功能分组说明每个方法。
### 7.1 ä¿å­˜é™„件关系
#### 1. `saveStorageAttachment(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId, List<StorageBlobDTO> storageBlobDTOS)`
作用:
- æŒ‰â€œæ–‡ä»¶ç”¨é€” + è®°å½•类型 + è®°å½• id”保存附件关系
逻辑:
1. æ ¡éªŒ `application`、`recordType`、`recordId`
2. å…ˆåˆ é™¤è¿™ç»„业务记录下的旧附件
3. æŠŠæ–°çš„ `storageBlobDTOS` è½¬æˆ `storage_attachment` è®°å½•后批量插入
适用场景:
- æŸæ¡ä¸šåŠ¡æ•°æ®é‡æ–°ä¿å­˜é™„ä»¶ï¼Œæ—§é™„ä»¶æ•´ä½“æ›¿æ¢æˆæ–°é™„ä»¶
#### 2. `saveStorageAttachmentByRecordTypeAndRecordId(String application, RecordTypeEnum recordType, Long recordId, List<StorageBlobDTO> storageBlobDTOS)`
作用:
- æŒ‰ `recordType + recordId` ä¿å­˜é™„件关系,`application` å¯æŒ‡å®šï¼Œä¹Ÿå¯ä»Žæ¯ä¸ªæ–‡ä»¶å¯¹è±¡é‡Œè¯»å–
逻辑特点:
- å¦‚æžœ `application == null`,会根据 `storageBlobDTO.application` åˆ†åˆ«åˆ é™¤æ—§å…³ç³»
- å¦‚果附件列表为空,会直接删除该业务记录的附件关系
- æ’入时会自动回填 `application`
适用场景:
- ä¸€æ¬¡æäº¤é‡Œå¯èƒ½åŒ…含多种用途的附件
- æˆ–者调用方不方便直接传枚举类型
### 7.2 åˆ é™¤æ–‡ä»¶ä¸»è¡¨ `storage_blob`
#### 3. `deleteStorageBlobs(List<Long> storageBlobIds)`
作用:
- æŒ‰æ–‡ä»¶ä¸»è¡¨ id æ‰¹é‡åˆ é™¤æ–‡ä»¶è®°å½•
#### 4. `deleteStorageBlobsByStorageAttachmentIds(List<Long> storageAttachmentIds)`
作用:
- å…ˆæ ¹æ®é™„件关联 id æŸ¥åˆ° `storageBlobId`
- å†åˆ é™¤å¯¹åº”的文件主表记录
#### 5. `deleteStorageBlobsByApplicationAndRecordTypeAndRecordIds(ApplicationTypeEnum application, RecordTypeEnum recordType, List<Long> recordIds)`
作用:
- æ ¹æ®ç”¨é€”、记录类型、多个业务 id,批量删除对应的文件主表记录
适用场景:
- æ‰¹é‡åˆ é™¤æŸç±»ä¸šåŠ¡æ•°æ®æ—¶ï¼ŒåŒæ—¶æ¸…ç†é™„ä»¶
#### 6. `deleteStorageBlobsByRecordTypeAndRecordId(RecordTypeEnum recordType, Long recordId)`
作用:
- æ ¹æ® `recordType + recordId` åˆ é™¤è¯¥ä¸šåŠ¡è®°å½•ä¸‹æ‰€æœ‰æ–‡ä»¶ä¸»è¡¨è®°å½•
### 7.3 åˆ é™¤é™„件关系 `storage_attachment`
#### 7. `deleteStorageAttachmentsByStorageAttachmentIds(List<Long> storageAttachmentIds)`
作用:
- å…ˆåˆ é™¤é™„件对应的文件主表记录
- å†åˆ é™¤é™„件关系表记录
#### 8. `deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId)`
作用:
- åˆ é™¤æŒ‡å®šç”¨é€”、指定业务记录下的附件关系
特点:
- ä¼šå…ˆåˆ  blob,再删 attachment
#### 9. `deleteStorageAttachmentsByRecordTypeAndRecordId(RecordTypeEnum recordType, Long recordId)`
作用:
- åˆ é™¤æŒ‡å®šä¸šåŠ¡è®°å½•ä¸‹å…¨éƒ¨é™„ä»¶å…³ç³»ï¼Œä¸åŒºåˆ†ç”¨é€”
#### 10. `deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordIds(ApplicationTypeEnum application, RecordTypeEnum recordType, List<Long> recordIds)`
作用:
- æŒ‰å¤šä¸ªä¸šåŠ¡ id æ‰¹é‡åˆ é™¤é™„件关系
### 7.4 æŸ¥è¯¢é™„件关系
#### 11. `getStorageAttachmentsByStorageAttachmentIds(List<Long> storageAttachmentIds)`
作用:
- æ ¹æ®é™„件关系 id æŸ¥è¯¢ `storage_attachment` è®°å½•
#### 12. `getStorageAttachmentsByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId)`
作用:
- æŒ‰ç”¨é€”、业务类型、业务 id æŸ¥è¯¢é™„件关系
#### 13. `getStorageAttachmentsByRecordTypeAndRecordId(RecordTypeEnum recordType, Long recordId)`
作用:
- æŒ‰ä¸šåŠ¡ç±»åž‹ã€ä¸šåŠ¡ id æŸ¥è¯¢é™„件关系
### 7.5 æŸ¥è¯¢æ–‡ä»¶ä¿¡æ¯ `StorageBlobVO`
#### 14. `getStorageBlobVOsByApplicationAndRecordTypeAndRecordId(StorageAttachmentDTO storageAttachmentDTO)`
作用:
- é€šè¿‡ `StorageAttachmentDTO` æ¡ä»¶æŸ¥è¯¢æ–‡ä»¶åˆ—表
特点:
- `application` å¯é€‰
- æœ€ç»ˆè¿”回的是带预览/下载地址的 `StorageBlobVO`
#### 15. `getStorageBlobVOsByStorageAttachmentIds(List<Long> storageAttachmentIds)`
作用:
- æ ¹æ®é™„件关系 id æŸ¥è¯¢æ–‡ä»¶åˆ—表
特点:
- ä¼šè‡ªåŠ¨æž„å»ºï¼š
  - `previewURL`
  - `downloadURL`
  - `storageAttachmentId`
#### 16. `getStorageBlobVOsByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId)`
作用:
- æŒ‰ç”¨é€”、业务类型、业务 id æŸ¥è¯¢æ–‡ä»¶åˆ—表
#### 17. `getStorageBlobVOsByRecordTypeAndRecordId(RecordTypeEnum recordType, Long recordId)`
作用:
- æŒ‰ä¸šåŠ¡ç±»åž‹ã€ä¸šåŠ¡ id æŸ¥è¯¢æ–‡ä»¶åˆ—表
#### 18. `getStorageBlobVOsByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId, BigDecimal expired)`
作用:
- å’Œç¬¬ 16 ä¸ªæ–¹æ³•类似,但可以自定义链接过期时间
说明:
- `expired` å•位是分钟
- è¿”回的预览/下载地址会按这个时间生成签名
#### 19. `getStorageBlobVOsByStorageAttachmentIds(List<Long> storageAttachmentIds, BigDecimal expired)`
作用:
- æ ¹æ®é™„件关系 id æŸ¥è¯¢æ–‡ä»¶åˆ—表,并自定义链接过期时间
### 7.6 æŸ¥è¯¢é™„件视图 `StorageAttachmentVO`
#### 20. `getStorageAttachmentVOSByStorageAttachmentIds(List<Long> storageAttachmentIds)`
作用:
- æŸ¥è¯¢é™„件视图对象
特点:
- æ¯æ¡é™„件记录里会嵌套自己的 `storageBlobVOS`
#### 21. `getStorageAttachmentVOSByStorageAttachmentIds(List<Long> storageAttachmentIds, BigDecimal expired)`
作用:
- æ ¹æ®é™„件关系 id æŸ¥è¯¢é™„件视图,并自定义链接过期时间
#### 22. `getStorageAttachmentVOSByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId)`
作用:
- æŒ‰ä¸šåŠ¡ç»´åº¦æŸ¥è¯¢é™„ä»¶è§†å›¾
#### 23. `getStorageAttachmentVOSByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId, BigDecimal expired)`
作用:
- æŒ‰ä¸šåŠ¡ç»´åº¦æŸ¥è¯¢é™„ä»¶è§†å›¾ï¼Œå¹¶è‡ªå®šä¹‰é“¾æŽ¥è¿‡æœŸæ—¶é—´
### 7.7 ä»…获取预览地址
#### 24. `getFilePreviewURLByStorageAttachmentIds(List<Long> storageAttachmentIds)`
作用:
- æ ¹æ®é™„件关系 id åˆ—表,返回预览地址列表
#### 25. `getFilePreviewURLByStorageAttachmentIds(List<Long> storageAttachmentIds, BigDecimal expired)`
作用:
- æ ¹æ®é™„件关系 id åˆ—表,返回带自定义过期时间的预览地址列表
#### 26. `getFilePreviewURLByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId)`
作用:
- æŒ‰ä¸šåŠ¡ç»´åº¦è¿”å›žé¢„è§ˆåœ°å€åˆ—è¡¨
#### 27. `getFilePreviewURLByApplicationAndRecordTypeAndRecordIdAndExpired(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId, BigDecimal expired)`
作用:
- æŒ‰ä¸šåŠ¡ç»´åº¦è¿”å›žå¸¦è‡ªå®šä¹‰è¿‡æœŸæ—¶é—´çš„é¢„è§ˆåœ°å€åˆ—è¡¨
### 7.8 ä»…获取下载地址
#### 28. `getFileDownloadURLByStorageAttachmentIds(List<Long> storageAttachmentIds)`
作用:
- æ ¹æ®é™„件关系 id åˆ—表,返回下载地址列表
#### 29. `getFileDownloadURLByStorageAttachmentIds(List<Long> storageAttachmentIds, BigDecimal expired)`
作用:
- æ ¹æ®é™„件关系 id åˆ—表,返回带自定义过期时间的下载地址列表
#### 30. `getFileDownloadURLByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId)`
作用:
- æŒ‰ä¸šåŠ¡ç»´åº¦è¿”å›žä¸‹è½½åœ°å€åˆ—è¡¨
#### 31. `getFileDownloadURLByApplicationAndRecordTypeAndRecordIdAndExpired(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId, BigDecimal expired)`
作用:
- æŒ‰ä¸šåŠ¡ç»´åº¦è¿”å›žå¸¦è‡ªå®šä¹‰è¿‡æœŸæ—¶é—´çš„ä¸‹è½½åœ°å€åˆ—è¡¨
### 7.9 æž„建签名 URL
#### 32. `buildSignedPreviewUrl(StorageBlobVO storageBlob)`
作用:
- ä½¿ç”¨ç³»ç»Ÿé»˜è®¤è¿‡æœŸæ—¶é—´ï¼Œç”Ÿæˆé¢„览链接
实际调用:
- å†…部等价于调用 `buildSignedUrl(storageBlob, "/preview/", properties.getExpired())`
#### 33. `buildSignedDownloadUrl(StorageBlobVO storageBlob)`
作用:
- ä½¿ç”¨ç³»ç»Ÿé»˜è®¤è¿‡æœŸæ—¶é—´ï¼Œç”Ÿæˆä¸‹è½½é“¾æŽ¥
实际调用:
- å†…部等价于调用 `buildSignedUrl(storageBlob, "/download/", properties.getExpired())`
#### 34. `buildSignedUrl(StorageBlobVO storageBlob, String actionPath, BigDecimal expired)`
作用:
- æž„建统一的带签名预览/下载地址
支持:
- `actionPath = "/preview/"`
- `actionPath = "/download/"`
核心逻辑:
1. æ ¡éªŒè·¯å¾„参数和文件信息
2. æ‹¼æŽ¥åŸºç¡€è®¿é—®åœ°å€
3. å¦‚æžœ `expired == -1`,不生成 token,直接走 `publicKey`
4. å¦åˆ™ç”Ÿæˆå¸¦è¿‡æœŸæ—¶é—´çš„ JWT token
5. æŠŠ token çš„使用次数信息写入 Redis
6. è¿”回最终 URL
重要说明:
- `expired` å•位为分钟
- é»˜è®¤è¿‡æœŸæ—¶é—´ä¸º 120 åˆ†é’Ÿ
- éžæ°¸ä¹…链接会受“过期时间 + ä½¿ç”¨æ¬¡æ•°é™åˆ¶â€åŒé‡æŽ§åˆ¶
### 7.10 token ä½¿ç”¨æŽ§åˆ¶
#### 35. `cacheTokenUsage(String token, long expiredMillis)`
作用:
- æŠŠ token ä½¿ç”¨æ¬¡æ•°åˆå§‹åŒ–到 Redis
特点:
- åˆå§‹å€¼å†™å…¥ä¸º `0`
- TTL ä¸Ž token è¿‡æœŸæ—¶é—´ä¿æŒä¸€è‡´
说明:
- è¿™æ˜¯ç§æœ‰æ–¹æ³•,供 `buildSignedUrl()` å†…部调用
#### 36. `buildTokenUsageKey(String token)`
作用:
- ç»Ÿä¸€ç”Ÿæˆ Redis key
格式:
- `file:token:usage:{token}`
说明:
- è¿™æ˜¯ç§æœ‰æ–¹æ³•
#### 37. `validateTokenUsage(String token)`
作用:
- æ ¡éªŒ token æ˜¯å¦è¿˜èƒ½ç»§ç»­ä½¿ç”¨
核心逻辑:
1. ä»Ž Redis è¯»å–当前使用次数
2. å¦‚果没有值,认为链接已过期或已失效
3. å¦‚果达到上限,立即删除 Redis è®°å½•并报错
4. å¦åˆ™è‡ªå¢žä¸€æ¬¡ä½¿ç”¨æ¬¡æ•°
5. å¦‚果自增后达到上限,再删除 Redis è®°å½•
说明:
- è¯¥æ–¹æ³•通常会在实际访问文件时由服务层调用
#### 38. `resolveLimit()`
作用:
- è§£æž token å¯ä½¿ç”¨æ¬¡æ•°ä¸Šé™
规则:
- `properties.getUseLimit() <= 0` æ—¶ï¼Œé»˜è®¤è¿”回 `10`
说明:
- è¿™æ˜¯ç§æœ‰æ–¹æ³•
### 7.11 è·¯å¾„与压缩
#### 39. `buildRelativePath()`
作用:
- ç”Ÿæˆæ–‡ä»¶å­˜å‚¨ç›¸å¯¹è·¯å¾„
格式:
- `yyyy/MMdd`
例如:
- `2026/0430`
用途:
- ä¸€èˆ¬ç”¨äºŽæŒ‰æ—¥æœŸåˆ†ç›®å½•保存上传文件
#### 40. `compressFile(File file)`
作用:
- å¯¹å›¾ç‰‡è¿›è¡ŒåŽ‹ç¼©ï¼Œéžå›¾ç‰‡æˆ–ä¸æ»¡è¶³æ¡ä»¶æ—¶è¿”å›žåŽŸæ–‡ä»¶
压缩条件:
1. å¼€å¯äº† `properties.getCompress()`
2. æ–‡ä»¶æ˜¯å›¾ç‰‡
3. æ–‡ä»¶å¤§å°å¤§äºŽ `properties.getNeedCompressSize()`
处理逻辑:
1. ç›®æ ‡æ–‡ä»¶åä¸º `thumb_原文件名`
2. å¦‚果压缩文件已存在,直接复用
3. ä½¿ç”¨ `Thumbnailator` æŒ‰åŽŸå°ºå¯¸åŽ‹ç¼©ç”»è´¨
4. å¦‚果压缩失败,降级返回原文件
说明:
- å½“前下载和预览接口都会调用这个方法
#### 41. `isImage(String fileName)`
作用:
- ç®€å•判断文件是否是图片
支持后缀:
- `jpg`
- `jpeg`
- `png`
说明:
- è¿™æ˜¯ç§æœ‰æ–¹æ³•,供 `compressFile()` ä½¿ç”¨
## 8. æŽ¨èä½¿ç”¨é¡ºåº
业务上最常见的接入顺序如下:
1. å‰ç«¯ä¸Šä¼ æ–‡ä»¶åˆ° `/common/upload`
2. æ‹¿åˆ°è¿”回结果中的文件 id
3. ä¸šåŠ¡ä¿å­˜æ—¶è°ƒç”¨ `/storageAttachment/add`
4. ä¼ å…¥ `application + recordType + recordId + storageBlobDTOs`
5. åŽç»­é¡µé¢å›žæ˜¾æ—¶æŒ‰ä¸šåŠ¡æ¡ä»¶è°ƒç”¨é™„ä»¶æŸ¥è¯¢
6. å‰ç«¯ä½¿ç”¨è¿”回的 `previewURL` æˆ– `downloadURL`
## 9. å¸¸è§æ³¨æ„ç‚¹
### 9.1 å…ˆä¸Šä¼ ï¼Œå†ç»‘定
- `/common/upload` åªè´Ÿè´£æ–‡ä»¶å…¥åº“
- `/storageAttachment/add` æ‰æ˜¯å’Œä¸šåŠ¡æ•°æ®å»ºç«‹å…³ç³»
### 9.2 `application` å¾ˆé‡è¦
- åŒä¸€æ¡ `recordId` ä¸‹å¯èƒ½æœ‰å¤šç»„不同用途附件
- åˆ é™¤å’ŒæŸ¥è¯¢æ—¶ï¼Œç»å¸¸ä¾èµ– `application`
### 9.3 ä¸‹è½½é“¾æŽ¥ä¸æ˜¯æ°¸ä¹…有效
- æ™®é€šé“¾æŽ¥ä¸€èˆ¬é€šè¿‡ JWT token æŽ§åˆ¶
- åŒæ—¶å—过期时间和使用次数限制
### 9.4 å…¬å…±æ–‡ä»¶è¦æ…Žç”¨
- `public/upload` ä¸Šä¼ çš„æ–‡ä»¶å¯èµ°æ°¸ä¹…公开访问
- é€‚合公开资源,不适合敏感文件
### 9.5 å›¾ç‰‡é¢„览/下载可能返回压缩文件
- å½“前控制器在下载和预览前都会调用 `compressFile()`
- å¤§å›¾åœ¨è®¿é—®æ—¶å¯èƒ½ä½¿ç”¨åŽ‹ç¼©åŽçš„å‰¯æœ¬
## 10. ä¸€å¥è¯æ€»ç»“
本项目的文件上传方案是“两阶段模型”:
- ç¬¬ä¸€é˜¶æ®µä¸Šä¼ æ–‡ä»¶ï¼Œç”Ÿæˆ `storage_blob`
- ç¬¬äºŒé˜¶æ®µç»‘定业务,生成 `storage_attachment`
而 `FileUtil` åˆ™è´Ÿè´£æŠŠâ€œä¸Šä¼ åŽçš„æ–‡ä»¶â€å˜æˆâ€œå¯æŸ¥è¯¢ã€å¯é¢„览、可下载、可删除、可控时效”的完整附件能力。
src/main/java/com/ruoyi/approve/service/impl/ApproveProcessServiceImpl.java
@@ -7,7 +7,6 @@
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.approve.vo.ApproveProcessVo;
import com.ruoyi.approve.bean.vo.ApproveGetAndUpdateVo;
import com.ruoyi.approve.bean.vo.ApproveProcessConfigNodeVo;
import com.ruoyi.approve.bean.vo.ApproveProcessVO;
@@ -20,7 +19,7 @@
import com.ruoyi.approve.service.ApproveProcessConfigNodeService;
import com.ruoyi.approve.service.IApproveNodeService;
import com.ruoyi.approve.service.IApproveProcessService;
import com.ruoyi.basic.enums.ApplicationTypeEnum;
import com.ruoyi.approve.vo.ApproveProcessVo;
import com.ruoyi.basic.enums.RecordTypeEnum;
import com.ruoyi.basic.utils.FileUtil;
import com.ruoyi.common.enums.FileNameType;
@@ -82,6 +81,9 @@
                .map(ApproveProcessConfigNodeVo::getApproverId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
                if(list.isEmpty()) {
            throw new RuntimeException("流程不存在");
        }
        if (CollectionUtils.isEmpty(nodeIds)) {
            autoPassPurchaseApproveIfNoApprover(approveProcessVO);
            return;
src/main/java/com/ruoyi/basic/controller/StorageAttachmentController.java
@@ -1,16 +1,10 @@
package com.ruoyi.basic.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.basic.dto.StorageAttachmentDTO;
import com.ruoyi.basic.dto.StorageBlobDTO;
import com.ruoyi.basic.dto.SupplierManageDto;
import com.ruoyi.basic.enums.ApplicationTypeEnum;
import com.ruoyi.basic.enums.RecordTypeEnum;
import com.ruoyi.basic.pojo.StorageAttachment;
import com.ruoyi.basic.service.StorageAttachmentService;
import com.ruoyi.common.constant.StorageAttachmentConstants;
import com.ruoyi.common.enums.StorageAttachmentRecordType;
import com.ruoyi.framework.web.domain.R;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;
@@ -18,26 +12,31 @@
@RestController
@AllArgsConstructor
    @RequestMapping("/basic/storage_attachment")
@Tag(name = "通用上传")
@RequestMapping("/storageAttachment")
public class StorageAttachmentController {
    private StorageAttachmentService storageAttachmentService;
    /**
     * åˆ†é¡µæŸ¥è¯¢é€šç”¨æ–‡ä»¶ä¸Šä¼ çš„附件信息
     *
     * @param storageAttachmentDTO å…³è”记录信息
     * @return åˆ†é¡µç»“æžœ
     */
    @GetMapping("/list")
    @Operation(summary = "分页查询通用文件上传的附件信息")
    public R list(StorageAttachmentDTO storageAttachmentDTO) {
        return R.ok(storageAttachmentService.list(storageAttachmentDTO));
    }
    /**
     * åˆ é™¤é€šç”¨æ–‡ä»¶ä¸Šä¼ çš„附件信息
     *
     * @param ids æ–‡ä»¶id列表
     * @return åˆ é™¤ç»“æžœ
     */
    @DeleteMapping("/delete")
    @Operation(summary = "删除通用文件上传的附件信息")
    public R batchDelete(@RequestBody List<Long> ids) {
        return R.ok(storageAttachmentService.batchDeleteStorageAttachment(ids));
    }
@@ -46,6 +45,7 @@
     * ä¿å­˜é€šç”¨æ–‡ä»¶ä¸Šä¼ çš„附件信息
     */
    @PostMapping("/add")
    @Operation(summary = "保存通用文件上传的附件信息")
    public R add(@RequestBody StorageAttachmentDTO storageAttachmentDTO) {
        storageAttachmentService.saveStorageAttachment(storageAttachmentDTO);
        return R.ok();
src/main/java/com/ruoyi/basic/mapper/StorageBlobMapper.java
@@ -3,6 +3,7 @@
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.basic.pojo.StorageBlob;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
 * <p>
@@ -15,4 +16,9 @@
@Mapper
public interface StorageBlobMapper extends BaseMapper<StorageBlob> {
    java.util.List<StorageBlob> selectOrphanBlobsByIdRange(@Param("lastId") long lastId, @Param("limit") int limit);
    int deleteByIdList(@Param("ids") java.util.List<Long> ids);
    java.util.List<String> selectExistingUidFilenames(@Param("fileNames") java.util.List<String> fileNames);
}
src/main/java/com/ruoyi/basic/pojo/ProductModel.java
@@ -37,6 +37,10 @@
    @Excel(name = "规格型号")
    private String model;
    @Excel(name = "产品编码")
    @TableField("product_code")
    private String productCode;
    /**
     * å•位
     */
src/main/java/com/ruoyi/basic/task/StorageBlobCleanupTask.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,184 @@
package com.ruoyi.basic.task;
import com.ruoyi.basic.mapper.StorageBlobMapper;
import com.ruoyi.basic.pojo.StorageBlob;
import com.ruoyi.common.config.FileProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.io.File;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
/**
 * æ¸…理无效文件定时任务。
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class StorageBlobCleanupTask {
    private static final int DB_BATCH_SIZE = 500;
    private static final int FILE_NAME_BATCH_SIZE = 1000;
    private final StorageBlobMapper storageBlobMapper;
    private final FileProperties fileProperties;
    private final AtomicBoolean running = new AtomicBoolean(false);
    /**
     * æ¯æœˆ 1 å·å‡Œæ™¨ 2 ç‚¹æ‰§è¡Œä¸€æ¬¡ï¼š
     * 1. åˆ é™¤ storage_blob ä¸­æœªè¢« storage_attachment å…³è”的记录及其文件
     * 2. åˆ é™¤ç£ç›˜ä¸Šä¸å­˜åœ¨äºŽ storage_blob.uid_filename çš„æ–‡ä»¶
     */
    @Scheduled(cron = "0 0 2 1 * ?")
    public void cleanupUnusedStorageFiles() {
        if (!running.compareAndSet(false, true)) {
            log.warn("文件清理任务正在执行,本次跳过");
            return;
        }
        long start = System.currentTimeMillis();
        log.info("文件清理任务开始执行,根目录:{}", fileProperties.getPath());
        try {
            int removedBlobCount = cleanupOrphanStorageBlobs();
            int removedDiskFileCount = cleanupOrphanDiskFiles();
            long cost = System.currentTimeMillis() - start;
            log.info("文件清理任务执行完成,删除孤儿 blob è®°å½•:{},删除磁盘无效文件:{},耗时:{} ms",
                    removedBlobCount, removedDiskFileCount, cost);
        } catch (Exception e) {
            log.error("文件清理任务执行失败", e);
        } finally {
            running.set(false);
        }
    }
    private int cleanupOrphanStorageBlobs() {
        long lastId = 0L;
        int removedCount = 0;
        while (true) {
            List<StorageBlob> orphanBlobs = storageBlobMapper.selectOrphanBlobsByIdRange(lastId, DB_BATCH_SIZE);
            if (CollectionUtils.isEmpty(orphanBlobs)) {
                break;
            }
            List<Long> ids = new ArrayList<>(orphanBlobs.size());
            for (StorageBlob storageBlob : orphanBlobs) {
                ids.add(storageBlob.getId());
                deleteBlobFiles(storageBlob);
            }
            storageBlobMapper.deleteByIdList(ids);
            removedCount += ids.size();
            lastId = orphanBlobs.get(orphanBlobs.size() - 1).getId();
            log.info("已删除一批孤儿 blob,batchSize={},lastId={}", ids.size(), lastId);
        }
        return removedCount;
    }
    private int cleanupOrphanDiskFiles() {
        File rootDirectory = new File(fileProperties.getPath());
        if (!rootDirectory.exists() || !rootDirectory.isDirectory()) {
            log.warn("文件根目录不存在或不是目录,跳过磁盘清理:{}", fileProperties.getPath());
            return 0;
        }
        int deletedCount = 0;
        Deque<File> directories = new ArrayDeque<>();
        directories.push(rootDirectory);
        while (!directories.isEmpty()) {
            File currentDirectory = directories.pop();
            File[] children = currentDirectory.listFiles();
            if (children == null || children.length == 0) {
                continue;
            }
            List<File> filesInDirectory = new ArrayList<>();
            for (File child : children) {
                if (child.isDirectory()) {
                    directories.push(child);
                } else if (child.isFile()) {
                    filesInDirectory.add(child);
                }
            }
            deletedCount += cleanupFilesInDirectory(filesInDirectory);
        }
        return deletedCount;
    }
    private int cleanupFilesInDirectory(List<File> filesInDirectory) {
        if (CollectionUtils.isEmpty(filesInDirectory)) {
            return 0;
        }
        int deletedCount = 0;
        for (int start = 0; start < filesInDirectory.size(); start += FILE_NAME_BATCH_SIZE) {
            int end = Math.min(start + FILE_NAME_BATCH_SIZE, filesInDirectory.size());
            List<File> batchFiles = filesInDirectory.subList(start, end);
            List<String> fileNames = new ArrayList<>(batchFiles.size());
            for (File file : batchFiles) {
                fileNames.add(file.getName());
            }
            Set<String> existingFileNames = new HashSet<>(storageBlobMapper.selectExistingUidFilenames(fileNames));
            for (File file : batchFiles) {
                if (!existingFileNames.contains(file.getName()) && safeDelete(file)) {
                    deletedCount++;
                }
            }
        }
        return deletedCount;
    }
    private void deleteBlobFiles(StorageBlob storageBlob) {
        File originalFile = resolveBlobFile(storageBlob);
        safeDelete(originalFile);
        File compressedFile = resolveCompressedFile(originalFile);
        safeDelete(compressedFile);
    }
    private File resolveBlobFile(StorageBlob storageBlob) {
        String basePath = fileProperties.getPath();
        if (!StringUtils.hasText(storageBlob.getPath())) {
            return new File(basePath, storageBlob.getUidFilename());
        }
        return new File(new File(basePath, storageBlob.getPath()), storageBlob.getUidFilename());
    }
    private File resolveCompressedFile(File originalFile) {
        if (originalFile == null) {
            return null;
        }
        File parent = originalFile.getParentFile();
        if (parent == null) {
            return null;
        }
        return new File(parent, "thumb_" + originalFile.getName());
    }
    private boolean safeDelete(File file) {
        if (file == null || !file.exists() || !file.isFile()) {
            return false;
        }
        if (file.delete()) {
            return true;
        }
        log.warn("删除文件失败:{}", file.getAbsolutePath());
        return false;
    }
}
src/main/java/com/ruoyi/common/enums/StockInQualifiedRecordTypeEnum.java
@@ -12,7 +12,8 @@
    PURCHASE_STOCK_IN("7", "采购-入库"),
    QUALITYINSPECT_STOCK_IN("6", "质检-合格入库"),
    DEFECTIVE_PASS("11", "不合格-让步放行"),
    RETURN_HE_IN("14", "销售退货-合格入库");
    RETURN_HE_IN("14", "销售退货-合格入库"),
    PICK_RETURN_IN("20", "销售退货-合格入库");
    private final String code;
src/main/java/com/ruoyi/measuringinstrumentledger/service/MeasuringInstrumentLedgerRecordService.java
@@ -4,9 +4,7 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.measuringinstrumentledger.pojo.MeasuringInstrumentLedgerRecord;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
 * @author :yys
@@ -25,5 +23,5 @@
    void export(HttpServletResponse response);
    boolean updateMeasuringInstrumentLedgerRecord(MeasuringInstrumentLedgerRecord measuringInstrumentLedgerRecord) throws IOException;
    boolean updateMeasuringInstrumentLedgerRecord(MeasuringInstrumentLedgerRecord measuringInstrumentLedgerRecord);
}
src/main/java/com/ruoyi/measuringinstrumentledger/service/impl/MeasuringInstrumentLedgerRecordServiceImpl.java
@@ -5,35 +5,21 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.enums.FileNameType;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.measuringinstrumentledger.mapper.MeasuringInstrumentLedgerMapper;
import com.ruoyi.measuringinstrumentledger.mapper.MeasuringInstrumentLedgerRecordMapper;
import com.ruoyi.measuringinstrumentledger.pojo.MeasuringInstrumentLedger;
import com.ruoyi.measuringinstrumentledger.pojo.MeasuringInstrumentLedgerRecord;
import com.ruoyi.measuringinstrumentledger.service.MeasuringInstrumentLedgerRecordService;
import com.ruoyi.other.mapper.TempFileMapper;
import com.ruoyi.other.pojo.TempFile;
import com.ruoyi.sales.mapper.CommonFileMapper;
import com.ruoyi.sales.pojo.CommonFile;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.List;
import java.util.UUID;
/**
 * @author :yys
@@ -47,10 +33,6 @@
    private final MeasuringInstrumentLedgerRecordMapper measuringInstrumentLedgerRecordMapper;
    private final MeasuringInstrumentLedgerMapper measuringInstrumentLedgerMapper;
    private final CommonFileMapper commonFileMapper;
    private final TempFileMapper tempFileMapper;
    @Value("${file.upload-dir}")
    private String uploadDir;
    @Override
    public IPage<MeasuringInstrumentLedgerRecord> listPage(Page page, MeasuringInstrumentLedgerRecord measuringInstrumentLedgerRecord) {
@@ -73,7 +55,7 @@
    }
    @Override
    public boolean updateMeasuringInstrumentLedgerRecord(MeasuringInstrumentLedgerRecord measuringInstrumentLedgerRecord) throws IOException {
    public boolean updateMeasuringInstrumentLedgerRecord(MeasuringInstrumentLedgerRecord measuringInstrumentLedgerRecord) {
        MeasuringInstrumentLedgerRecord measuringInstrumentLedgerRecord1 = measuringInstrumentLedgerRecordMapper.selectById(measuringInstrumentLedgerRecord.getId());
        if (measuringInstrumentLedgerRecord1 == null) {
            return false;
@@ -88,83 +70,6 @@
            measuringInstrumentLedgerMapper.updateById(measuringInstrumentLedger);
        }
        measuringInstrumentLedgerRecordMapper.updateById(measuringInstrumentLedgerRecord);
        // è®°å½•附件绑定
        migrateTempFilesToFormal(measuringInstrumentLedgerRecord.getId(), measuringInstrumentLedgerRecord.getTempFileIds(), FileNameType.MEASURINGRecord.getValue());
        return true;
    }
    /**
     * å°†ä¸´æ—¶æ–‡ä»¶è¿ç§»åˆ°æ­£å¼ç›®å½•
     *
     * @param businessId  ä¸šåŠ¡ID(销售台账ID)
     * @param tempFileIds ä¸´æ—¶æ–‡ä»¶ID列表
     * @throws IOException æ–‡ä»¶æ“ä½œå¼‚常
     */
    private void migrateTempFilesToFormal(Long businessId, List<String> tempFileIds,Integer fileType) throws IOException {
        if (com.baomidou.mybatisplus.core.toolkit.CollectionUtils.isEmpty(tempFileIds)) {
            return;
        }
        // æž„建正式目录路径(按业务类型和日期分组)
        String formalDir = uploadDir + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);
        Path formalDirPath = Paths.get(formalDir);
        // ç¡®ä¿æ­£å¼ç›®å½•存在(递归创建)
        if (!Files.exists(formalDirPath)) {
            Files.createDirectories(formalDirPath);
        }
        for (String tempFileId : tempFileIds) {
            // æŸ¥è¯¢ä¸´æ—¶æ–‡ä»¶è®°å½•
            TempFile tempFile = tempFileMapper.selectById(tempFileId);
            if (tempFile == null) {
                log.warn("临时文件不存在,跳过处理: {}", tempFileId);
                continue;
            }
            // æž„建正式文件名(包含业务ID和时间戳,避免冲突)
            String originalFilename = tempFile.getOriginalName();
            String fileExtension = FilenameUtils.getExtension(originalFilename);
            String formalFilename = businessId + "_" +
                    System.currentTimeMillis() + "_" +
                    UUID.randomUUID().toString().substring(0, 8) +
                    (StringUtils.hasText(fileExtension) ? "." + fileExtension : "");
            Path formalFilePath = formalDirPath.resolve(formalFilename);
            try {
                // æ‰§è¡Œæ–‡ä»¶è¿ç§»ï¼ˆä½¿ç”¨åŽŸå­æ“ä½œç¡®ä¿å®‰å…¨æ€§ï¼‰
//                Files.move(
//                        Paths.get(tempFile.getTempPath()),
//                        formalFilePath,
//                        StandardCopyOption.REPLACE_EXISTING,
//                        StandardCopyOption.ATOMIC_MOVE
//                );
                // åŽŸå­ç§»åŠ¨å¤±è´¥ï¼Œä½¿ç”¨å¤åˆ¶+删除
                Files.copy(Paths.get(tempFile.getTempPath()), formalFilePath, StandardCopyOption.REPLACE_EXISTING);
                Files.deleteIfExists(Paths.get(tempFile.getTempPath()));
                log.info("文件迁移成功: {} -> {}", tempFile.getTempPath(), formalFilePath);
                // æ›´æ–°æ–‡ä»¶è®°å½•(关联到业务ID)
                CommonFile fileRecord = new CommonFile();
                fileRecord.setCommonId(businessId);
                fileRecord.setName(originalFilename);
                fileRecord.setUrl(formalFilePath.toString());
                fileRecord.setCreateTime(LocalDateTime.now());
                fileRecord.setType(fileType);
                commonFileMapper.insert(fileRecord);
                // åˆ é™¤ä¸´æ—¶æ–‡ä»¶è®°å½•
                tempFileMapper.deleteById(tempFile);
                log.info("文件迁移成功: {} -> {}", tempFile.getTempPath(), formalFilePath);
            } catch (IOException e) {
                log.error("文件迁移失败: {}", tempFile.getTempPath(), e);
                // å¯é€‰æ‹©å›žæ»šäº‹åŠ¡æˆ–è®°å½•å¤±è´¥æ–‡ä»¶
                throw new IOException("文件迁移异常", e);
            }
        }
    }
}
src/main/java/com/ruoyi/measuringinstrumentledger/service/impl/MeasuringInstrumentLedgerServiceImpl.java
@@ -6,7 +6,6 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.enums.FileNameType;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.measuringinstrumentledger.dto.MeasuringInstrumentLedgerDto;
import com.ruoyi.measuringinstrumentledger.mapper.MeasuringInstrumentLedgerMapper;
@@ -14,8 +13,6 @@
import com.ruoyi.measuringinstrumentledger.pojo.MeasuringInstrumentLedger;
import com.ruoyi.measuringinstrumentledger.pojo.MeasuringInstrumentLedgerRecord;
import com.ruoyi.measuringinstrumentledger.service.MeasuringInstrumentLedgerService;
import com.ruoyi.other.mapper.TempFileMapper;
import com.ruoyi.other.pojo.TempFile;
import com.ruoyi.project.system.domain.SysUser;
import com.ruoyi.project.system.mapper.SysUserMapper;
import com.ruoyi.sales.mapper.CommonFileMapper;
@@ -23,22 +20,12 @@
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
/**
@@ -52,12 +39,7 @@
    private final MeasuringInstrumentLedgerMapper measuringInstrumentLedgerMapper;
    private final MeasuringInstrumentLedgerRecordMapper measuringInstrumentLedgerRecordMapper;
    private final TempFileMapper tempFileMapper;
    private final CommonFileMapper commonFileMapper;
    private final SysUserMapper sysUserMapper;
    @Value("${file.upload-dir}")
    private String uploadDir;
    @Override
    public IPage<MeasuringInstrumentLedger> listPage(Page page, MeasuringInstrumentLedger measuringInstrumentLedger) {
@@ -74,12 +56,6 @@
                collect = measuringInstrumentLedgerRecords.stream().map(MeasuringInstrumentLedgerRecord::getId).collect(Collectors.toList());
            }
            collect.add(item.getId());
            LambdaQueryWrapper<CommonFile> salesLedgerFileWrapper = new LambdaQueryWrapper<>();
            salesLedgerFileWrapper.in(CommonFile::getCommonId, collect)
                    .in(CommonFile::getType,types);
            List<CommonFile> commonFiles = commonFileMapper.selectList(salesLedgerFileWrapper);
            item.setCommonFiles(commonFiles);
        });
        return measuringInstrumentLedgerIPage;
    }
@@ -107,10 +83,6 @@
//            if(!CollectionUtils.isEmpty(req.getTempFileIds())){
//                migrateTempFilesToFormal(measuringInstrumentLedger.getId(), req.getTempFileIds(), FileNameType.MEASURING.getValue());
//            }
            // å°è´¦è®°å½•绑定一次
            if(!CollectionUtils.isEmpty(req.getTempFileIds())){
                migrateTempFilesToFormal(measuringInstrumentLedgerRecord.getId(), req.getTempFileIds(), FileNameType.MEASURINGRecord.getValue());
            }
            return true;
        }
        return false;
@@ -131,84 +103,7 @@
        }
        measuringInstrumentLedger.setUserName(sysUser.getUserName());
        measuringInstrumentLedgerMapper.insert(measuringInstrumentLedger);
        if(!CollectionUtils.isEmpty(measuringInstrumentLedger.getTempFileIds())){
            migrateTempFilesToFormal(measuringInstrumentLedger.getId(), measuringInstrumentLedger.getTempFileIds(), FileNameType.MEASURING.getValue());
        }
        return true;
    }
    /**
     * å°†ä¸´æ—¶æ–‡ä»¶è¿ç§»åˆ°æ­£å¼ç›®å½•
     *
     * @param businessId  ä¸šåŠ¡ID(销售台账ID)
     * @param tempFileIds ä¸´æ—¶æ–‡ä»¶ID列表
     * @throws IOException æ–‡ä»¶æ“ä½œå¼‚常
     */
    private void migrateTempFilesToFormal(Long businessId, List<String> tempFileIds,Integer fileType) throws IOException {
        if (CollectionUtils.isEmpty(tempFileIds)) {
            return;
        }
        // æž„建正式目录路径(按业务类型和日期分组)
        String formalDir = uploadDir + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);
        Path formalDirPath = Paths.get(formalDir);
        // ç¡®ä¿æ­£å¼ç›®å½•存在(递归创建)
        if (!Files.exists(formalDirPath)) {
            Files.createDirectories(formalDirPath);
        }
        for (String tempFileId : tempFileIds) {
            // æŸ¥è¯¢ä¸´æ—¶æ–‡ä»¶è®°å½•
            TempFile tempFile = tempFileMapper.selectById(tempFileId);
            if (tempFile == null) {
                log.warn("临时文件不存在,跳过处理: {}", tempFileId);
                continue;
            }
            // æž„建正式文件名(包含业务ID和时间戳,避免冲突)
            String originalFilename = tempFile.getOriginalName();
            String fileExtension = FilenameUtils.getExtension(originalFilename);
            String formalFilename = businessId + "_" +
                    System.currentTimeMillis() + "_" +
                    UUID.randomUUID().toString().substring(0, 8) +
                    (StringUtils.hasText(fileExtension) ? "." + fileExtension : "");
            Path formalFilePath = formalDirPath.resolve(formalFilename);
            try {
                // æ‰§è¡Œæ–‡ä»¶è¿ç§»ï¼ˆä½¿ç”¨åŽŸå­æ“ä½œç¡®ä¿å®‰å…¨æ€§ï¼‰
//                Files.move(
//                        Paths.get(tempFile.getTempPath()),
//                        formalFilePath,
//                        StandardCopyOption.REPLACE_EXISTING,
//                        StandardCopyOption.ATOMIC_MOVE
//                );
                // åŽŸå­ç§»åŠ¨å¤±è´¥ï¼Œä½¿ç”¨å¤åˆ¶+删除
                Files.copy(Paths.get(tempFile.getTempPath()), formalFilePath, StandardCopyOption.REPLACE_EXISTING);
                Files.deleteIfExists(Paths.get(tempFile.getTempPath()));
                log.info("文件迁移成功: {} -> {}", tempFile.getTempPath(), formalFilePath);
                // æ›´æ–°æ–‡ä»¶è®°å½•(关联到业务ID)
                CommonFile fileRecord = new CommonFile();
                fileRecord.setCommonId(businessId);
                fileRecord.setName(originalFilename);
                fileRecord.setUrl(formalFilePath.toString());
                fileRecord.setCreateTime(LocalDateTime.now());
                fileRecord.setType(fileType);
                commonFileMapper.insert(fileRecord);
                // åˆ é™¤ä¸´æ—¶æ–‡ä»¶è®°å½•
                tempFileMapper.deleteById(tempFile);
                log.info("文件迁移成功: {} -> {}", tempFile.getTempPath(), formalFilePath);
            } catch (IOException e) {
                log.error("文件迁移失败: {}", tempFile.getTempPath(), e);
                // å¯é€‰æ‹©å›žæ»šäº‹åŠ¡æˆ–è®°å½•å¤±è´¥æ–‡ä»¶
                throw new IOException("文件迁移异常", e);
            }
        }
    }
}
src/main/java/com/ruoyi/other/controller/TempFileController.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/other/service/TempFileService.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/other/service/impl/TempFileServiceImpl.java
ÎļþÒÑɾ³ý
src/main/java/com/ruoyi/procurementrecord/utils/StockUtils.java
@@ -4,7 +4,6 @@
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.ruoyi.procurementrecord.mapper.ProcurementRecordMapper;
import com.ruoyi.procurementrecord.mapper.ProcurementRecordOutMapper;
import com.ruoyi.stock.dto.StockInRecordDto;
import com.ruoyi.stock.dto.StockInventoryDto;
import com.ruoyi.stock.dto.StockUninventoryDto;
import com.ruoyi.stock.mapper.StockInventoryMapper;
@@ -14,15 +13,11 @@
import com.ruoyi.stock.service.StockInventoryService;
import com.ruoyi.stock.service.StockOutRecordService;
import com.ruoyi.stock.service.StockUninventoryService;
import com.ruoyi.stock.service.impl.StockInRecordServiceImpl;
import com.ruoyi.stock.service.impl.StockOutRecordServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@Component
@RequiredArgsConstructor
@@ -37,12 +32,13 @@
    /**
     * ä¸åˆæ ¼å…¥åº“
     *
     * @param productModelId
     * @param quantity
     * @param recordType
     * @param recordId
     */
    public void addUnStock(Long productModelId, BigDecimal quantity, String recordType,Long recordId) {
    public void addUnStock(Long productModelId, BigDecimal quantity, String recordType, Long recordId) {
        StockUninventoryDto stockUninventoryDto = new StockUninventoryDto();
        stockUninventoryDto.setRecordId(recordId);
        stockUninventoryDto.setRecordType(String.valueOf(recordType));
@@ -53,12 +49,13 @@
    /**
     * ä¸åˆæ ¼å‡ºåº“
     *
     * @param productModelId
     * @param quantity
     * @param recordType
     * @param recordId
     */
    public void subtractUnStock(Long productModelId, BigDecimal quantity, Integer recordType,Long recordId) {
    public void subtractUnStock(Long productModelId, BigDecimal quantity, Integer recordType, Long recordId) {
        StockUninventoryDto stockUninventoryDto = new StockUninventoryDto();
        stockUninventoryDto.setRecordId(recordId);
        stockUninventoryDto.setRecordType(String.valueOf(recordType));
@@ -74,7 +71,7 @@
     * @param recordType
     * @param recordId
     */
    public void addStock(Long productModelId, BigDecimal quantity, String recordType,Long recordId) {
    public void addStock(Long productModelId, BigDecimal quantity, String recordType, Long recordId) {
        StockInventoryDto stockInventoryDto = new StockInventoryDto();
        stockInventoryDto.setRecordId(recordId);
        stockInventoryDto.setRecordType(String.valueOf(recordType));
@@ -85,12 +82,13 @@
    /**
     * åˆæ ¼å‡ºåº“
     *
     * @param productModelId
     * @param quantity
     * @param recordType
     * @param recordId
     */
    public void substractStock(Long productModelId, BigDecimal quantity, String recordType,Long recordId) {
    public void substractStock(Long productModelId, BigDecimal quantity, String recordType, Long recordId) {
        StockInventoryDto stockInventoryDto = new StockInventoryDto();
        stockInventoryDto.setRecordId(recordId);
        stockInventoryDto.setRecordType(String.valueOf(recordType));
@@ -115,6 +113,7 @@
        }
    }
    public void deleteStockOutRecord(Long recordId, String recordType) {
        StockOutRecord one = stockOutRecordService.getOne(new QueryWrapper<StockOutRecord>()
                .lambda().eq(StockOutRecord::getRecordId, recordId)
src/main/java/com/ruoyi/production/bean/dto/ProductionAccountDto.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,63 @@
package com.ruoyi.production.bean.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.production.pojo.ProductionAccount;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
@Data
@Schema(name = "ProductionAccountDto", description = "production account query dto")
public class ProductionAccountDto extends ProductionAccount {
    @Schema(description = "sales contract no")
    private String salesContractNo;
    @Schema(description = "customer contract no")
    private String customerContractNo;
    @Schema(description = "project name")
    private String projectName;
    @Schema(description = "customer name")
    private String customerName;
    @Schema(description = "product category")
    private String productCategory;
    @Schema(description = "specification model")
    private String specificationModel;
    @Schema(description = "scheduling user id")
    private Long schedulingUserId;
    @Schema(description = "scheduling user name")
    private String schedulingUserName;
    @Schema(description = "process")
    private String process;
    @Schema(description = "date type(day/month)")
    private String dateType;
    @Schema(description = "day query date")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate entryDate;
    @Schema(description = "date range")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate[] dateRange;
    @Schema(description = "start date")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate entryDateStart;
    @Schema(description = "end date")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate entryDateEnd;
}
src/main/java/com/ruoyi/production/bean/dto/ProductionOperationTaskDto.java
@@ -1,10 +1,34 @@
package com.ruoyi.production.bean.dto;
import com.ruoyi.production.pojo.ProductionOperationTask;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
@EqualsAndHashCode(callSuper = true)
@Data
public class ProductionOperationTaskDto extends ProductionOperationTask {
    @Schema(description = "工序名称")
    private String processName;
    @Schema(description = "生产订单号")
    private String productOrderNpsNo;
    @Schema(description = "产品名称")
    private String productName;
    @Schema(description = "规格型号")
    private String model;
    @Schema(description = "单位")
    private String unit;
    @Schema(description = "报废数量")
    private BigDecimal scrapQty;
    @Schema(description = "完成进度")
    private BigDecimal completionStatus;
}
src/main/java/com/ruoyi/production/bean/dto/ProductionProductMainDto.java
@@ -12,57 +12,63 @@
@EqualsAndHashCode(callSuper = true)
@Data
@Schema(name = "ProductionProductMainDto", description = "生产报工查询对象")
@Schema(name = "ProductionProductMainDto", description = "production report query dto")
public class ProductionProductMainDto extends ProductionProductMain {
    @Schema(description = "产品工艺路线明细ID")
    @Schema(description = "product process route item id")
    private Long productProcessRouteItemId;
    @Schema(description = "生产报工表id")
    @Schema(description = "production report id")
    private Long productMainId;
    @Schema(description = "租户ID")
    @Schema(description = "tenant id")
    private Long tenantId;
    @Schema(description = "工单编号")
    @Schema(description = "work order no")
    private String workOrderNo;
    @Schema(description = "工单状态")
    @Schema(description = "work order status")
    private String workOrderStatus;
    @Schema(description = "昵称")
    @Schema(description = "nick name")
    private String nickName;
    @Schema(description = "数量")
    @Schema(description = "quantity")
    private BigDecimal quantity;
    @Schema(description = "报废数量")
    @Schema(description = "scrap quantity")
    private BigDecimal scrapQty;
    @Schema(description = "产品名称")
    @Schema(description = "product name")
    private String productName;
    @Schema(description = "产品型号名称")
    @Schema(description = "product model name")
    private String productModelName;
    @Schema(description = "单位")
    @Schema(description = "unit")
    private String unit;
    @Schema(description = "销售合同编号")
    @Schema(description = "sales contract no")
    private String salesContractNo;
    @Schema(description = "排产日期")
    @Schema(description = "scheduling date")
    private LocalDate schedulingDate;
    @Schema(description = "排产人员名称")
    @Schema(description = "scheduling user name")
    private String schedulingUserName;
    @Schema(description = "客户名称")
    @Schema(description = "customer name")
    private String customerName;
    @Schema(description = "工序")
    @Schema(description = "process")
    private String process;
    @Schema(description = "工序参数列表")
    @Schema(description = "salary quota")
    private BigDecimal workHours;
    @Schema(description = "wages")
    private BigDecimal wages;
    @Schema(description = "operation param list")
    private List<ProductionOrderRoutingOperationParam> productionOperationParamList;
}
src/main/java/com/ruoyi/production/bean/vo/ProductionAccountVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,59 @@
package com.ruoyi.production.bean.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
@Data
@Schema(name = "ProductionAccountVo", description = "production account page result")
public class ProductionAccountVo {
    @Schema(description = "customer contract no")
    private String customerContractNo;
    @Schema(description = "project name")
    private String projectName;
    @Schema(description = "customer name")
    private String customerName;
    @Schema(description = "product category")
    private String productCategory;
    @Schema(description = "specification model")
    private String specificationModel;
    @Schema(description = "unit")
    private String unit;
    @Schema(description = "scheduling user id")
    private Long schedulingUserId;
    @Schema(description = "scheduling user name")
    private String schedulingUserName;
    @Schema(description = "wages")
    private BigDecimal wages;
    @Schema(description = "finished quantity")
    private BigDecimal finishedNum;
    @Schema(description = "salary quota")
    private BigDecimal workHours;
    @Schema(description = "output rate")
    private String outputRate;
    @Schema(description = "process")
    private String process;
    @Schema(description = "scheduling date")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate schedulingDate;
    @Schema(description = "scheduling month(yyyy-MM)")
    private String schedulingMonth;
}
src/main/java/com/ruoyi/production/bean/vo/ProductionOperationTaskVo.java
@@ -26,9 +26,15 @@
    @Schema(description = "工序名称")
    private String operationName;
    @Schema(description = "工单类型 æ­£å¸¸ /返工返修")
    @Schema(description = "工单类型 æ­£å¸¸/返工返修")
    private String workOrderType;
    @Schema(description = "完成进度")
    private BigDecimal completionStatus;
    @Schema(description = "报工人员名称,多个使用逗号分隔")
    private String userNames;
    @Schema(description = "是否结束)")
    private Boolean endOrder;
}
src/main/java/com/ruoyi/production/bean/vo/ProductionOrderVo.java
@@ -6,6 +6,7 @@
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.util.List;
@EqualsAndHashCode(callSuper = true)
@@ -32,4 +33,7 @@
    @Schema(description = "bom编号")
    private String bomNo;
    @Schema(description = "完成进度")
    private BigDecimal completionStatus;
}
src/main/java/com/ruoyi/production/bean/vo/ProductionOrderWorkOrderDetailVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,66 @@
package com.ruoyi.production.bean.vo;
import com.ruoyi.production.pojo.ProductionOperationTask;
import com.ruoyi.production.pojo.ProductionOrderRoutingOperationParam;
import com.ruoyi.production.pojo.ProductionProductMain;
import com.ruoyi.production.pojo.ProductionProductOutput;
import com.ruoyi.quality.pojo.QualityInspect;
import com.ruoyi.quality.pojo.QualityInspectFile;
import com.ruoyi.quality.pojo.QualityInspectParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
@Schema(name = "ProductionOrderWorkOrderDetailVo", description = "Production order work order/report/inspect detail")
public class ProductionOrderWorkOrderDetailVo {
    @Schema(description = "Production order info")
    private ProductionOrderVo productionOrder;
    @Schema(description = "Work order list")
    private List<WorkOrderDetail> workOrderList;
    @Data
    @Schema(name = "WorkOrderDetail", description = "Work order detail")
    public static class WorkOrderDetail {
        @Schema(description = "Work order info")
        private ProductionOperationTask workOrder;
        @Schema(description = "Report list under current work order")
        private List<ReportDetail> reportList;
    }
    @Data
    @Schema(name = "ReportDetail", description = "Production report detail")
    public static class ReportDetail {
        @Schema(description = "Report main info")
        private ProductionProductMain reportMain;
        @Schema(description = "Report output list")
        private List<ProductionProductOutput> reportOutputList;
        @Schema(description = "Report process param list")
        private List<ProductionOrderRoutingOperationParam> reportParamList;
        @Schema(description = "Inspect list under current report")
        private List<InspectDetail> inspectList;
    }
    @Data
    @Schema(name = "InspectDetail", description = "Quality inspect detail")
    public static class InspectDetail {
        @Schema(description = "Inspect main info")
        private QualityInspect inspect;
        @Schema(description = "Inspect param list")
        private List<QualityInspectParam> inspectParamList;
        @Schema(description = "Inspect attachment list")
        private List<QualityInspectFile> inspectFileList;
    }
}
src/main/java/com/ruoyi/production/controller/ProductionAccountController.java
@@ -1,5 +1,16 @@
package com.ruoyi.production.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.production.bean.dto.ProductionAccountDto;
import com.ruoyi.production.bean.dto.ProductionProductMainDto;
import com.ruoyi.production.bean.vo.ProductionAccountVo;
import com.ruoyi.production.service.ProductionAccountService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -7,12 +18,24 @@
 * <p>
 * ç”Ÿäº§æ ¸ç®—表 å‰ç«¯æŽ§åˆ¶å™¨
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-04-21 03:55:52
 */
@RestController
@RequestMapping("/productionAccount")
@RequiredArgsConstructor
@Tag(name = "生产核算")
public class ProductionAccountController {
    private final ProductionAccountService productionAccountService;
    @GetMapping("/listPage")
    @Operation(summary = "生产核算分页查询")
    public R<IPage<ProductionAccountVo>> listPage(Page<ProductionAccountDto> page, ProductionAccountDto dto) {
        return R.ok(productionAccountService.listPage(page, dto));
    }
    @GetMapping("/listProductionDetails")
    @Operation(summary ="查询工人生产工资信息")
    public R<IPage<ProductionProductMainDto>> listProductionDetails(ProductionAccountDto productionAccountDto, Page page) {
        return R.ok(productionAccountService.listProductionDetails(productionAccountDto,page));
    }
}
src/main/java/com/ruoyi/production/controller/ProductionOperationTaskController.java
@@ -6,18 +6,11 @@
import com.ruoyi.production.bean.vo.ProductionOperationTaskVo;
import com.ruoyi.production.pojo.ProductionOperationTask;
import com.ruoyi.production.service.ProductionOperationTaskService;
import io.swagger.annotations.ApiOperation;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@@ -71,4 +64,20 @@
        return R.ok(productionOperationTaskService.updateProductWorkOrder(dto));
    }
    @Operation(summary = "指派报工人")
    @PostMapping("/assign")
    public R<Boolean> assign(@RequestBody ProductionOperationTaskDto dto) {
        return R.ok(productionOperationTaskService.assign(dto));
    }
    /**
     * å·¥å•流转卡下载
     * @param response
     * @param dto
     */
    @PostMapping("/down")
    public void down(HttpServletResponse response, @RequestBody ProductionOperationTaskDto dto) {
        productionOperationTaskService.down(response, dto);
    }
}
src/main/java/com/ruoyi/production/controller/ProductionOrderController.java
@@ -7,6 +7,7 @@
import com.ruoyi.production.bean.vo.ProductionOrderPickVo;
import com.ruoyi.production.bean.vo.ProductionOrderVo;
import com.ruoyi.production.bean.vo.ProductionPlanVo;
import com.ruoyi.production.bean.vo.ProductionOrderWorkOrderDetailVo;
import com.ruoyi.production.pojo.ProductionOrder;
import com.ruoyi.production.service.ProductionOrderService;
import io.swagger.v3.oas.annotations.Operation;
@@ -82,4 +83,16 @@
    public R<List<ProductionOrderPickVo>> pick(@PathVariable Long productionOrderId) {
        return R.ok(productionOrderService.pick(productionOrderId));
    }
    @GetMapping("/workOrder/detail/{productionOrderId}")
    @Operation(summary = "Query work orders/reports/inspects by production order id")
    public R<ProductionOrderWorkOrderDetailVo> getWorkOrderReportInspectDetail(@PathVariable Long productionOrderId) {
        return R.ok(productionOrderService.getWorkOrderReportInspectDetail(productionOrderId));
    }
    @Operation(summary = "更新订单状态")
    @PostMapping("/updateOrder")
    public R updateOrder(@RequestBody ProductionOrderDto productionOrderDto) {
        return R.ok(productionOrderService.updateOrder(productionOrderDto));
    }
}
src/main/java/com/ruoyi/production/controller/ProductionProductMainController.java
@@ -9,6 +9,7 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@@ -50,6 +51,7 @@
     * @return
     */
    @PostMapping("/addProductMain")
    @PreAuthorize("@ss.hasPermi('productionProductMain:add')")
    public R addProductMain(@RequestBody ProductionProductMainDto productionProductMainDto) {
        return R.ok(productionProductMainService.addProductMain(productionProductMainDto));
    }
src/main/java/com/ruoyi/production/mapper/ProductionAccountMapper.java
@@ -1,7 +1,11 @@
package com.ruoyi.production.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.production.bean.dto.ProductionAccountDto;
import com.ruoyi.production.bean.dto.UserAccountDto;
import com.ruoyi.production.bean.vo.ProductionAccountVo;
import com.ruoyi.production.pojo.ProductionAccount;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@@ -19,6 +23,8 @@
 */
@Mapper
public interface ProductionAccountMapper extends BaseMapper<ProductionAccount> {
    IPage<ProductionAccountVo> listPage(Page<ProductionAccountDto> page, @Param("c") ProductionAccountDto dto);
    UserAccountDto selectUserAccount(@Param("userId") Long userId, @Param("date") String date);
    List<Map<String, Object>> selectDailyWagesStats(@Param("startDate") String startDate, @Param("endDate") String endDate);
src/main/java/com/ruoyi/production/mapper/ProductionOperationTaskMapper.java
@@ -40,4 +40,5 @@
                                                                           @Param("userId") Long userId,
                                                                           @Param("processIds") List<Long> processIds);
    ProductionOperationTaskDto getProductWorkOrderFlowCard(@Param("id") Long id);
}
src/main/java/com/ruoyi/production/mapper/ProductionProductMainMapper.java
@@ -3,8 +3,8 @@
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.production.bean.dto.ProductionAccountDto;
import com.ruoyi.production.bean.dto.ProductionProductMainDto;
import com.ruoyi.production.bean.dto.SalesLedgerProductionAccountingDto;
import com.ruoyi.production.pojo.ProductionOrder;
import com.ruoyi.production.pojo.ProductionProductMain;
import org.apache.ibatis.annotations.Mapper;
@@ -30,7 +30,7 @@
     */
    ProductionOrder getOrderByMainId(@Param("productMainId") Long productMainId);
    IPage<ProductionProductMainDto> listProductionDetails(@Param("ew") SalesLedgerProductionAccountingDto salesLedgerProductionAccountingDto, Page page);
    IPage<ProductionProductMainDto> listProductionDetails(@Param("c") ProductionAccountDto productionAccountDto, Page page);
    ArrayList<Long> listMain(List<Long> idList);
}
src/main/java/com/ruoyi/production/pojo/ProductionOrder.java
@@ -74,6 +74,10 @@
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate planCompleteTime;
    @Schema(description = "状态(1.待开始 2.进行中 3.已完成 4.已取消)")
    @Schema(description = "状态(1.待开始 2.进行中 3.已完成 4.已取消 5.已结束)")
    private Integer status;
    @Schema(description = "是否结束)")
    @TableField("is_end_order")
    private Boolean endOrder;
}
src/main/java/com/ruoyi/production/pojo/ProductionOrderRoutingOperationParam.java
@@ -90,4 +90,7 @@
    @Schema(description = "生产订单工艺路线工序ID")
    private Long productionOrderRoutingOperationId;
    @Schema(description = "生产报工表ID")
    private Long productionProductMainId;
}
src/main/java/com/ruoyi/production/pojo/ProductionProductOutput.java
@@ -23,7 +23,7 @@
    @Schema(description = "产品id")
    private Long productModelId;
    @Schema(description = "报工数量(总数量)")
    @Schema(description = "合格数量")
    private BigDecimal quantity;
    @Schema(description = "创建时间")
src/main/java/com/ruoyi/production/service/ProductionAccountService.java
@@ -1,5 +1,10 @@
package com.ruoyi.production.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.production.bean.dto.ProductionAccountDto;
import com.ruoyi.production.bean.dto.ProductionProductMainDto;
import com.ruoyi.production.bean.vo.ProductionAccountVo;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.production.pojo.ProductionAccount;
@@ -12,5 +17,7 @@
 * @since 2026-04-21 03:55:52
 */
public interface ProductionAccountService extends IService<ProductionAccount> {
    IPage<ProductionAccountVo> listPage(Page<ProductionAccountDto> page, ProductionAccountDto dto);
    IPage<ProductionProductMainDto> listProductionDetails(ProductionAccountDto productionAccountDto, Page page);
}
src/main/java/com/ruoyi/production/service/ProductionOperationTaskService.java
@@ -6,6 +6,7 @@
import com.ruoyi.production.bean.dto.ProductionOperationTaskDto;
import com.ruoyi.production.bean.vo.ProductionOperationTaskVo;
import com.ruoyi.production.pojo.ProductionOperationTask;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
@@ -23,4 +24,8 @@
    boolean removeProductionOperationTask(List<Long> ids);
    int updateProductWorkOrder(ProductionOperationTaskDto dto);
    boolean assign(ProductionOperationTaskDto dto);
    void down(HttpServletResponse response, ProductionOperationTaskDto dto);
}
src/main/java/com/ruoyi/production/service/ProductionOrderService.java
@@ -7,6 +7,7 @@
import com.ruoyi.production.bean.vo.ProductionOrderPickVo;
import com.ruoyi.production.bean.vo.ProductionOrderVo;
import com.ruoyi.production.bean.vo.ProductionPlanVo;
import com.ruoyi.production.bean.vo.ProductionOrderWorkOrderDetailVo;
import com.ruoyi.production.pojo.ProductionOrder;
import java.util.List;
@@ -30,4 +31,8 @@
    List<ProductionPlanVo> getSource(Long id);
    List<ProductionOrderPickVo> pick(Long productionOrderId);
    ProductionOrderWorkOrderDetailVo getWorkOrderReportInspectDetail(Long productionOrderId);
    int updateOrder(ProductionOrderDto productionOrderDto);
}
src/main/java/com/ruoyi/production/service/impl/ProductionAccountServiceImpl.java
@@ -1,20 +1,73 @@
package com.ruoyi.production.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.production.bean.dto.ProductionAccountDto;
import com.ruoyi.production.bean.dto.ProductionProductMainDto;
import com.ruoyi.production.bean.vo.ProductionAccountVo;
import com.ruoyi.production.mapper.ProductionAccountMapper;
import com.ruoyi.production.mapper.ProductionProductMainMapper;
import com.ruoyi.production.pojo.ProductionAccount;
import com.ruoyi.production.service.ProductionAccountService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
 * <p>
 * ç”Ÿäº§æ ¸ç®—表 æœåŠ¡å®žçŽ°ç±»
 * </p>
 *
 * @author èŠ¯å¯¼è½¯ä»¶ï¼ˆæ±Ÿè‹ï¼‰æœ‰é™å…¬å¸
 * @since 2026-04-21 03:55:52
 */
import java.time.LocalDate;
@Service
@RequiredArgsConstructor
public class ProductionAccountServiceImpl extends ServiceImpl<ProductionAccountMapper, ProductionAccount> implements ProductionAccountService {
    private final ProductionProductMainMapper productionProductMainMapper;
    @Override
    public IPage<ProductionAccountVo> listPage(Page<ProductionAccountDto> page, ProductionAccountDto dto) {
        ProductionAccountDto queryDto = normalizeDateQuery(dto);
        return baseMapper.listPage(page, queryDto);
    }
    @Override
    public IPage<ProductionProductMainDto> listProductionDetails(ProductionAccountDto dto, Page page) {
        return productionProductMainMapper.listProductionDetails(normalizeDateQuery(dto), page);
    }
    private ProductionAccountDto normalizeDateQuery(ProductionAccountDto dto) {
        if (dto == null) {
            return new ProductionAccountDto();
        }
        LocalDate[] dateRange = dto.getDateRange();
        if ((dto.getEntryDateStart() == null || dto.getEntryDateEnd() == null)
                && dateRange != null
                && dateRange.length > 0) {
            if (dto.getEntryDateStart() == null) {
                dto.setEntryDateStart(dateRange[0]);
            }
            if (dto.getEntryDateEnd() == null) {
                dto.setEntryDateEnd(dateRange.length > 1 ? dateRange[1] : dateRange[0]);
            }
        }
        String dateType = dto.getDateType();
        if ("day".equalsIgnoreCase(dateType)) {
            if (dto.getEntryDate() == null && dateRange != null && dateRange.length > 0) {
                dto.setEntryDate(dateRange[0]);
            }
            if (dto.getEntryDate() == null) {
                dto.setEntryDate(dto.getEntryDateStart());
            }
            dto.setEntryDateStart(null);
            dto.setEntryDateEnd(null);
        } else if ("month".equalsIgnoreCase(dateType)) {
            if ((dto.getEntryDateStart() == null || dto.getEntryDateEnd() == null) && dto.getEntryDate() != null) {
                LocalDate monthDate = dto.getEntryDate();
                dto.setEntryDateStart(monthDate.withDayOfMonth(1));
                dto.setEntryDateEnd(monthDate.withDayOfMonth(monthDate.lengthOfMonth()));
            }
            dto.setEntryDate(null);
        }
        return dto;
    }
}
src/main/java/com/ruoyi/production/service/impl/ProductionOperationTaskServiceImpl.java
@@ -1,42 +1,91 @@
package com.ruoyi.production.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.data.PictureRenderData;
import com.deepoove.poi.data.PictureType;
import com.deepoove.poi.data.Pictures;
import com.ruoyi.basic.dto.StorageAttachmentDTO;
import com.ruoyi.basic.dto.StorageBlobVO;
import com.ruoyi.basic.enums.RecordTypeEnum;
import com.ruoyi.basic.utils.FileUtil;
import com.ruoyi.common.config.FileProperties;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.MatrixToImageWriter;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.production.bean.dto.ProductionOperationTaskDto;
import com.ruoyi.production.bean.vo.ProductionOperationTaskVo;
import com.ruoyi.production.mapper.ProductionOrderMapper;
import com.ruoyi.production.mapper.ProductionOperationTaskMapper;
import com.ruoyi.production.pojo.ProductionOrder;
import com.ruoyi.production.pojo.ProductionOperationTask;
import com.ruoyi.production.service.ProductionOperationTaskService;
import com.ruoyi.project.system.domain.SysUser;
import com.ruoyi.project.system.mapper.SysUserMapper;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.List;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.*;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class ProductionOperationTaskServiceImpl extends ServiceImpl<ProductionOperationTaskMapper, ProductionOperationTask> implements ProductionOperationTaskService {
    private final SysUserMapper sysUserMapper;
    private final ProductionOrderMapper productionOrderMapper;
    private final FileUtil fileUtil;
    private final FileProperties fileProperties;
    @Value("${file.temp-dir}")
    private String tempDir;
    @Override
    public IPage<ProductionOperationTaskVo> pageProductionOperationTask(Page<ProductionOperationTaskDto> page, ProductionOperationTaskDto dto) {
        Page<ProductionOperationTaskVo> voPage = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
        return baseMapper.pageProductionOperationTask(voPage, dto);
        IPage<ProductionOperationTaskVo> result = baseMapper.pageProductionOperationTask(voPage, dto);
        fillUserNames(result.getRecords());
        return result;
    }
    @Override
    public List<ProductionOperationTaskVo> listProductionOperationTask(ProductionOperationTaskDto dto) {
        return BeanUtil.copyToList(this.list(buildQueryWrapper(dto)), ProductionOperationTaskVo.class);
        List<ProductionOperationTaskVo> result = BeanUtil.copyToList(this.list(buildQueryWrapper(dto)), ProductionOperationTaskVo.class);
        fillUserNames(result);
        return result;
    }
    @Override
    public ProductionOperationTaskVo getProductionOperationTaskInfo(Long id) {
        ProductionOperationTask item = this.getById(id);
        return item == null ? null : BeanUtil.copyProperties(item, ProductionOperationTaskVo.class);
        if (item == null) {
            return null;
        }
        ProductionOperationTaskVo vo = BeanUtil.copyProperties(item, ProductionOperationTaskVo.class);
        if (item.getProductionOrderId() != null) {
            ProductionOrder productionOrder = productionOrderMapper.selectById(item.getProductionOrderId());
            if (productionOrder != null) {
                vo.setEndOrder(productionOrder.getEndOrder());
            }
        }
        fillUserNames(Collections.singletonList(vo));
        return vo;
    }
    @Override
@@ -47,6 +96,27 @@
    @Override
    public boolean removeProductionOperationTask(List<Long> ids) {
        return ids != null && !ids.isEmpty() && this.removeByIds(ids);
    }
    @Override
    public int updateProductWorkOrder(ProductionOperationTaskDto dto) {
        return baseMapper.updateById(dto);
    }
    @Override
    public boolean assign(ProductionOperationTaskDto dto) {
        if (dto == null || dto.getId() == null) {
            throw new ServiceException("工单ID不能为空");
        }
        ProductionOperationTask update = new ProductionOperationTask();
        update.setId(dto.getId());
        update.setUserIds(dto.getUserIds());
        int rows = baseMapper.updateById(update);
        if (rows <= 0) {
            throw new ServiceException("工单不存在或已删除");
        }
        return true;
    }
    private LambdaQueryWrapper<ProductionOperationTask> buildQueryWrapper(ProductionOperationTaskDto dto) {
@@ -62,8 +132,219 @@
                .orderByDesc(ProductionOperationTask::getId);
    }
    private void fillUserNames(List<ProductionOperationTaskVo> voList) {
        if (voList == null || voList.isEmpty()) {
            return;
        }
        Set<Long> userIdSet = new LinkedHashSet<>();
        for (ProductionOperationTaskVo vo : voList) {
            if (vo == null) {
                continue;
            }
            userIdSet.addAll(parseUserIdList(vo.getUserIds(), false));
        }
        if (userIdSet.isEmpty()) {
            return;
        }
        List<SysUser> userList = sysUserMapper.selectUsersByIds(new ArrayList<>(userIdSet));
        if (userList == null || userList.isEmpty()) {
            return;
        }
        Map<Long, String> userNameById = userList.stream()
                .filter(item -> item.getUserId() != null)
                .collect(Collectors.toMap(SysUser::getUserId, SysUser::getNickName, (left, right) -> left));
        for (ProductionOperationTaskVo vo : voList) {
            if (vo == null) {
                continue;
            }
            List<Long> userIds = parseUserIdList(vo.getUserIds(), false);
            if (userIds.isEmpty()) {
                vo.setUserNames(null);
                continue;
            }
            String userNames = userIds.stream()
                    .map(userNameById::get)
                    .filter(StringUtils::isNotBlank)
                    .collect(Collectors.joining(","));
            vo.setUserNames(userNames);
        }
    }
    private List<Long> parseUserIdList(String userIds, boolean strict) {
        if (StringUtils.isBlank(userIds)) {
            if (strict) {
                throw new ServiceException("userIds格式不正确,必须为JSON数字数组");
            }
            return new ArrayList<>();
        }
        String text = userIds.trim();
        try {
            List<Long> parsed = JSON.parseArray(text, Long.class);
            LinkedHashSet<Long> idSet = parsed == null ? new LinkedHashSet<>() : parsed.stream()
                    .filter(Objects::nonNull)
                    .collect(Collectors.toCollection(LinkedHashSet::new));
            if (strict && idSet.isEmpty()) {
                throw new ServiceException("userIds格式不正确,必须为JSON数字数组");
            }
            return new ArrayList<>(idSet);
        } catch (Exception e) {
            if (strict) {
                throw new ServiceException("userIds格式不正确,必须为JSON数字数组");
            }
            return new ArrayList<>();
        }
    }
    @Override
    public int updateProductWorkOrder(ProductionOperationTaskDto dto) {
        return baseMapper.updateById(dto);
    public void down(HttpServletResponse response, ProductionOperationTaskDto dto) {
        if (dto == null || dto.getId() == null) {
            throw new ServiceException("工单ID不能为空");
        }
        ProductionOperationTaskDto taskDto = baseMapper.getProductWorkOrderFlowCard(dto.getId());
        if (taskDto == null) {
            throw new ServiceException("工单不存在,ID=" + dto.getId());
        }
        String codePath;
        try {
            codePath = new MatrixToImageWriter().code(taskDto.getId().toString(), tempDir);
        } catch (Exception e) {
            throw new ServiceException("生成二维码失败");
        }
        List<Map<String, Object>> images = buildTaskAttachmentImages(taskDto.getId());
        Map<String, Object> renderData = new HashMap<>();
        renderData.put("process", taskDto.getProcessName());
        renderData.put("workOrderNo", taskDto.getWorkOrderNo());
        renderData.put("productOrderNpsNo", taskDto.getProductOrderNpsNo());
        renderData.put("productName", taskDto.getProductName());
        renderData.put("planQuantity", taskDto.getPlanQuantity());
        renderData.put("model", taskDto.getModel());
        renderData.put("completeQuantity", taskDto.getCompleteQuantity());
        renderData.put("scrapQty", taskDto.getScrapQty());
        renderData.put("planStartTime", taskDto.getPlanStartTime());
        renderData.put("planEndTime", taskDto.getPlanEndTime());
        renderData.put("actualStartTime", taskDto.getActualStartTime());
        renderData.put("actualEndTime", taskDto.getActualEndTime());
        renderData.put("twoCode", Pictures.ofLocal(codePath).create());
        renderData.put("images", images.isEmpty() ? null : images);
        try (InputStream inputStream = this.getClass().getResourceAsStream("/static/work-order-template.docx")) {
            if (inputStream == null) {
                throw new ServiceException("流转卡模板不存在");
            }
            XWPFTemplate template = XWPFTemplate.compile(inputStream).render(renderData);
            response.setContentType("application/msword");
            String fileName = URLEncoder.encode("流转卡", "UTF-8");
            response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
            response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".docx");
            try (OutputStream os = response.getOutputStream()) {
                template.write(os);
                os.flush();
            }
        } catch (Exception e) {
            throw new RuntimeException("导出失败");
        }
    }
    private List<Map<String, Object>> buildTaskAttachmentImages(Long taskId) {
        List<Map<String, Object>> images = new ArrayList<>();
        StorageAttachmentDTO storageAttachmentDTO = new StorageAttachmentDTO();
        storageAttachmentDTO.setRecordType(RecordTypeEnum.PRODUCTION_OPERATION_TASK.getType());
        storageAttachmentDTO.setRecordId(taskId);
        List<StorageBlobVO> taskWorkOrderFiles =
                fileUtil.getStorageBlobVOsByApplicationAndRecordTypeAndRecordId(storageAttachmentDTO);
        if (CollectionUtils.isEmpty(taskWorkOrderFiles)) {
            return images;
        }
        for (StorageBlobVO blobVO : taskWorkOrderFiles) {
            if (blobVO == null) {
                continue;
            }
            PictureType pictureType = resolvePictureType(blobVO);
            if (pictureType == null) {
                continue;
            }
            File imageFile = resolveImageFile(blobVO);
            if (imageFile == null || !imageFile.exists() || !imageFile.isFile()) {
                continue;
            }
            try (InputStream imageInputStream = new FileInputStream(imageFile)) {
                Map<String, Object> image = new HashMap<>();
                PictureRenderData pictureRenderData = Pictures.ofStream(imageInputStream, pictureType)
                        .sizeInCm(17, 20)
                        .create();
                image.put("url", pictureRenderData);
                images.add(image);
            } catch (Exception ignored) {
                // å•个附件解析失败时跳过,避免影响整个流转卡导出
            }
        }
        return images;
    }
    private File resolveImageFile(StorageBlobVO blobVO) {
        if (blobVO == null || StringUtils.isBlank(blobVO.getUidFilename())) {
            return null;
        }
        if (StringUtils.isBlank(blobVO.getPath())) {
            return new File(fileProperties.getPath(), blobVO.getUidFilename());
        }
        return new File(new File(fileProperties.getPath(), blobVO.getPath()), blobVO.getUidFilename());
    }
    private PictureType resolvePictureType(StorageBlobVO blobVO) {
        if (blobVO == null) {
            return null;
        }
        PictureType type = parsePictureTypeByFileName(blobVO.getOriginalFilename());
        if (type != null) {
            return type;
        }
        type = parsePictureTypeByFileName(blobVO.getUidFilename());
        if (type != null) {
            return type;
        }
        return parsePictureTypeByContentType(blobVO.getContentType());
    }
    private PictureType parsePictureTypeByFileName(String fileName) {
        if (StringUtils.isBlank(fileName) || !fileName.contains(".")) {
            return null;
        }
        try {
            return PictureType.suggestFileType(fileName);
        } catch (Exception ex) {
            return null;
        }
    }
    private PictureType parsePictureTypeByContentType(String contentType) {
        if (StringUtils.isBlank(contentType)) {
            return null;
        }
        String normalized = contentType.trim().toLowerCase(Locale.ROOT);
        switch (normalized) {
            case "image/jpeg":
            case "image/jpg":
            case "image/pjpeg":
                return PictureType.JPEG;
            case "image/png":
                return PictureType.PNG;
            case "image/gif":
                return PictureType.GIF;
            case "image/bmp":
            case "image/x-ms-bmp":
                return PictureType.BMP;
            case "image/tiff":
            case "image/tif":
                return PictureType.TIFF;
            case "image/svg+xml":
                return PictureType.SVG;
            default:
                return null;
        }
    }
}
src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickServiceImpl.java
@@ -3,6 +3,7 @@
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.production.bean.dto.ProductionOrderPickDto;
@@ -18,6 +19,7 @@
import com.ruoyi.stock.dto.StockInventoryDto;
import com.ruoyi.stock.mapper.StockInventoryMapper;
import com.ruoyi.stock.pojo.StockInventory;
import com.ruoyi.stock.service.StockInventoryService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -46,6 +48,7 @@
    private final ProductionOperationTaskMapper productionOperationTaskMapper;
    private final ProductionOrderPickRecordMapper productionOrderPickRecordMapper;
    private final StockInventoryMapper stockInventoryMapper;
    private final StockInventoryService stockInventoryService;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -59,7 +62,7 @@
            List<String> batchNoList = resolveBatchNoList(resolvedDto);
            String inventoryBatchNo = pickInventoryBatchNo(batchNoList);
            String storedBatchNo = formatBatchNoStorage(batchNoList);
            subtractInventory(resolvedDto.getProductModelId(), inventoryBatchNo, resolvedDto.getPickQuantity(), rowNo);
            subtractInventory(resolvedDto.getProductModelId(), storedBatchNo, resolvedDto.getPickQuantity(), rowNo);
            ProductionOrderPick orderPick = new ProductionOrderPick();
            orderPick.setProductionOrderId(resolvedDto.getProductionOrderId());
@@ -234,7 +237,7 @@
        List<String> batchNoList = resolveBatchNoList(dto);
        String inventoryBatchNo = pickInventoryBatchNo(batchNoList);
        String storedBatchNo = formatBatchNoStorage(batchNoList);
        subtractInventory(dto.getProductModelId(), inventoryBatchNo, dto.getPickQuantity(), rowNo);
        subtractInventory(dto.getProductModelId(), storedBatchNo, dto.getPickQuantity(), rowNo);
        ProductionOrderPick orderPick = new ProductionOrderPick();
        orderPick.setProductionOrderId(dto.getProductionOrderId());
@@ -296,7 +299,7 @@
        List<String> batchNoList = resolveBatchNoList(dto);
        String inventoryBatchNo = batchNoList.isEmpty()
                ? resolveInventoryBatchNoFromStored(oldPick.getBatchNo())
                : pickInventoryBatchNo(batchNoList);
                : formatBatchNoStorage(batchNoList);
        BigDecimal feedingQuantity = dto.getFeedingQuantity();
        subtractInventory(productModelId, inventoryBatchNo, feedingQuantity, rowNo);
@@ -388,13 +391,13 @@
        if (sameStockKey) {
            BigDecimal delta = newQuantity.subtract(oldQuantity);
            if (delta.compareTo(BigDecimal.ZERO) > 0) {
                subtractInventory(newProductModelId, newBatchNo, delta, rowNo);
                subtractInventory(newProductModelId, newStoredBatchNo, delta, rowNo);
            } else if (delta.compareTo(BigDecimal.ZERO) < 0) {
                addInventory(oldProductModelId, oldBatchNo, delta.abs());
            }
        } else {
            addInventory(oldProductModelId, oldBatchNo, oldQuantity);
            subtractInventory(newProductModelId, newBatchNo, newQuantity, rowNo);
            subtractInventory(newProductModelId, newStoredBatchNo, newQuantity, rowNo);
        }
        oldPick.setProductModelId(newProductModelId);
@@ -457,22 +460,61 @@
    }
    private void subtractInventory(Long productModelId, String batchNo, BigDecimal quantity, int rowNo) {
        StockInventory stockInventory = stockInventoryMapper.selectOne(buildStockWrapper(productModelId, batchNo));
        if (stockInventory == null) {
            throw new ServiceException("第" + rowNo + "条领料对应库存不存在");
        BigDecimal deductQuantity = defaultDecimal(quantity);
        if (deductQuantity.compareTo(BigDecimal.ZERO) <= 0) {
            return;
        }
        BigDecimal availableQuantity = defaultDecimal(stockInventory.getQualitity())
                .subtract(defaultDecimal(stockInventory.getLockedQuantity()));
        if (quantity.compareTo(availableQuantity) > 0) {
            throw new ServiceException("第" + rowNo + "条领料可用库存不足");
        List<String> batchNoList = parseBatchNoValue(batchNo);
        if (batchNoList.isEmpty()) {
            batchNoList = Collections.singletonList(null);
        }
        StockInventoryDto stockInventoryDto = new StockInventoryDto();
        stockInventoryDto.setProductModelId(productModelId);
        stockInventoryDto.setBatchNo(batchNo);
        stockInventoryDto.setQualitity(quantity);
        int affected = stockInventoryMapper.updateSubtractStockInventory(stockInventoryDto);
        if (affected <= 0) {
            throw new ServiceException("第" + rowNo + "条领料扣减库存失败");
        Map<String, BigDecimal> availableQuantityMap = new LinkedHashMap<>();
        BigDecimal totalAvailableQuantity = BigDecimal.ZERO;
        for (String currentBatchNo : batchNoList) {
            StockInventory stockInventory = stockInventoryMapper.selectOne(buildStockWrapper(productModelId, currentBatchNo));
            BigDecimal availableQuantity = BigDecimal.ZERO;
            if (stockInventory != null) {
                availableQuantity = defaultDecimal(stockInventory.getQualitity())
                        .subtract(defaultDecimal(stockInventory.getLockedQuantity()));
                if (availableQuantity.compareTo(BigDecimal.ZERO) < 0) {
                    availableQuantity = BigDecimal.ZERO;
                }
            }
            availableQuantityMap.put(currentBatchNo, availableQuantity);
            totalAvailableQuantity = totalAvailableQuantity.add(availableQuantity);
        }
        if (deductQuantity.compareTo(totalAvailableQuantity) > 0) {
            BigDecimal shortQuantity = deductQuantity.subtract(totalAvailableQuantity);
            throw new ServiceException("领料可用库存不足,可用库存为" + formatQuantity(totalAvailableQuantity)
                    + ",还差" + formatQuantity(shortQuantity));
        }
        BigDecimal remainingQuantity = deductQuantity;
        for (Map.Entry<String, BigDecimal> entry : availableQuantityMap.entrySet()) {
            if (remainingQuantity.compareTo(BigDecimal.ZERO) <= 0) {
                break;
            }
            BigDecimal availableQuantity = defaultDecimal(entry.getValue());
            if (availableQuantity.compareTo(BigDecimal.ZERO) <= 0) {
                continue;
            }
            BigDecimal currentDeductQuantity = remainingQuantity.min(availableQuantity);
            StockInventoryDto stockInventoryDto = new StockInventoryDto();
            stockInventoryDto.setProductModelId(productModelId);
            stockInventoryDto.setBatchNo(entry.getKey());
            stockInventoryDto.setQualitity(currentDeductQuantity);
            int affected = stockInventoryMapper.updateSubtractStockInventory(stockInventoryDto);
            if (affected <= 0) {
                throw new ServiceException("第" + rowNo + "条领料扣减库存失败");
            }
            remainingQuantity = remainingQuantity.subtract(currentDeductQuantity);
        }
        if (remainingQuantity.compareTo(BigDecimal.ZERO) > 0) {
            throw new ServiceException("第" + rowNo + "条领料扣减库存失败,剩余待扣减数量为" + formatQuantity(remainingQuantity));
        }
    }
@@ -481,25 +523,13 @@
        if (addQuantity.compareTo(BigDecimal.ZERO) <= 0) {
            return;
        }
        StockInventory stockInventory = stockInventoryMapper.selectOne(buildStockWrapper(productModelId, batchNo));
        if (stockInventory == null) {
            StockInventory newStockInventory = new StockInventory();
            newStockInventory.setProductModelId(productModelId);
            newStockInventory.setBatchNo(batchNo);
            newStockInventory.setQualitity(addQuantity);
            newStockInventory.setLockedQuantity(BigDecimal.ZERO);
            newStockInventory.setVersion(1);
            stockInventoryMapper.insert(newStockInventory);
            return;
        }
        StockInventoryDto stockInventoryDto = new StockInventoryDto();
        stockInventoryDto.setProductModelId(productModelId);
        stockInventoryDto.setBatchNo(batchNo);
        stockInventoryDto.setQualitity(addQuantity);
        int affected = stockInventoryMapper.updateAddStockInventory(stockInventoryDto);
        if (affected <= 0) {
            throw new ServiceException("库存回退失败,产品规格ID=" + productModelId);
        }
        stockInventoryDto.setRecordType(String.valueOf(StockInQualifiedRecordTypeEnum.PICK_RETURN_IN.getCode()));
        stockInventoryDto.setRecordId(0L);
        stockInventoryService.addStockInRecordOnly(stockInventoryDto);
    }
    private List<ProductionOrderPickDto> resolvePickItems(ProductionOrderPickDto dto) {
@@ -882,4 +912,8 @@
    private BigDecimal defaultDecimal(BigDecimal value) {
        return value == null ? BigDecimal.ZERO : value;
    }
    private String formatQuantity(BigDecimal value) {
        return defaultDecimal(value).stripTrailingZeros().toPlainString();
    }
}
src/main/java/com/ruoyi/production/service/impl/ProductionOrderRoutingOperationParamServiceImpl.java
@@ -75,6 +75,10 @@
        ProductionOrderRoutingOperationParam query = dto == null ? new ProductionOrderRoutingOperationParam() : dto;
        return Wrappers.<ProductionOrderRoutingOperationParam>lambdaQuery()
                .eq(query.getId() != null, ProductionOrderRoutingOperationParam::getId, query.getId())
                .eq(query.getProductionProductMainId() != null,
                        ProductionOrderRoutingOperationParam::getProductionProductMainId, query.getProductionProductMainId())
                .isNull(query.getProductionProductMainId() == null,
                        ProductionOrderRoutingOperationParam::getProductionProductMainId)
                .eq(query.getProductionOrderId() != null, ProductionOrderRoutingOperationParam::getProductionOrderId, query.getProductionOrderId())
                .eq(query.getProductionOrderRoutingOperationId() != null, ProductionOrderRoutingOperationParam::getProductionOrderRoutingOperationId, query.getProductionOrderRoutingOperationId())
                .eq(query.getTechnologyOperationId() != null,
@@ -148,6 +152,7 @@
    private void checkDuplicate(ProductionOrderRoutingOperationParam item) {
        boolean duplicate = productionOrderRoutingOperationParamMapper.selectCount(
                Wrappers.<ProductionOrderRoutingOperationParam>lambdaQuery()
                        .isNull(ProductionOrderRoutingOperationParam::getProductionProductMainId)
                        .eq(ProductionOrderRoutingOperationParam::getProductionOrderRoutingOperationId, item.getProductionOrderRoutingOperationId())
                        .eq(item.getTechnologyRoutingOperationParamId() != null,
                                ProductionOrderRoutingOperationParam::getTechnologyRoutingOperationParamId, item.getTechnologyRoutingOperationParamId())
src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java
@@ -19,9 +19,16 @@
import com.ruoyi.production.bean.vo.ProductionOrderPickVo;
import com.ruoyi.production.bean.vo.ProductionOrderVo;
import com.ruoyi.production.bean.vo.ProductionPlanVo;
import com.ruoyi.production.bean.vo.ProductionOrderWorkOrderDetailVo;
import com.ruoyi.production.enums.ProductOrderStatusEnum;
import com.ruoyi.production.mapper.*;
import com.ruoyi.production.pojo.*;
import com.ruoyi.quality.mapper.QualityInspectFileMapper;
import com.ruoyi.quality.mapper.QualityInspectMapper;
import com.ruoyi.quality.mapper.QualityInspectParamMapper;
import com.ruoyi.quality.pojo.QualityInspect;
import com.ruoyi.quality.pojo.QualityInspectFile;
import com.ruoyi.quality.pojo.QualityInspectParam;
import com.ruoyi.production.service.ProductionOrderService;
import com.ruoyi.sales.mapper.SalesLedgerMapper;
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
@@ -51,8 +58,12 @@
    private final ProductionOrderBomMapper productionOrderBomMapper;
    private final ProductionBomStructureMapper productionBomStructureMapper;
    private final ProductionProductMainMapper productionProductMainMapper;
    private final ProductionProductOutputMapper productionProductOutputMapper;
    private final ProductionOrderPickMapper productionOrderPickMapper;
    private final ProductionOrderPickRecordMapper productionOrderPickRecordMapper;
    private final QualityInspectMapper qualityInspectMapper;
    private final QualityInspectParamMapper qualityInspectParamMapper;
    private final QualityInspectFileMapper qualityInspectFileMapper;
    private final ProductionPlanMapper productionPlanMapper;
    private final StockInventoryMapper stockInventoryMapper;
    private final StorageAttachmentMapper storageAttachmentMapper;
@@ -219,8 +230,8 @@
        List<TechnologyRoutingOperation> routingOperations = technologyRoutingOperationMapper.selectList(
                Wrappers.<TechnologyRoutingOperation>lambdaQuery()
                        .eq(TechnologyRoutingOperation::getTechnologyRoutingId, technologyRouting.getId())
                        .orderByAsc(TechnologyRoutingOperation::getDragSort)
                        .orderByAsc(TechnologyRoutingOperation::getId));
                        .orderByDesc(TechnologyRoutingOperation::getDragSort)
                        .orderByDesc(TechnologyRoutingOperation::getId));
        Map<Long, String> operationNameMap = technologyOperationMapper.selectBatchIds(
                        routingOperations.stream()
                                .map(TechnologyRoutingOperation::getTechnologyOperationId)
@@ -228,6 +239,11 @@
                                .collect(Collectors.toSet()))
                .stream()
                .collect(Collectors.toMap(TechnologyOperation::getId, TechnologyOperation::getName, (a, b) -> a));
        Integer lastDragSort = routingOperations.stream()
                .map(TechnologyRoutingOperation::getDragSort)
                .filter(Objects::nonNull)
                .max(Integer::compareTo)
                .orElse(null);
        for (TechnologyRoutingOperation sourceOperation : routingOperations) {
            // è®¢å•工序保存的是工艺工序快照,后续报工只依赖快照,不再直接引用工艺主数据。
            ProductionOrderRoutingOperation targetOperation = new ProductionOrderRoutingOperation();
@@ -236,19 +252,23 @@
            targetOperation.setOrderRoutingId(orderRouting.getId());
            targetOperation.setProductModelId(sourceOperation.getProductModelId());
            targetOperation.setDragSort(sourceOperation.getDragSort());
            targetOperation.setIsProduction(sourceOperation.getIsProduction());
            targetOperation.setIsQuality(sourceOperation.getIsQuality());
            targetOperation.setOperationName(operationNameMap.get(sourceOperation.getTechnologyOperationId()));
            targetOperation.setTechnologyOperationId(sourceOperation.getTechnologyOperationId());
            productionOrderRoutingOperationMapper.insert(targetOperation);
            ProductionOperationTask task = new ProductionOperationTask();
            task.setProductionOrderRoutingOperationId(targetOperation.getId());
            task.setProductionOrderId(productionOrder.getId());
            task.setPlanQuantity(defaultDecimal(productionOrder.getQuantity()));
            task.setCompleteQuantity(BigDecimal.ZERO);
            task.setWorkOrderNo(generateNextTaskNo());
            task.setStatus(2);
            productionOperationTaskMapper.insert(task);
            boolean isLastOperation = lastDragSort != null && Objects.equals(sourceOperation.getDragSort(), lastDragSort);
            if (isLastOperation || Boolean.TRUE.equals(targetOperation.getIsProduction())) {
                ProductionOperationTask task = new ProductionOperationTask();
                task.setProductionOrderRoutingOperationId(targetOperation.getId());
                task.setProductionOrderId(productionOrder.getId());
                task.setPlanQuantity(defaultDecimal(productionOrder.getQuantity()));
                task.setCompleteQuantity(BigDecimal.ZERO);
                task.setWorkOrderNo(generateNextTaskNo());
                task.setStatus(2);
                productionOperationTaskMapper.insert(task);
            }
            List<TechnologyRoutingOperationParam> sourceParams = technologyRoutingOperationParamMapper.selectList(
                    Wrappers.<TechnologyRoutingOperationParam>lambdaQuery()
@@ -661,6 +681,168 @@
    }
    @Override
    public ProductionOrderWorkOrderDetailVo getWorkOrderReportInspectDetail(Long productionOrderId) {
        if (productionOrderId == null) {
            throw new ServiceException("productionOrderId can not be null");
        }
        ProductionOrderVo orderInfo = getProductionOrderInfo(productionOrderId);
        if (orderInfo == null) {
            throw new ServiceException("production order not found");
        }
        ProductionOrderWorkOrderDetailVo detailVo = new ProductionOrderWorkOrderDetailVo();
        detailVo.setProductionOrder(orderInfo);
        List<ProductionOperationTask> workOrderList = productionOperationTaskMapper.selectList(
                Wrappers.<ProductionOperationTask>lambdaQuery()
                        .eq(ProductionOperationTask::getProductionOrderId, productionOrderId)
                        .orderByAsc(ProductionOperationTask::getId));
        if (workOrderList == null || workOrderList.isEmpty()) {
            detailVo.setWorkOrderList(Collections.emptyList());
            return detailVo;
        }
        List<Long> workOrderIdList = workOrderList.stream()
                .map(ProductionOperationTask::getId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        List<ProductionProductMain> reportMainList = workOrderIdList.isEmpty()
                ? Collections.emptyList()
                : productionProductMainMapper.selectList(
                Wrappers.<ProductionProductMain>lambdaQuery()
                        .in(ProductionProductMain::getProductionOperationTaskId, workOrderIdList)
                        .orderByAsc(ProductionProductMain::getId));
        Map<Long, List<ProductionProductMain>> reportMainMap = new LinkedHashMap<>();
        for (ProductionProductMain reportMain : reportMainList) {
            if (reportMain == null || reportMain.getProductionOperationTaskId() == null) {
                continue;
            }
            reportMainMap.computeIfAbsent(reportMain.getProductionOperationTaskId(), k -> new ArrayList<>()).add(reportMain);
        }
        List<Long> reportMainIdList = reportMainList.stream()
                .map(ProductionProductMain::getId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        Map<Long, List<ProductionProductOutput>> reportOutputMap = new LinkedHashMap<>();
        Map<Long, List<ProductionOrderRoutingOperationParam>> reportParamMap = new LinkedHashMap<>();
        Map<Long, List<QualityInspect>> inspectMap = new LinkedHashMap<>();
        Map<Long, List<QualityInspectParam>> inspectParamMap = new LinkedHashMap<>();
        Map<Long, List<QualityInspectFile>> inspectFileMap = new LinkedHashMap<>();
        if (!reportMainIdList.isEmpty()) {
            List<ProductionProductOutput> reportOutputList = productionProductOutputMapper.selectList(
                    Wrappers.<ProductionProductOutput>lambdaQuery()
                            .in(ProductionProductOutput::getProductionProductMainId, reportMainIdList)
                            .orderByAsc(ProductionProductOutput::getId));
            for (ProductionProductOutput reportOutput : reportOutputList) {
                if (reportOutput == null) {
                    continue;
                }
                Long reportMainId = reportOutput.getProductionProductMainId() != null
                        ? reportOutput.getProductionProductMainId()
                        : reportOutput.getProductMainId();
                if (reportMainId == null) {
                    continue;
                }
                reportOutputMap.computeIfAbsent(reportMainId, k -> new ArrayList<>()).add(reportOutput);
            }
            List<ProductionOrderRoutingOperationParam> reportParamList = productionOrderRoutingOperationParamMapper.selectList(
                    Wrappers.<ProductionOrderRoutingOperationParam>lambdaQuery()
                            .in(ProductionOrderRoutingOperationParam::getProductionProductMainId, reportMainIdList)
                            .orderByAsc(ProductionOrderRoutingOperationParam::getId));
            for (ProductionOrderRoutingOperationParam reportParam : reportParamList) {
                if (reportParam == null || reportParam.getProductionProductMainId() == null) {
                    continue;
                }
                reportParamMap.computeIfAbsent(reportParam.getProductionProductMainId(), k -> new ArrayList<>()).add(reportParam);
            }
            List<QualityInspect> inspectList = qualityInspectMapper.selectList(
                    Wrappers.<QualityInspect>lambdaQuery()
                            .in(QualityInspect::getProductMainId, reportMainIdList)
                            .orderByAsc(QualityInspect::getId));
            for (QualityInspect inspect : inspectList) {
                if (inspect == null || inspect.getProductMainId() == null) {
                    continue;
                }
                inspectMap.computeIfAbsent(inspect.getProductMainId(), k -> new ArrayList<>()).add(inspect);
            }
            List<Long> inspectIdList = inspectList.stream()
                    .map(QualityInspect::getId)
                    .filter(Objects::nonNull)
                    .collect(Collectors.toList());
            if (!inspectIdList.isEmpty()) {
                List<QualityInspectParam> inspectParamList = qualityInspectParamMapper.selectList(
                        Wrappers.<QualityInspectParam>lambdaQuery()
                                .in(QualityInspectParam::getInspectId, inspectIdList)
                                .orderByAsc(QualityInspectParam::getId));
                for (QualityInspectParam inspectParam : inspectParamList) {
                    if (inspectParam == null || inspectParam.getInspectId() == null) {
                        continue;
                    }
                    inspectParamMap.computeIfAbsent(inspectParam.getInspectId(), k -> new ArrayList<>()).add(inspectParam);
                }
                List<QualityInspectFile> inspectFileList = qualityInspectFileMapper.selectList(
                        Wrappers.<QualityInspectFile>lambdaQuery()
                                .in(QualityInspectFile::getInspectId, inspectIdList)
                                .orderByAsc(QualityInspectFile::getId));
                for (QualityInspectFile inspectFile : inspectFileList) {
                    if (inspectFile == null || inspectFile.getInspectId() == null) {
                        continue;
                    }
                    inspectFileMap.computeIfAbsent(inspectFile.getInspectId(), k -> new ArrayList<>()).add(inspectFile);
                }
            }
        }
        List<ProductionOrderWorkOrderDetailVo.WorkOrderDetail> workOrderDetailList = new ArrayList<>();
        for (ProductionOperationTask workOrder : workOrderList) {
            ProductionOrderWorkOrderDetailVo.WorkOrderDetail workOrderDetail = new ProductionOrderWorkOrderDetailVo.WorkOrderDetail();
            workOrderDetail.setWorkOrder(workOrder);
            List<ProductionProductMain> workOrderReportMainList = reportMainMap.get(workOrder.getId());
            if (workOrderReportMainList == null || workOrderReportMainList.isEmpty()) {
                workOrderDetail.setReportList(Collections.emptyList());
                workOrderDetailList.add(workOrderDetail);
                continue;
            }
            List<ProductionOrderWorkOrderDetailVo.ReportDetail> reportDetailList = new ArrayList<>();
            for (ProductionProductMain reportMain : workOrderReportMainList) {
                Long reportMainId = reportMain.getId();
                ProductionOrderWorkOrderDetailVo.ReportDetail reportDetail = new ProductionOrderWorkOrderDetailVo.ReportDetail();
                reportDetail.setReportMain(reportMain);
                reportDetail.setReportOutputList(reportOutputMap.getOrDefault(reportMainId, Collections.emptyList()));
                reportDetail.setReportParamList(reportParamMap.getOrDefault(reportMainId, Collections.emptyList()));
                List<QualityInspect> reportInspectList = inspectMap.get(reportMainId);
                if (reportInspectList == null || reportInspectList.isEmpty()) {
                    reportDetail.setInspectList(Collections.emptyList());
                } else {
                    List<ProductionOrderWorkOrderDetailVo.InspectDetail> inspectDetailList = new ArrayList<>();
                    for (QualityInspect inspect : reportInspectList) {
                        ProductionOrderWorkOrderDetailVo.InspectDetail inspectDetail = new ProductionOrderWorkOrderDetailVo.InspectDetail();
                        inspectDetail.setInspect(inspect);
                        inspectDetail.setInspectParamList(inspectParamMap.getOrDefault(inspect.getId(), Collections.emptyList()));
                        inspectDetail.setInspectFileList(inspectFileMap.getOrDefault(inspect.getId(), Collections.emptyList()));
                        inspectDetailList.add(inspectDetail);
                    }
                    reportDetail.setInspectList(inspectDetailList);
                }
                reportDetailList.add(reportDetail);
            }
            workOrderDetail.setReportList(reportDetailList);
            workOrderDetailList.add(workOrderDetail);
        }
        detailVo.setWorkOrderList(workOrderDetailList);
        return detailVo;
    }
    @Override
    public List<ProductionOrderPickVo> pick(Long productionOrderId) {
        if (productionOrderId == null) {
            return Collections.emptyList();
@@ -733,4 +915,10 @@
        }
        return new ArrayList<>(mergedPickMap.values());
    }
    @Override
    public int updateOrder(ProductionOrderDto productionOrderDto) {
        productionOrderDto.setStatus(5);
        return baseMapper.updateById(productionOrderDto);
    }
}
src/main/java/com/ruoyi/production/service/impl/ProductionProductMainServiceImpl.java
@@ -27,6 +27,8 @@
import com.ruoyi.project.system.mapper.SysUserMapper;
import com.ruoyi.quality.mapper.*;
import com.ruoyi.quality.pojo.*;
import com.ruoyi.stock.dto.StockInventoryDto;
import com.ruoyi.stock.service.StockInventoryService;
import com.ruoyi.technology.mapper.TechnologyOperationMapper;
import com.ruoyi.technology.mapper.TechnologyRoutingOperationMapper;
import com.ruoyi.technology.pojo.TechnologyOperation;
@@ -40,10 +42,14 @@
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@Service
@@ -72,10 +78,13 @@
    private final TechnologyRoutingOperationMapper technologyRoutingOperationMapper;
    private final TechnologyOperationMapper technologyOperationMapper;
    private final StockUtils stockUtils;
    private final StockInventoryService stockInventoryService;
    @Override
    public IPage<ProductionProductMainDto> listPageProductionProductMainDto(Page page, ProductionProductMainDto productionProductMainDto) {
        return productionProductMainMapper.listPageProductionProductMainDto(page, productionProductMainDto);
        IPage<ProductionProductMainDto> result = productionProductMainMapper.listPageProductionProductMainDto(page, productionProductMainDto);
        fillOperationParamList(result.getRecords());
        return result;
    }
    @Override
@@ -85,9 +94,119 @@
    @Override
    public ProductionProductMainDto getProductionProductMainInfo(Long id) {
        return productionProductMainMapper.listPageProductionProductMainDto(new Page<>(1, 1), new ProductionProductMainDto() {{
        return listPageProductionProductMainDto(new Page<>(1, 1), new ProductionProductMainDto() {{
            setId(id);
        }}).getRecords().stream().findFirst().orElse(null);
    }
    private void fillOperationParamList(List<ProductionProductMainDto> recordList) {
        if (recordList == null || recordList.isEmpty()) {
            return;
        }
        Set<Long> mainIdSet = recordList.stream()
                .map(ProductionProductMainDto::getId)
                .filter(Objects::nonNull)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        if (mainIdSet.isEmpty()) {
            recordList.forEach(item -> item.setProductionOperationParamList(Collections.emptyList()));
            return;
        }
        List<ProductionOrderRoutingOperationParam> paramList = productionOrderRoutingOperationParamMapper.selectList(
                Wrappers.<ProductionOrderRoutingOperationParam>lambdaQuery()
                        .in(ProductionOrderRoutingOperationParam::getProductionProductMainId, mainIdSet)
                        .orderByAsc(ProductionOrderRoutingOperationParam::getId));
        Map<Long, List<ProductionOrderRoutingOperationParam>> paramGroupMap = new HashMap<>();
        for (ProductionOrderRoutingOperationParam param : paramList) {
            if (param == null || param.getProductionProductMainId() == null) {
                continue;
            }
            paramGroupMap.computeIfAbsent(param.getProductionProductMainId(), key -> new ArrayList<>()).add(param);
        }
        Set<Long> missingMainIdSet = new LinkedHashSet<>();
        for (ProductionProductMainDto item : recordList) {
            Long mainId = item.getId();
            if (mainId == null) {
                item.setProductionOperationParamList(Collections.emptyList());
                continue;
            }
            List<ProductionOrderRoutingOperationParam> params = paramGroupMap.get(mainId);
            if (params != null && !params.isEmpty()) {
                item.setProductionOperationParamList(params);
                continue;
            }
            missingMainIdSet.add(mainId);
        }
        if (missingMainIdSet.isEmpty()) {
            return;
        }
        // å…¼å®¹åŽ†å²æ•°æ®ï¼šæ—§æŠ¥å·¥è®°å½•æ²¡æœ‰æŒ‰æŠ¥å·¥ID落参数快照时,回退展示工序模板参数。
        List<ProductionProductMain> mainList = productionProductMainMapper.selectBatchIds(missingMainIdSet);
        Map<Long, Long> mainIdToTaskIdMap = mainList.stream()
                .filter(Objects::nonNull)
                .filter(item -> item.getId() != null)
                .collect(Collectors.toMap(ProductionProductMain::getId,
                        ProductionProductMain::getProductionOperationTaskId, (left, right) -> left));
        Set<Long> taskIdSet = mainIdToTaskIdMap.values().stream()
                .filter(Objects::nonNull)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        if (taskIdSet.isEmpty()) {
            for (ProductionProductMainDto item : recordList) {
                if (item.getId() != null && missingMainIdSet.contains(item.getId())) {
                    item.setProductionOperationParamList(Collections.emptyList());
                }
            }
            return;
        }
        List<ProductionOperationTask> taskList = productionOperationTaskMapper.selectList(
                Wrappers.<ProductionOperationTask>lambdaQuery()
                        .in(ProductionOperationTask::getId, taskIdSet));
        Map<Long, Long> taskIdToRoutingOperationIdMap = taskList.stream()
                .filter(Objects::nonNull)
                .filter(item -> item.getId() != null)
                .collect(Collectors.toMap(ProductionOperationTask::getId,
                        ProductionOperationTask::getProductionOrderRoutingOperationId, (left, right) -> left));
        Set<Long> routingOperationIdSet = taskIdToRoutingOperationIdMap.values().stream()
                .filter(Objects::nonNull)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        if (routingOperationIdSet.isEmpty()) {
            for (ProductionProductMainDto item : recordList) {
                if (item.getId() != null && missingMainIdSet.contains(item.getId())) {
                    item.setProductionOperationParamList(Collections.emptyList());
                }
            }
            return;
        }
        List<ProductionOrderRoutingOperationParam> fallbackParamList = productionOrderRoutingOperationParamMapper.selectList(
                Wrappers.<ProductionOrderRoutingOperationParam>lambdaQuery()
                        .in(ProductionOrderRoutingOperationParam::getProductionOrderRoutingOperationId, routingOperationIdSet)
                        .isNull(ProductionOrderRoutingOperationParam::getProductionProductMainId)
                        .orderByAsc(ProductionOrderRoutingOperationParam::getId));
        Map<Long, List<ProductionOrderRoutingOperationParam>> fallbackGroupMap = new HashMap<>();
        for (ProductionOrderRoutingOperationParam param : fallbackParamList) {
            if (param == null || param.getProductionOrderRoutingOperationId() == null) {
                continue;
            }
            fallbackGroupMap.computeIfAbsent(param.getProductionOrderRoutingOperationId(), key -> new ArrayList<>()).add(param);
        }
        for (ProductionProductMainDto item : recordList) {
            Long mainId = item.getId();
            if (mainId == null || !missingMainIdSet.contains(mainId)) {
                continue;
            }
            Long taskId = mainIdToTaskIdMap.get(mainId);
            Long routingOperationId = taskId == null ? null : taskIdToRoutingOperationIdMap.get(taskId);
            if (routingOperationId == null) {
                item.setProductionOperationParamList(Collections.emptyList());
                continue;
            }
            item.setProductionOperationParamList(fallbackGroupMap.getOrDefault(routingOperationId, Collections.emptyList()));
        }
    }
    @Override
@@ -132,7 +251,6 @@
        if (productionOrder == null) {
            throw new ServiceException("生产订单不存在");
        }
        syncOperationParamInputValue(dto, routingOperation.getId());
        TechnologyRoutingOperation technologyRoutingOperation = technologyRoutingOperationMapper.selectById(routingOperation.getTechnologyRoutingOperationId());
        TechnologyOperation technologyOperation = technologyRoutingOperation == null ? null
                : technologyOperationMapper.selectById(technologyRoutingOperation.getTechnologyOperationId());
@@ -149,11 +267,16 @@
        productionProductMain.setProductionOperationTaskId(taskId);
        productionProductMain.setStatus(0);
        productionProductMainMapper.insert(productionProductMain);
        syncOperationParamInputValue(dto, routingOperation.getId(), productionProductMain.getId());
        List<ProductStructureDto> productStructureDtos = resolveInputStructures(
                productionOrder.getId(), routingOperation, productModel.getId());
       // å¦‚果没有bom子节点了,那么投入就是他本身
        if (productStructureDtos.isEmpty()) {
            throw new ServiceException("未找到当前工序对应的BOM投入节点");
            ProductStructureDto fallbackInput = new ProductStructureDto();
            fallbackInput.setProductModelId(productModel.getId());
            fallbackInput.setUnitQuantity(BigDecimal.ONE);
            productStructureDtos.add(fallbackInput);
        }
        for (ProductStructureDto item : productStructureDtos) {
            // å½“前实现按工序成品直接作为投入,后续若接入领料记录可在这里替换来源。
@@ -173,7 +296,8 @@
        productionProductOutput.setQuantity(defaultDecimal(dto.getQuantity()));
        productionProductOutput.setScrapQty(defaultDecimal(dto.getScrapQty()));
        productionProductOutputMapper.insert(productionProductOutput);
        BigDecimal productQty = productionProductOutput.getQuantity().subtract(productionProductOutput.getScrapQty());
        BigDecimal reportQty = defaultDecimal(productionProductOutput.getQuantity());
        BigDecimal productQty = reportQty;
        List<ProductionOrderRoutingOperation> routingOperationList = productionOrderRoutingOperationMapper.selectList(
                Wrappers.<ProductionOrderRoutingOperation>lambdaQuery()
@@ -199,7 +323,7 @@
                qualityInspect.setProductModelId(productModel.getId());
                qualityInspectMapper.insert(qualityInspect);
                List<QualityTestStandard> qualityTestStandard = qualityTestStandardMapper.getQualityTestStandardByProductId(product.getId(), inspectType, process);
                if (qualityTestStandard.size() > 0) {
                if (!qualityTestStandard.isEmpty()) {
                    qualityInspect.setTestStandardId(qualityTestStandard.get(0).getId());
                    qualityInspectMapper.updateById(qualityInspect);
                    qualityTestStandardParamMapper.selectList(Wrappers.<QualityTestStandardParam>lambdaQuery()
@@ -213,8 +337,12 @@
                            });
                }
            } else {
                stockUtils.addStock(productModel.getId(), productQty,
                        StockInQualifiedRecordTypeEnum.PRODUCTION_REPORT_STOCK_IN.getCode(), productionProductMain.getId());
                StockInventoryDto stockInventoryDto = new StockInventoryDto();
                stockInventoryDto.setRecordId(productionProductMain.getId());
                stockInventoryDto.setRecordType(String.valueOf(StockInQualifiedRecordTypeEnum.PRODUCTION_REPORT_STOCK_IN.getCode()));
                stockInventoryDto.setQualitity(productQty);
                stockInventoryDto.setProductModelId(productModel.getId());
                stockInventoryService.addStockInRecordOnly(stockInventoryDto);
            }
            productionOperationTask.setCompleteQuantity(defaultDecimal(productionOperationTask.getCompleteQuantity()).add(productQty));
@@ -263,27 +391,40 @@
            productionAccount.setSchedulingDate(LocalDateTime.now());
            productionAccountMapper.insert(productionAccount);
        }
        if (defaultDecimal(dto.getScrapQty()).compareTo(BigDecimal.ZERO) > 0) {
            stockUtils.addUnStock(productModel.getId(), dto.getScrapQty(),
                    StockInUnQualifiedRecordTypeEnum.PRODUCTION_SCRAP.getCode(), productionProductMain.getId());
        }
//        if (defaultDecimal(dto.getScrapQty()).compareTo(BigDecimal.ZERO) > 0) {
//            stockUtils.addUnStock(productModel.getId(), dto.getScrapQty(),
//                    StockInUnQualifiedRecordTypeEnum.PRODUCTION_SCRAP.getCode(), productionProductMain.getId());
//        }
        return true;
    }
    private void syncOperationParamInputValue(ProductionProductMainDto dto, Long productionOrderRoutingOperationId) {
        if (dto == null || productionOrderRoutingOperationId == null) {
    private void syncOperationParamInputValue(ProductionProductMainDto dto,
                                              Long productionOrderRoutingOperationId,
                                              Long productionProductMainId) {
        if (dto == null || productionOrderRoutingOperationId == null || productionProductMainId == null) {
            return;
        }
        List<ProductionOrderRoutingOperationParam> paramList = dto.getProductionOperationParamList();
        if (paramList == null || paramList.isEmpty()) {
            return;
        }
        Set<Long> sourceParamIdSet = paramList.stream()
                .filter(Objects::nonNull)
                .map(ProductionOrderRoutingOperationParam::getId)
                .filter(Objects::nonNull)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        if (sourceParamIdSet.isEmpty()) {
            return;
        }
        List<ProductionOrderRoutingOperationParam> dbParamList = productionOrderRoutingOperationParamMapper.selectList(
                Wrappers.<ProductionOrderRoutingOperationParam>lambdaQuery()
                        .in(ProductionOrderRoutingOperationParam::getId, sourceParamIdSet)
                        .eq(ProductionOrderRoutingOperationParam::getProductionOrderRoutingOperationId, productionOrderRoutingOperationId));
        if (dbParamList == null || dbParamList.isEmpty()) {
            return;
        }
        Map<Long, ProductionOrderRoutingOperationParam> dbParamMap = dbParamList.stream()
                .filter(item -> item != null && item.getId() != null)
                .collect(Collectors.toMap(ProductionOrderRoutingOperationParam::getId, item -> item, (left, right) -> left));
@@ -295,14 +436,31 @@
            if (dbParam == null) {
                throw new ServiceException("工序参数不存在或不属于当前工单工序,ID=" + param.getId());
            }
            if (Objects.equals(dbParam.getInputValue(), param.getInputValue())) {
                continue;
            }
            ProductionOrderRoutingOperationParam updateParam = new ProductionOrderRoutingOperationParam();
            updateParam.setId(dbParam.getId());
            updateParam.setInputValue(param.getInputValue());
            productionOrderRoutingOperationParamMapper.updateById(updateParam);
            productionOrderRoutingOperationParamMapper.insert(buildReportParamSnapshot(dbParam, param.getInputValue(), productionProductMainId));
        }
    }
    private ProductionOrderRoutingOperationParam buildReportParamSnapshot(ProductionOrderRoutingOperationParam source,
                                                                          String inputValue,
                                                                          Long productionProductMainId) {
        ProductionOrderRoutingOperationParam target = new ProductionOrderRoutingOperationParam();
        target.setProductionOrderId(source.getProductionOrderId());
        target.setTechnologyRoutingOperationParamId(source.getTechnologyRoutingOperationParamId());
        target.setParamCode(source.getParamCode());
        target.setParamName(source.getParamName());
        target.setParamType(source.getParamType());
        target.setParamFormat(source.getParamFormat());
        target.setUnit(source.getUnit());
        target.setIsRequired(source.getIsRequired());
        target.setRemark(source.getRemark());
        target.setParamId(source.getParamId());
        target.setTechnologyOperationId(source.getTechnologyOperationId());
        target.setTechnologyOperationParamId(source.getTechnologyOperationParamId());
        target.setStandardValue(source.getStandardValue());
        target.setInputValue(inputValue);
        target.setProductionOrderRoutingOperationId(source.getProductionOrderRoutingOperationId());
        target.setProductionProductMainId(productionProductMainId);
        return target;
    }
    private List<ProductStructureDto> resolveInputStructures(Long productionOrderId,
@@ -384,8 +542,8 @@
        ProductionOperationTask productionOperationTask = productionOperationTaskMapper.selectById(productionProductMain.getProductionOperationTaskId());
        if (productionOperationTask != null && productionProductOutput != null) {
            BigDecimal validQuantity = defaultDecimal(productionProductOutput.getQuantity()).subtract(defaultDecimal(productionProductOutput.getScrapQty()));
            productionOperationTask.setCompleteQuantity(defaultDecimal(productionOperationTask.getCompleteQuantity()).subtract(validQuantity));
            BigDecimal reportQuantity = defaultDecimal(productionProductOutput.getQuantity());
            productionOperationTask.setCompleteQuantity(defaultDecimal(productionOperationTask.getCompleteQuantity()).subtract(reportQuantity));
            productionOperationTask.setActualEndTime(null);
            if (defaultDecimal(productionOperationTask.getCompleteQuantity()).compareTo(BigDecimal.ZERO) <= 0) {
                productionOperationTask.setCompleteQuantity(BigDecimal.ZERO);
@@ -406,7 +564,7 @@
                                .eq(ProductionOrderRoutingOperation::getProductionOrderId, routingOperation.getProductionOrderId()));
                boolean isLastOperation = routingOperation.getDragSort() != null && routingOperation.getDragSort().equals(routingOperationList.size());
                if (isLastOperation) {
                    BigDecimal newCompleteQty = defaultDecimal(productionOrder.getCompleteQuantity()).subtract(validQuantity);
                    BigDecimal newCompleteQty = defaultDecimal(productionOrder.getCompleteQuantity()).subtract(reportQuantity);
                    productionOrder.setCompleteQuantity(newCompleteQty.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : newCompleteQty);
                    productionOrder.setEndTime(null);
                }
@@ -431,6 +589,9 @@
                .eq(ProductionProductOutput::getProductionProductMainId, productionProductMain.getId()));
        productionProductInputMapper.delete(new LambdaQueryWrapper<ProductionProductInput>()
                .eq(ProductionProductInput::getProductionProductMainId, productionProductMain.getId()));
        productionOrderRoutingOperationParamMapper.delete(
                Wrappers.<ProductionOrderRoutingOperationParam>lambdaQuery()
                        .eq(ProductionOrderRoutingOperationParam::getProductionProductMainId, productionProductMain.getId()));
        stockUtils.deleteStockInRecord(productionProductMain.getId(), StockInUnQualifiedRecordTypeEnum.PRODUCTION_SCRAP.getCode());
        stockUtils.deleteStockInRecord(productionProductMain.getId(), StockInQualifiedRecordTypeEnum.PRODUCTION_REPORT_STOCK_IN.getCode());
        stockUtils.deleteStockOutRecord(productionProductMain.getId(), StockOutQualifiedRecordTypeEnum.PRODUCTION_REPORT_STOCK_OUT.getCode());
src/main/java/com/ruoyi/project/common/CommonController.java
@@ -8,8 +8,6 @@
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.ContentDisposition;
import org.springframework.http.MediaType;
@@ -33,99 +31,11 @@
@RestController
@RequestMapping("/common")
public class CommonController {
    private static final Logger log = LoggerFactory.getLogger(CommonController.class);
    private final StorageBlobService storageBlobService;
    private final FileUtil fileUtil;
//    /**
//     * é€šç”¨ä¸‹è½½è¯·æ±‚
//     *
//     * @param fileName æ–‡ä»¶åç§°
//     * @param delete æ˜¯å¦åˆ é™¤
//     */
//    @GetMapping("/download")
//    public void fileDownload(String fileName, Boolean delete, HttpServletResponse response, HttpServletRequest request)
//    {
//        try
//        {
//            if (!FileUtils.checkAllowDownload(fileName))
//            {
//                throw new Exception(StringUtils.format("文件名称({})非法,不允许下载。 ", fileName));
//            }
//            String realFileName =  fileName.substring(fileName.indexOf("_") + 1);
//
//            response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
//            FileUtils.setAttachmentResponseHeader(response, realFileName);
//            FileUtils.writeBytes(fileName, response.getOutputStream());
    /// /            if (delete)
    /// /            {
    /// /                FileUtils.deleteFile(fileName);
    /// /            }
//        }
//        catch (Exception e)
//        {
//            log.error("下载文件失败", e);
//        }
//    }
//
//    /**
//     * é€šç”¨ä¸Šä¼ è¯·æ±‚(单个)
//     */
//    @PostMapping("/upload")
//    public AjaxResult uploadFile(MultipartFile file) throws Exception
//    {
//        try
//        {
//            // ä¸Šä¼ æ–‡ä»¶è·¯å¾„
//            String filePath = RuoYiConfig.getUploadPath();
//            // ä¸Šä¼ å¹¶è¿”回新文件名称
//            String fileName = FileUploadUtils.upload(filePath, file);
//            String url = serverConfig.getUrl() + fileName;
//            AjaxResult ajax = AjaxResult.success();
//            ajax.put("url", url);
//            ajax.put("fileName", fileName);
//            ajax.put("newFileName", FileUtils.getName(fileName));
//            ajax.put("originalFilename", file.getOriginalFilename());
//            return ajax;
//        }
//        catch (Exception e)
//        {
//            return AjaxResult.error(e.getMessage());
//        }
//    }
//
//    /**
//     * æœ¬åœ°èµ„源通用下载
//     */
//    @GetMapping("/download/resource")
//    public void resourceDownload(String resource, HttpServletRequest request, HttpServletResponse response)
//            throws Exception
//    {
//        try
//        {
//            if (!FileUtils.checkAllowDownload(resource))
//            {
//                throw new Exception(StringUtils.format("资源文件({})非法,不允许下载。 ", resource));
//            }
//            // æœ¬åœ°èµ„源路径
//            String localPath = RuoYiConfig.getProfile();
//            // æ•°æ®åº“资源地址
//            String downloadPath = localPath + StringUtils.substringAfter(resource, Constants.RESOURCE_PREFIX);
//            // ä¸‹è½½åç§°
//            String downloadName = StringUtils.substringAfterLast(downloadPath, "/");
//            response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
//            FileUtils.setAttachmentResponseHeader(response, downloadName);
//            FileUtils.writeBytes(downloadPath, response.getOutputStream());
//        }
//        catch (Exception e)
//        {
//            log.error("下载文件失败", e);
//        }
//    }
    @PostMapping({"/upload"})
    @Operation(summary = "文件上传")
    public R upload(@RequestParam("files") List<MultipartFile> files) {
src/main/java/com/ruoyi/purchase/controller/InvoicePurchaseController.java
@@ -10,11 +10,10 @@
import com.ruoyi.purchase.pojo.InvoicePurchase;
import com.ruoyi.purchase.service.IInvoicePurchaseService;
import com.ruoyi.sales.service.ICommonFileService;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
@@ -80,12 +79,4 @@
        return toAjax(invoicePurchaseService.delInvoice(ids));
    }
    @PostMapping("/upload")
    public AjaxResult uploadFile(MultipartFile file, Long id, Integer type) {
        try {
            return AjaxResult.success(commonFileService.uploadFile(file, id, type));
        } catch (Exception e) {
            return AjaxResult.error(e.getMessage());
        }
    }
}
src/main/java/com/ruoyi/purchase/controller/TicketRegistrationController.java
@@ -19,18 +19,15 @@
import com.ruoyi.purchase.service.ITicketRegistrationService;
import com.ruoyi.purchase.service.impl.PaymentRegistrationServiceImpl;
import com.ruoyi.sales.service.ICommonFileService;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.parameters.P;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
@@ -161,15 +158,6 @@
    @Transactional(rollbackFor = Exception.class)
    public AjaxResult delRegistration(@RequestBody Long[] ids) {
        return toAjax(ticketRegistrationService.delRegistration(ids));
    }
    @PostMapping("/upload")
    public AjaxResult uploadFile(MultipartFile file, Long id, Integer type) {
        try {
            return AjaxResult.success(commonFileService.uploadFile(file, id, type));
        } catch (Exception e) {
            return AjaxResult.error(e.getMessage());
        }
    }
    /**
src/main/java/com/ruoyi/purchase/dto/PurchaseLedgerDto.java
@@ -2,6 +2,8 @@
import com.baomidou.mybatisplus.annotation.TableField;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.basic.dto.StorageBlobDTO;
import com.ruoyi.basic.dto.StorageBlobVO;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import com.ruoyi.sales.pojo.CommonFile;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
@@ -197,4 +199,7 @@
    private String templateName;
    @Schema(description = "审批人id")
    private Integer approverId;
    private List<StorageBlobVO> storageBlobVOS;
    private List<StorageBlobDTO> storageBlobDTOS;
}
src/main/java/com/ruoyi/purchase/service/impl/InvoicePurchaseServiceImpl.java
@@ -50,10 +50,7 @@
    private final CommonFileMapper commonFileMapper;
    private final TempFileMapper tempFileMapper;
    @Value("${file.upload-dir}")
    private String uploadDir;
    @Override
    public List<InvoicePurchaseDto> selectInvoicePurchaseList(InvoicePurchaseDto invoicePurchaseDto) {
src/main/java/com/ruoyi/purchase/service/impl/PurchaseLedgerServiceImpl.java
@@ -6,15 +6,18 @@
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.approve.bean.vo.ApproveProcessVO;
import com.ruoyi.approve.pojo.ApproveProcess;
import com.ruoyi.approve.service.impl.ApproveProcessServiceImpl;
import com.ruoyi.approve.bean.vo.ApproveProcessVO;
import com.ruoyi.basic.enums.ApplicationTypeEnum;
import com.ruoyi.basic.enums.RecordTypeEnum;
import com.ruoyi.basic.mapper.ProductMapper;
import com.ruoyi.basic.mapper.ProductModelMapper;
import com.ruoyi.basic.mapper.SupplierManageMapper;
import com.ruoyi.basic.pojo.Product;
import com.ruoyi.basic.pojo.ProductModel;
import com.ruoyi.basic.pojo.SupplierManage;
import com.ruoyi.basic.utils.FileUtil;
import com.ruoyi.common.enums.FileNameType;
import com.ruoyi.common.exception.base.BaseException;
import com.ruoyi.common.utils.SecurityUtils;
@@ -23,7 +26,6 @@
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.other.mapper.TempFileMapper;
import com.ruoyi.other.pojo.TempFile;
import com.ruoyi.procurementrecord.mapper.ProcurementRecordMapper;
import com.ruoyi.procurementrecord.pojo.ProcurementRecordStorage;
import com.ruoyi.project.system.domain.SysUser;
@@ -59,22 +61,15 @@
import com.ruoyi.sales.service.impl.CommonFileServiceImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
@@ -116,9 +111,7 @@
    private final QualityInspectParamMapper qualityInspectParamMapper;
    private final ApproveProcessServiceImpl approveProcessService;
    private final ProcurementRecordMapper procurementRecordStorageMapper;
    @Value("${file.upload-dir}")
    private String uploadDir;
    private final FileUtil fileUtil;
    @Override
    public List<PurchaseLedger> selectPurchaseLedgerList(PurchaseLedger purchaseLedger) {
@@ -199,9 +192,7 @@
//            }
//        }
        // 5. è¿ç§»ä¸´æ—¶æ–‡ä»¶åˆ°æ­£å¼ç›®å½•
        if (purchaseLedgerDto.getTempFileIds() != null && !purchaseLedgerDto.getTempFileIds().isEmpty()) {
            migrateTempFilesToFormal(purchaseLedger.getId(), purchaseLedgerDto.getTempFileIds());
        }
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.PURCHASE_LEDGER, purchaseLedger.getId(), purchaseLedgerDto.getStorageBlobDTOS());
        return 1;
    }
@@ -322,80 +313,6 @@
        }
    }
    /**
     * å°†ä¸´æ—¶æ–‡ä»¶è¿ç§»åˆ°æ­£å¼ç›®å½•
     *
     * @param businessId  ä¸šåŠ¡ID(销售台账ID)
     * @param tempFileIds ä¸´æ—¶æ–‡ä»¶ID列表
     * @throws IOException æ–‡ä»¶æ“ä½œå¼‚常
     */
    private void migrateTempFilesToFormal(Long businessId, List<String> tempFileIds) throws IOException {
        if (CollectionUtils.isEmpty(tempFileIds)) {
            return;
        }
        // æž„建正式目录路径(按业务类型和日期分组)
        String formalDir = uploadDir + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);
        Path formalDirPath = Paths.get(formalDir);
        // ç¡®ä¿æ­£å¼ç›®å½•存在(递归创建)
        if (!Files.exists(formalDirPath)) {
            Files.createDirectories(formalDirPath);
        }
        for (String tempFileId : tempFileIds) {
            // æŸ¥è¯¢ä¸´æ—¶æ–‡ä»¶è®°å½•
            TempFile tempFile = tempFileMapper.selectById(tempFileId);
            if (tempFile == null) {
                log.warn("临时文件不存在,跳过处理: {}", tempFileId);
                continue;
            }
            // æž„建正式文件名(包含业务ID和时间戳,避免冲突)
            String originalFilename = tempFile.getOriginalName();
            String fileExtension = FilenameUtils.getExtension(originalFilename);
            String formalFilename = businessId + "_" +
                    System.currentTimeMillis() + "_" +
                    UUID.randomUUID().toString().substring(0, 8) +
                    (StringUtils.hasText(fileExtension) ? "." + fileExtension : "");
            Path formalFilePath = formalDirPath.resolve(formalFilename);
            try {
                // æ‰§è¡Œæ–‡ä»¶è¿ç§»ï¼ˆä½¿ç”¨åŽŸå­æ“ä½œç¡®ä¿å®‰å…¨æ€§ï¼‰
//                Files.move(
//                        Paths.get(tempFile.getTempPath()),
//                        formalFilePath,
//                        StandardCopyOption.REPLACE_EXISTING,
//                        StandardCopyOption.ATOMIC_MOVE
//                );
                // åŽŸå­ç§»åŠ¨å¤±è´¥ï¼Œä½¿ç”¨å¤åˆ¶+删除
                Files.copy(Paths.get(tempFile.getTempPath()), formalFilePath, StandardCopyOption.REPLACE_EXISTING);
                Files.deleteIfExists(Paths.get(tempFile.getTempPath()));
                log.info("文件迁移成功: {} -> {}", tempFile.getTempPath(), formalFilePath);
                // æ›´æ–°æ–‡ä»¶è®°å½•(关联到业务ID)
                CommonFile fileRecord = new CommonFile();
                fileRecord.setCommonId(businessId);
                fileRecord.setName(originalFilename);
                fileRecord.setUrl(formalFilePath.toString());
                fileRecord.setCreateTime(LocalDateTime.now());
                fileRecord.setType(FileNameType.PURCHASE.getValue());
                commonFileMapper.insert(fileRecord);
                // åˆ é™¤ä¸´æ—¶æ–‡ä»¶è®°å½•
                tempFileMapper.deleteById(tempFile);
                log.info("文件迁移成功: {} -> {}", tempFile.getTempPath(), formalFilePath);
            } catch (IOException e) {
                log.error("文件迁移失败: {}", tempFile.getTempPath(), e);
                // å¯é€‰æ‹©å›žæ»šäº‹åŠ¡æˆ–è®°å½•å¤±è´¥æ–‡ä»¶
                throw new IOException("文件迁移异常", e);
            }
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int deletePurchaseLedgerByIds(Long[] ids) {
@@ -493,11 +410,6 @@
                .eq(SalesLedgerProduct::getType, purchaseLedgerDto.getType());
        List<SalesLedgerProduct> products = salesLedgerProductMapper.selectList(productWrapper);
        // 3.查询上传文件
        LambdaQueryWrapper<CommonFile> salesLedgerFileWrapper = new LambdaQueryWrapper<>();
        salesLedgerFileWrapper.eq(CommonFile::getCommonId, purchaseLedger.getId())
                .eq(CommonFile::getType,FileNameType.PURCHASE.getValue());
        List<CommonFile> salesLedgerFiles = commonFileMapper.selectList(salesLedgerFileWrapper);
        // 4. è½¬æ¢ DTO
        PurchaseLedgerDto resultDto = new PurchaseLedgerDto();
@@ -505,7 +417,7 @@
        if (!products.isEmpty()) {
            resultDto.setHasChildren(true);
            resultDto.setProductData(products);
            resultDto.setSalesLedgerFiles(salesLedgerFiles);
            resultDto.setStorageBlobVOS(fileUtil.getStorageBlobVOsByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum.FILE, RecordTypeEnum.PURCHASE_LEDGER, purchaseLedger.getId()));
        }
        return resultDto;
    }
src/main/java/com/ruoyi/purchase/service/impl/TicketRegistrationServiceImpl.java
@@ -14,7 +14,6 @@
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.bean.BeanUtils;
import com.ruoyi.other.mapper.TempFileMapper;
import com.ruoyi.other.pojo.TempFile;
import com.ruoyi.purchase.dto.PaymentRegistrationDto;
import com.ruoyi.purchase.dto.PurchaseLedgerDto;
import com.ruoyi.purchase.dto.TicketRegistrationDto;
@@ -34,21 +33,17 @@
import com.ruoyi.sales.service.ISalesLedgerProductService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
@@ -77,9 +72,6 @@
    private final ISalesLedgerProductService salesLedgerProductService;
    private final PaymentRegistrationMapper paymentRegistrationMapper;
    @Value("${file.upload-dir}")
    private String uploadDir;
    @Override
@@ -175,78 +167,7 @@
                throw new RuntimeException("产品开票数都为0,请检查");
            }
        }
        // è¿ç§»ä¸´æ—¶æ–‡ä»¶åˆ°æ­£å¼ç›®å½•
        if (ticketRegistrationDto.getTempFileIds() != null && !ticketRegistrationDto.getTempFileIds().isEmpty()) {
            migrateTempFilesToFormal(ticketRegistration.getId(), ticketRegistrationDto.getTempFileIds());
        }
        return rowsAffected;
    }
    /**
     * å°†ä¸´æ—¶æ–‡ä»¶è¿ç§»åˆ°æ­£å¼ç›®å½•
     *
     * @param businessId  ä¸šåŠ¡ID(销售台账ID)
     * @param tempFileIds ä¸´æ—¶æ–‡ä»¶ID列表
     * @throws IOException æ–‡ä»¶æ“ä½œå¼‚常
     */
    public void migrateTempFilesToFormal(Long businessId, List<String> tempFileIds) throws IOException {
        if (CollectionUtils.isEmpty(tempFileIds)) {
            return;
        }
        // æž„建正式目录路径(按业务类型和日期分组)
        String formalDir = uploadDir + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);
        Path formalDirPath = Paths.get(formalDir);
        // ç¡®ä¿æ­£å¼ç›®å½•存在(递归创建)
        if (!Files.exists(formalDirPath)) {
            Files.createDirectories(formalDirPath);
        }
        for (String tempFileId : tempFileIds) {
            // æŸ¥è¯¢ä¸´æ—¶æ–‡ä»¶è®°å½•
            TempFile tempFile = tempFileMapper.selectById(tempFileId);
            if (tempFile == null) {
                log.warn("临时文件不存在,跳过处理: {}", tempFileId);
                continue;
            }
            // æž„建正式文件名(包含业务ID和时间戳,避免冲突)
            String originalFilename = tempFile.getOriginalName();
            String fileExtension = FilenameUtils.getExtension(originalFilename);
            String baseName = FilenameUtils.getBaseName(originalFilename);
            String formalFilename = businessId + "_" +
                    System.currentTimeMillis() + "_" +
                    UUID.randomUUID().toString().substring(0, 8) +baseName+
                    (com.ruoyi.common.utils.StringUtils.hasText(fileExtension) ? "." + fileExtension : "");
            Path formalFilePath = formalDirPath.resolve(formalFilename);
            try {
                // åŽŸå­ç§»åŠ¨å¤±è´¥ï¼Œä½¿ç”¨å¤åˆ¶+删除
                Files.copy(Paths.get(tempFile.getTempPath()), formalFilePath, StandardCopyOption.REPLACE_EXISTING);
                Files.deleteIfExists(Paths.get(tempFile.getTempPath()));
                log.info("文件迁移成功: {} -> {}", tempFile.getTempPath(), formalFilePath);
                // æ›´æ–°æ–‡ä»¶è®°å½•(关联到业务ID)
                CommonFile fileRecord = new CommonFile();
                fileRecord.setCommonId(businessId);
                fileRecord.setName(originalFilename);
                fileRecord.setUrl(formalFilePath.toString());
                fileRecord.setCreateTime(LocalDateTime.now());
                fileRecord.setType(4);
                commonFileMapper.insert(fileRecord);
                log.info("文件迁移成功: {} -> {}", tempFile.getTempPath(), formalFilePath);
            } catch (IOException e) {
                log.error("文件迁移失败: {}", tempFile.getTempPath(), e);
                // å¯é€‰æ‹©å›žæ»šäº‹åŠ¡æˆ–è®°å½•å¤±è´¥æ–‡ä»¶
                throw new IOException("文件迁移异常", e);
            }
        }
    }
src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java
@@ -9,7 +9,6 @@
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
import com.ruoyi.common.enums.StockOutQualifiedRecordTypeEnum;
import com.ruoyi.common.utils.HackLoopTableRenderPolicy;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.procurementrecord.service.ProcurementRecordService;
@@ -24,12 +23,14 @@
import com.ruoyi.quality.service.IQualityInspectParamService;
import com.ruoyi.quality.service.IQualityInspectService;
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
import com.ruoyi.stock.dto.StockInventoryDto;
import com.ruoyi.stock.service.StockInventoryService;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import jakarta.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
@@ -43,6 +44,7 @@
public class QualityInspectServiceImpl extends ServiceImpl<QualityInspectMapper, QualityInspect> implements IQualityInspectService {
    private final StockUtils stockUtils;
    private final StockInventoryService stockInventoryService;
    private QualityInspectMapper qualityInspectMapper;
    private IQualityInspectParamService qualityInspectParamService;
@@ -98,7 +100,14 @@
            qualityUnqualifiedMapper.insert(qualityUnqualified);
        } else {
            //合格直接入库
            stockUtils.addStock(qualityInspect.getProductModelId(), qualityInspect.getQuantity(), StockInQualifiedRecordTypeEnum.QUALITYINSPECT_STOCK_IN.getCode(), qualityInspect.getId());
            // stockUtils.addStock(qualityInspect.getProductModelId(), qualityInspect.getQuantity(), StockInQualifiedRecordTypeEnum.QUALITYINSPECT_STOCK_IN.getCode(), qualityInspect.getId());
            //仅添加入库记录
            StockInventoryDto stockInventoryDto = new StockInventoryDto();
            stockInventoryDto.setRecordType(String.valueOf(StockInQualifiedRecordTypeEnum.QUALITYINSPECT_STOCK_IN.getCode()));
            stockInventoryDto.setRecordId(qualityInspect.getId());
            stockInventoryDto.setProductModelId(qualityInspect.getProductModelId());
            stockInventoryDto.setQualitity(qualityInspect.getQuantity());
            stockInventoryService.addStockInRecordOnly(stockInventoryDto);
        }
        qualityInspect.setInspectState(1);//已提交
        return qualityInspectMapper.updateById(qualityInspect);
src/main/java/com/ruoyi/safe/controller/SafeTrainingController.java
@@ -29,8 +29,8 @@
@RequiredArgsConstructor
public class SafeTrainingController {
    private SafeTrainingService safeTrainingService;
    private SafeTrainingDetailsService safeTrainingDetailsService;
    private final SafeTrainingService safeTrainingService;
    private final SafeTrainingDetailsService safeTrainingDetailsService;
    @GetMapping("/page")
    @Operation(summary = "分页查询")
src/main/java/com/ruoyi/sales/service/ICommonFileService.java
@@ -1,15 +1,9 @@
package com.ruoyi.sales.service;
import com.ruoyi.sales.pojo.CommonFile;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
public interface ICommonFileService {
    int deleteSalesLedgerByIds(Long[] ids);
    CommonFile uploadFile(MultipartFile file, Long id, Integer type) throws IOException;
    int delCommonFileByIds(Long[] ids);
}
src/main/java/com/ruoyi/sales/service/impl/CommonFileServiceImpl.java
@@ -1,32 +1,17 @@
package com.ruoyi.sales.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.other.mapper.TempFileMapper;
import com.ruoyi.other.pojo.TempFile;
import com.ruoyi.sales.mapper.CommonFileMapper;
import com.ruoyi.sales.pojo.CommonFile;
import com.ruoyi.sales.service.ICommonFileService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
@Service
@RequiredArgsConstructor
@@ -36,9 +21,6 @@
    private final CommonFileMapper commonFileMapper;
    private final TempFileMapper tempFileMapper;
    @Value("${file.upload-dir}")
    private String uploadDir;
    public List<CommonFile> getFileListByBusinessId(Long businessId,Integer type) {
        return commonFileMapper.selectList(new LambdaQueryWrapper<CommonFile>().eq(CommonFile::getCommonId, businessId)
@@ -66,105 +48,9 @@
        return commonFileMapper.deleteBatchIds(Arrays.asList(ids));
    }
    @Override
    public CommonFile uploadFile(MultipartFile file, Long id, Integer type) throws IOException {
        // 1. ç”Ÿæˆæ­£å¼æ–‡ä»¶ID和路径
        String tempId = UUID.randomUUID().toString();
        Path tempFilePath = Paths.get(uploadDir, tempId + "_" + file.getOriginalFilename());
        // 2. ç¡®ä¿ç›®å½•存在
        Path parentDir = tempFilePath.getParent();
        if (parentDir != null) {
            Files.createDirectories(parentDir); // é€’归创建目录
        }
        // 3. ä¿å­˜æ–‡ä»¶åˆ°ç›®å½•
        file.transferTo(tempFilePath.toFile());
        // 4. ä¿å­˜æ–‡ä»¶è®°å½•
        CommonFile commonFile = new CommonFile();
        commonFile.setCommonId(id);
        commonFile.setName(file.getOriginalFilename());
        commonFile.setUrl(tempFilePath.toString());
        commonFile.setType(type);
        commonFileMapper.insert(commonFile);
        return commonFile;
    }
    @Override
    public int delCommonFileByIds(Long[] ids) {
        return commonFileMapper.deleteBatchIds(Arrays.asList(ids));
    }
    /**
     * å°†ä¸´æ—¶æ–‡ä»¶è¿ç§»åˆ°æ­£å¼ç›®å½•
     *
     * @param businessId  ä¸šåŠ¡ID(销售台账ID)
     * @param tempFileIds ä¸´æ—¶æ–‡ä»¶ID列表
     * @throws IOException æ–‡ä»¶æ“ä½œå¼‚常
     */
    @Transactional(rollbackFor = Exception.class)
    public void migrateTempFilesToFormal(Long businessId, List<String> tempFileIds) throws IOException {
        if (CollectionUtils.isEmpty(tempFileIds)) {
            return;
        }
        // æž„建正式目录路径(按业务类型和日期分组)
        String formalDir = uploadDir + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);
        Path formalDirPath = Paths.get(formalDir);
        // ç¡®ä¿æ­£å¼ç›®å½•存在(递归创建)
        if (!Files.exists(formalDirPath)) {
            Files.createDirectories(formalDirPath);
        }
        for (String tempFileId : tempFileIds) {
            // æŸ¥è¯¢ä¸´æ—¶æ–‡ä»¶è®°å½•
            TempFile tempFile = tempFileMapper.selectById(tempFileId);
            if (tempFile == null) {
                log.warn("临时文件不存在,跳过处理: {}", tempFileId);
                continue;
            }
            // æž„建正式文件名(包含业务ID和时间戳,避免冲突)
            String originalFilename = tempFile.getOriginalName();
            String fileExtension = FilenameUtils.getExtension(originalFilename);
            String formalFilename = businessId + "_" +
                    System.currentTimeMillis() + "_" +
                    UUID.randomUUID().toString().substring(0, 8) +
                    (com.ruoyi.common.utils.StringUtils.hasText(fileExtension) ? "." + fileExtension : "");
            Path formalFilePath = formalDirPath.resolve(formalFilename);
            try {
                // æ‰§è¡Œæ–‡ä»¶è¿ç§»ï¼ˆä½¿ç”¨åŽŸå­æ“ä½œç¡®ä¿å®‰å…¨æ€§ï¼‰
//                Files.move(
//                        Paths.get(tempFile.getTempPath()),
//                        formalFilePath,
//                        StandardCopyOption.REPLACE_EXISTING,
//                        StandardCopyOption.ATOMIC_MOVE
//                );
                // åŽŸå­ç§»åŠ¨å¤±è´¥ï¼Œä½¿ç”¨å¤åˆ¶+删除
                Files.copy(Paths.get(tempFile.getTempPath()), formalFilePath, StandardCopyOption.REPLACE_EXISTING);
                Files.deleteIfExists(Paths.get(tempFile.getTempPath()));
                log.info("文件迁移成功: {} -> {}", tempFile.getTempPath(), formalFilePath);
                // æ›´æ–°æ–‡ä»¶è®°å½•(关联到业务ID)
                CommonFile fileRecord = new CommonFile();
                fileRecord.setCommonId(businessId);
                fileRecord.setName(originalFilename);
                fileRecord.setUrl(formalFilePath.toString());
                fileRecord.setCreateTime(LocalDateTime.now());
                fileRecord.setType(tempFile.getType());
                commonFileMapper.insert(fileRecord);
                log.info("文件迁移成功: {} -> {}", tempFile.getTempPath(), formalFilePath);
            } catch (IOException e) {
                log.error("文件迁移失败: {}", tempFile.getTempPath(), e);
                // å¯é€‰æ‹©å›žæ»šäº‹åŠ¡æˆ–è®°å½•å¤±è´¥æ–‡ä»¶
                throw new IOException("文件迁移异常", e);
            }
        }
    }
}
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java
@@ -29,7 +29,6 @@
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.other.mapper.TempFileMapper;
import com.ruoyi.other.pojo.TempFile;
import com.ruoyi.production.mapper.ProductionProductInputMapper;
import com.ruoyi.production.mapper.ProductionProductMainMapper;
import com.ruoyi.production.mapper.ProductionProductOutputMapper;
@@ -48,7 +47,6 @@
import com.ruoyi.sales.vo.SalesLedgerVo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
@@ -58,15 +56,10 @@
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.YearMonth;
@@ -89,13 +82,11 @@
    private static final String LOCK_PREFIX = "contract_no_lock:";
    private static final long LOCK_WAIT_TIMEOUT = 10; // é”ç­‰å¾…超时时间(秒)
    private static final long LOCK_EXPIRE_TIME = 30;  // é”è‡ªåŠ¨è¿‡æœŸæ—¶é—´ï¼ˆç§’ï¼‰
    private final AccountIncomeService accountIncomeService;
    private final SalesLedgerMapper salesLedgerMapper;
    private final CustomerMapper customerMapper;
    private final SalesLedgerProductMapper salesLedgerProductMapper;
    private final SalesLedgerProductServiceImpl salesLedgerProductServiceImpl;
    private final CommonFileMapper commonFileMapper;
    private final TempFileMapper tempFileMapper;
    private final ReceiptPaymentMapper receiptPaymentMapper;
    private final ShippingInfoServiceImpl shippingInfoServiceImpl;
    private final CommonFileServiceImpl commonFileService;
@@ -103,15 +94,9 @@
    private final InvoiceLedgerMapper invoiceLedgerMapper;
    private final InvoiceRegistrationProductMapper invoiceRegistrationProductMapper;
    private final InvoiceRegistrationMapper invoiceRegistrationMapper;
    private final ProductionProductMainMapper productionProductMainMapper;
    private final ProductionProductOutputMapper productionProductOutputMapper;
    private final ProductionProductInputMapper productionProductInputMapper;
    private final QualityInspectMapper qualityInspectMapper;
    private final ProductModelMapper productModelMapper;
    private final RedisTemplate<String, String> redisTemplate;
    private final SysDeptMapper sysDeptMapper;
    @Value("${file.upload-dir}")
    private String uploadDir;
    private final ProductionProductMainService productionProductMainService;
    private final PurchaseReturnOrderProductsMapper purchaseReturnOrderProductsMapper;
    private final SysUserMapper sysUserMapper;
@@ -621,81 +606,6 @@
            fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.SALES_LEDGER, salesLedger.getId(), salesLedgerDto.getStorageBlobDTOs());
        }
        return 1;
    }
    /**
     * å°†ä¸´æ—¶æ–‡ä»¶è¿ç§»åˆ°æ­£å¼ç›®å½•
     *
     * @param businessId  ä¸šåŠ¡ID(销售台账ID)
     * @param tempFileIds ä¸´æ—¶æ–‡ä»¶ID列表
     * @throws IOException æ–‡ä»¶æ“ä½œå¼‚常
     */
    private void migrateTempFilesToFormal(Long businessId, List<String> tempFileIds) throws IOException {
        if (CollectionUtils.isEmpty(tempFileIds)) {
            return;
        }
        // æž„建正式目录路径(按业务类型和日期分组)
        String formalDir = uploadDir + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);
        Path formalDirPath = Paths.get(formalDir);
        // ç¡®ä¿æ­£å¼ç›®å½•存在(递归创建)
        if (!Files.exists(formalDirPath)) {
            Files.createDirectories(formalDirPath);
        }
        for (String tempFileId : tempFileIds) {
            // æŸ¥è¯¢ä¸´æ—¶æ–‡ä»¶è®°å½•
            TempFile tempFile = tempFileMapper.selectById(tempFileId);
            if (tempFile == null) {
                log.warn("临时文件不存在,跳过处理: {}", tempFileId);
                continue;
            }
            // æž„建正式文件名(包含业务ID和时间戳,避免冲突)
            String originalFilename = tempFile.getOriginalName();
            String fileExtension = FilenameUtils.getExtension(originalFilename);
            String formalFilename = businessId + "_" +
                    System.currentTimeMillis() + "_" +
                    UUID.randomUUID().toString().substring(0, 8) +
                    (StringUtils.hasText(fileExtension) ? "." + fileExtension : "");
            Path formalFilePath = formalDirPath.resolve(formalFilename);
            try {
                // æ‰§è¡Œæ–‡ä»¶è¿ç§»ï¼ˆä½¿ç”¨åŽŸå­æ“ä½œç¡®ä¿å®‰å…¨æ€§ï¼‰
//                Files.move(
//                        Paths.get(tempFile.getTempPath()),
//                        formalFilePath,
//                        StandardCopyOption.REPLACE_EXISTING,
//                        StandardCopyOption.ATOMIC_MOVE
//                );
                // åŽŸå­ç§»åŠ¨å¤±è´¥ï¼Œä½¿ç”¨å¤åˆ¶+删除
                Files.copy(Paths.get(tempFile.getTempPath()), formalFilePath, StandardCopyOption.REPLACE_EXISTING);
                Files.deleteIfExists(Paths.get(tempFile.getTempPath()));
                log.info("文件迁移成功: {} -> {}", tempFile.getTempPath(), formalFilePath);
                // æ›´æ–°æ–‡ä»¶è®°å½•(关联到业务ID)
                CommonFile fileRecord = new CommonFile();
                fileRecord.setCommonId(businessId);
                fileRecord.setName(originalFilename);
                fileRecord.setUrl(formalFilePath.toString());
                fileRecord.setCreateTime(LocalDateTime.now());
                //销售
                fileRecord.setType(FileNameType.SALE.getValue());
                commonFileMapper.insert(fileRecord);
                // åˆ é™¤ä¸´æ—¶æ–‡ä»¶è®°å½•
                tempFileMapper.deleteById(tempFile);
                log.info("文件迁移成功: {} -> {}", tempFile.getTempPath(), formalFilePath);
            } catch (IOException e) {
                log.error("文件迁移失败: {}", tempFile.getTempPath(), e);
                // å¯é€‰æ‹©å›žæ»šäº‹åŠ¡æˆ–è®°å½•å¤±è´¥æ–‡ä»¶
                throw new IOException("文件迁移异常", e);
            }
        }
    }
    // æ–‡ä»¶è¿ç§»æ–¹æ³•
src/main/java/com/ruoyi/sales/service/impl/ShippingInfoServiceImpl.java
@@ -11,7 +11,6 @@
import com.ruoyi.basic.utils.FileUtil;
import com.ruoyi.common.enums.FileNameType;
import com.ruoyi.common.enums.StockOutQualifiedRecordTypeEnum;
import com.ruoyi.other.service.impl.TempFileServiceImpl;
import com.ruoyi.procurementrecord.utils.StockUtils;
import com.ruoyi.sales.dto.SalesLedgerProductDto;
import com.ruoyi.sales.dto.ShippingInfoDto;
@@ -23,7 +22,6 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Collections;
@@ -41,7 +39,6 @@
    private final ShippingInfoMapper shippingInfoMapper;
    private final TempFileServiceImpl tempFileService;
    private final SalesLedgerProductMapper salesLedgerProductMapper;
src/main/java/com/ruoyi/stock/service/impl/StockInventoryServiceImpl.java
@@ -4,12 +4,14 @@
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.basic.mapper.ProductModelMapper;
import com.ruoyi.basic.pojo.ProductModel;
import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
import com.ruoyi.common.enums.StockInUnQualifiedRecordTypeEnum;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.exception.base.BaseException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.web.domain.R;
@@ -21,18 +23,21 @@
import com.ruoyi.stock.dto.StockUninventoryDto;
import com.ruoyi.stock.execl.StockInventoryExportData;
import com.ruoyi.stock.mapper.StockInventoryMapper;
import com.ruoyi.stock.pojo.StockInRecord;
import com.ruoyi.stock.pojo.StockInventory;
import com.ruoyi.stock.service.StockInRecordService;
import com.ruoyi.stock.service.StockInventoryService;
import com.ruoyi.stock.service.StockOutRecordService;
import com.ruoyi.stock.service.StockUninventoryService;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.http.HttpServletResponse;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -55,6 +60,7 @@
    private StockOutRecordService stockOutRecordService;
    private StockUninventoryService stockUninventoryService;
    private SalesLedgerProductMapper salesLedgerProductMapper;
    private ProductModelMapper productModelMapper;
    @Override
    public IPage<StockInventoryDto> pagestockInventory(Page page, StockInventoryDto stockInventoryDto) {
        return stockInventoryMapper.pagestockInventory(page, stockInventoryDto);
@@ -69,14 +75,15 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean addstockInventory(StockInventoryDto stockInventoryDto) {
        String batchNo = StringUtils.trim(stockInventoryDto.getBatchNo());
        if (StringUtils.isEmpty(batchNo)) {
            batchNo = generateAutoBatchNo(stockInventoryDto.getProductModelId());
        }
        stockInventoryDto.setBatchNo(batchNo);
        LambdaQueryWrapper<StockInventory> eq = new QueryWrapper<StockInventory>().lambda()
                .eq(StockInventory::getProductModelId, stockInventoryDto.getProductModelId());
        if (StringUtils.isEmpty(stockInventoryDto.getBatchNo())) {
            eq.isNull(StockInventory::getBatchNo);
            stockInventoryDto.setBatchNo(null);
        } else {
            eq.eq(StockInventory::getBatchNo, stockInventoryDto.getBatchNo());
        }
        eq.eq(StockInventory::getBatchNo, stockInventoryDto.getBatchNo());
        //新增入库记录再添加库存
        StockInRecordDto stockInRecordDto = new StockInRecordDto();
        stockInRecordDto.setRecordId(stockInventoryDto.getRecordId());
@@ -147,11 +154,17 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean addStockInRecordOnly(StockInventoryDto stockInventoryDto) {
        String batchNo = StringUtils.trim(stockInventoryDto.getBatchNo());
        if (StringUtils.isEmpty(batchNo)) {
            batchNo = generateAutoBatchNo(stockInventoryDto.getProductModelId());
        }
        stockInventoryDto.setBatchNo(batchNo);
        StockInRecordDto stockInRecordDto = new StockInRecordDto();
        stockInRecordDto.setRecordId(stockInventoryDto.getRecordId());
        stockInRecordDto.setRecordType(stockInventoryDto.getRecordType());
        stockInRecordDto.setStockInNum(stockInventoryDto.getQualitity());
        stockInRecordDto.setBatchNo(stockInventoryDto.getBatchNo());
        stockInRecordDto.setBatchNo(batchNo);
        stockInRecordDto.setProductModelId(stockInventoryDto.getProductModelId());
        stockInRecordDto.setType("0");
        stockInRecordDto.setRemark(stockInventoryDto.getRemark());
@@ -159,6 +172,82 @@
        return true;
    }
    //规则生成:20260424-产品编号-001
    private String generateAutoBatchNo(Long productModelId) {
        if (productModelId == null) {
            throw new ServiceException("产品规格ID不能为空");
        }
        ProductModel productModel = productModelMapper.selectById(productModelId);
        if (productModel == null) {
            throw new ServiceException("产品规格不存在,ID=" + productModelId);
        }
        String productCode = StringUtils.trim(productModel.getProductCode());
        if (StringUtils.isEmpty(productCode)) {
            throw new ServiceException("产品规格未维护产品编码,ID=" + productModelId);
        }
        String dateText = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
        String prefix = dateText + "-" + productCode + "-";
        int maxSequence = resolveMaxSequence(prefix);
        int sequence = maxSequence + 1;
        while (sequence < 1_000_000) {
            String batchNo = prefix + String.format("%03d", sequence);
            if (!isBatchNoExists(batchNo)) {
                return batchNo;
            }
            sequence++;
        }
        throw new ServiceException("批号序号超出范围,请检查批号数据");
    }
    private int resolveMaxSequence(String prefix) {
        int maxSequence = 0;
        List<StockInventory> stockInventoryList = stockInventoryMapper.selectList(
                Wrappers.<StockInventory>lambdaQuery()
                        .select(StockInventory::getBatchNo)
                        .likeRight(StockInventory::getBatchNo, prefix));
        for (StockInventory stockInventory : stockInventoryList) {
            maxSequence = Math.max(maxSequence, parseSequence(stockInventory.getBatchNo(), prefix));
        }
        List<StockInRecord> stockInRecordList = stockInRecordService.list(
                Wrappers.<StockInRecord>lambdaQuery()
                        .select(StockInRecord::getBatchNo)
                        .likeRight(StockInRecord::getBatchNo, prefix));
        for (StockInRecord stockInRecord : stockInRecordList) {
            maxSequence = Math.max(maxSequence, parseSequence(stockInRecord.getBatchNo(), prefix));
        }
        return maxSequence;
    }
    private int parseSequence(String batchNo, String prefix) {
        if (StringUtils.isEmpty(batchNo) || StringUtils.isEmpty(prefix) || !batchNo.startsWith(prefix)) {
            return 0;
        }
        String sequenceText = batchNo.substring(prefix.length());
        if (StringUtils.isEmpty(sequenceText) || !sequenceText.matches("\\d+")) {
            return 0;
        }
        try {
            return Integer.parseInt(sequenceText);
        } catch (NumberFormatException ignored) {
            return 0;
        }
    }
    private boolean isBatchNoExists(String batchNo) {
        if (StringUtils.isEmpty(batchNo)) {
            return false;
        }
        Long inventoryCount = stockInventoryMapper.selectCount(
                Wrappers.<StockInventory>lambdaQuery().eq(StockInventory::getBatchNo, batchNo));
        if (inventoryCount != null && inventoryCount > 0) {
            return true;
        }
        return stockInRecordService.count(
                Wrappers.<StockInRecord>lambdaQuery().eq(StockInRecord::getBatchNo, batchNo)) > 0;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Boolean addStockOutRecordOnly(StockInventoryDto stockInventoryDto) {
src/main/java/com/ruoyi/technology/controller/TechnologyRoutingController.java
@@ -52,7 +52,7 @@
    /**
     * ä¿®æ”¹å·¥è‰ºè·¯çº¿ã€‚
     */
    @PutMapping("editTechRoute")
    @PutMapping("/editTechRoute")
    @Operation(summary = "修改工艺路线")
    public R edit(@RequestBody TechnologyRouting technologyRouting) {
        return R.ok(technologyRoutingService.updateTechnologyRouting(technologyRouting));
@@ -67,5 +67,4 @@
        return R.ok(technologyRoutingService.removeTechnologyRouting(ids));
    }
    //TODO å¢žåŠ å·¥è‰ºè·¯çº¿é™„ä»¶ä¸Šä¼  @陈海杰
}
src/main/java/com/ruoyi/technology/service/impl/TechnologyRoutingServiceImpl.java
@@ -116,13 +116,13 @@
                Wrappers.<TechnologyBomStructure>lambdaQuery()
                        .eq(TechnologyBomStructure::getBomId, technologyRouting.getBomId())
                        .isNotNull(TechnologyBomStructure::getOperationId)
                        .orderByAsc(TechnologyBomStructure::getId)
                        .orderByDesc(TechnologyBomStructure::getId)
        );
        if (bomStructures.isEmpty()) {
            throw new ServiceException("bom产品结构为空!");
        }
        // åŒä¸€ä¸ª BOM ä¸­å¯èƒ½é‡å¤å¼•用相同工序,这里按首次出现顺序去重。
        // åŒä¸€ä¸ª BOM ä¸­å¯èƒ½é‡å¤å¼•用相同工序,按照上一层的父节点的产品是否相同和工序是否相同
        Map<Long, TechnologyBomStructure> structureById = new HashMap<>();
        for (TechnologyBomStructure bomStructure : bomStructures) {
            if (bomStructure != null && bomStructure.getId() != null) {
src/main/resources/application-dev-pro.yml
@@ -254,8 +254,8 @@
# æ–‡ä»¶ä¸Šä¼ é…ç½®
file:
  temp-dir: D:/ruoyi/temp/uploads   # ä¸´æ—¶ç›®å½•
  upload-dir: D:/ruoyi/prod/uploads # æ­£å¼ç›®å½•
  temp-dir: D:/ruoyi/temp/uploads   # ä¸´æ—¶ç›®å½• åŽæœŸåˆ é™¤
  upload-dir: D:/ruoyi/prod/uploads # æ­£å¼ç›®å½• åŽæœŸåˆ é™¤
  path: C:/Users/12631/Desktop/download/uploads # ä¸Šä¼ ç›®å½•
  urlPrefix: /common # é“¾æŽ¥å‰ç¼€
  domain: http://127.0.0.1:7003 # åŸŸåå‰ç¼€
src/main/resources/mapper/basic/StorageAttachmentMapper.xml
@@ -10,13 +10,9 @@
                    <result column="deleted" property="deleted" />
                    <result column="record_type" property="recordType" />
                    <result column="record_id" property="recordId" />
                    <result column="name" property="name" />
                    <result column="application" property="application" />
                    <result column="storage_blob_id" property="storageBlobId" />
        </resultMap>
        <!-- é€šç”¨æŸ¥è¯¢ç»“果列 -->
        <sql id="Base_Column_List">
            id, create_time, update_time, deleted, record_type, record_id, name, storage_blob_id
        </sql>
</mapper>
</mapper>
src/main/resources/mapper/basic/StorageBlobMapper.xml
@@ -2,21 +2,50 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.basic.mapper.StorageBlobMapper">
        <!-- é€šç”¨æŸ¥è¯¢æ˜ å°„结果 -->
        <resultMap id="BaseResultMap" type="com.ruoyi.basic.pojo.StorageBlob">
                    <id column="id" property="id" />
                    <result column="create_time" property="createTime" />
                    <result column="key" property="key" />
                    <result column="content_type" property="contentType" />
                    <result column="original_filename" property="originalFilename" />
                    <result column="bucket_filename" property="bucketFilename" />
                    <result column="bucket_name" property="bucketName" />
                    <result column="byte_size" property="byteSize" />
        </resultMap>
    <!-- é€šç”¨æŸ¥è¯¢æ˜ å°„结果 -->
    <resultMap id="BaseResultMap" type="com.ruoyi.basic.pojo.StorageBlob">
        <id column="id" property="id"/>
        <result column="resource_key" property="resourceKey"/>
        <result column="content_type" property="contentType"/>
        <result column="original_filename" property="originalFilename"/>
        <result column="uid_filename" property="uidFilename"/>
        <result column="byte_size" property="byteSize"/>
        <result column="path" property="path"/>
    </resultMap>
        <!-- é€šç”¨æŸ¥è¯¢ç»“果列 -->
        <sql id="Base_Column_List">
            id, create_time, key, content_type, original_filename,bucket_filename,bucket_name,  byte_size
        </sql>
    <!-- é€šç”¨æŸ¥è¯¢ç»“果列 -->
    <sql id="Base_Column_List">
        id, resource_key, content_type, original_filename, uid_filename, byte_size, path
    </sql>
</mapper>
    <select id="selectOrphanBlobsByIdRange" resultMap="BaseResultMap">
        SELECT
        <include refid="Base_Column_List"/>
        FROM storage_blob sb
        LEFT JOIN storage_attachment sa
        ON sa.storage_blob_id = sb.id
        AND sa.deleted = 0
        WHERE sb.id <![CDATA[>]]> #{lastId}
        AND sa.id IS NULL
        ORDER BY sb.id ASC
        LIMIT #{limit}
    </select>
    <delete id="deleteByIdList">
        DELETE FROM storage_blob
        WHERE id IN
        <foreach collection="ids" item="id" open="(" separator="," close=")">
            #{id}
        </foreach>
    </delete>
    <select id="selectExistingUidFilenames" resultType="java.lang.String">
        SELECT uid_filename
        FROM storage_blob
        WHERE uid_filename IN
        <foreach collection="fileNames" item="fileName" open="(" separator="," close=")">
            #{fileName}
        </foreach>
    </select>
</mapper>
src/main/resources/mapper/production/ProductionAccountMapper.xml
@@ -21,6 +21,93 @@
        <result column="dept_id" property="deptId" />
    </resultMap>
    <select id="listPage" resultType="com.ruoyi.production.bean.vo.ProductionAccountVo">
        select
        group_concat(distinct p_parent.product_name order by p_parent.product_name separator ',') as productCategory,
        group_concat(distinct pm.model order by pm.model separator ',') as specificationModel,
        group_concat(distinct pm.unit order by pm.unit separator ',') as unit,
        pa.scheduling_user_id as schedulingUserId,
        pa.scheduling_user_name as schedulingUserName,
        cast(sum(
            ifnull(pa.work_hours, 0) * ifnull(pa.finished_num, 0) *
            case
                when substring_index(pm.model, '*', -1) regexp '^[0-9]+(\\.[0-9]+)?$'
                then cast(substring_index(pm.model, '*', -1) as decimal(18,4))
                else 1
            end
        ) as decimal(18,4)) as wages,
        cast(sum(ifnull(pa.finished_num, 0)) as decimal(18,4)) as finishedNum,
        cast(sum(ifnull(pa.work_hours, 0)) as decimal(18,4)) as workHours,
        case
            when sum(ifnull(ppo.quantity, 0) + ifnull(ppo.scrapQty, 0)) = 0 then '0%'
            else concat(
                cast(
                    round(
                        sum(ifnull(ppo.quantity, 0)) /
                        sum(ifnull(ppo.quantity, 0) + ifnull(ppo.scrapQty, 0)) * 100, 2
                    ) as char
                ),
                '%'
            )
        end as outputRate,
        group_concat(distinct pa.technology_operation_name order by pa.technology_operation_name separator ',') as process,
        case
            when count(distinct date(pa.scheduling_date)) = 1 then min(date(pa.scheduling_date))
            else null
        end as schedulingDate,
        case
            when count(distinct date_format(pa.scheduling_date, '%Y-%m')) = 1 then min(date_format(pa.scheduling_date, '%Y-%m'))
            else null
        end as schedulingMonth
        from production_account pa
        left join production_product_main ppm on ppm.id = pa.production_product_main_id
        left join production_operation_task pot on ppm.production_operation_task_id = pot.id
        left join production_order po on pot.production_order_id = po.id
        left join production_order_routing_operation poro on pot.production_order_routing_operation_id = poro.id
        left join product_model pm on pm.id = ifnull(poro.product_model_id, po.product_model_id)
        left join product p on pm.product_id = p.id
        left join product p_parent on p_parent.id = p.parent_id
        left join (
            select production_product_main_id,
                   cast(sum(ifnull(quantity, 0)) as decimal(18,4)) as quantity,
                   cast(sum(ifnull(scrap_qty, 0)) as decimal(18,4)) as scrapQty
            from production_product_output
            group by production_product_main_id
        ) ppo on ppo.production_product_main_id = ppm.id
        <where>
            <if test="c != null">
                <if test="c.productCategory != null and c.productCategory != ''">
                    and p_parent.product_name like concat('%', #{c.productCategory}, '%')
                </if>
                <if test="c.specificationModel != null and c.specificationModel != ''">
                    and pm.model like concat('%', #{c.specificationModel}, '%')
                </if>
                <if test="c.schedulingUserId != null">
                    and pa.scheduling_user_id = #{c.schedulingUserId}
                </if>
                <if test="c.schedulingUserName != null and c.schedulingUserName != ''">
                    and pa.scheduling_user_name like concat('%', #{c.schedulingUserName}, '%')
                </if>
                <if test="c.process != null and c.process != ''">
                    and pa.technology_operation_name like concat('%', #{c.process}, '%')
                </if>
                <if test="c.entryDate != null">
                    and date(pa.scheduling_date) = #{c.entryDate}
                </if>
                <if test="c.entryDateStart != null">
                    and date(pa.scheduling_date) &gt;= #{c.entryDateStart}
                </if>
                <if test="c.entryDateEnd != null">
                    and date(pa.scheduling_date) &lt;= #{c.entryDateEnd}
                </if>
            </if>
        </where>
        group by pa.scheduling_user_id,
        pa.scheduling_user_name
        order by wages desc,
        pa.scheduling_user_id asc
    </select>
    <select id="selectUserAccount" resultType="com.ruoyi.production.bean.dto.UserAccountDto">
        select ifnull(sum(finished_num), 0) as accountBalance,
               ifnull(sum(work_hours), 0) as account
src/main/resources/mapper/production/ProductionOperationTaskMapper.xml
@@ -24,6 +24,7 @@
    <select id="pageProductionOperationTask" resultType="com.ruoyi.production.bean.vo.ProductionOperationTaskVo">
        select pot.*,
               po.nps_no as npsNo,
               po.is_end_order as endOrder,
               p.product_name as productName,
               pm.model as model,
               pm.unit as unit,
@@ -120,4 +121,28 @@
        group by poro.technology_operation_id, poro.operation_name
    </select>
    <select id="getProductWorkOrderFlowCard" resultType="com.ruoyi.production.bean.dto.ProductionOperationTaskDto">
        SELECT pot.*,
               poro.operation_name AS processName,
               pm.model AS model,
               pm.unit AS unit,
               p.product_name AS productName,
               po.nps_no AS productOrderNpsNo,
               ROUND(IFNULL(pot.complete_quantity, 0) / NULLIF(pot.plan_quantity, 0) * 100, 2) AS completionStatus,
               IFNULL(scrapStat.scrapQty, 0) AS scrapQty
        FROM production_operation_task pot
                 LEFT JOIN production_order po ON pot.production_order_id = po.id
                 LEFT JOIN production_order_routing_operation poro ON pot.production_order_routing_operation_id = poro.id
                 LEFT JOIN product_model pm ON pm.id = ifnull(poro.product_model_id, po.product_model_id)
                 LEFT JOIN product p ON p.id = pm.product_id
                 LEFT JOIN (
            SELECT ppm.production_operation_task_id AS taskId,
                   SUM(IFNULL(ppo.scrap_qty, 0)) AS scrapQty
            FROM production_product_main ppm
                     LEFT JOIN production_product_output ppo ON ppo.production_product_main_id = ppm.id
            GROUP BY ppm.production_operation_task_id
        ) scrapStat ON scrapStat.taskId = pot.id
        WHERE pot.id = #{id}
    </select>
</mapper>
src/main/resources/mapper/production/ProductionOrderMapper.xml
@@ -49,7 +49,9 @@
        po_sales.customerName,
        p.product_name as productName,
        pm.model as model,
        po.is_end_order as endOrder,
        tr.process_route_code as processRouteCode,
        ROUND(po.complete_quantity / po.quantity * 100, 2) AS completionStatus,
        tb.bom_no as bomNo
    </sql>
src/main/resources/mapper/production/ProductionProductMainMapper.xml
@@ -95,17 +95,59 @@
               p.product_name as productName,
               pm.model as productModelName,
               pm.unit,
               poro.operation_name as process,
               pa.technology_operation_name as process,
               ifnull(ppo.quantity, 0) as quantity,
               ifnull(ppo.scrap_qty, 0) as scrapQty
        from production_product_main ppm
               ifnull(ppo.scrap_qty, 0) as scrapQty,
               date(pa.scheduling_date) as schedulingDate,
               pa.scheduling_user_name as schedulingUserName,
               cast(ifnull(pa.work_hours, 0) as decimal(18,4)) as workHours,
               cast(
                   ifnull(pa.work_hours, 0) * ifnull(pa.finished_num, 0) *
                   case
                       when substring_index(pm.model, '*', -1) regexp '^[0-9]+(\\.[0-9]+)?$'
                       then cast(substring_index(pm.model, '*', -1) as decimal(18,4))
                       else 1
                   end
                   as decimal(18,4)
               ) as wages
        from production_account pa
                 left join production_product_main ppm on ppm.id = pa.production_product_main_id
                 left join production_operation_task pot on ppm.production_operation_task_id = pot.id
                 left join production_order po on pot.production_order_id = po.id
                 left join production_order_routing_operation poro on pot.production_order_routing_operation_id = poro.id
                 left join product_model pm on pm.id = ifnull(poro.product_model_id, po.product_model_id)
                 left join product p on pm.product_id = p.id
                 left join product p_parent on p_parent.id = p.parent_id
                 left join production_product_output ppo on ppo.production_product_main_id = ppm.id
        order by ppm.create_time desc
        <where>
            <if test="c != null">
                <if test="c.productCategory != null and c.productCategory != ''">
                    and p_parent.product_name like concat('%', #{c.productCategory}, '%')
                </if>
                <if test="c.specificationModel != null and c.specificationModel != ''">
                    and pm.model like concat('%', #{c.specificationModel}, '%')
                </if>
                <if test="c.schedulingUserId != null">
                    and pa.scheduling_user_id = #{c.schedulingUserId}
                </if>
                <if test="c.schedulingUserName != null and c.schedulingUserName != ''">
                    and pa.scheduling_user_name like concat('%', #{c.schedulingUserName}, '%')
                </if>
                <if test="c.process != null and c.process != ''">
                    and pa.technology_operation_name like concat('%', #{c.process}, '%')
                </if>
                <if test="c.entryDate != null">
                    and date(pa.scheduling_date) = #{c.entryDate}
                </if>
                <if test="c.entryDateStart != null">
                    and date(pa.scheduling_date) &gt;= #{c.entryDateStart}
                </if>
                <if test="c.entryDateEnd != null">
                    and date(pa.scheduling_date) &lt;= #{c.entryDateEnd}
                </if>
            </if>
        </where>
        order by pa.scheduling_date desc, pa.id desc
    </select>
    <select id="listMain" resultType="java.lang.Long">
src/main/resources/mapper/system/SysUserMapper.xml
@@ -153,13 +153,18 @@
    <select id="checkEmailUnique" parameterType="String" resultMap="SysUserResult">
        select user_id, email from sys_user where email = #{email} and del_flag = '0' limit 1
    </select>
    <select id="selectUserByIds" resultType="com.ruoyi.project.system.domain.SysUser">
        <include refid="selectUserVo"/>
        where u.user_id in <foreach collection="userIds" item="item" open="(" separator="," close=")">
             #{item}
        </foreach>
        and u.del_flag = '0'
    </select>
    <select id="selectUserByIds" resultType="com.ruoyi.project.system.domain.SysUser">
        <include refid="selectUserVo"/>
        <where>
            <if test="userIds != null and userIds.size > 0">
                and u.user_id in
                <foreach collection="userIds" item="item" open="(" separator="," close=")">
                    #{item}
                </foreach>
            </if>
            and u.del_flag = '0'
        </where>
    </select>
    <select id="selectRegistrantIds" resultType="com.ruoyi.project.system.domain.SysUser">
        SELECT user_id, nick_name FROM sys_user
        <where>