2 天以前 f350465db70d6f9db9494483ee0cec7793a9007b
feat(approve): 实现审批流程或签会签功能并优化数据库配置

- 添加审批模式字段支持或签和会签
- 实现同节点多审批人的分组处理逻辑
- 重构审批节点初始化和状态更新流程
- 优化审批流程查询和排序逻辑
- 添加用印审批类型支持和导出功能
- 集成审批节点附件管理功能
- 优化数据库连接配置参数
- 更新项目依赖配置文件
已修改32个文件
已添加15个文件
3309 ■■■■■ 文件已修改
.gitignore 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/inspectiontask_module_logic.md 246 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/不合格管理模块逻辑分析.md 486 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/approve_or_and_mode.md 223 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/auto_quality_inspect_on_dispatch.md 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/production_report_person_detail.md 181 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/production_work_order_add_qty.md 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/sales_ledger_auto_invoice_receipt.md 109 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/seal_approval_unified_review.md 177 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/stock_inventory_model_query.md 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/work_order_batch_report_fix.md 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/production_report_person_create.sql 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/quality_inspect_add_work_order_id.sql 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/seal_application_add_approve_process.sql 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/controller/ApproveProcessController.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/ApproveNode.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/ApproveProcess.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/pojo/ApproveProcessConfigNode.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/IApproveNodeService.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/IApproveProcessService.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApproveNodeServiceImpl.java 482 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApproveProcessConfigNodeServiceImpl.java 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApproveProcessServiceImpl.java 348 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/controller/SealApplicationManagementController.java 96 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/collaborativeApproval/pojo/SealApplicationManagement.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/customervisits/service/impl/CustomerVisitsServiceImpl.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/dto/ProductWorkOrderDto.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/dto/ProductionProductMainDto.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/mapper/ProductionReportPersonMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/pojo/ProductionReportPerson.java 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/ProductionProductMainService.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProcessRouteServiceImpl.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductOrderServiceImpl.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductProcessRouteItemServiceImpl.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductWorkOrderServiceImpl.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionProductMainServiceImpl.java 271 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/project/common/CommonController.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/pojo/QualityInspect.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerProductServiceImpl.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-dev.yml 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductProcessMapper.xml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductWorkOrderMapper.xml 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionProductMainMapper.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionProductOutputMapper.xml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/SalesLedgerProductionAccountingMapper.xml 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/quality/QualityInspectMapper.xml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.gitignore
@@ -13,6 +13,7 @@
### STS ###
.apt_generated
.claude
.classpath
.factorypath
.project
doc/inspectiontask_module_logic.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,246 @@
# inspectiontask æ¨¡å—逻辑说明
## 1. æ¨¡å—定位
`inspectiontask` æ˜¯ä¸€ä¸ªå›´ç»•“定时生成巡检任务、人工录入巡检结果、扫码记录留痕”的业务模块。它把巡检计划、巡检单、二维码、扫码记录四类能力串成一条闭环。
核心目标有三个:
1. ç»´æŠ¤å·¡æ£€è®¡åˆ’,并通过 Quartz å®šæ—¶è§¦å‘。
2. ç”Ÿæˆå·¡æ£€å•,支持人工补录、编辑、导出、删除。
3. è®°å½•二维码与扫码行为,作为巡检现场的辅助台账。
## 2. æ¨¡å—拆分
### 2.1 å®šæ—¶å·¡æ£€ä»»åŠ¡
对应类:
- `TimingTaskController`
- `TimingTaskService`
- `TimingTaskServiceImpl`
- `TimingTaskScheduler`
- `TimingTaskJob`
- `QuartzConfig`
职责:
- ç»´æŠ¤å·¡æ£€è®¡åˆ’。
- è®¡ç®—首次执行时间和下次执行时间。
- æŠŠå·¡æ£€è®¡åˆ’注册到 Quartz。
- åˆ°ç‚¹åŽè‡ªåŠ¨ç”Ÿæˆå·¡æ£€ä»»åŠ¡ã€‚
### 2.2 å·¡æ£€ä»»åŠ¡è®°å½•
对应类:
- `InspectionTaskController`
- `InspectionTaskService`
- `InspectionTaskServiceImpl`
职责:
- æŸ¥è¯¢å·¡æ£€ä»»åŠ¡åˆ—è¡¨ã€‚
- æŸ¥çœ‹æŸä¸ªå®šæ—¶ä»»åŠ¡ä¸‹çš„å·¡æ£€è®°å½•ã€‚
- æ‰¹é‡æ–°å¢žæˆ–编辑巡检任务。
- åˆ é™¤å·¡æ£€ä»»åŠ¡åŠå…¶å…³è”é™„ä»¶ã€‚
### 2.3 äºŒç»´ç ç®¡ç†
对应类:
- `QrCodeController`
- `QrCodeService`
- `QrCodeServiceImpl`
职责:
- ç»´æŠ¤äºŒç»´ç åŸºç¡€ä¿¡æ¯ã€‚
- æä¾›åˆ—表、增改、删除能力。
### 2.4 æ‰«ç è®°å½•管理
对应类:
- `QrCodeScanRecordController`
- `QrCodeScanRecordService`
- `QrCodeScanRecordServiceImpl`
职责:
- è®°å½•二维码扫码行为。
- å…³è”二维码信息与附件信息。
- æ”¯æŒåˆ—表、增改、删除。
## 3. ä¸»ä¸šåŠ¡æµç¨‹
### 3.1 å®šæ—¶ä»»åŠ¡åˆ›å»ºä¸Žå¯åœ
前端通过 `POST /timingTask/addOrEditTimingTask` åˆ›å»ºæˆ–修改巡检计划。
后端处理顺序是:
1. å¤åˆ¶è¯·æ±‚数据到 `TimingTask`。
2. æ ¹æ®é€‰æ‹©çš„设备列表补齐任务名称、区域、设备串等信息。
3. è®¡ç®—首次执行时间。
4. ä¿å­˜åˆ°æ•°æ®åº“。
5. åŒæ­¥ Quartz è°ƒåº¦çŠ¶æ€ã€‚
如果后续调用 `POST /timingTask/changeEnable`:
1. å…ˆæ›´æ–°æ•°æ®åº“里的启用状态。
2. å†é‡æ–°æ³¨å†Œæˆ–刷新 Quartz è§¦å‘器。
3. å¯ç”¨æ—¶æ¢å¤æ‰§è¡Œï¼Œç¦ç”¨æ—¶æš‚停执行。
删除时会先删除数据库记录,再从 Quartz ä¸­å–消对应任务。
### 3.2 å®šæ—¶ç”Ÿæˆå·¡æ£€å•
Quartz åˆ°ç‚¹åŽè§¦å‘ `TimingTaskJob`。
执行逻辑是:
1. é€šè¿‡ä»»åŠ¡ ID è¯»å–当前巡检计划。
2. å¦‚果任务已禁用,直接退出。
3. è§£æžè¯¥ä»»åŠ¡ç»‘å®šçš„è®¾å¤‡åˆ—è¡¨ã€‚
4. æŒ‰è®¾å¤‡é€ä¸ªç”Ÿæˆå·¡æ£€å•。
5. æŒ‰è®¾å¤‡æ•°é‡å†³å®šç”Ÿæˆæ¬¡æ•°ã€‚
6. å†™å…¥ `inspection_task`。
7. æ›´æ–°è¯¥å®šæ—¶ä»»åŠ¡çš„æœ€åŽæ‰§è¡Œæ—¶é—´å’Œä¸‹æ¬¡æ‰§è¡Œæ—¶é—´ã€‚
这一步是模块的核心自动化入口。
### 3.3 å·¡æ£€å•人工录入与异常联动
前端通过 `POST /inspectionTask/addOrEditInspectionTask` æ‰¹é‡æäº¤å·¡æ£€å•。
后端逻辑是:
1. é€æ¡ä¿å­˜æˆ–更新巡检单。
2. è‹¥å·¡æ£€ç»“果为异常,则按区域收集异常任务。
3. åŒä¸€æ‰¹æäº¤é‡Œï¼ŒåŒä¸€åŒºåŸŸåªä¿ç•™ä¸€æ¡å¼‚常记录用于后续生成维修单。
4. åœ¨å½“天范围内,如果该区域已经存在待维修单,则跳过,避免重复生成。
5. å¦‚果不存在,则自动创建一条维修单。
因此,这个模块不仅保存巡检结果,还承担“巡检异常 -> è‡ªåŠ¨ç”Ÿæˆç»´ä¿®ä»»åŠ¡â€çš„è”åŠ¨èŒè´£ã€‚
### 3.4 å·¡æ£€å•列表展示
巡检单列表接口会做一次“按定时任务聚合”的整理,而不是简单返回数据库原始行。
聚合逻辑大致是:
1. æŒ‰å®šæ—¶ä»»åŠ¡ ID å½’组。
2. åŒä¸€ç»„里只返回一条代表记录。
3. åˆå¹¶ä»»åŠ¡åç§°ã€å·¡æ£€äººã€å¤‡æ³¨ã€åœ°ç‚¹ã€é¢‘çŽ‡ç­‰å±•ç¤ºä¿¡æ¯ã€‚
4. åˆå¹¶è¯¥ç»„下的附件列表。
5. æŒ‰å®šæ—¶ä»»åŠ¡å’Œåˆ›å»ºæ—¶é—´æŽ’åºåŽåˆ†é¡µè¿”å›žã€‚
所以列表页看到的是“业务视角的巡检任务卡片”,不是逐条底层记录。
### 3.5 å·¡æ£€è®°å½•按定时任务查询
`GET /timingTask/recordList/{timingId}` ä¼šæŸ¥è¯¢æŸä¸ªå®šæ—¶ä»»åŠ¡åœ¨å½“å¤©ç”Ÿæˆçš„å·¡æ£€è®°å½•ã€‚
这里的范围限定为当天,是为了让页面查看“今日执行情况”更直接。
### 3.6 äºŒç»´ç ä¸Žæ‰«ç è®°å½•
二维码模块只负责二维码本身的增删改查。
扫码记录模块负责把扫码行为、扫码人、二维码信息、附件信息串起来:
1. å…ˆåˆ†é¡µæŸ¥æ‰«ç è®°å½•。
2. å†æ‰¹é‡æŸ¥å…³è”二维码。
3. å†æ‰¹é‡æŸ¥å…³è”附件。
4. å†æŸ¥é™„件对应的文件元数据。
5. æœ€åŽç»„装成前端可直接展示的 DTO。
它的角色更像“巡检现场的辅助留痕模块”。
## 4. è°ƒåº¦ä¸Žæ—¶é—´è®¡ç®—
### 4.1 Quartz æŽ¥å…¥æ–¹å¼
`QuartzConfig` æä¾›äº† `SchedulerFactoryBean` å’Œ `Scheduler`,并通过自定义 JobFactory æ”¯æŒ Job å†…部自动注入 Spring ä¾èµ–。
`TimingTaskScheduler` è´Ÿè´£ï¼š
- åˆ›å»º JobDetail。
- åˆ›å»º Trigger。
- é‡æ–°è°ƒåº¦ã€‚
- æš‚停、恢复、删除任务。
### 4.2 æ”¯æŒçš„频率类型
代码里支持的频率类型包括:
- `DAILY`
- `WEEKLY`
- `MONTHLY`
- `QUARTERLY`
- `YEARLY`
时间表达式由服务层和调度层共同解析,最终转换为 Quartz cron。
### 4.3 éœ€è¦æ³¨æ„çš„实现细节
1. æœåŠ¡å±‚å’Œ Job å±‚都各自实现了一套“下次执行时间”计算逻辑。
2. Job å±‚执行完后,会直接更新 `timing_task` çš„æœ€åŽæ‰§è¡Œæ—¶é—´å’Œä¸‹æ¬¡æ‰§è¡Œæ—¶é—´ã€‚
3. æœåŠ¡å±‚åœ¨æ–°å¢žä»»åŠ¡æ—¶ä¼šå…ˆè®¡ç®—é¦–æ¬¡æ‰§è¡Œæ—¶é—´ï¼Œä½†å­£åº¦ç±»åž‹åœ¨å½“å‰å®žçŽ°é‡Œé¦–æ¬¡æ—¶é—´è®¡ç®—æ–¹æ³•ä¸ºç©ºï¼Œå±žäºŽä¸€ä¸ªéœ€è¦æ³¨æ„çš„å®žçŽ°ç¼ºå£ã€‚
## 5. æ•°æ®ä¸Žå…³è”关系
这个模块的主要关联关系可以理解为:
- `TimingTask` æ˜¯å·¡æ£€è®¡åˆ’。
- `InspectionTask` æ˜¯å·¡æ£€æ‰§è¡Œè®°å½•。
- `QrCode` æ˜¯çŽ°åœºäºŒç»´ç å°è´¦ã€‚
- `QrCodeScanRecord` æ˜¯æ‰«ç è¡Œä¸ºè®°å½•。
- å·¡æ£€å¼‚常会联动生成维修单。
- å·¡æ£€å•和扫码记录都会关联附件资源。
## 6. å¯¹å‰ç«¯çš„æ„ä¹‰
前端对这个模块的使用方式可以概括为:
1. å…ˆç»´æŠ¤å·¡æ£€è®¡åˆ’,再由系统自动生成巡检单。
2. å·¡æ£€å•支持人工补录、编辑、导出、删除。
3. å®šæ—¶ä»»åŠ¡æ”¯æŒå¯ç”¨ã€ç¦ç”¨å’Œé‡æ–°è°ƒåº¦ã€‚
4. å·¡æ£€å¼‚常后,系统会自动补一条维修单,前端需要能及时刷新相关页面。
5. äºŒç»´ç å’Œæ‰«ç è®°å½•属于独立的辅助管理页面。
## 7. æŽ¥å£æ€»è§ˆ
### å·¡æ£€ä»»åŠ¡
- `GET /inspectionTask/list`
- `POST /inspectionTask/export`
- `POST /inspectionTask/addOrEditInspectionTask`
- `DELETE /inspectionTask/delInspectionTask`
### å®šæ—¶ä»»åŠ¡
- `GET /timingTask/list`
- `GET /timingTask/recordList/{timingId}`
- `POST /timingTask/export`
- `POST /timingTask/addOrEditTimingTask`
- `POST /timingTask/changeEnable`
- `DELETE /timingTask/delTimingTask`
### äºŒç»´ç 
- `GET /qrCode/list`
- `POST /qrCode/addOrEditQrCode`
- `DELETE /qrCode/delQrCode`
### æ‰«ç è®°å½•
- `GET /qrCodeScanRecord/list`
- `POST /qrCodeScanRecord/addOrEditQrCodeRecord`
- `DELETE /qrCodeScanRecord/delSalesRecord`
## 8. ç»“论
`inspectiontask` æ¨¡å—本质上是一条“计划 -> è°ƒåº¦ -> ç”Ÿæˆå·¡æ£€å• -> å½•入结果 -> å¼‚常联动维修 -> ç•™ç—•查询”的业务链路。
如果后续要继续拆前后端联调,优先关注三块:定时任务的时间表达式、巡检结果的异常联动、以及巡检列表的聚合展示逻辑。
doc/²»ºÏ¸ñ¹ÜÀíÄ£¿éÂß¼­·ÖÎö.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,486 @@
# ä¸åˆæ ¼ç®¡ç†æ¨¡å—逻辑分析
## 1. æ¨¡å—定位
`quality` åŒ…下的"不合格管理"是一个独立的业务闭环:**检验(质检) â†’ ä¸åˆæ ¼å“ç™»è®° â†’ ä¸åˆæ ¼å“å¤„理 â†’ ä¸åˆæ ¼å“å¤„理单(审批流)**。此外还包含检测标准维护、检验报告生成、质量报表统计三个配套能力。
核心目标:
1. è¦†ç›–原材料检验、过程检验、出厂检验三种质检场景。
2. æ£€éªŒä¸åˆæ ¼æ—¶è‡ªåŠ¨ç™»è®°ä¸åˆæ ¼å“ï¼Œæ”¯æŒè¿”å·¥/返修/报废/让步放行四种处理方式。
3. ä¸åˆæ ¼å“å¤„理单走审批流程(草稿→待审批→审批中→已完成/已驳回)。
4. æä¾›åˆæ ¼çŽ‡ã€æœˆåº¦è¶‹åŠ¿ã€æ£€æµ‹æŒ‡æ ‡æŽ’åç­‰ç»Ÿè®¡æŠ¥è¡¨ã€‚
---
## 2. æ•°æ®åº“表结构
### 2.1 quality_inspect(检验记录主表)
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| id | BIGINT | ä¸»é”®ï¼Œè‡ªå¢ž |
| inspect_type | INT | **必填**。0=原材料检验,1=过程检验,2=出厂检验 |
| inspect_state | INT | 0=未提交(草稿),1=已提交 |
| check_time | DATE | æ£€æµ‹æ—¥æœŸ |
| supplier | VARCHAR | ä¾›åº”商名称(原材料检验使用) |
| customer | VARCHAR | å®¢æˆ·åç§°ï¼ˆè¿‡ç¨‹/出厂检验使用) |
| process | VARCHAR | å·¥åº/部件类型(过程检验使用) |
| process_type | INT | å·¥åºç±»åž‹ï¼Œéœ€åŒ¹é…æ•°æ®å­—å…¸ `product_process_type` |
| check_name | VARCHAR | æ£€éªŒå‘˜ |
| product_id | BIGINT | **必填**。关联产品ID |
| product_name | VARCHAR | äº§å“åç§° |
| model | VARCHAR | è§„格型号 |
| unit | VARCHAR | å•位 |
| quantity | DECIMAL | æ•°é‡ |
| check_company | VARCHAR | æ£€æµ‹å•位 |
| check_result | VARCHAR | æ£€æµ‹ç»“果("合格"/"不合格") |
| defective_phenomena | VARCHAR | ä¸åˆæ ¼çŽ°è±¡æè¿° |
| product_main_id | BIGINT | å…³è”报工ID(过程/出厂检验使用) |
| product_model_id | BIGINT | å…³è”产品型号ID |
| purchase_ledger_id | BIGINT | å…³è”采购台账ID(原材料检验使用) |
| test_standard_id | BIGINT | å…³è”检测标准主表ID |
| create_time | DATETIME | åˆ›å»ºæ—¶é—´ï¼ˆè‡ªåŠ¨å¡«å……ï¼‰ |
| create_user | INT | åˆ›å»ºç”¨æˆ·ï¼ˆè‡ªåŠ¨å¡«å……ï¼‰ |
| update_time | DATETIME | ä¿®æ”¹æ—¶é—´ï¼ˆè‡ªåŠ¨å¡«å……ï¼‰ |
| update_user | INT | ä¿®æ”¹ç”¨æˆ·ï¼ˆè‡ªåŠ¨å¡«å……ï¼‰ |
| tenant_id | BIGINT | ç§Ÿæˆ·ID(自动填充) |
| dept_id | BIGINT | éƒ¨é—¨ID(自动填充) |
### 2.2 quality_inspect_param(检验参数明细)
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| id | BIGINT | ä¸»é”®ï¼Œè‡ªå¢ž |
| parameter_item | VARCHAR | æ£€æµ‹æŒ‡æ ‡åç§° |
| unit | VARCHAR | å•位 |
| standard_value | VARCHAR | æ ‡å‡†å€¼ |
| control_value | VARCHAR | å†…控值 |
| test_value | VARCHAR | æ£€éªŒå€¼ |
| inspect_id | BIGINT | **必填**。关联 quality_inspect.id |
| create_time / update_time / create_user / update_user / tenant_id / dept_id | â€” | é€šç”¨å®¡è®¡å­—段 |
> ä¸€æ¡æ£€éªŒè®°å½• (quality_inspect) å¯¹å¤šæ¡æ£€éªŒå‚æ•° (quality_inspect_param)。
### 2.3 quality_inspect_file(检验附件)
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| id | BIGINT | ä¸»é”®ï¼Œè‡ªå¢ž |
| name | VARCHAR | æ–‡ä»¶åç§° |
| url | VARCHAR | æ–‡ä»¶è·¯å¾„ |
| file_size | INT | æ–‡ä»¶å¤§å° |
| inspect_id | BIGINT | **必填**。关联 quality_inspect.id |
| create_time / update_time / create_user / update_user / tenant_id / dept_id | â€” | é€šç”¨å®¡è®¡å­—段 |
### 2.4 quality_unqualified(不合格品记录)
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| id | BIGINT | ä¸»é”®ï¼Œè‡ªå¢ž |
| inspect_type | INT | **必填**。0=原材料检验,1=过程检验,2=出厂检验 |
| inspect_state | INT | **必填**。0=待处理,1=已处理 |
| check_time | DATE | æ£€æµ‹æ—¥æœŸ |
| check_name | VARCHAR | æ£€éªŒå‘˜ |
| product_id | BIGINT | å…³è”产品ID |
| product_name | VARCHAR | äº§å“åç§° |
| model | VARCHAR | è§„格型号(存储的可能是 product_model.id æˆ–直接文本) |
| unit | VARCHAR | å•位 |
| quantity | DECIMAL | æ•°é‡ |
| defective_phenomena | VARCHAR | ä¸åˆæ ¼çŽ°è±¡ |
| deal_result | VARCHAR | å¤„理结果:**"返修"/"返工"/"报废"/"让步放行"** |
| deal_name | VARCHAR | å¤„理人 |
| deal_time | DATE | å¤„理日期 |
| reason_analysis | VARCHAR | åŽŸå› åˆ†æž |
| preventive_corrective | VARCHAR | é¢„防与纠正措施 |
| loss_working | VARCHAR | å·¥æ—¶æŸå¤± |
| loss_material | VARCHAR | ææ–™è´¹æŸå¤± |
| inspect_id | BIGINT | å…³è” quality_inspect.id(如果是检验不合格自动生成) |
| create_time / update_time / create_user / update_user / tenant_id / dept_id | â€” | é€šç”¨å®¡è®¡å­—段 |
| temp_file_ids (虚拟) | List\<String\> | ä¸´æ—¶æ–‡ä»¶ID列表(仅用于接收前端参数) |
| common_file_list (虚拟) | List\<CommonFile\> | æ­£å¼é™„件列表(查询时回填) |
| method (虚拟) | Boolean | æ ‡è¯† model å­—段是否存储的是 product_model.id(用于列表展示时解析型号) |
### 2.5 quality_unqualified_order(军歌化工不合格品处理单)
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| id | BIGINT | ä¸»é”®ï¼Œè‡ªå¢ž |
| order_no | VARCHAR | å¤„理单编号,前缀 "BHG"+ æ—¥æœŸ + åºå·ï¼Œè‡ªåŠ¨ç”Ÿæˆ |
| unqualified_id | BIGINT | å…³è” quality_unqualified.id |
| project_name | VARCHAR | é¡¹ç›®åç§° |
| project_no | VARCHAR | é¡¹ç›®ç¼–号 |
| equipment_id | BIGINT | å…³è”设备ID |
| equipment_name | VARCHAR | è®¾å¤‡åç§° |
| equipment_drawing_no | VARCHAR | è®¾å¤‡å›¾å· |
| material_name | VARCHAR | ç‰©æ–™/部件名称 |
| product_model_id | BIGINT | å…³è”产品型号ID |
| material_drawing_no | VARCHAR | ç‰©æ–™å›¾å· |
| specification_model | VARCHAR | åž‹å·è§„æ ¼ |
| material_quality | VARCHAR | æè´¨ |
| quantity | DECIMAL | æ€»æ•°é‡ |
| unqualified_quantity | DECIMAL | ä¸åˆæ ¼æ•°é‡ |
| unqualified_process | TINYINT | ä¸åˆæ ¼å·¥åºï¼š1=来料,2=制程,3=成品 |
| supplier_name | VARCHAR | ä¾›åº”商名称 |
| inspector_name | VARCHAR | æ£€éªŒå‘˜ |
| inspect_date | DATE | æ£€éªŒæ—¥æœŸ |
| responsible_person | VARCHAR | è´£ä»»äºº |
| responsible_dept | VARCHAR | è´£ä»»éƒ¨é—¨ |
| problem_description | VARCHAR | é—®é¢˜æè¿° |
| reason_analysis | VARCHAR | åŽŸå› åˆ†æžåŠå»ºè®® |
| correction_action | VARCHAR | çº æ­£æŽªæ–½ |
| disposal_method | TINYINT | å¤„置方式:1=让步接收,2=厂内维修,3=返厂维修,4=换货,5=退货,6=报废 |
| repair_evaluation | VARCHAR | åނ内/返厂维修评估 |
| preventive_action | VARCHAR | é¢„防措施 |
| status | TINYINT | çŠ¶æ€ï¼š0=草稿,1=待审批,2=审批中,3=已完成,4=已驳回 |
| remark | VARCHAR | å¤‡æ³¨ |
| create_by / update_by / create_time / update_time / tenant_id / dept_id | â€” | é€šç”¨å®¡è®¡å­—段 |
| deleted | TINYINT | é€»è¾‘删除:0=否,1=是 |
### 2.6 quality_test_standard(检测标准主表)
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| id | BIGINT | ä¸»é”®ï¼Œè‡ªå¢ž |
| standard_no | VARCHAR | æ ‡å‡†ç¼–号 |
| standard_name | VARCHAR | æ ‡å‡†åç§° |
| state | VARCHAR | çŠ¶æ€ |
| remark | VARCHAR | å¤‡æ³¨ |
| inspect_type | INT | ç±»åˆ«ï¼š0=原材料检验,1=过程检验,2=出厂检验 |
| process_id | INT | å·¥åºID |
| process_type | INT | å·¥åºç±»åž‹ |
| create_time / update_time / create_user / update_user / tenant_id / dept_id | â€” | é€šç”¨å®¡è®¡å­—段 |
### 2.7 quality_test_standard_param(检测标准参数)
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| id | BIGINT | ä¸»é”®ï¼Œè‡ªå¢ž |
| parameter_item | VARCHAR | å‚数项(指标名称) |
| unit | VARCHAR | å•位 |
| standard_value | VARCHAR | æ ‡å‡†å€¼ |
| control_value | VARCHAR | å†…控值 |
| default_value | VARCHAR | é»˜è®¤å€¼ |
| test_standard_id | BIGINT | å…³è” quality_test_standard.id |
| create_time / update_time / create_user / update_user / tenant_id / dept_id | â€” | é€šç”¨å®¡è®¡å­—段 |
### 2.8 quality_test_standard_binding(检测标准-产品关联)
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| id | INT | ä¸»é”®ï¼Œè‡ªå¢ž |
| product_id | BIGINT | äº§å“ID |
| test_standard_id | INT | å…³è” quality_test_standard.id |
| create_time / update_time / create_user / update_user / tenant_id / dept_id | â€” | é€šç”¨å®¡è®¡å­—段 |
> å¤šå¯¹å¤šå…³ç³»ï¼šä¸€ä¸ªäº§å“å¯ç»‘定多个检测标准,一个检测标准可用于多个产品。
---
## 3. æ ¸å¿ƒä¸šåŠ¡æµç¨‹
### 3.1 è´¨æ£€å®Œæ•´é“¾è·¯
```
检测标准维护 â†’ åˆ›å»ºæ£€éªŒå• â†’ å¡«å†™æ£€éªŒå‚æ•° â†’ æäº¤æ£€éªŒ
                                   â†“
                    â”Œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”´â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”
                    â†“                              â†“
              ç»“æžœ=不合格                        ç»“æžœ=合格
                    â†“                              â†“
         è‡ªåŠ¨ç”Ÿæˆä¸åˆæ ¼å“è®°å½•                  è‡ªåŠ¨åˆæ ¼å…¥åº“
         (quality_unqualified)              (调用 StockUtils)
                    â†“
              ä¸åˆæ ¼å“å¤„理(deal)
                    â†“
    â”Œâ”€â”€â”€â”€â”€â”€â”€â”¬â”€â”€â”€â”€â”€â”€â”€â”€â”¼â”€â”€â”€â”€â”€â”€â”€â”€â”
    â†“       â†“        â†“        â†“
  è¿”ä¿®    è¿”å·¥      æŠ¥åºŸ    è®©æ­¥æ”¾è¡Œ
(过程/出厂)               (全部类型)
    â†“       â†“        â†“        â†“
 å…‹éš†ç”Ÿäº§  å…‹éš†ç”Ÿäº§  å…¥ä¸åˆæ ¼åº“  å…¥åˆæ ¼åº“
 è®¢å•+路线   åŒä¸Š   (unqualified (qualified
 +工单            stock)      stock)
    â†“
  ä¸åˆæ ¼å“å¤„理单(approval flow)
  è‰ç¨¿ â†’ å¾…审批 â†’ å®¡æ‰¹ä¸­ â†’ å·²å®Œæˆ/已驳回
```
### 3.2 æ£€éªŒæäº¤é€»è¾‘(QualityInspectServiceImpl.submit)
1. æ ¡éªŒ `checkResult` ä¸èƒ½ä¸ºç©ºã€‚
2. å¦‚æžœ `checkResult = "不合格"`:
   - è‡ªåŠ¨åˆ›å»ºä¸€æ¡ `quality_unqualified` è®°å½•,`inspect_state=0`(待处理)。
   - ä¸åˆæ ¼çŽ°è±¡è‡ªåŠ¨ç”Ÿæˆä¸ºï¼šæ‰€æœ‰æ£€éªŒå‚æ•°åç§°æ‹¼æŽ¥ + "这些指标中存在不合格"。
3. å¦‚æžœ `checkResult = "合格"`:
   - è°ƒç”¨ `StockUtils.addStock()` ç›´æŽ¥å…¥åº“(入库类型 `QUALITYINSPECT_STOCK_IN`)。
4. å°†æ£€éªŒå• `inspect_state` æ›´æ–°ä¸º 1(已提交)。
### 3.3 ä¸åˆæ ¼å“å¤„理逻辑(QualityUnqualifiedServiceImpl.deal)
核心参数:`dealResult` å†³å®šå¤„理路径,分为两大分支。
#### åˆ†æ”¯A:过程检验/出厂检验(inspectType != 0 ä¸”有 productMainId)
| dealResult | å¤„理逻辑 |
|------------|----------|
| **"返修"或"返工"** | 1. æ ¹æ® `productMainId` æŸ¥åˆ°åŽŸå§‹ç”Ÿäº§è®¢å•(ProductOrder)、工艺路线主表(ProductProcessRoute)、工艺路线子表(ProductProcessRouteItem) |
| | 2. å…‹éš†ä¸€ä»½æ–°çš„生产订单(quantity=不合格数量,completeQuantity=0,时间清空) |
| | 3. å…‹éš†ä¸€ä»½æ–°çš„工艺路线主表(关联新订单) |
| | 4. é€æ¡å…‹éš†å·¥è‰ºè·¯çº¿å­è¡¨ï¼Œæ¯æ¡å…‹éš†åŽè‡ªåŠ¨ç”Ÿæˆå¯¹åº”å·¥å•(ProductWorkOrder):工单号规则 `"FG" + yyyyMMdd + 3位自增序号` |
| **"报废"** | è°ƒç”¨ `StockUtils.addUnStock()` å…¥ä¸åˆæ ¼åº“(类型 `DEFECTIVE_SCRAP`) |
| **"让步放行"** | è°ƒç”¨ `StockUtils.addStock()` å…¥åˆæ ¼åº“(类型 `DEFECTIVE_PASS`) |
#### åˆ†æ”¯B:原材料检验(inspectType == 0)
| dealResult | å¤„理逻辑 |
|------------|----------|
| **"报废"** | å…ˆé€šè¿‡ productName + model æŸ¥ product_model.id,再调用 `StockUtils.addUnStock()` å…¥ä¸åˆæ ¼åº“ |
| **"让步放行"** | åŒä¸ŠæŸ¥æ‰¾ modelId,再调用 `StockUtils.addStock()` å…¥åˆæ ¼åº“ |
| **"返修"/"返工"** | åŽŸææ–™æ£€éªŒä¸æ”¯æŒè¿”ä¿®/返工(代码中无此分支,不执行任何操作) |
> å…³é”®åˆ¤æ–­é€»è¾‘位于 `QualityUnqualifiedServiceImpl.java:81`:
> ```java
> if (ObjectUtils.isNotNull(qualityInspect) && qualityInspect.getInspectType() != 0)
> ```
> åªæœ‰å…³è”了检验单且检验类型不是原材料(0)时,才走过程/出厂的处理分支(包含返修/返工逻辑)。
处理完成后将 `inspect_state` ç½®ä¸º 1(已处理)。
### 3.4 ä¸åˆæ ¼å“å¤„理单流程(QualityUnqualifiedOrder)
处理单是对不合格品的正式处置记录,包含更丰富的字段和审批状态机:
```
草稿(0) â†’ å¾…审批(1) â†’ å®¡æ‰¹ä¸­(2) â†’ å·²å®Œæˆ(3)
  â†“                      â†“
 å¯ç¼–辑/删除          å·²é©³å›ž(4)
```
- **编号自动生成**:前缀 `"BHG"` + å½“日自增序号(通过 `OrderUtils.countTodayByCreateTime` å®žçŽ°ï¼‰ã€‚
- **处置方式枚举**:1=让步接收,2=厂内维修,3=返厂维修,4=换货,5=退货,6=报废。
- **分页查询**:支持按 status、projectName、orderNo ç­›é€‰ã€‚
- **CRUD**:支持新增、修改、删除(逻辑删除通过 deleted å­—段)。
### 3.5 æ£€éªŒå•删除逻辑(QualityInspectController.del)
1. æ ¡éªŒæ˜¯å¦å·²æäº¤ï¼ˆ`inspectState==1` ä¸å…è®¸åˆ é™¤ï¼‰ã€‚
2. çº§è”删除关联的检验参数 (quality_inspect_param)。
3. çº§è”删除关联的检验附件 (quality_inspect_file)。
4. åˆ é™¤æ£€éªŒå•本身。
### 3.6 ä¸åˆæ ¼å“åˆ é™¤é€»è¾‘(QualityUnqualifiedController.del)
1. æ ¡éªŒæ˜¯å¦å·²å¤„理(`inspectState==1` ä¸å…è®¸åˆ é™¤ï¼‰ã€‚
2. çº§è”删除关联附件(通过 commonFileService æŒ‰ businessId + type åˆ é™¤ï¼‰ã€‚
3. æ‰¹é‡åˆ é™¤ä¸åˆæ ¼å“è®°å½•。
### 3.7 æ£€æµ‹æ ‡å‡†ç»´æŠ¤
- æ£€æµ‹æ ‡å‡†ä¸»è¡¨ (quality_test_standard) ç»´æŠ¤æ ‡å‡†ç¼–号、名称、适用检验类型、工序类型。
- æ£€æµ‹æ ‡å‡†å‚æ•° (quality_test_standard_param) ç»´æŠ¤æŒ‡æ ‡ã€å•位、标准值、内控值、默认值。
- æ£€æµ‹æ ‡å‡†-产品绑定 (quality_test_standard_binding) å®žçŽ°å¤šå¯¹å¤šå…³è”ã€‚
- **删除保护**:删除检测标准前会检查是否被检验单引用,引用则禁止删除。
- **参数复制**:支持 `copyParam` å°†æŸä¸ªæ ‡å‡†çš„参数复制到另一个标准。
- **审核**:提供 `qualityTestStandardAudit` æŽ¥å£ã€‚
---
## 4. API æŽ¥å£æ€»è§ˆ
### 4.1 æ£€éªŒè®°å½•
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | `/quality/qualityInspect/add` | æ–°å¢žæ£€éªŒå•(含检验参数) |
| DELETE | `/quality/qualityInspect/del` | æ‰¹é‡åˆ é™¤æ£€éªŒå•(级联删除参数和附件) |
| GET | `/quality/qualityInspect/{id}` | æŸ¥çœ‹è¯¦æƒ…(含检验参数) |
| POST | `/quality/qualityInspect/update` | ä¿®æ”¹æ£€éªŒå•(先删旧参数再插新参数) |
| GET | `/quality/qualityInspect/listPage` | åˆ†é¡µæŸ¥è¯¢ï¼ˆæŒ‰ç±»åž‹ JOIN ä¸åŒä¸šåŠ¡è¡¨ï¼‰ |
| POST | `/quality/qualityInspect/export` | å¯¼å‡º Excel(不同类型隐藏不同列) |
| POST | `/quality/qualityInspect/submit` | æäº¤æ£€éªŒï¼ˆåˆæ ¼å…¥åº“/不合格登记) |
| POST | `/quality/qualityInspect/down` | ä¸‹è½½ Word æ£€éªŒæŠ¥å‘Šï¼ˆpoi-tl æ¨¡æ¿æ¸²æŸ“) |
### 4.2 ä¸åˆæ ¼å“ç®¡ç†
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | `/quality/qualityUnqualified/add` | æ–°å¢žä¸åˆæ ¼å“ï¼ˆæ‰‹å·¥å½•入,状态=待处理) |
| DELETE | `/quality/qualityUnqualified/del` | æ‰¹é‡åˆ é™¤ï¼ˆå·²å¤„理不可删) |
| GET | `/quality/qualityUnqualified/{id}` | è¯¦æƒ…(含附件列表) |
| POST | `/quality/qualityUnqualified/update` | ä¿®æ”¹ä¸åˆæ ¼å“ |
| GET | `/quality/qualityUnqualified/listPage` | åˆ†é¡µæŸ¥è¯¢ï¼ˆæŒ‰ç±»åž‹ã€çŠ¶æ€ã€äº§å“åã€æ—¥æœŸç­›é€‰ï¼‰ |
| POST | `/quality/qualityUnqualified/export` | å¯¼å‡º Excel |
| POST | `/quality/qualityUnqualified/deal` | **处理不合格品**(返修/返工/报废/让步放行) |
### 4.3 ä¸åˆæ ¼å“å¤„理单
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | `/qualityUnqualifiedOrder/listPage` | åˆ†é¡µæŸ¥è¯¢ï¼ˆæ”¯æŒ status/projectName/orderNo ç­›é€‰ï¼‰ |
| POST | `/qualityUnqualifiedOrder/save` | æ–°å¢žï¼ˆè‡ªåŠ¨ç”Ÿæˆç¼–å·ï¼‰ |
| PUT | `/qualityUnqualifiedOrder/update` | ä¿®æ”¹ |
| DELETE | `/qualityUnqualifiedOrder/delete` | æ‰¹é‡åˆ é™¤ |
### 4.4 æ£€æµ‹æ ‡å‡†
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | `/qualityTestStandard/add` | æ–°å¢žæ£€æµ‹æ ‡å‡† |
| DELETE | `/qualityTestStandard/del` | åˆ é™¤ï¼ˆæœ‰å¼•用时禁止) |
| POST | `/qualityTestStandard/update` | ä¿®æ”¹ |
| GET | `/qualityTestStandard/listPage` | åˆ†é¡µæŸ¥è¯¢ |
| POST | `/qualityTestStandard/copyParam` | å¤åˆ¶å‚数到另一个标准 |
| POST | `/qualityTestStandard/qualityTestStandardAudit` | å®¡æ ¸æ£€æµ‹æ ‡å‡† |
| GET | `/qualityTestStandard/getQualityTestStandardByProductId` | æŒ‰äº§å“ID获取标准(通过绑定表 JOIN) |
| GET | `/qualityTestStandard/getQualityTestStandardParamByTestStandardId` | æŒ‰æ ‡å‡†ID获取参数列表 |
### 4.5 æ£€æµ‹æ ‡å‡†ç»‘定
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | `/qualityTestStandardBinding/add` | æ–°å¢žç»‘定 |
| DELETE | `/qualityTestStandardBinding/del` | åˆ é™¤ç»‘定 |
| GET | `/qualityTestStandardBinding/list` | æŸ¥è¯¢ç»‘定列表 |
### 4.6 æ£€æµ‹æ ‡å‡†å‚æ•°
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | `/qualityTestStandardParam/add` | æ–°å¢žå‚æ•° |
| DELETE | `/qualityTestStandardParam/del` | åˆ é™¤å‚æ•° |
| POST | `/qualityTestStandardParam/update` | ä¿®æ”¹å‚æ•° |
| GET | `/qualityTestStandardParam/list` | æŸ¥è¯¢å‚数列表 |
### 4.7 æ£€éªŒå‚æ•°
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | `/quality/qualityInspectParam/{inspectId}` | æŒ‰æ£€éªŒID查参数列表 |
| POST | `/quality/qualityInspectParam/update` | ä¿®æ”¹å‚æ•° |
| DELETE | `/quality/qualityInspectParam/del` | åˆ é™¤å‚æ•° |
### 4.8 æ£€éªŒé™„ä»¶
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | `/quality/qualityInspectFile/add` | æ–°å¢žé™„ä»¶ |
| DELETE | `/quality/qualityInspectFile/del` | åˆ é™¤é™„ä»¶ |
| GET | `/quality/qualityInspectFile/listPage` | åˆ†é¡µæŸ¥è¯¢ |
### 4.9 è´¨é‡æŠ¥è¡¨
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | `/qualityReport/getInspectStatistics` | æ£€éªŒç»Ÿè®¡ï¼ˆæŒ‰åŽŸææ–™/半成品/成品分组的总数和完成数) |
| GET | `/qualityReport/getPassRateStatistics` | åˆæ ¼çŽ‡ç»Ÿè®¡ï¼ˆæŒ‰æœŸæ±‡æ€»ï¼šæ€»æ•°ã€å®Œæˆæ•°ã€åˆæ ¼æ•°ã€ä¸åˆæ ¼æ•°ã€å®ŒæˆçŽ‡ã€åˆæ ¼çŽ‡ï¼‰ |
| GET | `/qualityReport/getMonthlyPassRateStatistics` | æœˆåº¦åˆæ ¼çŽ‡ï¼ˆ12个月 Ã— 3类,CTE递归) |
| GET | `/qualityReport/getYearlyPassRateStatistics` | å¹´åº¦åˆæ ¼çއ |
| GET | `/qualityReport/getMonthlyCompletionDetails` | æœˆåº¦å®Œæˆæ˜Žç»†ï¼ˆæ¯æœˆåŽŸææ–™/半成品/成品完成数) |
| GET | `/qualityReport/getTopParameters` | æ£€æµ‹æŒ‡æ ‡æŽ’名(Top 4 + å…¶ä»–汇总) |
---
## 5. å…³é”®æ•°æ®å…³è”
```
product (产品分类)
  â””── product (具体产品)
        â””── product_model (产品型号)
              â”œâ”€â”€ quality_inspect.product_model_id
              â”œâ”€â”€ quality_unqualified (通过 model å­—段关联)
              â””── quality_test_standard_binding.product_id
production_product_main (生产报工)
  â””── quality_inspect.product_main_id (过程/出厂检验)
        â””── production_product_main.work_order_id â†’ product_work_order
              â””── product_order â†’ sales_ledger (销售台账)
purchase_ledger (采购台账)
  â””── quality_inspect.purchase_ledger_id (原材料检验)
product_process_route (工艺路线主表)
  â””── product_process_route_item (工艺路线子表)
        â””── product_work_order (工单)
```
### åˆ†é¡µæŸ¥è¯¢æ—¶ JOIN ç­–ç•¥
- **原材料检验 (inspectType=0)**:LEFT JOIN `purchase_ledger` èŽ·å–é‡‡è´­åˆåŒå·ã€‚
- **过程/出厂检验 (inspectType != 0)**:LEFT JOIN `production_product_main` â†’ `product_work_order` â†’ `product_order` â†’ `sales_ledger` èŽ·å–å·¥å•å·å’Œé”€å”®åˆåŒå·ã€‚
- **不合格品列表**:LEFT JOIN `product_model` è§£æžåž‹å·ï¼ˆmodel å­—段可能是 ID æˆ–文本)。
---
## 6. ä¸Žå¤–部模块的交互
| äº¤äº’点 | è°ƒç”¨æ–¹å¼ | è¯´æ˜Ž |
|--------|----------|------|
| åº“存管理 (stock) | `StockUtils.addStock()` / `StockUtils.addUnStock()` | åˆæ ¼å…¥åº“ / ä¸åˆæ ¼å…¥åº“ |
| ç”Ÿäº§ç®¡ç† (production) | ç›´æŽ¥æ“ä½œ Mapper | è¿”å·¥/返修时克隆生产订单+工艺路线+工单 |
| é™„件管理 (commonFile) | `CommonFileService` / `TempFileService` | ä¸´æ—¶æ–‡ä»¶è½¬æ­£å¼æ–‡ä»¶ã€æŒ‰ä¸šåŠ¡ID+类型查询删除 |
| æ•°æ®å­—å…¸ (dict) | `DictUtils` | processType æ ¡éªŒï¼Œå¯¼å‡ºæ—¶ translate |
| é‡‡è´­å°è´¦ (procurement) | `ProcurementRecordService` | åŽŸææ–™æ£€éªŒå…³è”é‡‡è´­è®°å½• |
---
## 7. æžšä¸¾å€¼æ±‡æ€»
### inspectType(检验类别)
| å€¼ | å«ä¹‰ |
|----|------|
| 0 | åŽŸææ–™æ£€éªŒ |
| 1 | è¿‡ç¨‹æ£€éªŒ |
| 2 | å‡ºåŽ‚æ£€éªŒ |
### inspectState(检验单状态 / ä¸åˆæ ¼å“çŠ¶æ€ï¼‰
| å€¼ | å«ä¹‰ï¼ˆæ£€éªŒå•) | å«ä¹‰ï¼ˆä¸åˆæ ¼å“ï¼‰ |
|----|---------------|------------------|
| 0 | æœªæäº¤(草稿) | å¾…处理 |
| 1 | å·²æäº¤ | å·²å¤„理 |
### dealResult(不合格品处理结果)
| å€¼ | å«ä¹‰ | é€‚用场景 |
|----|------|----------|
| è¿”ä¿® | è¿”修处理 | ä»…过程/出厂检验 |
| è¿”å·¥ | è¿”工处理(重建生产订单) | ä»…过程/出厂检验 |
| æŠ¥åºŸ | å…¥ä¸åˆæ ¼åº“ | å…¨éƒ¨ç±»åž‹ |
| è®©æ­¥æ”¾è¡Œ | å…¥åˆæ ¼åº“ | å…¨éƒ¨ç±»åž‹ |
### disposalMethod(处理单处置方式)
| å€¼ | å«ä¹‰ |
|----|------|
| 1 | è®©æ­¥æŽ¥æ”¶ |
| 2 | åŽ‚å†…ç»´ä¿® |
| 3 | è¿”厂维修 |
| 4 | æ¢è´§ |
| 5 | é€€è´§ |
| 6 | æŠ¥åºŸ |
### unqualifiedProcess(不合格工序)
| å€¼ | å«ä¹‰ |
|----|------|
| 1 | æ¥æ–™ |
| 2 | åˆ¶ç¨‹ |
| 3 | æˆå“ |
### status(处理单状态)
| å€¼ | å«ä¹‰ |
|----|------|
| 0 | è‰ç¨¿ |
| 1 | å¾…审批 |
| 2 | å®¡æ‰¹ä¸­ |
| 3 | å·²å®Œæˆ |
| 4 | å·²é©³å›ž |
---
## 8. æŠ€æœ¯å®žçŽ°è¦ç‚¹
1. **MyBatis-Plus è‡ªåЍ填充**:create_time、create_user、update_time、update_user、tenant_id、dept_id å‡é€šè¿‡ `@TableField(fill = ...)` è‡ªåŠ¨å¡«å……ã€‚
2. **事务管理**:`QualityInspectServiceImpl` æ ‡æ³¨ `@Transactional(rollbackFor = Exception.class)`,确保新增/修改检验单与参数操作的原子性。
3. **Word æŠ¥å‘Šç”Ÿæˆ**:使用 poi-tl æ¨¡æ¿å¼•擎,模板文件 `/static/report-template.docx`,通过 `HackLoopTableRenderPolicy` æ¸²æŸ“参数列表表格。
4. **Excel å¯¼å‡ºåŠ¨æ€åˆ—**:根据 inspectType åŠ¨æ€éšè—ä¸ç›¸å…³åˆ—ï¼ˆåŽŸææ–™æ£€éªŒéšè— customer/process,出厂检验隐藏 supplier/customer/process)。
5. **月度统计 CTE**:`getMonthlyPassRateStatistics` ä½¿ç”¨ MySQL WITH RECURSIVE ç”Ÿæˆ 1-12 æœˆåºåˆ—,CROSS JOIN 3 ç§äº§å“ç±»åž‹ï¼ŒLEFT JOIN å®žé™…数据,确保每月每类型都有数据行。
6. **不合格品 model å­—段兼容**:model å­—段可能存储 product_model.id(数字)或直接文本,查询时 LEFT JOIN product_model å°è¯•解析,并用 method å­—段标记。
7. **返工工单号生成**:日期前缀 `yyyyMMdd` + 3 ä½è‡ªå¢žåºå·ï¼Œå‰ç¼€ `"FG"`,通过查询当日最大工单号实现自增。
8. **文件管理**:前端传 tempFileIds,后端通过 `TempFileService.migrateTempFilesToFormal()` å°†ä¸´æ—¶æ–‡ä»¶è½¬ä¸ºæ­£å¼æ–‡ä»¶å¹¶ç»‘定到业务ID。
docs/approve_or_and_mode.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,223 @@
# å®¡æ‰¹æµç¨‹æ”¯æŒæˆ–ç­¾/会签模式
## æ¶‰åŠé¡µé¢
- å®¡æ‰¹ç®¡ç†é…ç½®é¡µï¼š`collaborativeApproval/approvalManagement/index.vue`
- å®¡æ‰¹åˆ—表页(协同审批):`collaborativeApproval/approvalProcess/index.vue`
- å®¡æ‰¹åˆ—表页(危险作业 type 8):`safeProduction/safeWorkApproval/index.vue`
- å®¡æ‰¹å¼¹æ¡†ï¼ˆååŒå®¡æ‰¹ï¼‰ï¼š`collaborativeApproval/approvalProcess/components/approvalDia.vue`
- å®¡æ‰¹å¼¹æ¡†ï¼ˆå±é™©ä½œä¸š type 8):`safeProduction/safeWorkApproval/components/approvalDia.vue`
## API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /approveProcessConfigNode/listNode | æŸ¥è¯¢å®¡æ‰¹èŠ‚ç‚¹é…ç½®ï¼ˆå“åº”æ–°å¢ž `approveMode` å­—段) |
| POST | /approveProcessConfigNode/addApproveProcessConfigNodes | ä¿å­˜èŠ‚ç‚¹é…ç½®ï¼ˆæ¯è¡Œä¸€ä¸ªå®¡æ‰¹äººï¼ŒåŒä¸€ `nodeOrder` å¤šè¡Œå³åŒä¸€èŠ‚ç‚¹å¤šäººï¼Œéœ€ä¼  `approveMode`) |
| POST | /approveProcess/addApprove | å‘起审批(新增响应/快照字段 `approve_node_config`,节点含多人时写入 `approve_user_current_ids/names`) |
| GET | /approveProcess/details | å®¡æ‰¹è¯¦æƒ…(节点列表新增 `approveMode` å­—段;`isShen` ä»…当前待审节点且当前登录人可审时返回 true) |
| POST | /approveNode/updateApproveNode | æäº¤å®¡æ‰¹ï¼ˆ**不再需要 `isLast` å‚æ•°**,流转判断由后端按节点模式完成) |
### èŠ‚ç‚¹é…ç½®ä¿å­˜è¯·æ±‚å‚æ•°ï¼ˆaddApproveProcessConfigNodes)
同一节点多审批人通过多条记录表达,`nodeOrder` ç›¸åŒã€`approveMode` æ ‡è¯†æ¨¡å¼ï¼š
```json
[
  { "approveType": 1, "nodeOrder": 1, "approverId": 11, "approverName": "张三", "approveMode": "or" },
  { "approveType": 1, "nodeOrder": 1, "approverId": 12, "approverName": "李四", "approveMode": "or" },
  { "approveType": 1, "nodeOrder": 2, "approverId": 21, "approverName": "王五", "approveMode": "and" },
  { "approveType": 1, "nodeOrder": 2, "approverId": 22, "approverName": "赵六", "approveMode": "and" }
]
```
- `approveMode`:`or`=或签(任一人通过即流转),`and`=会签(需所有人通过才流转)
- ä»»ä¸€å®¡æ‰¹äººæ‹’绝 â†’ æ•´ä¸ªæµç¨‹ç«‹å³é©³å›žï¼ˆæˆ–ç­¾/会签一致,一票否决)
### å®¡æ‰¹è¯¦æƒ…响应(details)新增字段
每个节点新增:
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| approveMode | String | èŠ‚ç‚¹å®¡æ‰¹æ¨¡å¼ï¼šor / and |
| approveNodeOrder | Integer | èŠ‚ç‚¹åºå·ï¼ˆåŒä¸€èŠ‚ç‚¹çš„å¤šäººè®°å½•ç›¸åŒï¼‰ |
| isShen | Boolean | æ˜¯å¦ä¸ºå½“前登录人的待审节点 |
流程级新增(列表接口返回):`approveUserCurrentIds`、`approveUserCurrentNames`(当前节点全部待审人)。
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. é…ç½®é¡µï¼šèŠ‚ç‚¹å¡ç‰‡æ”¯æŒå¤šå®¡æ‰¹äºº + æˆ–ç­¾/会签
`collaborativeApproval/approvalManagement/index.vue`
审批人改为多选,新增模式单选:
```html
<el-form-item label="审批人">
  <el-select v-model="item.approverIds" multiple filterable placeholder="选择审批人(可多选)">
    <el-option v-for="u in allUsers" :key="u.userId" :label="u.nickName" :value="u.userId" />
  </el-select>
</el-form-item>
<el-form-item label="审批模式">
  <el-radio-group v-model="item.approveMode">
    <el-radio-button value="or">或签(任意一人通过)</el-radio-button>
    <el-radio-button value="and">会签(需所有人通过)</el-radio-button>
  </el-radio-group>
</el-form-item>
```
`loadData`:将接口扁平配置按 `nodeOrder` åˆå¹¶ä¸ºå¡ç‰‡æ•°æ®ï¼š
```js
const groupMap = new Map();
(res.data || []).forEach(r => {
  const order = r.nodeOrder;
  if (!groupMap.has(order)) groupMap.set(order, { nodeOrder: order, approverIds: [], approverNames: [], approveMode: r.approveMode || 'or' });
  const g = groupMap.get(order);
  if (!g.approverIds.includes(r.approverId)) {
    g.approverIds.push(r.approverId);
    g.approverNames.push(r.approverName);
  }
});
nodeCards.value = Array.from(groupMap.values()).sort((a, b) => a.nodeOrder - b.nodeOrder);
```
`handleSave`:每张卡片按审批人展开成多行 payload:
```js
const list = [];
nodeCards.value.forEach(card => {
  card.approverIds.forEach((uid, i) => {
    list.push({
      approveType: currentType,
      nodeOrder: card.nodeOrder,
      approverId: uid,
      approverName: card.approverNames[i],
      approveMode: card.approveMode || 'or',
    });
  });
});
await addApproveProcessConfigNodes(list);
```
### 2. åˆ—表页:当前审批人列 + å®¡æ ¸æŒ‰é’®æƒé™
`collaborativeApproval/approvalProcess/index.vue`、`safeProduction/safeWorkApproval/index.vue`
当前审批人列展示同节点全部待审人,富化行数据:
```js
tableData.value = (res.data.records || []).map(r => ({
  ...r,
  currentApproverNames: r.approveUserCurrentNames || r.approveUserCurrentName || '-',
  _canApprove:
    String(r.approveUserCurrentIds ?? '').split(',').filter(Boolean).includes(String(userStore.id)) ||
    r.approveUserCurrentId === userStore.id,
}));
```
审核按钮禁用条件:
```js
disabled: row =>
  row.approveStatus == 2 || row.approveStatus == 3 || row.approveStatus == 4 || !row._canApprove,
```
### 3. å®¡æ‰¹å¼¹æ¡†ï¼šæŒ‰èŠ‚ç‚¹åˆ†ç»„å±•ç¤º
`collaborativeApproval/approvalProcess/components/approvalDia.vue`、`safeProduction/safeWorkApproval/components/approvalDia.vue`
新增分组计算属性,同一 `approveNodeOrder` çš„多名审批人归为一个步骤组:
```js
const stepGroups = computed(() => {
  const map = new Map();
  (activities.value || []).forEach(a => {
    const order = a.approveNodeOrder;
    if (!map.has(order)) map.set(order, []);
    map.get(order).push(a);
  });
  return Array.from(map.values());
});
```
`el-steps` æ”¹ä¸ºéåކ `stepGroups`,每组一个 `el-step`,组内循环渲染成员(姓名、状态、意见、签名、待审输入框):
```html
<el-steps :active="getActiveStep()" ...>
  <el-step v-for="(group, gIndex) in stepGroups" :key="gIndex" ...>
    <template #title>
      <span>{{ getNodeTitle(gIndex, stepGroups.length) }}</span>
      <el-tag :type="group[0]?.approveMode === 'and' ? 'warning' : 'info'">
        {{ group[0]?.approveMode === 'and' ? '会签' : '或签' }}
      </el-tag>
    </template>
    <template #description>
      <div v-for="(activity, aIndex) in group" :key="activity.id || aIndex" class="node-user-item">
        <!-- æˆå‘˜å§“名、状态、审批意见;仅 isShen æˆå‘˜æ¸²æŸ“输入框 -->
      </div>
    </template>
  </el-step>
</el-steps>
```
当前激活步骤取含待审成员的组下标:
```js
const getActiveStep = () => {
  const idx = stepGroups.value.findIndex(group => group.some(a => a.isShen === true));
  if (idx === -1) return stepGroups.value.length;
  return idx;
};
```
提交审批不再传 `isLast`:
```js
currentActivity.approveNodeStatus = status;
updateApproveNode({ ...currentActivity }).then(() => {
  proxy.$modal.msgSuccess("提交成功");
  closeDia();
});
```
## ç®¡ç†å‘˜å®¡æ‰¹ä¸å—审批人限制
管理员账号(角色为 **普通2 / admin / æœ€é«˜1**)可审批任意流程,不再受"审批人必须是流程节点配置的审批人"限制。
### åŽç«¯
- `details`:管理员对当前待审节点的首个待审记录返回 `isShen=true`(代审整节点)。
- `updateApproveNode`:管理员跳过"无权审批该节点"校验;审批通过时自动将同节点其余待审记录标记为"管理员代审通过"(或签/会签均整节点放行);拒绝仍按一票否决处理。
### å‰ç«¯
两个列表页(approvalProcess / safeWorkApproval)新增 `isAdminUser` åˆ¤æ–­ï¼š
```js
const isAdminUser = computed(() =>
  ["admin", "普通2", "最高1"].some(r => (userStore.roles || []).includes(r))
);
```
`_canApprove` å¯¹ç®¡ç†å‘˜æ’为 `true`,审核按钮不再禁用:
```js
_canApprove: isAdminUser.value || String(r.approveUserCurrentIds ?? '').split(',').filter(Boolean).includes(String(userStore.id)) || r.approveUserCurrentId === userStore.id
```
### æ³¨æ„
- ç®¡ç†å‘˜ä»éœ€æŒ‰èŠ‚ç‚¹é¡ºåºå®¡æ‰¹ï¼ˆ"尚未轮到该节点审批"校验保留)。
- ç®¡ç†å‘˜æ‹’绝任意节点 = ä¸€ç¥¨å¦å†³ï¼Œæ•´ä¸ªæµç¨‹ç«‹å³é©³å›žã€‚
## æ³¨æ„äº‹é¡¹
- **后端流转判断**:`updateApproveNode` å·²æ”¹ä¸ºæŒ‰èŠ‚ç‚¹æ¨¡å¼ï¼ˆæˆ–ç­¾/会签)统计同 order çš„同意数决定是否流转,前端**必须删除 `isLast` ä¼ å‚**,否则旧字段会被忽略(不影响结果,但无意义)。
- **或签放行**:或签任一审批人通过后,同节点其余未审记录会被后端自动标记为"已同意(或签:其他审批人已通过)",前端无需处理。
- **会签部分通过**:节点停留在当前步骤,列表"当前审批人"会更新为剩余未审人员。
- **历史数据兼容**:存量节点 `approveMode` ä¸ºç©ºæŒ‰"或签"处理;存量流程 `approveUserCurrentNames` ä¸ºç©ºæ—¶å‰ç«¯å…œåº• `approveUserCurrentName`。
- **编辑重建**:流程编辑时后端优先按 `approve_node_config` å¿«ç…§é‡å»ºèŠ‚ç‚¹ä»¥ä¿æŒåˆ†ç»„ä¸Žæ¨¡å¼ï¼Œæ— å¿«ç…§æ—¶æŒ‰"一节点一人 or"回退。
- é…ç½®é¡µå·²è¡¥å…… **type 8 å±é™©ä½œä¸šå®¡æ‰¹** çš„æ ‡ç­¾ä¸Žå›¾æ ‡ã€‚
docs/auto_quality_inspect_on_dispatch.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,76 @@
# æŽ’产下达自动触发第一道工序质检
## åŠŸèƒ½è¯´æ˜Ž
生产排产下达(生成生产工单)后,若第一道工序需要质检(`isQuality=true`),系统自动为该工序预生成一张过程检验(单道工序时生成出厂检)质检单。质检员无需等待该工序报工审核即可直接在质检列表中开始检验。
## æ¶‰åŠé¡µé¢
- ç”Ÿäº§æŽ’产/生产订单下达
- è´¨æ£€ç®¡ç†ï¼ˆè¿‡ç¨‹æ£€éªŒ / å‡ºåŽ‚æ£€éªŒ åˆ—表页)
## è¡Œä¸ºå˜åŒ–(纯后端,无新增接口)
### 1. æŽ’产下达自动预生成质检单
覆盖全部工单创建入口:
| å…¥å£ | è¯´æ˜Ž |
|------|------|
| ç”Ÿäº§è®¢å•绑定工艺路线 `bindingRoute` | ç”Ÿæˆå·¥å•后预生成质检单 |
| é”€å”®å°è´¦ç”Ÿæˆç”Ÿäº§æ•°æ® `addProductionData` | ç”Ÿæˆå·¥å•后预生成质检单 |
| å°åˆ·å•逆时针倒排 `processRouteAnticlockwise` | ç”Ÿæˆå·¥å•后预生成质检单 |
预生成规则:
- å–工艺路线第一道工序(`dragSort` å‡åº + `id` å‡åºï¼‰ï¼›
- ç¬¬ä¸€é“工序 `isQuality` éž true åˆ™ä¸ç”Ÿæˆï¼›
- ä»…一道工序 â†’ `inspect_type=2`(出厂检),否则 `inspect_type=1`(过程检,`process`=第一道工序名);
- `quantity`=工单计划数量;
- å…³é”®å­—段:`work_order_id`=第一道工序工单 id,`product_main_id=NULL`(未关联报工)。
> è¯†åˆ«é¢„生成质检单:`quality_inspect` è¡¨ä¸­ `product_main_id IS NULL` ä¸” `work_order_id` éžç©ºçš„记录即为排产预生成单。
### 2. æŠ¥å·¥å®¡æ ¸æ—¶è®¤é¢†å¹¶æ›´æ–°
第一道工序报工审核(`nextAddProductMain`)时:
- è‹¥è¯¥å·¥å•存在预生成质检单(`work_order_id` åŒ¹é…ä¸” `product_main_id IS NULL`)→ **认领**:回填 `product_main_id`=报工 id,`quantity` æ›´æ–°ä¸ºå®žé™…入库数量(产出 + åŠ æ”¾ - æŠ¥åºŸï¼‰ï¼›
- è‹¥è¯¥é¢„生成单已提交(`inspect_state=1`)且为出厂检 â†’ å…ˆåˆ é™¤æŒ‰è®¡åˆ’数量入库的库存记录,再按实际数量重新入库,保证库存准确;
- è‹¥ä¸å­˜åœ¨é¢„生成单(如工序临时新增)→ èµ°åŽŸæœ‰é€»è¾‘ç”Ÿæˆæ–°è´¨æ£€å•ã€‚
后续工序(非第一道)报工审核时仍按原有逻辑生成质检单。
### 3. åˆ—表/报告展示
- è´¨æ£€åˆ—表的工单号通过 `COALESCE(qi.work_order_id, ppm.work_order_id)` å…³è”,预生成单(无报工)也能正常显示工单号;
- æ£€éªŒæŠ¥å‘Šä¸‹è½½ï¼šé¢„生成单(`product_main_id=NULL`)按 `work_order_id` â†’ ç”Ÿäº§è®¢å• â†’ é”€å”®å°è´¦ åæŸ¥å®¢æˆ·åç§°ã€‚
### 4. åˆ é™¤æ¸…理
删除生产订单 / åˆ é™¤é”€å”®å°è´¦ç”Ÿäº§æ•°æ® / åˆ é™¤ç”Ÿäº§å·¥è‰ºè·¯çº¿å·¥åºé¡¹æ—¶ï¼Œè‡ªåŠ¨æ¸…ç†å¯¹åº”çš„é¢„ç”Ÿæˆè´¨æ£€å•åŠå…¶æ£€éªŒå‚æ•°ï¼›è‹¥é¢„ç”Ÿæˆå•å·²æäº¤ï¼ˆ`inspect_state=1`),则拒绝删除并提示“已提交的检验单不能删除”。
## API
无新增、无修改接口。均为后端行为变更。
## å‰ç«¯ä¿®æ”¹ç‚¹
无前端代码改动。
若需在界面区分“排产预生成”的质检单,可参考 `work_order_id`(非空)且 `product_main_id` ä¸ºç©ºè¿›è¡Œåˆ¤æ–­ã€‚
## æ•°æ®åº“变更
执行 `sql/quality_inspect_add_work_order_id.sql`:
```sql
ALTER TABLE quality_inspect ADD COLUMN work_order_id BIGINT NULL COMMENT '生产工单id(排产预生成质检单关联)' AFTER product_main_id;
ALTER TABLE quality_inspect ADD INDEX idx_quality_inspect_work_order_id (work_order_id);
```
## æ³¨æ„äº‹é¡¹
- é¢„生成质检单仅针对**第一道需质检工序**;中途工序仍需报工审核后才生成。
- è´¨æ£€å‘˜å¯åœ¨æŠ¥å·¥å‰ç›´æŽ¥å¡«å†™æ£€éªŒç»“果并提交;出厂检提交时会按计划数量入库,报工审核后自动按实际数量修正库存。
- å·²æäº¤çš„预生成质检单在删除订单/工单时会提示“已提交的检验单不能删除”,需先撤销或删除检验单。
docs/production_report_person_detail.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,181 @@
# ç”Ÿäº§æŠ¥å·¥æŒ‰ç­ç»„拆分产量/时长
## æ¶‰åŠé¡µé¢
- ç”Ÿäº§å·¥å•列表页(PDA æŠ¥å·¥å¼¹çª—,数据来源 `/productWorkOrder/getProductWorkOrderById`)
- æŠ¥å·¥æäº¤ï¼ˆ`POST /productionProductMain/addProductMain`)
- ç”Ÿäº§æ˜Žç»†é¡µï¼ˆç”Ÿäº§æ˜Žç»†å·¥æ—¶å›žæ˜¾ï¼‰
## API
### 1. æŠ¥å·¥å¼¹çª—数据查询(返回默认班组成员)
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /productWorkOrder/getProductWorkOrderById | æŠ¥å·¥å¼¹çª—数据,响应新增 `reportPersons` é»˜è®¤ç­ç»„成员(合并工单排产人员与机台上机操作员,去重) |
**响应新增字段:**
| å‚æ•° | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| reportPersons | Array | æŠ¥å·¥ç­ç»„默认成员,每项 `{ userId, userName }`,用作报工弹窗录入行初始化 |
### 2. æŠ¥å·¥æäº¤ï¼ˆæ–°å¢ž personList æ˜Žç»†ï¼‰
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /productionProductMain/addProductMain | æŠ¥å·¥æäº¤ï¼Œè¯·æ±‚体新增 `personList` ç­ç»„人员产量时长明细 |
**请求参数新增:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| personList | Array | æ˜¯ | ç­ç»„人员产量时长明细,每项 `{ userId, userName, quantity, workHour }` |
| personList[].userId | Long | æ˜¯ | ç”Ÿäº§äºº id |
| personList[].userName | String | æ˜¯ | ç”Ÿäº§äººå§“名 |
| personList[].quantity | BigDecimal | æ˜¯ | è¯¥äººå‘˜äº§é‡ |
| personList[].workHour | Integer | æ˜¯ | è¯¥äººå‘˜ç”Ÿäº§æ—¶é•¿ï¼ˆåˆ†é’Ÿï¼‰ |
**校验规则:** å„人员 `quantity` ä¹‹å’Œå¿…须等于报工总产量 `quantity`,否则后端返回 `各班组产量之和必须等于报工总产量`。
### 3. ç”Ÿäº§æ˜Žç»†ï¼ˆå·¥æ—¶å›žæ˜¾ä¿®æ­£ï¼‰
`/productionProductMain/page` å¯¹åº”的生产明细中,新增 `personWorkMinutes` å­—段用于回显各班组人员录入的工时。
**响应新增字段:**
| å‚æ•° | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| personWorkMinutes | String | å„班组人员录入的生产时长(分钟),来源 `sales_ledger_production_accounting.work_hour` |
### 4. ç”Ÿäº§æ ¸ç®—/明细报表产量列修正
`GET /salesLedger/productionAccounting/listProductionDetails` æ¯è¡ŒæŒ‰ç”Ÿäº§äººç”Ÿæˆï¼ˆä¸€äººä¸€è¡Œï¼‰ï¼Œ"生产数量"字段 `quantity` ä¿®æ­£ä¸ºï¼š
- å¤šäººæ‹†åˆ†æŠ¥å·¥ï¼ˆ`production_report_person` æœ‰è®°å½•):显示**该生产人报工时输入的产量**(`production_report_person.quantity`),不再显示整条报工的总产量;
- æœªæ‹†åˆ†/单人报工:回退显示核算 `finished_num`(净值)。
```sql
-- ä¿®æ­£åŽå–值
COALESCE(prp.quantity, slpa.finished_num) AS quantity
-- å…³è”条件:prp.product_main_id = ppm.id AND prp.user_id = slpa.scheduling_user_id
```
`GET /salesLedger/productionAccounting/page`(左侧按人汇总台账)的"产量"字段 `output_num` åŒæ ·ä¿®æ­£ï¼š
- åŽŸï¼š`SUM(ppout.quantity)`(该生产人关联报工的**总产量之和**,多人拆分时是总量);
- ä¿®æ­£ï¼š`SUM(COALESCE(prp.quantity, slpa.finished_num))`(各人报工时输入的产量之和,未拆分回退净值)。
```sql
-- ä¿®æ­£åŽå–值
SUM(COALESCE(prp.quantity, slpa.finished_num)) AS output_num
```
**响应字段 `quantity`/`output_num` è¯­ä¹‰å˜åŒ–:** ç”±"报工总产量"改为"该生产人的产量",前端列显示无需改动。
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. data æ•°æ®ï¼ˆæŠ¥å·¥å¼¹çª—)
```js
data() {
  return {
    reportForm: {
      workOrderId: null,
      productProcessRouteItemId: null,
      quantity: null,   // äº§å‡ºæ•°é‡
      addQty: 0,        // åŠ æ”¾æ•°
      scrapQty: 0,      // æŠ¥åºŸæ•°é‡
    },
    personList: [],     // ç­ç»„人员产量时长明细 [{ userId, userName, quantity, workHour }]
    teamList: [],       // ç­ç»„信息(原有)
    reportDialogVisible: false,
  }
}
```
### 2. æŠ¥å·¥å¼¹çª—打开时初始化班组录入行
```js
openReport(row) {
  // row ä¸º getProductWorkOrderById è¿”回的工单数据
  this.reportForm.workOrderId = row.id;
  this.reportForm.productProcessRouteItemId = row.productProcessRouteItemId;
  this.reportForm.quantity = null;
  this.reportForm.addQty = row.processRouteAddNum ? Number(row.processRouteAddNum) : 0;
  this.reportForm.scrapQty = 0;
  // ç”¨é»˜è®¤ç­ç»„成员初始化逐行录入(数量/时长需逐行填写)
  this.personList = (row.reportPersons || []).map(p => ({
    userId: p.userId,
    userName: p.userName,
    quantity: null,
    workHour: null,
  }));
  // teamList ä¹ŸåŒæ­¥ä¸ºé»˜è®¤æˆå‘˜
  this.teamList = (row.reportPersons || []).map(p => ({ userId: p.userId, userName: p.userName }));
  this.reportDialogVisible = true;
}
```
### 3. æŠ¥å·¥å¼¹çª—逐行录入产量/时长表格
```html
<el-table :data="personList" border>
  <el-table-column label="生产人" prop="userName" min-width="100" align="center" />
  <el-table-column label="产量" min-width="120" align="center">
    <template slot-scope="scope">
      <el-input-number v-model="scope.row.quantity" :min="0" :controls="false" placeholder="产量" style="width: 100%" />
    </template>
  </el-table-column>
  <el-table-column label="生产时长(分钟)" min-width="140" align="center">
    <template slot-scope="scope">
      <el-input-number v-model="scope.row.workHour" :min="0" :controls="false" placeholder="时长(分钟)" style="width: 100%" />
    </template>
  </el-table-column>
</el-table>
```
### 4. æŠ¥å·¥æäº¤å‚数(携带 personList)
```js
handleSubmit() {
  // æ ¡éªŒå„班组产量之和 = æŠ¥å·¥æ€»äº§é‡
  const total = this.personList.reduce((sum, p) => sum + (Number(p.quantity) || 0), 0);
  if (Number(this.reportForm.quantity) !== total) {
    this.$message.error('各班组产量之和必须等于报工总产量');
    return;
  }
  const params = {
    workOrderId: this.reportForm.workOrderId,
    productProcessRouteItemId: this.reportForm.productProcessRouteItemId,
    quantity: this.reportForm.quantity,
    addQty: this.reportForm.addQty,
    scrapQty: this.reportForm.scrapQty,
    teamList: this.teamList,
    personList: this.personList,
    deviceId: this.deviceId,
    deviceName: this.deviceName,
    startTime: this.startTime,
    endTime: this.endTime,
  };
  // è°ƒç”¨ addProductMain ...
}
```
### 5. ç”Ÿäº§æ˜Žç»†è¡¨æ–°å¢žäººå‘˜å·¥æ—¶åˆ—
```html
<el-table-column prop="personWorkMinutes" label="人员工时(分钟)" width="120" align="center" />
```
## æ³¨æ„äº‹é¡¹
- `personList` æ¯äººçš„ `quantity` ä¹‹å’Œå¿…须等于报工总产量 `quantity`,前端提交前需校验,后端也会拦截。
- `workHour` ä¸ºåˆ†é’Ÿæ•°ï¼Œç”±æ“ä½œå‘˜åœ¨æŠ¥å·¥å¼¹çª—手动录入,各班组各自记录生产时长。
- å®¡æ ¸é€šè¿‡åŽï¼ŒåŽç«¯æŒ‰ `personList` æ¯äººç”Ÿæˆä¸€æ¡ç”Ÿäº§æ ¸ç®—记录(`sales_ledger_production_accounting`):
  - `finished_num`(完成数量)= æ— æŠ¥åºŸæ—¶å–个人产量;有报废时按各人产量占比分摊净值(产出+加放-报废),四舍五入保留 2 ä½ã€‚
  - `work_hour` = è¯¥äººå‘˜å½•入的生产时长。
  - å…¨éƒ¨ç­ç»„的工时自动汇总到生产核算,计件/计时工资按人统计。
- å…¼å®¹æ—§æ•°æ®ï¼šè‹¥æŠ¥å·¥æœªä¼  `personList`,后端回退为按机台操作员生成核算(`finished_num`=净值,`work_hour`=机台上机时长),旧报工不受影响。
- åŽ†å²æŠ¥å·¥æ•°æ® `production_report_person` ä¸ºç©ºï¼Œç”Ÿäº§æ˜Žç»†ä¸­ `personWorkMinutes` å›žé€€å±•示报工时长 `workMinutes`。
- æ”¯æŒåˆ†æ‰¹æŠ¥å·¥ï¼šå®Œæˆæ•°é‡æœªè¾¾è®¡åˆ’数量时可多次报工,每次报工独立按 `personList` æ¯äººç”Ÿæˆä¸€æ¡æ ¸ç®—,不受上机记录报工状态限制;工单实际结束时间仅在完成数量达到计划数量时填充。详见 `docs/work_order_batch_report_fix.md`。
docs/production_work_order_add_qty.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,94 @@
# ç”Ÿäº§å·¥å•带出加放数 & æŠ¥å·¥æ ¸ç®—统一
## æ¶‰åŠé¡µé¢
- ç”Ÿäº§å·¥å•列表页(`/productWorkOrder/page`)
- PDA æŠ¥å·¥å¼¹çª—(数据来源 `/productWorkOrder/getProductWorkOrderById`)
## API
### 1. ç”Ÿäº§å·¥å•分页查询
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /productWorkOrder/page | ç”Ÿäº§å·¥å•分页查询,响应新增加放数字段 |
**响应新增字段(ProductWorkOrderDto):**
| å‚æ•° | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| processRouteAddNum | String | åŠ æ”¾æ•°ï¼ˆå–è‡ªå·¥è‰ºè·¯çº¿é¡¹ç›® `product_process_route_item.process_route_add_num`) |
### 2. PDA æŠ¥å·¥æ•°æ®æŸ¥è¯¢
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /productWorkOrder/getProductWorkOrderById | æŠ¥å·¥å¼¹çª—数据,响应新增加放数字段(用作报工弹窗加放数默认值) |
**响应新增字段:**
| å‚æ•° | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| processRouteAddNum | String | åŠ æ”¾æ•° |
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. ç”Ÿäº§å·¥å•列表表格增加加放数列
```html
<el-table-column prop="processRouteAddNum" label="加放数" width="100" align="center" />
```
### 2. data æ•°æ®ï¼ˆæŠ¥å·¥å¼¹çª—表单)
```js
data() {
  return {
    reportForm: {
      quantity: null, // äº§å‡ºæ•°é‡
      addQty: 0,      // åŠ æ”¾æ•°ï¼ˆé»˜è®¤å€¼ï¼Œå¯ä¿®æ”¹ï¼‰
      scrapQty: 0,    // æŠ¥åºŸæ•°é‡
    },
  }
}
```
### 3. æŠ¥å·¥å¼¹çª—打开时带出加放数默认值
```js
openReport(row) {
  // row ä¸º getProductWorkOrderById è¿”回的工单数据
  this.reportForm.quantity = null;
  this.reportForm.addQty = row.processRouteAddNum ? Number(row.processRouteAddNum) : 0;
  this.reportForm.scrapQty = 0;
  this.reportDialogVisible = true;
}
```
### 4. æŠ¥å·¥æäº¤å‚æ•°
调用 `POST /productionProductMain/addProductMain` æ—¶ï¼Œè¯·æ±‚体需包含加放数:
```js
const params = {
  workOrderId: this.reportForm.workOrderId,
  productProcessRouteItemId: this.reportForm.productProcessRouteItemId,
  quantity: this.reportForm.quantity,   // äº§å‡ºæ•°é‡
  addQty: this.reportForm.addQty,       // åŠ æ”¾æ•°
  scrapQty: this.reportForm.scrapQty,   // æŠ¥åºŸæ•°é‡
  teamList: this.teamList,
  deviceId: this.deviceId,
  deviceName: this.deviceName,
  startTime: this.startTime,
  endTime: this.endTime,
};
```
## æ³¨æ„äº‹é¡¹
- åŠ æ”¾æ•° `processRouteAddNum` ä¸ºå­—符串类型,前端需 `Number()` è½¬æ¢åŽå†ä½œä¸ºé»˜è®¤å€¼å¡«å…¥ã€‚
- æŠ¥å·¥æ•°é‡æ ¸ç®—分两个口径:
  - **工序流转 / æ€»äº§é‡**(不扣报废,保持连贯):工单完成数量、生产订单完成数量、各工序总产量统计、每日产出统计 = **产出数量 + åŠ æ”¾æ•°**。
  - **实际入库 / è´¨æ£€ / è®¡ä»¶å·¥èµ„**(扣报废):入库数量、质检数量、生产核算(计件工资)、每日工资统计 = **产出数量 + åŠ æ”¾æ•° - æŠ¥åºŸæ•°é‡**。
- æŠ¥å·¥æŠ¥åºŸï¼ˆscrapQty)仅做数据记录,不扣减工序流转基数,各工序总产量保持连贯。
- åŽ†å²æŠ¥å·¥æ•°æ®è‹¥ `add_qty` ä¸ºç©ºï¼ŒåŽç«¯æŒ‰ 0 å¤„理,不影响旧数据核算。
docs/sales_ledger_auto_invoice_receipt.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,109 @@
# æ–°å¢žé”€å”®å°è´¦è‡ªåŠ¨å¼€ç¥¨ã€è‡ªåŠ¨å›žæ¬¾
新增销售台账(页面新增、Excel å¯¼å…¥ï¼‰æ—¶ï¼ŒåŽç«¯è‡ªåŠ¨æŒ‰åˆåŒå…¨é¢ç”Ÿæˆå¼€ç¥¨ç™»è®°ä¸Žå›žæ¬¾ç™»è®°ï¼Œæ— éœ€å‰ç«¯å†å•ç‹¬è°ƒç”¨å¼€ç¥¨ã€å›žæ¬¾æŽ¥å£ã€‚
## æ¶‰åŠé¡µé¢
- é”€å”®å°è´¦åˆ—表页(新增/编辑弹窗、导入)
- å¼€ç¥¨ç™»è®°é¡µé¢
- å›žæ¬¾ç™»è®°é¡µé¢
- è´¢åŠ¡ç®¡ç† - æ”¶å…¥ç®¡ç†é¡µé¢
## API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /sales/ledger/addOrUpdateSalesLedger | æ–°å¢ž/修改销售台账(仅新增时触发自动开票、回款) |
| POST | /sales/ledger/import | å¯¼å…¥é”€å”®å°è´¦ï¼ˆæ¯æ¡å¯¼å…¥æˆåŠŸçš„å°è´¦å‡è§¦å‘è‡ªåŠ¨å¼€ç¥¨ã€å›žæ¬¾ï¼‰ |
**请求参数:** ä¸ŽåŽŸæœ‰å®Œå…¨ä¸€è‡´ï¼Œæ— æ–°å¢žå­—æ®µã€‚
`/sales/ledger/addOrUpdateSalesLedger` è¯·æ±‚示例:
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| id | Long | å¦ | ä¸ºç©º=新增(触发自动开票回款),非空=修改(不触发) |
| customerId | Long | æ˜¯ | å®¢æˆ·ID |
| projectName | String | å¦ | é¡¹ç›®åç§° |
| entryDate | String | å¦ | å½•入日期 yyyy-MM-dd |
| salesman | String | å¦ | ä¸šåŠ¡å‘˜ |
| entryPerson | String | å¦ | å½•入人 |
| type | Integer | æ˜¯ | å°è´¦ç±»åž‹ 1-销售 |
| productData | Array | æ˜¯ | äº§å“åˆ—表(quantity、taxInclusiveTotalPrice ç­‰ï¼ŒåŒåŽŸç»“æž„ï¼‰ |
| tempFileIds | Array | å¦ | ä¸´æ—¶é™„ä»¶ID列表 |
**响应:** `{ "code": 200, "msg": "操作成功" }`(结构不变)
## åŽç«¯è‡ªåŠ¨ç”Ÿæˆçš„æ•°æ®ï¼ˆå‰ç«¯æ— éœ€è°ƒç”¨æŽ¥å£ï¼‰
新增成功后,后端在同一事务内自动生成:
1. å¼€ç¥¨ç™»è®°ï¼š`invoice_registration` ä¸»è¡¨ + æ¯ä¸ªäº§å“ä¸€æ¡ `invoice_registration_product` + æ¯ä¸ªäº§å“ä¸€æ¡ `invoice_ledger`;
   - å¼€ç¥¨æ•° = äº§å“æ•°é‡ï¼Œå¼€ç¥¨é‡‘额 = äº§å“å«ç¨Žæ€»ä»·ï¼ˆå…¨é¢å¼€ç¥¨ï¼‰ï¼›
   - å‘票号自动生成,格式 `AUTO + æ—¶é—´æˆ³ + äº§å“ID`(如 `AUTO1755500000000123`);
   - å¼€ç¥¨æ—¶é—´ = å½“天,开票人 = å½“前登录人。
2. å›žæ¬¾ç™»è®°ï¼šæ¯ä¸ªäº§å“ä¸€æ¡ `receipt_payment`;
   - å›žæ¬¾é‡‘额 = å¼€ç¥¨å…¨é¢ï¼Œå›žæ¬¾å½¢å¼ = 0(电汇),来款日期 = å½“天;
   - å·²å…³è”对应的开票台账 `invoiceLedgerId`。
3. è´¢åŠ¡æ”¶å…¥ï¼šæ¯æ¡å›žæ¬¾å¯¹åº”ä¸€æ¡ `account_income`(收入类型=回款收入)。
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. æ–°å¢žå¼¹çª—:无需改动请求参数
新增保存成功后,建议提示语中说明已自动开票回款:
```html
<el-button type="primary" @click="submitForm">保 å­˜</el-button>
```
```js
submitForm() {
  addOrUpdateSalesLedger(this.form).then(res => {
    if (res.code === 200) {
      const msg = this.form.id ? '修改成功' : '新增成功,已自动全额开票并全额回款';
      this.$modal.msgSuccess(msg);
      this.open = false;
      this.getList();
    }
  });
}
```
### 2. åˆ—表编辑按钮:新增后默认不可编辑
后端列表接口 `/sales/ledger/listPage` è¿”回的 `isEdit` è§„则不变:只要存在开票或回款记录即为 `false`。由于新增即自动开票回款,新台账 `isEdit` æ’为 `false`,编辑按钮应禁用:
```html
<el-button
  size="mini"
  type="text"
  :disabled="scope.row.isEdit === false"
  @click="handleUpdate(scope.row)"
>修改</el-button>
```
```js
data() {
  return {
    // æ— éœ€æ–°å¢žå­—段,沿用 scope.row.isEdit
  }
}
```
如需修改已自动开票回款的台账,请走「删除」后重新新增(删除会级联清理开票、回款及财务收入记录)。
### 3. å¼€ç¥¨ç™»è®° / å›žæ¬¾ç™»è®°é¡µé¢
- å¼€ç¥¨ç™»è®°åˆ—表会出现自动生成的记录,发票号带 `AUTO` å‰ç¼€ï¼Œå¯åšå±•示区分;
- æ–°å¢žå°è´¦åŽï¼Œå¯¹åº”产品的「未开票数/未开票金额」为 0,开票登记弹窗中该产品不可再次开票(后端会校验超量);
- å›žæ¬¾ç™»è®°é¡µé¢åŒç†ï¼Œè‡ªåŠ¨å›žæ¬¾è®°å½•å·²å…³è”å‘ç¥¨å·ã€‚
## æ³¨æ„äº‹é¡¹
- ä»… `id` ä¸ºç©ºçš„æ–°å¢žä¼šè§¦å‘自动开票回款,修改台账不会触发。
- è‡ªåŠ¨å¼€ç¥¨å›žæ¬¾ä»…é’ˆå¯¹ `type=1`(销售)的产品。
- äº§å“æ•°é‡æˆ–含税总价为空/为 0 æ—¶è·³è¿‡è¯¥äº§å“çš„自动开票回款,不阻断台账新增。
- Excel å¯¼å…¥ä¸Žé¡µé¢æ–°å¢žè¡Œä¸ºä¸€è‡´ï¼Œå‡è‡ªåŠ¨å¼€ç¥¨å›žæ¬¾ã€‚
- åˆ é™¤é”€å”®å°è´¦ä¼šçº§è”删除自动生成的开票登记、开票台账、回款登记及对应财务收入记录。
- å°è´¦æ–°å¢žåŽå¦‚需调整,请删除重建,避免直接修改导致开票/回款数据不一致。
docs/seal_approval_unified_review.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,177 @@
# ç”¨å°å®¡æ‰¹æŽ¥å…¥ååŒç»Ÿä¸€å®¡æ ¸
## æ¶‰åŠé¡µé¢
- `src/views/collaborativeApproval/sealManagement/index.vue`(用印管理:移除页面内审批,申请/重新申请发起协同审批,支持附件上传)
- `src/api/collaborativeApproval/sealManagement.js`(新增 `getSealDetail` æŽ¥å£ï¼‰
- `src/views/collaborativeApproval/approvalProcess/index.vue`(协同审批:新增"用印审批"页签)
- `src/views/collaborativeApproval/approvalProcess/components/approvalDia.vue`(审批/详情弹框:展示用印详情与附件)
- `src/views/collaborativeApproval/approvalManagement/index.vue`(审批管理:新增"用印审批"类型配置)
## API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /common/uploadsList | é€šç”¨å¤šæ–‡ä»¶ä¸Šä¼ ï¼ˆæœ¬åœ°ç£ç›˜ï¼Œéž minio),返回文件列表 |
| POST | /sealApplicationManagement/add | æ–°å¢ž/重新申请用印,发起 type=9 ååŒå®¡æ‰¹ï¼Œä¿å­˜é™„ä»¶ |
| GET | /sealApplicationManagement/detail | æŒ‰å®¡æ‰¹æµç¨‹ id æŸ¥è¯¢ç”¨å°ç”³è¯·è¯¦æƒ…(含附件) |
| GET | /sealApplicationManagement/getList | åˆ†é¡µæŸ¥è¯¢ï¼ˆæ¯æ¡è®°å½•附带 commonFileList é™„件) |
| POST | /approveProcess/exportNine | ç”¨å°å®¡æ‰¹å¯¼å‡º |
### 1. ä¸Šä¼ é™„ä»¶
**POST /common/uploadsList**
- è¯·æ±‚:`multipart/form-data`,字段名 `files`(可多个)
- å“åº”:
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": [
    { "name": "原始文件名.pdf", "url": "http://host/profile/upload/xxx.pdf" }
  ]
}
```
### 2. æ–°å¢ž/重新申请用印
**POST /sealApplicationManagement/add**
请求参数(JSON):
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| id | Long | å¦ | å­˜åœ¨æ—¶ä¸ºé‡æ–°ç”³è¯·ï¼ˆå¤ç”¨æ—§å•重新发起审批) |
| applicationNum | String | æ˜¯ | ç”³è¯·ç¼–号 |
| title | String | æ˜¯ | ç”³è¯·æ ‡é¢˜ |
| sealType | String | æ˜¯ | ç”¨å°ç±»åž‹ï¼ˆofficial/contract/finance/legal) |
| reason | String | æ˜¯ | ç”³è¯·åŽŸå›  |
| urgency | String | å¦ | ç´§æ€¥ç¨‹åº¦ï¼ˆnormal/urgent/very-urgent) |
| deptId | Long | æ˜¯ | ç”³è¯·éƒ¨é—¨ |
| attachmentList | Array | å¦ | é™„ä»¶ `[{ name, url }]` |
说明:
- æœåŠ¡ç«¯å¼ºåˆ¶ `status = "pending"`,自动创建审批流程(`approveType = 9`),流程号存回 `approve_process_id`。
- é™„件写入 `common_file` è¡¨ï¼ˆ`common_id` = å®¡æ‰¹æµç¨‹ DB ä¸»é”®ï¼Œ`type` = 8 ApproveProcess)。
- é‡æ–°ç”³è¯·æ—¶ä¼šå…ˆæ¸…理旧审批流程关联的附件。
### 3. ç”¨å°è¯¦æƒ…
**GET /sealApplicationManagement/detail?approveProcessId=xxx**
- è¯·æ±‚参数:`approveProcessId`(审批流程 DB ä¸»é”®ï¼Œå³ååŒå®¡æ‰¹åˆ—表记录的 `id`)
- å“åº” data ä¸ºç”¨å°å•对象,含 `commonFileList` é™„件列表。
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. ç”¨å°ç®¡ç†é¡µï¼ˆsealManagement/index.vue)
- ç§»é™¤å®¡æ‰¹äººé€‰æ‹©ã€å®¡æ‰¹/拒绝操作按钮;操作列仅保留"重新申请"(状态为 rejected ä¸”为本人创建)与"详情"。
- é™„件上传改为原生 el-upload,指向 `/common/uploadsList`:
```html
<el-upload
  multiple
  :show-file-list="false"
  :action="sealUploadUrl"
  name="files"
  :headers="uploadHeaders"
  :limit="10"
  :before-upload="handleBeforeUpload"
  :on-success="handleUploadSuccess"
  :on-error="handleUploadError"
  :on-exceed="handleUploadExceed"
>
  <el-button type="primary">点击上传附件</el-button>
</el-upload>
```
```js
const sealUploadUrl = import.meta.env.VITE_APP_BASE_API + '/common/uploadsList'
const uploadHeaders = ref({ Authorization: 'Bearer ' + getToken() })
const sealForm = reactive({
  id: null, applicationNum: '', title: '', sealType: '',
  reason: '', urgency: 'normal', status: 'pending', attachmentList: []
})
// ä¸Šä¼ æˆåŠŸå›žè°ƒï¼šæŠŠ {name, url} è¿½åŠ è¿› attachmentList
const handleUploadSuccess = (res) => {
  if (res.code === 200) {
    const list = res.data || []
    list.forEach(item => sealForm.attachmentList.push({ name: item.name, url: item.url }))
    ElMessage.success('上传成功')
  } else {
    ElMessage.error(res.msg || '上传失败')
  }
}
```
- æäº¤ä¸Žé‡æ–°ç”³è¯·å‡è°ƒç”¨ `addSealApplication(sealForm)`;重新申请预填 `attachmentList: row.commonFileList || []`。
- é¢„览附件必须用完整 URL ç›´å¼€ï¼š`filePreviewRef.value.openUrl(url)`(不要用 `open`,会再拼 host)。
- ä¸‹è½½é™„件使用 `<a>` é”šç‚¹æ–¹å¼ï¼ˆ`download` æ’ä»¶æ—  `byUrl` æ–¹æ³•)。
### 2. API å°è£…(sealManagement.js)
```js
// ç”¨å°ç”³è¯·è¯¦æƒ…(按审批流程id)
export function getSealDetail(approveProcessId) {
  return request({
    url: "/sealApplicationManagement/detail",
    method: "get",
    params: { approveProcessId },
  });
}
```
### 3. ååŒå®¡æ‰¹é¡µï¼ˆapprovalProcess/index.vue)
- `approveTypes` æ–°å¢ž `{ value: '9', label: '用印审批', icon: 'Stamp', color: '#7D3C98' }`。
- ç±»åž‹ 9 éšè—"新增/删除"按钮(`v-if` æ¡ä»¶è¿½åŠ  `&& currentApproveType !== 9`)。
- ç±»åž‹ 9 çš„"编辑"按钮 `disabled`(与 5/6/7 ä¸€è‡´ï¼‰ã€‚
- ç±»åž‹ 9 ä¸å±•示"附件"操作按钮(附件走 common_file,不走 storage_attachment):
```js
if (!isQuotationType && currentApproveType.value !== 9) {
  actionOperations.push({ name: "附件", /* ... */ })
}
```
- å¯¼å‡ºæ˜ å°„追加:
```js
const urlMap = { /* ... */ 9: "/approveProcess/exportNine" }
const nameMap = { /* ... */ 9: "用印审批表" }
```
### 4. å®¡æ‰¹å¼¹æ¡†ï¼ˆapprovalDia.vue)
- æ–°å¢ž `isSealApproval = computed(() => Number(props.approveType) === 9)`。
- ç”¨å°å®¡æ‰¹æ‰“开弹框时,用 `row.id`(审批流程 DB ä¸»é”®ï¼‰è¯·æ±‚ `getSealDetail(row.id)`,渲染用印详情 + é™„件表格(预览/下载)。
```js
if (isSealApproval.value) {
  sealLoading.value = true
  getSealDetail(row.id).then((res) => {
    currentSeal.value = res.data || {}
  }).catch((err) => {
    proxy.$modal.msgError('查询用印详情失败')
  }).finally(() => {
    sealLoading.value = false
  })
}
```
- é™„件预览/下载与用印管理页一致(`openUrl` + é”šç‚¹ä¸‹è½½ï¼‰ã€‚
### 5. å®¡æ‰¹ç®¡ç†é…ç½®é¡µï¼ˆapprovalManagement/index.vue)
- `approveTypes`、`approveTypeNameMap`、`typeIconMap`、`typeColorMap` å‡è¿½åŠ  type 9(label ç”¨å°å®¡æ‰¹ï¼Œicon Stamp)。
## æ³¨æ„äº‹é¡¹
- é™„件上传不允许使用 minio,统一走 `/common/uploadsList`(本地磁盘存储)。
- ç”¨å°å•与审批流程通过 `seal_application_management.approve_process_id`(审批流程 DB ä¸»é”®ï¼‰å…³è”;协同审批列表中该记录的 `id` å³å®¡æ‰¹æµç¨‹ DB ä¸»é”®ã€‚
- ç”¨å°å®¡æ‰¹ç±»åž‹å€¼ä¸º `approveType = 9`;审批节点配置需在审批管理-用印审批页签下配置(DB åˆå§‹åŒ–脚本见 `sql/seal_application_add_approve_process.sql`)。
- å®¡æ‰¹é€šè¿‡/驳回后,服务端会回调更新用印单 `status`(approved/rejected/pending),前端列表刷新即可看到最新状态。
- `filePreview.open(url)` ä¼šåœ¨ URL å‰æ‹¼æŽ¥ `window.location.host`,传入完整 URL æ—¶å¿…须使用 `filePreview.openUrl(url)`。
- å‰ç«¯ `download` æ’件不存在 `byUrl` æ–¹æ³•,完整 URL ä¸‹è½½è¯·ä½¿ç”¨ `<a>` é”šç‚¹æ–¹å¼ã€‚
docs/stock_inventory_model_query.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,68 @@
# åº“存汇总列表规格型号模糊查询
## æ¶‰åŠé¡µé¢
- åº“存管理 - åº“存汇总列表
## API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /stockInventory/pageListCombinedStockInventory | åº“存汇总列表查询,新增规格型号模糊查询参数 |
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| model | String | å¦ | è§„格型号,模糊查询(匹配 product_model.model) |
| productName | String | å¦ | äº§å“åç§°ï¼Œæ¨¡ç³ŠæŸ¥è¯¢ |
| topParentProductId | Long | å¦ | é¡¶éƒ¨çˆ¶äº§å“ id |
**响应:** å“åº”结构不变,仍为 `StockInventoryDto` åˆ†é¡µåˆ—表,包含 `model`、`productName`、`qualifiedQuantity`、`unQualifiedQuantity` ç­‰å­—段。
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. æœç´¢æ¡ä»¶
在库存汇总列表页面的搜索表单中增加规格型号输入框:
```html
<el-form-item label="规格型号" prop="model">
  <el-input v-model="queryParams.model" placeholder="请输入规格型号" clearable />
</el-form-item>
```
### 2. data æ•°æ®
```js
data() {
  return {
    queryParams: {
      pageNum: 1,
      pageSize: 10,
      productName: undefined,
      model: undefined,
      topParentProductId: undefined,
    },
  }
}
```
### 3. æ–¹æ³•
```js
/** æŸ¥è¯¢åº“存汇总列表 */
function getList() {
  loading.value = true
  listStockInventory(queryParams.value).then(response => {
    stockList.value = response.rows
    total.value = response.total
    loading.value = false
  })
}
```
## æ³¨æ„äº‹é¡¹
- `model` ä¸ºå¯é€‰å‚数,不传时返回全部数据,不影响原有查询逻辑。
- è§„格型号模糊查询同时作用于合格库存(stock_inventory)和不合格库存(stock_uninventory)合并后的数据。
docs/work_order_batch_report_fix.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,55 @@
# å·¥å•分批报工修复
## é—®é¢˜æè¿°
报工一次后(完成数量未达计划数量):
1. å·¥å•「实际结束时间」被自动填充;
2. å·¥å•列表返回 `hasUnreportedMachine=true`,前端「报工」按钮被禁用,导致无法继续分批报工直至完成。
## ä¿®å¤å†…容(纯后端行为 + å‰ç«¯ä¸€è¡Œæ”¹åŠ¨ï¼Œæ— æ–°å¢ž/修改接口)
### 1. å®žé™…结束时间仅在完成后填充
`ProductionProductMainServiceImpl.addProductMain`:
- åŽŸé€»è¾‘ï¼šæ¯æ¬¡æŠ¥å·¥æ— æ¡ä»¶è®¾ç½®å·¥å• `actualEndTime` / `endProductTime` ä¸ºæŠ¥å·¥ç»“束时间;
- ä¿®æ”¹åŽï¼šä»…当 `completeQuantity >= planQuantity` æ—¶å¡«å……;未完成时显式清空 `actualEndTime`/`endProductTime`(自愈历史脏数据,`updateById` é»˜è®¤å¿½ç•¥ null å­—段,故用 `UpdateWrapper` æ˜¾å¼ç½®ç©ºï¼‰ã€‚
### 2. åˆ†æ‰¹æŠ¥å·¥æ ¸ç®—按每次报工独立生成
`ProductionProductMainServiceImpl.nextAddProductMain`:
- åŽŸé€»è¾‘ï¼šç”Ÿäº§æ ¸ç®—åœ¨ã€ŒæœªæŠ¥å·¥çš„ä¸Šæœºè®°å½•ã€å¾ªçŽ¯å†…ç”Ÿæˆï¼Œç¬¬ä¸€æ¬¡æŠ¥å·¥åŽä¸Šæœºè®°å½• `report_status=true`,第二次报工被 `continue` è·³è¿‡ï¼Œ**不会生成核算**;
- ä¿®æ”¹åŽï¼šæŠ¥å·¥ä¼ äº†ç­ç»„人员明细 `personList` æ—¶ï¼Œ**每次报工独立按人生成一条核算**(`finished_num` æŒ‰å„人毛产量占比分摊净值,`work_hour` å–录入时长),不再受上机记录报工状态影响;设备取报工单 `deviceId`,未填写时回退取该工单任一台机。
- æœªä¼  `personList`(旧数据)时仍回退按未报工的机台上机操作员生成核算,原逻辑保留。
### 3. å‰ç«¯æŠ¥å·¥æŒ‰é’®ä¸å†å—上机记录报工状态限制
`src/views/productionManagement/workOrder/index.vue`:
```js
// ä¿®æ”¹å‰
disabled: row =>
  row.completeQuantity >= row.planQuantity ||
  !canCurrentUserReport(row) ||
  row.hasUnreportedMachine,
// ä¿®æ”¹åŽ
disabled: row =>
  row.completeQuantity >= row.planQuantity ||
  !canCurrentUserReport(row),
```
「报工」按钮仅在「完成数量 >= è®¡åˆ’数量」或「当前用户不在工单/工序报工人中」时禁用,允许分批报工直至完成。
## æ•°æ®åº“行为验证
以工单 `DC26081690001`(计划 16037)为例,报工 100 åŽï¼š
- `actual_end_time` ä¸å†å¡«å……(`completeQuantity=100 < 16037`);
- å·¥å•列表 `has_unreported_machine` ä»ä¼šè®¡ç®—返回(前端已不再用于禁用报工按钮);
- ç»§ç»­æŠ¥å·¥æ—¶ï¼Œç¬¬äºŒæ¬¡æŠ¥å·¥ä¹Ÿä¼šæ­£å¸¸ç”Ÿæˆç”Ÿäº§æ ¸ç®—。
## æ¶‰åŠæ–‡ä»¶
- `src/main/java/com/ruoyi/production/service/impl/ProductionProductMainServiceImpl.java`
- `src/views/productionManagement/workOrder/index.vue`
sql/production_report_person_create.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
# æŠ¥å·¥ç­ç»„人员产量时长明细表
create table if not exists production_report_person
(
    id               bigint auto_increment primary key,
    product_main_id  bigint        not null comment '报工主表id(production_product_main.id)',
    user_id          bigint        not null comment '生产人id',
    user_name        varchar(50)   null comment '生产人姓名',
    quantity         decimal(16,4) not null default 0 comment '该人员产出数量',
    work_hour        int           not null default 0 comment '该人员生产时长(分钟)',
    tenant_id        bigint        null comment '租户id',
    create_time      datetime      null comment '创建时间',
    update_time      datetime      null comment '更新时间',
    create_user      bigint        null comment '创建人',
    update_user      bigint        null comment '更新人',
    dept_id          bigint        null comment '部门id'
) engine=innodb default charset=utf8mb4 comment='报工班组人员产量时长明细';
sql/quality_inspect_add_work_order_id.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,5 @@
# è´¨é‡æ£€éªŒè¡¨å¢žåŠ ç”Ÿäº§å·¥å•id,用于排产下达时预生成第一道工序质检单(此时无报工id)
alter table quality_inspect
    add column work_order_id bigint null comment '生产工单id(排产下达预生成质检单用)' after product_main_id;
create index idx_quality_inspect_work_order_id on quality_inspect (work_order_id);
sql/seal_application_add_approve_process.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
-- ç”¨å°ç”³è¯·ç®¡ç†æŽ¥å…¥ååŒç»Ÿä¸€å®¡æ ¸
-- ä½¿ç”¨å‰ç¡®è®¤å½“前数据库为业务库(如 product-inventory-management-ygcy)
-- 1. ç”¨å°ç”³è¯·å•增加关联协同审批流程id字段
ALTER TABLE seal_application_management
    ADD COLUMN approve_process_id bigint DEFAULT NULL COMMENT '关联协同审批流程id(approve_process.id)' AFTER approve_user_id;
-- 2. å®¡æ‰¹æµç¨‹é…ç½®ï¼šæ–°å¢žâ€œç”¨å°å®¡æ‰¹â€(approve_type=9)审批节点
--    é»˜è®¤å®¡æ‰¹äººï¼šç¬¬ä¸€çº§ ç®¡ç†éƒ¨(236),第二级 èµµæ‹¥å†›(214),可在【审批管理】菜单调整
INSERT INTO approve_process_config_node (approve_type, node_order, approver_id, approver_name, timeout_hours, tenant_id, create_user, create_time, update_user, update_time, delete_flag, dept_id)
SELECT 9, 1, 236, '管理部', NULL, 1, 1, NOW(), 1, NOW(), 0, NULL FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM approve_process_config_node WHERE approve_type = 9 AND delete_flag = 0);
INSERT INTO approve_process_config_node (approve_type, node_order, approver_id, approver_name, timeout_hours, tenant_id, create_user, create_time, update_user, update_time, delete_flag, dept_id)
SELECT 9, 2, 214, '赵拥军', NULL, 1, 1, NOW(), 1, NOW(), 0, NULL FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM approve_process_config_node WHERE approve_type = 9 AND node_order = 2 AND delete_flag = 0);
src/main/java/com/ruoyi/approve/controller/ApproveProcessController.java
@@ -188,4 +188,14 @@
        ExcelUtil<ApproveProcess> util = new ExcelUtil<ApproveProcess>(ApproveProcess.class);
        util.exportExcel(response, accountExpenses, "危险作业审批导出");
    }
    @ApiOperation(value = "用印审批导出")
    @PostMapping("/exportNine")
    public void exportNine(HttpServletResponse response) {
        List<ApproveProcess> accountExpenses = approveProcessService.list(new LambdaQueryWrapper<ApproveProcess>()
                .eq(ApproveProcess::getApproveDelete, 0)
                .eq(ApproveProcess::getApproveType, 9));
        ExcelUtil<ApproveProcess> util = new ExcelUtil<ApproveProcess>(ApproveProcess.class);
        util.exportExcel(response, accountExpenses, "用印审批导出");
    }
}
src/main/java/com/ruoyi/approve/pojo/ApproveNode.java
@@ -48,6 +48,11 @@
    private Integer approveNodeOrder;
    /**
     * å®¡æ‰¹æ¨¡å¼ï¼šor=或签,and=会签
     */
    private String approveMode;
    /**
     * å®¡æ‰¹èŠ‚ç‚¹ç”¨æˆ·åå­—
     */
    private String approveNodeUser;
src/main/java/com/ruoyi/approve/pojo/ApproveProcess.java
@@ -92,6 +92,21 @@
    private String approveUserCurrentName;
    /**
     * å½“前节点待审人ID(逗号分隔,或签/会签多人)
     */
    private String approveUserCurrentIds;
    /**
     * å½“前节点待审人姓名(逗号分隔)
     */
    private String approveUserCurrentNames;
    /**
     * èŠ‚ç‚¹ç»“æž„å¿«ç…§JSON:[{nodeOrder,mode,userIds,userNames}],用于编辑重建审批节点
     */
    private String approveNodeConfig;
    /**
     * ç”³è¯·æ—¥æœŸ
     */
    @JsonFormat(pattern = "yyyy-MM-dd")
src/main/java/com/ruoyi/approve/pojo/ApproveProcessConfigNode.java
@@ -49,6 +49,12 @@
    @ApiModelProperty("节点顺序")
    private Integer nodeOrder;
    /**
     * å®¡æ‰¹æ¨¡å¼ï¼šor=或签,and=会签
     */
    @ApiModelProperty("审批模式:or=或签,and=会签")
    private String approveMode;
    /**
     * å®¡æ‰¹äººIDs(多个用逗号分隔)
src/main/java/com/ruoyi/approve/service/IApproveNodeService.java
@@ -1,6 +1,7 @@
package com.ruoyi.approve.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.approve.bean.vo.ApproveProcessConfigNodeVo;
import com.ruoyi.approve.pojo.ApproveNode;
import java.io.IOException;
@@ -9,6 +10,11 @@
public interface IApproveNodeService extends IService<ApproveNode> {
    void initApproveNodes(String approveUserIds,String approveID,Long tenantId);
    /**
     * æŒ‰é…ç½®èŠ‚ç‚¹åˆ—è¡¨åˆå§‹åŒ–å®¡æ‰¹èŠ‚ç‚¹ï¼ˆæ”¯æŒåŒèŠ‚ç‚¹å¤šå®¡æ‰¹äººä¸Žæˆ–ç­¾/会签模式)
     */
    void initApproveNodes(List<ApproveProcessConfigNodeVo> config, String approveID, Long tenantId);
    /**
     * è¯¦æƒ…
     * @param id
src/main/java/com/ruoyi/approve/service/IApproveProcessService.java
@@ -15,8 +15,9 @@
    /**
     * æ·»åŠ å®¡æ‰¹æµç¨‹
     * @param approveProcessVO å®¡æ‰¹æµç¨‹VO对象
     * @return å®¡æ‰¹æµç¨‹ä¸»é”®id
     */
    void addApprove(ApproveProcessVO approveProcessVO) throws  Exception;
    Long addApprove(ApproveProcessVO approveProcessVO) throws  Exception;
    /**
     * æ ¹æ®éƒ¨é—¨id查询部门信息
     * @param deptIds éƒ¨é—¨ID数组
src/main/java/com/ruoyi/approve/service/impl/ApproveNodeServiceImpl.java
@@ -6,11 +6,14 @@
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.ApproveProcessConfigNodeVo;
import com.ruoyi.approve.mapper.ApproveNodeMapper;
import com.ruoyi.approve.mapper.ApproveProcessMapper;
import com.ruoyi.approve.pojo.ApproveNode;
import com.ruoyi.approve.pojo.ApproveProcess;
import com.ruoyi.approve.service.IApproveNodeService;
import com.ruoyi.collaborativeApproval.mapper.SealApplicationManagementMapper;
import com.ruoyi.collaborativeApproval.pojo.SealApplicationManagement;
import com.ruoyi.common.enums.FileNameType;
import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
import com.ruoyi.common.enums.StockOutQualifiedRecordTypeEnum;
@@ -47,10 +50,13 @@
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@@ -73,6 +79,7 @@
    private final QualityTestStandardMapper qualityTestStandardMapper;
    private final QualityTestStandardParamMapper qualityTestStandardParamMapper;
    private final QualityInspectParamMapper qualityInspectParamMapper;
    private final SealApplicationManagementMapper sealApplicationManagementMapper;
    public ApproveProcess getApproveById(String id) {
@@ -87,25 +94,69 @@
    @Override
    public void initApproveNodes(String approveUserIds, String approveID, Long tenantId) {
        if (approveUserIds == null || approveUserIds.isEmpty()) {
            return;
        }
        List<ApproveProcessConfigNodeVo> config = new ArrayList<>();
        String[] ids = approveUserIds.split(",");
        for (int i = 0; i < ids.length; i++) {
            if (ids[i] == null || ids[i].trim().isEmpty()) {
                continue;
            }
            ApproveProcessConfigNodeVo vo = new ApproveProcessConfigNodeVo();
            vo.setNodeOrder(i + 1);
            vo.setApproverId(Long.parseLong(ids[i].trim()));
            vo.setApproveMode("or");
            config.add(vo);
        }
        initApproveNodes(config, approveID, tenantId);
    }
    @Override
    public void initApproveNodes(List<ApproveProcessConfigNodeVo> config, String approveID, Long tenantId) {
        Long userId = SecurityUtils.getLoginUser().getUser().getUserId();
        String[] names = approveUserIds.split(",");
        for (int i = 0; i < names.length; i++) {
            SysUser sysUser = sysUserMapper.selectUserById(Long.parseLong(names[i]));
            if (sysUser == null) continue;
            ApproveNode approveNode = new ApproveNode();
            approveNode.setApproveProcessId(approveID);
            approveNode.setApproveNodeOrder(i + 1);
            approveNode.setApproveNodeUser(sysUser.getNickName());
            approveNode.setApproveNodeUserId(sysUser.getUserId());
            approveNode.setApproveNodeTime(new Date());
            approveNode.setApproveNodeStatus(0);
            approveNode.setTenantId(tenantId);
            approveNode.setDeleteFlag(0);
            approveNode.setCreateUser(userId);
            approveNode.setUpdateUser(userId);
            approveNode.setCreateTime(LocalDateTime.now());
            approveNode.setUpdateTime(LocalDateTime.now());
            approveNodeMapper.insert(approveNode);
        // æŒ‰èŠ‚ç‚¹é¡ºåºåˆ†ç»„ï¼Œä¿è¯åŒä¸€èŠ‚ç‚¹çš„å¤šå®¡æ‰¹äººå†™å…¥ç›¸åŒ approveNodeOrder
        Map<Integer, List<ApproveProcessConfigNodeVo>> groups = new LinkedHashMap<>();
        for (ApproveProcessConfigNodeVo vo : config) {
            if (vo.getApproverId() == null) {
                continue;
            }
            int order = vo.getNodeOrder() == null ? groups.size() + 1 : vo.getNodeOrder();
            groups.computeIfAbsent(order, k -> new ArrayList<>()).add(vo);
        }
        for (Map.Entry<Integer, List<ApproveProcessConfigNodeVo>> entry : groups.entrySet()) {
            Integer order = entry.getKey();
            String mode = entry.getValue().get(0).getApproveMode();
            if (mode == null || mode.isEmpty()) {
                mode = "or";
            }
            List<Long> seenIds = new ArrayList<>();
            for (ApproveProcessConfigNodeVo vo : entry.getValue()) {
                Long approverId = vo.getApproverId();
                if (seenIds.contains(approverId)) {
                    continue;
                }
                seenIds.add(approverId);
                SysUser sysUser = sysUserMapper.selectUserById(approverId);
                if (sysUser == null) {
                    continue;
                }
                ApproveNode approveNode = new ApproveNode();
                approveNode.setApproveProcessId(approveID);
                approveNode.setApproveNodeOrder(order);
                approveNode.setApproveMode(mode);
                approveNode.setApproveNodeUser(sysUser.getNickName());
                approveNode.setApproveNodeUserId(sysUser.getUserId());
                approveNode.setApproveNodeTime(new Date());
                approveNode.setApproveNodeStatus(0);
                approveNode.setTenantId(tenantId);
                approveNode.setDeleteFlag(0);
                approveNode.setCreateUser(userId);
                approveNode.setUpdateUser(userId);
                approveNode.setCreateTime(LocalDateTime.now());
                approveNode.setUpdateTime(LocalDateTime.now());
                approveNodeMapper.insert(approveNode);
            }
        }
    }
@@ -114,76 +165,56 @@
        LambdaQueryWrapper<ApproveNode> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(ApproveNode::getApproveProcessId, id);
        queryWrapper.eq(ApproveNode::getDeleteFlag, 0);
//        queryWrapper.eq(ApproveNode::getApproveNodeStatus, 0);
        List<ApproveNode> list = list(queryWrapper);
        // æŒ‰ç…§ approveNodeOrder å­—段升序排序
        list.sort(Comparator.comparingInt(ApproveNode::getApproveNodeOrder));
        LambdaQueryWrapper<ApproveProcess> approveProcessLambdaQueryWrapper = new LambdaQueryWrapper<>();
        approveProcessLambdaQueryWrapper.eq(ApproveProcess::getApproveId, id)
                .eq(ApproveProcess::getApproveDelete, 0)
//                .eq(ApproveProcess::getApproveStatus, 0)
                .last("limit 1");
        ApproveProcess approveProcess = approveProcessMapper.selectOne(approveProcessLambdaQueryWrapper);
        if (approveProcess != null && approveProcess.getApproveStatus() == 3) {
        // æŒ‰ approveNodeOrder å‡åºã€åŒèŠ‚ç‚¹æŒ‰ id å‡åºï¼Œä¿è¯åŒèŠ‚ç‚¹å¤šå®¡æ‰¹äººé¡ºåºç¨³å®š
        list.sort(Comparator.comparingInt(ApproveNode::getApproveNodeOrder).thenComparingLong(ApproveNode::getId));
        bindNodeUrl(list);
        ApproveProcess approveProcess = getApproveById(id);
        if (approveProcess == null || approveProcess.getApproveStatus() == 2
                || approveProcess.getApproveStatus() == 3 || approveProcess.getApproveStatus() == 4) {
            // æµç¨‹å·²å®Œæˆ/驳回/重新提交,全部只读
            return list;
        }
        Integer currentOrder = pendingOrder(id);
        Long loginUserId = SecurityUtils.getLoginUser().getUser().getUserId();
        boolean admin = SecurityUtils.isAdmin(loginUserId);
        boolean ishenSet = false;
        for (ApproveNode approveNode : list) {
            List<CommonFile> commonFiles = fileMapper.selectList(new LambdaQueryWrapper<CommonFile>()
                    .eq(CommonFile::getCommonId, approveNode.getId())
                    .eq(CommonFile::getType, FileNameType.ApproveNode.getValue()));
            if (!CollectionUtils.isEmpty(commonFiles)) {
                approveNode.setUrl(commonFiles.get(0).getUrl());
            // ç®¡ç†å‘˜ä¸å—审批人限制:代审当前节点首个待审记录(整节点代审由 updateApproveNode å®Œæˆï¼‰
            if (approveNode.getApproveNodeOrder().equals(currentOrder)
                    && approveNode.getApproveNodeStatus() == 0
                    && (admin || approveNode.getApproveNodeUserId().equals(loginUserId))
                    && !ishenSet) {
                approveNode.setIsShen(true);
                ishenSet = true;
            }
            if (approveNode.getApproveNodeStatus() == 1) {
                continue;
            }
            approveNode.setIsShen(true);
            break;
        }
        return list;
    }
    public void updateApproveProcessStatus(ApproveNode approveNode, Integer status) throws IOException {
        LambdaQueryWrapper<ApproveProcess> approveProcessLambdaQueryWrapper = new LambdaQueryWrapper<>();
        approveProcessLambdaQueryWrapper.eq(ApproveProcess::getApproveId, approveNode.getApproveProcessId())
                .eq(ApproveProcess::getApproveDelete, 0)
                .last("limit 1");
        ApproveProcess approveProcess = approveProcessMapper.selectOne(approveProcessLambdaQueryWrapper);
        if (approveProcess == null) throw new RuntimeException("审批不存在");
        LambdaQueryWrapper<ApproveNode> approveNodeLambdaQueryWrapper = new LambdaQueryWrapper<>();
        approveNodeLambdaQueryWrapper.eq(ApproveNode::getApproveProcessId, approveNode.getApproveProcessId())
                .eq(ApproveNode::getApproveNodeOrder, approveNode.getApproveNodeOrder() + 1)
                .eq(ApproveNode::getDeleteFlag, 0)
                .eq(ApproveNode::getApproveNodeStatus, 0)
                .last("limit 1");
        ApproveNode approveNode1 = approveNodeMapper.selectOne(approveNodeLambdaQueryWrapper);
        approveProcess.setApproveStatus(status);
        if (approveNode1 != null) {
            approveProcess.setApproveUserCurrentId(approveNode1.getApproveNodeUserId());
            approveProcess.setApproveUserCurrentName(approveNode1.getApproveNodeUser());
        }
        if (approveProcess.getApproveStatus().equals(2) || approveProcess.getApproveStatus().equals(3) || approveProcess.getApproveStatus().equals(4)) {
            approveProcess.setApproveOverTime(new Date());
        }
        approveProcessMapper.updateById(approveProcess);
        //采购审核
        if(approveProcess.getApproveType().equals(5)){
    /**
     * ä¸šåŠ¡å›žè°ƒï¼šæŒ‰å®¡æ‰¹ç±»åž‹æ›´æ–°å¯¹åº”ä¸šåŠ¡å•æ®çŠ¶æ€
     * status: 1=审核中(会签部分通过), 2=审核完成(同意), 3=审核未通过(拒绝)
     */
    private void applyBusinessStatus(ApproveProcess approveProcess, Integer status) {
        // é‡‡è´­å®¡æ‰¹
        if (approveProcess.getApproveType().equals(5)) {
            PurchaseLedger purchaseLedger = purchaseLedgerMapper.selectOne(new LambdaQueryWrapper<PurchaseLedger>()
                    .eq(PurchaseLedger::getPurchaseContractNumber, approveProcess.getApproveReason())
                    .last("limit 1"));
            if(purchaseLedger != null) {
            if (purchaseLedger != null) {
                if (status.equals(2)) {
                    // åŒæ„
                    // åŒæ„ï¼šç”Ÿæˆè´¨æ£€/入库
                    purchaseLedger.setApprovalStatus(3);
                    List<SalesLedgerProduct> salesLedgerProducts = salesLedgerProductMapper.selectList(new QueryWrapper<SalesLedgerProduct>()
                            .lambda().eq(SalesLedgerProduct::getSalesLedgerId, purchaseLedger.getId()).eq(SalesLedgerProduct::getType, 2));
                    for (SalesLedgerProduct salesLedgerProduct : salesLedgerProducts) {
                        // è´¨æ£€
                        if (salesLedgerProduct.getIsChecked()) {
                            addQualityInspect(purchaseLedger, salesLedgerProduct);                        }else {
                            //直接入库
                            stockUtils.addStock(salesLedgerProduct.getProductModelId(), salesLedgerProduct.getQuantity(), StockInQualifiedRecordTypeEnum.PURCHASE_STOCK_IN.getCode(), purchaseLedger.getId(),"-","-","-");
                            addQualityInspect(purchaseLedger, salesLedgerProduct);
                        } else {
                            // ç›´æŽ¥å…¥åº“
                            stockUtils.addStock(salesLedgerProduct.getProductModelId(), salesLedgerProduct.getQuantity(),
                                    StockInQualifiedRecordTypeEnum.PURCHASE_STOCK_IN.getCode(), purchaseLedger.getId(), "-", "-", "-");
                        }
                    }
                } else if (status.equals(3)) {
@@ -197,78 +228,265 @@
            }
        }
        // é”€å”®æŠ¥ä»·çŠ¶æ€ä¿®æ”¹
        if(approveProcess.getApproveType().equals(6)){
        if (approveProcess.getApproveType().equals(6)) {
            SalesQuotation salesQuote = salesQuotationMapper.selectOne(new LambdaQueryWrapper<SalesQuotation>()
                    .eq(SalesQuotation::getQuotationNo, approveProcess.getApproveReason())
                    .last("limit 1"));
            // åŒæ„
            if(status.equals(2) && salesQuote != null){
                salesQuote.setStatus("通过");
            }else if(status.equals(3) && salesQuote != null){
                salesQuote.setStatus("拒绝");
            }else if(status.equals(1) && salesQuote != null){
                salesQuote.setStatus("审核中");
            }
            salesQuotationMapper.updateById(salesQuote);
        }
        // å‡ºåº“审批修改
        if(approveProcess.getApproveType().equals(7)){
            String[] split = approveProcess.getApproveReason().split(":");
            ShippingInfo shippingInfo = shippingInfoMapper.selectOne(new LambdaQueryWrapper<ShippingInfo>()
                    .eq(ShippingInfo::getShippingNo, split[1])
                    .orderByDesc(ShippingInfo::getCreateTime)
                    .last("limit 1"));
            if(shippingInfo != null){
                if(status.equals(2)){
                    shippingInfo.setStatus("审核通过");
                }else if(status.equals(3)){
                    shippingInfo.setStatus("审核拒绝");
                }else if(status.equals(1)){
                    shippingInfo.setStatus("审核中");
            if (salesQuote != null) {
                if (status.equals(2)) {
                    salesQuote.setStatus("通过");
                } else if (status.equals(3)) {
                    salesQuote.setStatus("拒绝");
                } else if (status.equals(1)) {
                    salesQuote.setStatus("审核中");
                }
                shippingInfoMapper.updateById(shippingInfo);
                salesQuotationMapper.updateById(salesQuote);
            }
        }
        // ç»‘定附件
        if(!CollectionUtils.isEmpty(approveNode.getTempFileIds()) && approveNode.getApproveNodeStatus() == 1){
            tempFileService.migrateTempFilesToFormal(approveNode.getId(), approveNode.getTempFileIds(), FileNameType.ApproveNode.getValue());
        // å‡ºåº“审批状态修改
        if (approveProcess.getApproveType().equals(7)) {
            String[] split = approveProcess.getApproveReason().split(":");
            if (split.length > 1) {
                ShippingInfo shippingInfo = shippingInfoMapper.selectOne(new LambdaQueryWrapper<ShippingInfo>()
                        .eq(ShippingInfo::getShippingNo, split[1])
                        .orderByDesc(ShippingInfo::getCreateTime)
                        .last("limit 1"));
                if (shippingInfo != null) {
                    if (status.equals(2)) {
                        shippingInfo.setStatus("审核通过");
                    } else if (status.equals(3)) {
                        shippingInfo.setStatus("审核拒绝");
                    } else if (status.equals(1)) {
                        shippingInfo.setStatus("审核中");
                    }
                    shippingInfoMapper.updateById(shippingInfo);
                }
            }
        }
        // ç”¨å°å®¡æ‰¹çŠ¶æ€ä¿®æ”¹
        if (approveProcess.getApproveType().equals(9)) {
            SealApplicationManagement seal = sealApplicationManagementMapper.selectOne(new LambdaQueryWrapper<SealApplicationManagement>()
                    .eq(SealApplicationManagement::getApproveProcessId, approveProcess.getId())
                    .last("limit 1"));
            if (seal != null) {
                if (status.equals(2)) {
                    seal.setStatus("approved");
                } else if (status.equals(3)) {
                    seal.setStatus("rejected");
                } else if (status.equals(1)) {
                    seal.setStatus("pending");
                }
                sealApplicationManagementMapper.updateById(seal);
            }
        }
    }
    /**
     * å½“前待审批节点序号:所有未审批节点中最小的 approveNodeOrder
     */
    private Integer pendingOrder(String approveId) {
        return list(new LambdaQueryWrapper<ApproveNode>()
                .eq(ApproveNode::getApproveProcessId, approveId)
                .eq(ApproveNode::getDeleteFlag, 0)
                .eq(ApproveNode::getApproveNodeStatus, 0))
                .stream()
                .map(ApproveNode::getApproveNodeOrder)
                .min(Integer::compareTo)
                .orElse(null);
    }
    /**
     * æŸ¥è¯¢æŒ‡å®šæµç¨‹æŒ‡å®šèŠ‚ç‚¹çš„å…¨éƒ¨å®¡æ‰¹èŠ‚ç‚¹ï¼ˆåŒèŠ‚ç‚¹å¤šå®¡æ‰¹äººï¼ŒæŒ‰ id å‡åºï¼‰
     */
    private List<ApproveNode> nodesByOrder(String approveId, Integer order) {
        return list(new LambdaQueryWrapper<ApproveNode>()
                .eq(ApproveNode::getApproveProcessId, approveId)
                .eq(ApproveNode::getApproveNodeOrder, order)
                .eq(ApproveNode::getDeleteFlag, 0)
                .orderByAsc(ApproveNode::getId));
    }
    /**
     * è®¾ç½®æµç¨‹å½“前节点待审人(多值逗号分隔,兼容旧的单值字段)
     */
    private void setCurrentApprovers(ApproveProcess approveProcess, List<ApproveNode> nodes) {
        if (nodes == null || nodes.isEmpty()) {
            approveProcess.setApproveUserCurrentIds(null);
            approveProcess.setApproveUserCurrentNames(null);
            approveProcess.setApproveUserCurrentId(null);
            approveProcess.setApproveUserCurrentName(null);
            return;
        }
        approveProcess.setApproveUserCurrentIds(nodes.stream().map(ApproveNode::getApproveNodeUserId)
                .map(String::valueOf).collect(Collectors.joining(",")));
        approveProcess.setApproveUserCurrentNames(nodes.stream().map(ApproveNode::getApproveNodeUser)
                .collect(Collectors.joining(",")));
        approveProcess.setApproveUserCurrentId(nodes.get(0).getApproveNodeUserId());
        approveProcess.setApproveUserCurrentName(nodes.get(0).getApproveNodeUser());
    }
    /**
     * é€šçŸ¥å½“前节点全部待审人
     */
    private void notifyApproveUsers(List<Long> userIds, ApproveProcess approveProcess) {
        if (userIds == null || userIds.isEmpty()) {
            return;
        }
        List<Long> ids = userIds.stream().distinct().collect(Collectors.toList());
        if (approveProcess.getApproveType() == 8) {
            sysNoticeService.simpleNoticeByUser(approveProcessType(approveProcess.getApproveType()),
                    approveProcess.getApproveId() + "流程编号的审批需要您审核!!!!!",
                    ids,
                    "/safeProduction/safeWorkApproval?approveType=" + approveProcess.getApproveType() + "&approveId=" + approveProcess.getApproveId());
        } else {
            sysNoticeService.simpleNoticeByUser(approveProcessType(approveProcess.getApproveType()),
                    approveProcess.getApproveId() + "流程编号的审批需要您审核!!!!!",
                    ids,
                    "/collaborativeApproval/approvalProcess?approveType=" + approveProcess.getApproveType() + "&approveId=" + approveProcess.getApproveId());
        }
    }
    /**
     * å¡«å……审批节点附件 url
     */
    private void bindNodeUrl(List<ApproveNode> nodes) {
        if (CollectionUtils.isEmpty(nodes)) {
            return;
        }
        for (ApproveNode approveNode : nodes) {
            List<CommonFile> commonFiles = fileMapper.selectList(new LambdaQueryWrapper<CommonFile>()
                    .eq(CommonFile::getCommonId, approveNode.getId())
                    .eq(CommonFile::getType, FileNameType.ApproveNode.getValue()));
            if (!CollectionUtils.isEmpty(commonFiles)) {
                approveNode.setUrl(commonFiles.get(0).getUrl());
            }
        }
    }
    /**
     * ä¸´æ—¶é™„件迁移为正式附件
     */
    private void bindNodeFiles(ApproveNode approveNode, List<String> tempFileIds) throws IOException {
        if (!CollectionUtils.isEmpty(tempFileIds) && approveNode.getApproveNodeStatus() != null
                && approveNode.getApproveNodeStatus() == 1) {
            tempFileService.migrateTempFilesToFormal(approveNode.getId(), tempFileIds, FileNameType.ApproveNode.getValue());
        }
    }
    @Override
    public void updateApproveNode(ApproveNode approveNode) throws IOException {
        // å®¡æ‰¹èŠ‚ç‚¹çŠ¶æ€:1同意,2拒绝,0尚未审核
        switch (approveNode.getApproveNodeStatus()){
            case 1:
                updateApproveProcessStatus(approveNode, Boolean.TRUE.equals(approveNode.getIsLast()) ? 2 : 1);
                /*消息通知*/
                Integer nodeOrder = approveNode.getApproveNodeOrder();
                ApproveProcess approveProcess = approveProcessMapper.selectList(Wrappers.<ApproveProcess>lambdaQuery()
                        .eq(ApproveProcess::getApproveId, approveNode.getApproveProcessId())).get(0);
                if (approveProcess.getApproveUserIds().split(",").length > nodeOrder){
                    String id = approveProcess.getApproveUserIds().split(",")[nodeOrder];
                    if (approveProcess.getApproveType()==8){
                        sysNoticeService.simpleNoticeByUser(approveProcessType(approveProcess.getApproveType()),
                                approveProcess.getApproveId() + "流程编号的审批需要您审核!!!!!",
                                Arrays.asList(Long.valueOf(id)),
                                "/safeProduction/safeWorkApproval?approveType=" + approveProcess.getApproveType() + "&approveId=" + approveProcess.getApproveId());
                    }else {
                        sysNoticeService.simpleNoticeByUser(approveProcessType(approveProcess.getApproveType()),
                                approveProcess.getApproveId() + "流程编号的审批需要您审核!!!!!",
                                Arrays.asList(Long.valueOf(id)),
                                "/collaborativeApproval/approvalProcess?approveType=" + approveProcess.getApproveType() + "&approveId=" + approveProcess.getApproveId());
                    }
                }
                break;
            case 2:
                updateApproveProcessStatus(approveNode, 3);
                break;
            default:
                break;
        ApproveNode db = approveNodeMapper.selectById(approveNode.getId());
        if (db == null || db.getDeleteFlag() != null && db.getDeleteFlag() == 1) {
            throw new RuntimeException("审批节点不存在");
        }
        approveNode.setApproveNodeTime(new Date());
        approveNodeMapper.updateById(approveNode);
        if (db.getApproveNodeStatus() != null && db.getApproveNodeStatus() != 0) {
            throw new RuntimeException("该节点已审批,请勿重复操作");
        }
        Long loginUserId = SecurityUtils.getLoginUser().getUser().getUserId();
        boolean admin = SecurityUtils.isAdmin(loginUserId);
        // ç®¡ç†å‘˜ä¸å—审批人限制
        if (!admin && !db.getApproveNodeUserId().equals(loginUserId)) {
            throw new RuntimeException("无权审批该节点");
        }
        ApproveProcess approveProcess = getApproveById(db.getApproveProcessId());
        if (approveProcess == null) {
            throw new RuntimeException("审批流程不存在");
        }
        if (approveProcess.getApproveStatus() == null
                || (approveProcess.getApproveStatus() != 0 && approveProcess.getApproveStatus() != 1)) {
            throw new RuntimeException("当前流程状态不允许审批");
        }
        Integer curOrder = pendingOrder(db.getApproveProcessId());
        if (!db.getApproveNodeOrder().equals(curOrder)) {
            throw new RuntimeException("尚未轮到该节点审批");
        }
        db.setApproveNodeStatus(approveNode.getApproveNodeStatus());
        db.setApproveNodeReason(approveNode.getApproveNodeReason());
        db.setApproveNodeRemark(approveNode.getApproveNodeRemark());
        db.setApproveNodeTime(new Date());
        db.setUpdateUser(loginUserId);
        approveNodeMapper.updateById(db);
        // æ‹’绝:一票否决,立即驳回整个流程
        if (db.getApproveNodeStatus() != null && db.getApproveNodeStatus() == 2) {
            approveProcess.setApproveStatus(3);
            approveProcess.setApproveOverTime(new Date());
            approveProcessMapper.updateById(approveProcess);
            applyBusinessStatus(approveProcess, 3);
            bindNodeFiles(db, approveNode.getTempFileIds());
            return;
        }
        String mode = db.getApproveMode();
        if (mode == null || mode.isEmpty()) {
            mode = "or";
        }
        List<ApproveNode> sameOrder = nodesByOrder(db.getApproveProcessId(), db.getApproveNodeOrder());
        // ç®¡ç†å‘˜ä»£å®¡ï¼šå½“前节点其余待审记录一并通过
        if (admin) {
            for (ApproveNode n : sameOrder) {
                if (n.getApproveNodeStatus() == null || n.getApproveNodeStatus() == 0) {
                    n.setApproveNodeStatus(1);
                    n.setApproveNodeRemark("管理员代审通过");
                    n.setApproveNodeTime(new Date());
                    approveNodeMapper.updateById(n);
                }
            }
        }
        boolean released;
        if ("and".equals(mode)) {
            // ä¼šç­¾ï¼šèŠ‚ç‚¹å†…æ‰€æœ‰äººå‡åŒæ„æ‰æ”¾è¡Œ
            released = sameOrder.stream().allMatch(n -> n.getApproveNodeStatus() != null && n.getApproveNodeStatus() == 1);
        } else {
            // æˆ–签:节点内任一同意即放行
            released = sameOrder.stream().anyMatch(n -> n.getApproveNodeStatus() != null && n.getApproveNodeStatus() == 1);
        }
        // ä¼šç­¾æœªæ”¾è¡Œï¼šèŠ‚ç‚¹å†…ä»æœ‰å¾…å®¡äººï¼Œæµç¨‹ä¿æŒå®¡æ ¸ä¸­
        if (!released) {
            List<ApproveNode> pending = sameOrder.stream()
                    .filter(n -> n.getApproveNodeStatus() == null || n.getApproveNodeStatus() == 0)
                    .collect(Collectors.toList());
            approveProcess.setApproveStatus(1);
            setCurrentApprovers(approveProcess, pending);
            approveProcessMapper.updateById(approveProcess);
            applyBusinessStatus(approveProcess, 1);
            bindNodeFiles(db, approveNode.getTempFileIds());
            return;
        }
        // æˆ–签放行:同节点其余未审批记录标记为自动同意
        if (!"and".equals(mode)) {
            for (ApproveNode n : sameOrder) {
                if (n.getApproveNodeStatus() == null || n.getApproveNodeStatus() == 0) {
                    n.setApproveNodeStatus(1);
                    n.setApproveNodeRemark("或签:其他审批人已通过");
                    n.setApproveNodeTime(new Date());
                    approveNodeMapper.updateById(n);
                }
            }
        }
        List<ApproveNode> next = nodesByOrder(db.getApproveProcessId(), db.getApproveNodeOrder() + 1).stream()
                .filter(n -> n.getApproveNodeStatus() == null || n.getApproveNodeStatus() == 0)
                .collect(Collectors.toList());
        if (next.isEmpty()) {
            // æœ€åŽèŠ‚ç‚¹æ”¾è¡Œï¼šæµç¨‹å®Œæˆ
            approveProcess.setApproveStatus(2);
            approveProcess.setApproveOverTime(new Date());
            setCurrentApprovers(approveProcess, new ArrayList<>());
            approveProcessMapper.updateById(approveProcess);
            applyBusinessStatus(approveProcess, 2);
        } else {
            // æµè½¬ä¸‹ä¸€èŠ‚ç‚¹
            approveProcess.setApproveStatus(1);
            setCurrentApprovers(approveProcess, next);
            approveProcessMapper.updateById(approveProcess);
            applyBusinessStatus(approveProcess, 1);
            notifyApproveUsers(next.stream().map(ApproveNode::getApproveNodeUserId).collect(Collectors.toList()), approveProcess);
        }
        bindNodeFiles(db, approveNode.getTempFileIds());
    }
    @Override
@@ -304,6 +522,8 @@
                return "发货审批";
            case 8:
                return "危险作业审批";
            case 9:
                return "用印审批";
        }
        return null;
    }
src/main/java/com/ruoyi/approve/service/impl/ApproveProcessConfigNodeServiceImpl.java
@@ -27,7 +27,10 @@
    private final ApproveProcessConfigNodeMapper approveProcessConfigNodeMapper;
    @Override
    public List<ApproveProcessConfigNodeVo> listNode(Integer type) {
        List<ApproveProcessConfigNode> approveProcessConfigNodes = approveProcessConfigNodeMapper.selectList(new QueryWrapper<ApproveProcessConfigNode>().lambda().eq(ApproveProcessConfigNode::getApproveType, type).orderByAsc(ApproveProcessConfigNode::getNodeOrder));
        List<ApproveProcessConfigNode> approveProcessConfigNodes = approveProcessConfigNodeMapper.selectList(new QueryWrapper<ApproveProcessConfigNode>().lambda()
                .eq(ApproveProcessConfigNode::getApproveType, type)
                .orderByAsc(ApproveProcessConfigNode::getNodeOrder)
                .orderByAsc(ApproveProcessConfigNode::getId));
        return approveProcessConfigNodes.stream()
                .map(node -> {
                    ApproveProcessConfigNodeVo vo = new ApproveProcessConfigNodeVo();
@@ -43,6 +46,10 @@
        approveProcessConfigNodeMapper.delete(new QueryWrapper<ApproveProcessConfigNode>().lambda().eq(ApproveProcessConfigNode::getApproveType, approveProcessConfigNodes.get(0).getApproveType()));
        //新增 æ•°æ®
        for (ApproveProcessConfigNode node : approveProcessConfigNodes) {
            // é»˜è®¤æˆ–签模式,兼容未传 approve_mode çš„æ—§æ•°æ®
            if (node.getApproveMode() == null || node.getApproveMode().isEmpty()) {
                node.setApproveMode("or");
            }
            approveProcessConfigNodeMapper.insert(node);
        }
        return true;
src/main/java/com/ruoyi/approve/service/impl/ApproveProcessServiceImpl.java
@@ -16,6 +16,8 @@
import com.ruoyi.approve.service.ApproveProcessConfigNodeService;
import com.ruoyi.approve.service.IApproveNodeService;
import com.ruoyi.approve.service.IApproveProcessService;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.common.enums.FileNameType;
import com.ruoyi.common.utils.OrderUtils;
import com.ruoyi.common.utils.SecurityUtils;
@@ -50,6 +52,7 @@
@RequiredArgsConstructor
public class ApproveProcessServiceImpl extends ServiceImpl<ApproveProcessMapper, ApproveProcess> implements IApproveProcessService {
    private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd");
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    private final SysDeptMapper sysDeptMapper;
    private final IApproveNodeService approveNodeService;
@@ -65,10 +68,14 @@
    private final ApproveProcessConfigNodeService approveProcessConfigNodeService;
    @Override
    public void addApprove(ApproveProcessVO approveProcessVO) throws Exception {
    public Long addApprove(ApproveProcessVO approveProcessVO) throws Exception {
        SysUser sysUser = SecurityUtils.getLoginUser().getUser();
        SysDept sysDept = sysDeptMapper.selectDeptById(SecurityUtils.getLoginUser().getCurrentDeptId());
        List<ApproveProcessConfigNodeVo> list = approveProcessConfigNodeService.listNode( approveProcessVO.getApproveType());
        List<ApproveProcessConfigNodeVo> list = approveProcessConfigNodeService.listNode(approveProcessVO.getApproveType());
        // æŒ‰èŠ‚ç‚¹é¡ºåºåˆ†ç»„ï¼ˆåŒèŠ‚ç‚¹å¤šå®¡æ‰¹äººï¼‰
        List<List<ApproveProcessConfigNodeVo>> groups = groupByOrder(list);
        if (groups.isEmpty()) throw new RuntimeException("添加审批节点和人员");
        List<ApproveProcessConfigNodeVo> firstGroup = groups.get(0);
        List<Long> nodeIds = list.stream()
                .map(ApproveProcessConfigNodeVo::getApproverId)
                .collect(Collectors.toList());
@@ -104,40 +111,23 @@
        approveProcess.setApproveType(approveProcessVO.getApproveType());
        approveProcess.setCreateTime(LocalDateTime.now());
        approveProcess.setTenantId(approveProcessVO.getApproveDeptId());
        approveProcess.setApproveUserCurrentId(nodeIds.get(0));
        approveProcess.setApproveUserCurrentName(sysUsers
                .stream()
                .filter(SysUser -> SysUser.getUserId().equals(nodeIds.get(0)))
                .collect(Collectors.toList())
                .get(0)
                .getNickName());
        setFirstNodeApprovers(approveProcess, firstGroup, sysUsers);
        // è®¾ç½®çŠ¶æ€ä¸ºé‡æ–°æäº¤
        if (approveProcessVO.getId() != null) {
            ApproveProcess approveProcess1 = approveProcessMapper.selectById(approveProcessVO.getId());
            approveProcess1.setApproveStatus(4);
            approveProcessMapper.updateById(approveProcess1);
        }
        // èŠ‚ç‚¹ç»“æž„å¿«ç…§ï¼ˆä¾›ç¼–è¾‘é‡å»ºå®¡æ‰¹èŠ‚ç‚¹æ—¶ä¿åˆ†ç»„ä¸Žæ¨¡å¼ï¼‰
        approveProcess.setApproveNodeConfig(buildNodeConfig(list));
        save(approveProcess);
        //初始化审批节点
        String nodeIdStr = nodeIds.stream()
                .map(String::valueOf)
                .collect(Collectors.joining(","));
        approveNodeService.initApproveNodes(nodeIdStr, no, approveProcessVO.getApproveDeptId());
        //初始化审批节点(按配置节点分组,支持同节点多审批人/或签会签)
        approveNodeService.initApproveNodes(list, no, approveProcessVO.getApproveDeptId());
        // é™„件绑定
        tempFileService.migrateTempFilesToFormal(approveProcess.getId(), approveProcessVO.getTempFileIds(), FileNameType.ApproveProcess.getValue());
        /*消息通知*/
        Long id = nodeIds.get(0);
        if (approveProcess.getApproveType()==8){
            sysNoticeService.simpleNoticeByUser(approveProcessType(approveProcess.getApproveType()),
                    approveProcess.getApproveId() + "流程编号的审批需要您审核!!!!!",
                    Collections.singletonList(id),
                    "/safeProduction/safeWorkApproval?approveType=" + approveProcess.getApproveType() + "&approveId=" + approveProcess.getApproveId());
        }else {
            sysNoticeService.simpleNoticeByUser(approveProcessType(approveProcess.getApproveType()),
                    approveProcess.getApproveId() + "流程编号的审批需要您审核!!!!!",
                    Collections.singletonList(id),
                    "/collaborativeApproval/approvalProcess?approveType=" + approveProcess.getApproveType() + "&approveId=" + approveProcess.getApproveId());
        }
        /*消息通知(通知第一节点全部审批人)*/
        notifyApproveUsers(approveProcess, firstGroupUserIds(approveProcess));
        return approveProcess.getId();
    }
    @Override
@@ -297,112 +287,254 @@
                .last("limit 1");
        ApproveProcess approveProcess = approveProcessMapper.selectOne(approveProcessLambdaQueryWrapper);
        if (approveProcess == null) throw new RuntimeException("请选择审批人");
        String[] split = approveGetAndUpdateVo.getApproveUserIds().split(",");
        if (split.length == 0) {
        String newIds = approveGetAndUpdateVo.getApproveUserIds();
        if (newIds == null || newIds.trim().isEmpty()) {
            throw new RuntimeException("请选择审批人");
        }
        List<SysUser> sysUsers = sysUserMapper.selectUserByIds(Arrays.asList(split).stream().map(Long::parseLong).collect(Collectors.toList()));
        List<Long> newIdList = parseUserIds(newIds);
        List<SysUser> sysUsers = sysUserMapper.selectUserByIds(newIdList);
        if (CollectionUtils.isEmpty(sysUsers)) throw new RuntimeException("请选择审批人");
        //审核中不可以编辑审核人
        if (approveProcess.getApproveStatus() != 1) {
            approveProcess.setApproveUserCurrentId(Long.parseLong(split[0]));
            approveProcess.setApproveUserCurrentName(sysUsers.stream().filter(user -> user.getUserId().equals(Long.parseLong(split[0]))).collect(Collectors.toList()).get(0).getNickName());
        }
        approveProcess.setApproveUserIds(newIds);
        approveProcess.setApproveUserNames(sysUsers.stream().map(SysUser::getNickName).collect(Collectors.joining(",")));
        if (approveGetAndUpdateVo.getApproveStatus() != null) {
            approveProcess.setApproveStatus(approveGetAndUpdateVo.getApproveStatus());
        }
        approveProcess.setApproveUserIds(approveGetAndUpdateVo.getApproveUserIds());
        updateById(approveProcess);
        //修改审批人
        // å…ˆåˆ é™¤ åŽæ–°å¢ž
        LambdaQueryWrapper<ApproveNode> approveNodeLambdaQueryWrapper = new LambdaQueryWrapper<>();
        approveNodeLambdaQueryWrapper.eq(ApproveNode::getApproveProcessId, approveProcess.getApproveId())
                .eq(ApproveNode::getDeleteFlag, 0)
//                .eq(ApproveNode::getTenantId, SecurityUtils.getLoginUser().getTenantId())
                .orderByAsc(ApproveNode::getApproveNodeOrder);
        approveNodeMapper.delete(approveNodeLambdaQueryWrapper);
        approveNodeService.initApproveNodes(approveGetAndUpdateVo.getApproveUserIds(), approveProcess.getApproveId(), approveProcess.getTenantId());
        /*消息通知*/
        String id = approveProcess.getApproveUserIds().split(",")[0];
        if (approveProcess.getApproveType()==8){
            sysNoticeService.simpleNoticeByUser(approveProcessType(approveProcess.getApproveType()),
                    approveProcess.getApproveId() + "流程编号的审批需要您审核!!!!!",
                    Arrays.asList(Long.valueOf(id)),
                    "/safeProduction/safeWorkApproval?approveType=" + approveProcess.getApproveType() + "&approveId=" + approveProcess.getApproveId());
        }else {
            sysNoticeService.simpleNoticeByUser(approveProcessType(approveProcess.getApproveType()),
                    approveProcess.getApproveId() + "流程编号的审批需要您审核!!!!!",
                    Arrays.asList(Long.valueOf(id)),
                    "/collaborativeApproval/approvalProcess?approveType=" + approveProcess.getApproveType() + "&approveId=" + approveProcess.getApproveId());
        // å…ˆåˆ é™¤ åŽæ–°å¢žï¼ˆä¼˜å…ˆæŒ‰ approve_node_config å¿«ç…§ä¿åˆ†ç»„与模式)
        approveNodeMapper.delete(new LambdaQueryWrapper<ApproveNode>()
                .eq(ApproveNode::getApproveProcessId, approveProcess.getApproveId())
                .eq(ApproveNode::getDeleteFlag, 0));
        rebuildApproveNodes(approveProcess, newIds);
        // å®¡æ ¸ä¸­ä¸å¯ä»¥ç¼–辑审核人
        if (approveProcess.getApproveStatus() != 1) {
            refreshCurrentApprovers(approveProcess);
        } else {
            approveProcessMapper.updateById(approveProcess);
        }
        /*消息通知(通知第一节点全部审批人)*/
        notifyApproveUsers(approveProcess, firstGroupUserIds(approveProcess));
    }
    @Override
    public void updateByApproveId(ApproveGetAndUpdateVo approveGetAndUpdateVo) throws IOException {
        ApproveProcess approve = approveProcessMapper.selectById(approveGetAndUpdateVo.getId());
        Integer oldStatus = approve.getApproveStatus();
        BeanUtils.copyProperties(approveGetAndUpdateVo, approve);
        approve.setApproveUserIds(approveGetAndUpdateVo.getApproveUserIds());
        // BeanUtils ä¼šæ‹·è´ null,防止把审批状态覆盖为空
        if (approveGetAndUpdateVo.getApproveStatus() == null) {
            approve.setApproveStatus(oldStatus);
        }
        approve.setApproveReason(approveGetAndUpdateVo.getApproveReason());
        SysUser sysUser = sysUserMapper.selectUserById(approveGetAndUpdateVo.getApproveUser());
        String[] split = approveGetAndUpdateVo.getApproveUserIds().split(",");
        if (split.length == 0) {
        String newIds = approveGetAndUpdateVo.getApproveUserIds();
        if (newIds == null || newIds.trim().isEmpty()) {
            throw new RuntimeException("请选择审批人");
        }
        List<SysUser> sysUsers = sysUserMapper.selectUserByIds(Arrays.asList(split).stream().map(Long::parseLong).collect(Collectors.toList()));
        List<Long> newIdList = parseUserIds(newIds);
        List<SysUser> sysUsers = sysUserMapper.selectUserByIds(newIdList);
        if (CollectionUtils.isEmpty(sysUsers)) throw new RuntimeException("请选择审批人");
        SysUser sysUser = sysUserMapper.selectUserById(approveGetAndUpdateVo.getApproveUser());
        if (sysUser == null) throw new RuntimeException("申请人不存在");
        approve.setApproveUserName(sysUser.getNickName());
        approve.setApproveUser(sysUser.getUserId());
        //审核中不可以编辑审核人
        approve.setApproveUserIds(newIds);
        approve.setApproveUserNames(sysUsers.stream().map(SysUser::getNickName).collect(Collectors.joining(",")));
        //修改审批人:先删除后新增(优先按 approve_node_config å¿«ç…§ä¿åˆ†ç»„与模式)
        approveNodeMapper.delete(new LambdaQueryWrapper<ApproveNode>()
                .eq(ApproveNode::getApproveProcessId, approve.getApproveId())
                .eq(ApproveNode::getDeleteFlag, 0));
        rebuildApproveNodes(approve, newIds);
        // å®¡æ ¸ä¸­ä¸å¯ä»¥ç¼–辑审核人
        if (approve.getApproveStatus() != 1) {
            approve.setApproveUserCurrentId(Long.parseLong(split[0]));
            approve.setApproveUserCurrentName(sysUsers.stream().filter(user -> user.getUserId().equals(Long.parseLong(split[0]))).collect(Collectors.toList()).get(0).getNickName());
            refreshCurrentApprovers(approve);
        } else {
            approveProcessMapper.updateById(approve);
        }
        updateById(approve);
        //修改审批人
        // å…ˆåˆ é™¤ åŽæ–°å¢ž
        LambdaQueryWrapper<ApproveNode> approveNodeLambdaQueryWrapper = new LambdaQueryWrapper<>();
        approveNodeLambdaQueryWrapper.eq(ApproveNode::getApproveProcessId, approve.getApproveId())
                .eq(ApproveNode::getDeleteFlag, 0)
//                .eq(ApproveNode::getTenantId, SecurityUtils.getLoginUser().getTenantId())
                .orderByAsc(ApproveNode::getApproveNodeOrder);
        approveNodeMapper.delete(approveNodeLambdaQueryWrapper);
        approveNodeService.initApproveNodes(approveGetAndUpdateVo.getApproveUserIds(), approve.getApproveId(), approve.getTenantId());
//        int i = 0;
//        for (ApproveNode approveNode : list) {
//            int finalI = i;
//            if(i >= split.length){
//                approveNode.setDeleteFlag(1);
//            }else{
//                List<SysUser> collect = sysUsers.stream().filter(user -> user.getUserId().equals(Long.parseLong(split[finalI]))).collect(Collectors.toList());
//                if(CollectionUtils.isEmpty(collect)){
//                    throw new RuntimeException("请选择正确的审批人");
//                }
//                approveNode.setApproveNodeUserId(collect.get(0).getUserId());
//                approveNode.setApproveNodeUser(collect.get(0).getNickName());
//            }
//            approveNodeMapper.updateById(approveNode);
//            i++;
//        }
        tempFileService.migrateTempFilesToFormal(approve.getId(), approveGetAndUpdateVo.getTempFileIds(), FileNameType.ApproveProcess.getValue());
        /*消息通知*/
        String id = approve.getApproveUserIds().split(",")[0];
        if (approve.getApproveType()==8){
            sysNoticeService.simpleNoticeByUser(approveProcessType(approve.getApproveType()),
                    approve.getApproveId() + "流程编号的审批需要您审核!!!!!",
                    Arrays.asList(Long.valueOf(id)),
                    "/safeProduction/safeWorkApproval?approveType=" + approve.getApproveType() + "&approveId=" + approve.getApproveId());
        }else {
            sysNoticeService.simpleNoticeByUser(approveProcessType(approve.getApproveType()),
                    approve.getApproveId() + "流程编号的审批需要您审核!!!!!",
                    Arrays.asList(Long.valueOf(id)),
                    "/collaborativeApproval/approvalProcess?approveType=" + approve.getApproveType() + "&approveId=" + approve.getApproveId());
        }
        /*消息通知(通知第一节点全部审批人)*/
        notifyApproveUsers(approve, firstGroupUserIds(approve));
    }
    /**
     * æŒ‰èŠ‚ç‚¹é¡ºåºåˆ†ç»„ï¼ˆä¿åºï¼‰ï¼ŒåŒèŠ‚ç‚¹å¤šå®¡æ‰¹äººå½’å…¥ä¸€ç»„
     */
    private List<List<ApproveProcessConfigNodeVo>> groupByOrder(List<ApproveProcessConfigNodeVo> list) {
        Map<Integer, List<ApproveProcessConfigNodeVo>> map = new LinkedHashMap<>();
        for (ApproveProcessConfigNodeVo vo : list) {
            if (vo.getApproverId() == null) {
                continue;
            }
            int order = vo.getNodeOrder() == null ? map.size() + 1 : vo.getNodeOrder();
            map.computeIfAbsent(order, k -> new ArrayList<>()).add(vo);
        }
        return new ArrayList<>(map.values());
    }
    /**
     * è®¾ç½®æµç¨‹é¦–节点待审人(多值逗号分隔,兼容旧的单值字段)
     */
    private void setFirstNodeApprovers(ApproveProcess approveProcess, List<ApproveProcessConfigNodeVo> firstGroup, List<SysUser> sysUsers) {
        List<Long> ids = firstGroup.stream()
                .map(ApproveProcessConfigNodeVo::getApproverId)
                .distinct()
                .collect(Collectors.toList());
        Map<Long, String> nameMap = sysUsers.stream()
                .collect(Collectors.toMap(SysUser::getUserId, SysUser::getNickName, (a, b) -> a));
        List<String> names = ids.stream().map(nameMap::get).filter(Objects::nonNull).collect(Collectors.toList());
        approveProcess.setApproveUserCurrentIds(ids.stream().map(String::valueOf).collect(Collectors.joining(",")));
        approveProcess.setApproveUserCurrentNames(String.join(",", names));
        if (!ids.isEmpty()) {
            approveProcess.setApproveUserCurrentId(ids.get(0));
            approveProcess.setApproveUserCurrentName(names.isEmpty() ? null : names.get(0));
        }
    }
    /**
     * èŠ‚ç‚¹ç»“æž„å¿«ç…§JSON:[{nodeOrder,mode,userIds,userNames}],供编辑重建审批节点
     */
    private String buildNodeConfig(List<ApproveProcessConfigNodeVo> list) {
        try {
            List<Map<String, Object>> nodes = new ArrayList<>();
            for (List<ApproveProcessConfigNodeVo> group : groupByOrder(list)) {
                String mode = group.get(0).getApproveMode();
                if (mode == null || mode.isEmpty()) {
                    mode = "or";
                }
                List<Long> ids = group.stream().map(ApproveProcessConfigNodeVo::getApproverId).distinct().collect(Collectors.toList());
                List<SysUser> users = sysUserMapper.selectUserByIds(ids);
                Map<String, Object> node = new LinkedHashMap<>();
                node.put("nodeOrder", group.get(0).getNodeOrder());
                node.put("mode", mode);
                node.put("userIds", ids.stream().map(String::valueOf).collect(Collectors.joining(",")));
                node.put("userNames", users.stream().map(SysUser::getNickName).collect(Collectors.joining(",")));
                nodes.add(node);
            }
            return OBJECT_MAPPER.writeValueAsString(nodes);
        } catch (Exception e) {
            return null;
        }
    }
    private List<String> splitIds(String ids) {
        if (ids == null || ids.trim().isEmpty()) {
            return new ArrayList<>();
        }
        return Arrays.stream(ids.split(",")).map(String::trim).filter(s -> !s.isEmpty()).collect(Collectors.toList());
    }
    private List<Long> parseUserIds(String ids) {
        return splitIds(ids).stream().map(Long::parseLong).collect(Collectors.toList());
    }
    /**
     * é‡å»ºå®¡æ‰¹èŠ‚ç‚¹ï¼šä¼˜å…ˆæŒ‰ approve_node_config å¿«ç…§ä¿åˆ†ç»„与模式重建,
     * æ–°é€‰çš„æ‰å¹³å®¡æ‰¹äººä¸²æŒ‰èŠ‚ç‚¹æ§½ä½é¡ºåºä¾æ¬¡å¡«å……ï¼›æ— å¿«ç…§åˆ™å›žé€€æ‰å¹³é€»è¾‘ï¼ˆä¸€èŠ‚ç‚¹ä¸€äºº or)
     */
    private void rebuildApproveNodes(ApproveProcess approveProcess, String approveUserIds) {
        List<ApproveProcessConfigNodeVo> config = null;
        if (StringUtils.hasText(approveProcess.getApproveNodeConfig())) {
            config = buildConfigFromSnapshot(approveProcess.getApproveNodeConfig(), approveUserIds);
        }
        if (config == null) {
            approveNodeService.initApproveNodes(approveUserIds, approveProcess.getApproveId(), approveProcess.getTenantId());
        } else {
            approveNodeService.initApproveNodes(config, approveProcess.getApproveId(), approveProcess.getTenantId());
        }
    }
    private List<ApproveProcessConfigNodeVo> buildConfigFromSnapshot(String snapshotJson, String approveUserIds) {
        List<String> newIds = splitIds(approveUserIds);
        try {
            List<Map<String, Object>> nodes = OBJECT_MAPPER.readValue(snapshotJson,
                    new TypeReference<List<Map<String, Object>>>() {});
            List<ApproveProcessConfigNodeVo> config = new ArrayList<>();
            int cursor = 0;
            for (Map<String, Object> node : nodes) {
                Object orderObj = node.get("nodeOrder");
                int nodeOrder = orderObj instanceof Number ? ((Number) orderObj).intValue() : 0;
                String mode = node.get("mode") == null ? "or" : String.valueOf(node.get("mode"));
                List<String> snapshotIds = splitIds(node.get("userIds") == null ? null : String.valueOf(node.get("userIds")));
                int slot = Math.max(1, snapshotIds.size());
                for (int j = 0; j < slot; j++) {
                    String uid = cursor < newIds.size() ? newIds.get(cursor++) : (j < snapshotIds.size() ? snapshotIds.get(j) : null);
                    if (uid == null) {
                        continue;
                    }
                    ApproveProcessConfigNodeVo vo = new ApproveProcessConfigNodeVo();
                    vo.setNodeOrder(nodeOrder);
                    vo.setApproverId(Long.parseLong(uid));
                    vo.setApproveMode(mode);
                    config.add(vo);
                }
            }
            // æ–°é€‰å®¡æ‰¹äººè‹¥å¤šäºŽå¿«ç…§æ§½ä½ï¼Œè¿½åŠ åˆ°æœ€åŽä¸€ä¸ªèŠ‚ç‚¹
            while (cursor < newIds.size()) {
                ApproveProcessConfigNodeVo vo = new ApproveProcessConfigNodeVo();
                vo.setNodeOrder(nodes.isEmpty() ? 1 : ((Number) nodes.get(nodes.size() - 1).get("nodeOrder")).intValue());
                vo.setApproverId(Long.parseLong(newIds.get(cursor++)));
                vo.setApproveMode("or");
                config.add(vo);
            }
            return config;
        } catch (Exception e) {
            return null;
        }
    }
    /**
     * åˆ·æ–°æµç¨‹å½“前审批人为第一节点待审人(用于编辑后重建)
     */
    private void refreshCurrentApprovers(ApproveProcess approveProcess) {
        List<ApproveNode> first = approveNodeMapper.selectList(new LambdaQueryWrapper<ApproveNode>()
                .eq(ApproveNode::getApproveProcessId, approveProcess.getApproveId())
                .eq(ApproveNode::getApproveNodeOrder, 1)
                .eq(ApproveNode::getDeleteFlag, 0)
                .orderByAsc(ApproveNode::getId));
        if (first.isEmpty()) {
            approveProcess.setApproveUserCurrentIds(null);
            approveProcess.setApproveUserCurrentNames(null);
            approveProcess.setApproveUserCurrentId(null);
            approveProcess.setApproveUserCurrentName(null);
        } else {
            approveProcess.setApproveUserCurrentIds(first.stream()
                    .map(n -> String.valueOf(n.getApproveNodeUserId())).collect(Collectors.joining(",")));
            approveProcess.setApproveUserCurrentNames(first.stream()
                    .map(ApproveNode::getApproveNodeUser).collect(Collectors.joining(",")));
            approveProcess.setApproveUserCurrentId(first.get(0).getApproveNodeUserId());
            approveProcess.setApproveUserCurrentName(first.get(0).getApproveNodeUser());
        }
        approveProcessMapper.updateById(approveProcess);
    }
    private List<Long> firstGroupUserIds(ApproveProcess approveProcess) {
        List<ApproveNode> first = approveNodeMapper.selectList(new LambdaQueryWrapper<ApproveNode>()
                .eq(ApproveNode::getApproveProcessId, approveProcess.getApproveId())
                .eq(ApproveNode::getApproveNodeOrder, 1)
                .eq(ApproveNode::getDeleteFlag, 0)
                .orderByAsc(ApproveNode::getId));
        return first.stream().map(ApproveNode::getApproveNodeUserId).distinct().collect(Collectors.toList());
    }
    /**
     * é€šçŸ¥ç¬¬ä¸€èŠ‚ç‚¹å…¨éƒ¨å¾…å®¡äºº
     */
    private void notifyApproveUsers(ApproveProcess approveProcess, List<Long> userIds) {
        if (userIds == null || userIds.isEmpty()) {
            return;
        }
        List<Long> ids = userIds.stream().distinct().collect(Collectors.toList());
        if (approveProcess.getApproveType() == 8) {
            sysNoticeService.simpleNoticeByUser(approveProcessType(approveProcess.getApproveType()),
                    approveProcess.getApproveId() + "流程编号的审批需要您审核!!!!!",
                    ids,
                    "/safeProduction/safeWorkApproval?approveType=" + approveProcess.getApproveType() + "&approveId=" + approveProcess.getApproveId());
        } else {
            sysNoticeService.simpleNoticeByUser(approveProcessType(approveProcess.getApproveType()),
                    approveProcess.getApproveId() + "流程编号的审批需要您审核!!!!!",
                    ids,
                    "/collaborativeApproval/approvalProcess?approveType=" + approveProcess.getApproveType() + "&approveId=" + approveProcess.getApproveId());
        }
    }
    //审批类型获取(与前端页面对应)
    private String approveProcessType(Integer approveType) {
@@ -423,6 +555,8 @@
                return "发货审批";
            case 8:
                return "危险作业审批";
            case 9:
                return "用印审批";
        }
        return null;
    }
src/main/java/com/ruoyi/collaborativeApproval/controller/SealApplicationManagementController.java
@@ -1,25 +1,31 @@
package com.ruoyi.collaborativeApproval.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.approve.pojo.KnowledgeBase;
import com.ruoyi.approve.bean.vo.ApproveProcessVO;
import com.ruoyi.approve.service.IApproveProcessService;
import com.ruoyi.collaborativeApproval.dto.SealApplicationManagementDTO;
import com.ruoyi.collaborativeApproval.pojo.SealApplicationManagement;
import com.ruoyi.collaborativeApproval.service.SealApplicationManagementService;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.enums.FileNameType;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.project.system.service.ISysNoticeService;
import com.ruoyi.sales.mapper.CommonFileMapper;
import com.ruoyi.sales.pojo.CommonFile;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;
import java.util.Map;
@AllArgsConstructor
@RestController
@@ -30,24 +36,65 @@
    private SealApplicationManagementService sealApplicationManagementService;
    @Autowired
    private ISysNoticeService sysNoticeService;
    private IApproveProcessService approveProcessService;
    @Autowired
    private CommonFileMapper commonFileMapper;
    @GetMapping("/getList")
    @ApiOperation("分页查询")
    public AjaxResult listPage(Page page, SealApplicationManagement sealApplicationManagement){
        return AjaxResult.success(sealApplicationManagementService.listPage(page,sealApplicationManagement));
        IPage<SealApplicationManagementDTO> result = sealApplicationManagementService.listPage(page,sealApplicationManagement);
        for (SealApplicationManagementDTO record : result.getRecords()) {
            if (record.getApproveProcessId() != null) {
                record.setCommonFileList(commonFileMapper.selectList(new LambdaQueryWrapper<CommonFile>()
                        .eq(CommonFile::getCommonId, record.getApproveProcessId())
                        .eq(CommonFile::getType, FileNameType.ApproveProcess.getValue())));
            }
        }
        return AjaxResult.success(result);
    }
    @PostMapping("/add")
    @ApiOperation("新增")
    public AjaxResult add(@RequestBody SealApplicationManagement sealApplicationManagement){
        sealApplicationManagementService.save(sealApplicationManagement);
        //消息通知
        sysNoticeService.simpleNoticeByUser("用印审批",
                "申请编号:"+sealApplicationManagement.getApplicationNum()+"\t"
                +"申请标题:"+sealApplicationManagement.getTitle(),
                Arrays.asList(sealApplicationManagement.getApproveUserId()),
                "/collaborativeApproval/sealManagement?applicationNum="+sealApplicationManagement.getApplicationNum());
    @ApiOperation("新增/重新申请用印")
    @Transactional(rollbackFor = Exception.class)
    public AjaxResult add(@RequestBody SealApplicationManagement sealApplicationManagement) throws Exception {
        sealApplicationManagement.setStatus("pending");
        if (sealApplicationManagement.getId() == null) {
            sealApplicationManagementService.save(sealApplicationManagement);
        } else {
            // é‡æ–°ç”³è¯·ï¼šæ¸…理旧审批流程关联的附件,避免遗留
            SealApplicationManagement old = sealApplicationManagementService.getById(sealApplicationManagement.getId());
            if (old != null && old.getApproveProcessId() != null) {
                commonFileMapper.delete(new LambdaQueryWrapper<CommonFile>()
                        .eq(CommonFile::getCommonId, old.getApproveProcessId())
                        .eq(CommonFile::getType, FileNameType.ApproveProcess.getValue()));
            }
            sealApplicationManagementService.updateById(sealApplicationManagement);
        }
        // å‘起协同统一审核流程(用印审批)
        ApproveProcessVO vo = new ApproveProcessVO();
        vo.setApproveType(9);
        vo.setApproveReason(sealApplicationManagement.getApplicationNum() + ":" + sealApplicationManagement.getTitle());
        vo.setApproveDeptId(sealApplicationManagement.getDeptId());
        vo.setApproveTime(new SimpleDateFormat("yyyy-MM-dd").format(new Date()));
        Long approveProcessId = approveProcessService.addApprove(vo);
        // å›žå¡«å®¡æ‰¹æµç¨‹id,供审批回调更新状态、详情查询附件
        sealApplicationManagement.setApproveProcessId(approveProcessId);
        sealApplicationManagementService.updateById(sealApplicationManagement);
        // ä¿å­˜ç”¨å°ç”³è¯·é™„件,关联到审批流程
        if (!CollectionUtils.isEmpty(sealApplicationManagement.getAttachmentList())) {
            for (CommonFile cf : sealApplicationManagement.getAttachmentList()) {
                CommonFile record = new CommonFile();
                record.setCommonId(approveProcessId);
                record.setName(cf.getName());
                record.setUrl(cf.getUrl());
                record.setType(FileNameType.ApproveProcess.getValue());
                record.setCreateTime(LocalDateTime.now());
                record.setUpdateTime(LocalDateTime.now());
                commonFileMapper.insert(record);
            }
        }
        return AjaxResult.success();
    }
@@ -66,6 +113,21 @@
        return AjaxResult.success(sealApplicationManagementService.removeBatchByIds(ids));
    }
    @GetMapping("/detail")
    @ApiOperation("用印申请详情(按审批流程id)")
    public AjaxResult detail(Long approveProcessId){
        SealApplicationManagement seal = sealApplicationManagementService.getOne(new LambdaQueryWrapper<SealApplicationManagement>()
                .eq(SealApplicationManagement::getApproveProcessId, approveProcessId)
                .last("limit 1"));
        if (seal == null) {
            throw new RuntimeException("用印申请不存在");
        }
        seal.setCommonFileList(commonFileMapper.selectList(new LambdaQueryWrapper<CommonFile>()
                .eq(CommonFile::getCommonId, approveProcessId)
                .eq(CommonFile::getType, FileNameType.ApproveProcess.getValue())));
        return AjaxResult.success(seal);
    }
    @ApiOperation(value = "用印申请管理导出")
    @PostMapping("/export")
    public void export(HttpServletResponse response) {
src/main/java/com/ruoyi/collaborativeApproval/pojo/SealApplicationManagement.java
@@ -3,11 +3,13 @@
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import com.ruoyi.sales.pojo.CommonFile;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import java.util.List;
@Data
@TableName("seal_application_management")
@@ -56,6 +58,24 @@
     * å®¡æ‰¹äºº
     */
    private Long approveUserId;
    /**
     * å…³è”的协同审批流程主键id(approve_process.id)
     */
    @ApiModelProperty("协同审批流程id")
    private Long approveProcessId;
    /**
     * é™„件列表(提交用,来自前端 /common/upload ä¸Šä¼ ç»“果)
     */
    @TableField(exist = false)
    private List<CommonFile> attachmentList;
    /**
     * é™„件列表(查询展示用)
     */
    @TableField(exist = false)
    private List<CommonFile> commonFileList;
    /**
     * åˆ›å»ºè€…
     */
src/main/java/com/ruoyi/customervisits/service/impl/CustomerVisitsServiceImpl.java
@@ -38,6 +38,8 @@
            }
        }
        wrapper.orderByDesc(CustomerVisits::getPurposeDate);
        return customerVisitsMapper.selectPage(page, wrapper);
    }
src/main/java/com/ruoyi/production/dto/ProductWorkOrderDto.java
@@ -6,6 +6,7 @@
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.util.List;
@EqualsAndHashCode(callSuper = true)
@Data
@@ -55,4 +56,17 @@
    private Boolean hasUnreportedMachine;
    private String finalProductModel;
    //加放数
    @ApiModelProperty(value = "加放数")
    private String processRouteAddNum;
    @ApiModelProperty("报工班组默认成员(id+姓名)")
    private List<ReportPerson> reportPersons;
    @Data
    public static class ReportPerson {
        private Long userId;
        private String userName;
    }
}
src/main/java/com/ruoyi/production/dto/ProductionProductMainDto.java
@@ -83,8 +83,12 @@
    private Long deviceId;
    private String deviceName;
    private String workMinutes;
    private String personWorkMinutes;
    private List<Team> teamList;
    @ApiModelProperty("报工班组人员产量时长明细")
    private List<Person> personList;
@@ -97,4 +101,14 @@
        private Long userId;
        private String userName;
    }
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Person {
        private Long userId;
        private String userName;
        private BigDecimal quantity; // è¯¥äººå‘˜äº§é‡
        private Integer workHour;    // è¯¥äººå‘˜ç”Ÿäº§æ—¶é•¿(分钟)
    }
}
src/main/java/com/ruoyi/production/mapper/ProductionReportPersonMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,9 @@
package com.ruoyi.production.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.production.pojo.ProductionReportPerson;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ProductionReportPersonMapper extends BaseMapper<ProductionReportPerson> {
}
src/main/java/com/ruoyi/production/pojo/ProductionReportPerson.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,58 @@
package com.ruoyi.production.pojo;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * æŠ¥å·¥ç­ç»„人员产量时长明细
 */
@Data
@TableName("production_report_person")
@ApiModel(value = "ProductionReportPerson对象", description = "报工班组人员产量时长明细表")
public class ProductionReportPerson {
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @ApiModelProperty("报工主表id(production_product_main.id)")
    private Long productMainId;
    @ApiModelProperty("生产人id")
    private Long userId;
    @ApiModelProperty("生产人姓名")
    private String userName;
    @ApiModelProperty("该人员产出数量")
    private BigDecimal quantity;
    @ApiModelProperty("该人员生产时长(分钟)")
    private Integer workHour;
    @TableField(fill = FieldFill.INSERT)
    private Long tenantId;
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
}
src/main/java/com/ruoyi/production/service/ProductionProductMainService.java
@@ -24,4 +24,14 @@
    void nextAddProductMain(@NotNull ProductionProductOutput productionProductOutput);
    ArrayList<Long> listMain(List<Long> idList);
    /**
     * æŽ’产下达后,为第一道需质检工序预生成过程/出厂检验质检单
     */
    void preGenerateQualityInspectForOrder(Long productOrderId);
    /**
     * æ¸…理排产预生成的质检单(未关联报工)
     */
    void cleanPreGeneratedQualityInspects(List<Long> workOrderIds);
}
src/main/java/com/ruoyi/production/service/impl/ProcessRouteServiceImpl.java
@@ -52,6 +52,8 @@
    private ProductOrderService productOrderService;
    @Autowired
    private ProductProcessService productProcessService;
    @Autowired
    private ProductionProductMainService productionProductMainService;
    @Override
    public IPage<ProcessRouteDto> pageProcessRouteDto(Page<ProcessRouteDto> page, ProcessRouteDto processRouteDto) {
@@ -114,6 +116,8 @@
        // 6. æ–°å¢žç”Ÿäº§å·¥è‰ºè·¯çº¿å­è¡¨
        buildProductProcessRouteItems(processRouteAnticlockwiseDtos, productProcessRoute.getId(), productModelId, productOrderId);
        // æŽ’产下达后自动触发第一道需质检工序的过程检验
        productionProductMainService.preGenerateQualityInspectForOrder(productOrderId);
        return processRoute.getId();
    }
src/main/java/com/ruoyi/production/service/impl/ProductOrderServiceImpl.java
@@ -18,6 +18,7 @@
import com.ruoyi.production.service.ProductProcessService;
import com.ruoyi.production.service.ProductWorkOrderService;
import com.ruoyi.production.service.ProductionPrintOrderService;
import com.ruoyi.production.service.ProductionProductMainService;
import com.ruoyi.quality.mapper.QualityInspectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
@@ -68,6 +69,8 @@
    private SalesLedgerProductionAccountingMapper salesLedgerProductionAccountingMapper;
    @Autowired
    private StockUtils stockUtils;
    @Autowired
    private ProductionProductMainService productionProductMainService;
    @Override
    public IPage<ProductOrderDto> pageProductOrder(Page page, ProductOrderDto productOrder) {
@@ -116,6 +119,7 @@
            productProcessRouteItem.setProductOrderId(productOrder.getId());
            productProcessRouteItem.setProductRouteId(productProcessRoute.getId());
            productProcessRouteItem.setDragSort(processRouteItem.getDragSort());
            productProcessRouteItem.setIsQuality(processRouteItem.getIsQuality());
            int insert = productProcessRouteItemMapper.insert(productProcessRouteItem);
            if (insert > 0) {
@@ -129,6 +133,8 @@
                productWorkOrderMapper.insert(productWorkOrder);
            }
        }
        // æŽ’产下达后自动触发第一道需质检工序的过程检验
        productionProductMainService.preGenerateQualityInspectForOrder(productOrder.getId());
        return productOrderMapper.updateById(productOrder);
    }
@@ -166,6 +172,9 @@
            if (productionProductMains.size() > 0) {
                throw new RuntimeException("生产订单已经开始生产,不能删除");
            }
            // æ¸…理排产预生成的质检单
            productionProductMainService.cleanPreGeneratedQualityInspects(
                    productWorkOrders.stream().map(ProductWorkOrder::getId).collect(Collectors.toList()));
            //删除工单
            productWorkOrderMapper.delete(Wrappers.<ProductWorkOrder>lambdaQuery().in(ProductWorkOrder::getProductOrderId, ids));
        }
src/main/java/com/ruoyi/production/service/impl/ProductProcessRouteItemServiceImpl.java
@@ -48,6 +48,7 @@
    private ProductionProductMainMapper productionProductMainMapper;
    private SalesLedgerProductionAccountingMapper salesLedgerProductionAccountingMapper;
    private ProductionReportPersonMapper productionReportPersonMapper;
    private ProductWorkOrderMapper productWorkOrderMapper;
@@ -102,10 +103,15 @@
//                            .eq(QualityInspect::getProductMainId, mainId));
                    salesLedgerProductionAccountingMapper.delete(new LambdaQueryWrapper<SalesLedgerProductionAccounting>()
                            .eq(SalesLedgerProductionAccounting::getProductMainId, main.getId()));
                    productionReportPersonMapper.delete(new LambdaQueryWrapper<ProductionReportPerson>()
                            .eq(ProductionReportPerson::getProductMainId, main.getId()));
                }
            }
            // æŸ¥è¯¢è®¢å• + åˆ é™¤æ ¸ç®—
            // æ¸…理排产预生成的质检单
            productionProductMainService.cleanPreGeneratedQualityInspects(Collections.singletonList(workOrderId));
            // åˆ é™¤å…³è”工单
            productWorkOrderMapper.delete(new LambdaQueryWrapper<ProductWorkOrder>()
                    .eq(ProductWorkOrder::getProductProcessRouteItemId, routeItemId));
src/main/java/com/ruoyi/production/service/impl/ProductWorkOrderServiceImpl.java
@@ -16,9 +16,13 @@
import com.ruoyi.production.dto.ProductWorkOrderDto;
import com.ruoyi.production.mapper.ProductWorkOrderFileMapper;
import com.ruoyi.production.mapper.ProductWorkOrderMapper;
import com.ruoyi.production.mapper.ProductionMachineRecordMapper;
import com.ruoyi.production.pojo.ProductWorkOrder;
import com.ruoyi.production.pojo.ProductWorkOrderFile;
import com.ruoyi.production.pojo.ProductionMachineRecord;
import com.ruoyi.production.service.ProductWorkOrderService;
import com.ruoyi.project.system.domain.SysUser;
import com.ruoyi.project.system.mapper.SysUserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@@ -32,8 +36,10 @@
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Service
@Transactional(rollbackFor = Exception.class)
@@ -43,6 +49,10 @@
    private ProductWorkOrderMapper productWorkOrderMapper;
    @Autowired
    private ProductWorkOrderFileMapper productWorkOrderFileMapper;
    @Autowired
    private ProductionMachineRecordMapper productionMachineRecordMapper;
    @Autowired
    private SysUserMapper sysUserMapper;
    @Value("${file.temp-dir}")
    private String tempDir;
@@ -127,7 +137,43 @@
    @Override
    public ProductWorkOrderDto getProductWorkOrderById(Long id) {
        return productWorkOrderMapper.getProductWorkOrderFlowCard(id);
        ProductWorkOrderDto productWorkOrderDto = productWorkOrderMapper.getProductWorkOrderFlowCard(id);
        // åˆå¹¶å·¥å•排产人员与机台上机操作员,去重后作为报工弹窗默认班组成员
        Set<Long> userIdSet = new LinkedHashSet<>();
        if (productWorkOrderDto != null && StrUtil.isNotBlank(productWorkOrderDto.getUserIds())) {
            for (String uid : productWorkOrderDto.getUserIds().split(",")) {
                if (StrUtil.isNotBlank(uid)) {
                    userIdSet.add(Long.parseLong(uid.trim()));
                }
            }
        }
        List<ProductionMachineRecord> machineRecords = productionMachineRecordMapper.selectList(
                Wrappers.<ProductionMachineRecord>lambdaQuery()
                        .eq(ProductionMachineRecord::getWorkOrderId, id)
                        .isNotNull(ProductionMachineRecord::getOperatorId));
        for (ProductionMachineRecord machineRecord : machineRecords) {
            if (StrUtil.isNotBlank(machineRecord.getOperatorId())) {
                for (String uid : machineRecord.getOperatorId().split(",")) {
                    if (StrUtil.isNotBlank(uid)) {
                        userIdSet.add(Long.parseLong(uid.trim()));
                    }
                }
            }
        }
        List<ProductWorkOrderDto.ReportPerson> reportPersons = new ArrayList<>();
        for (Long userId : userIdSet) {
            ProductWorkOrderDto.ReportPerson reportPerson = new ProductWorkOrderDto.ReportPerson();
            reportPerson.setUserId(userId);
            SysUser sysUser = sysUserMapper.selectUserById(userId);
            if (sysUser != null) {
                reportPerson.setUserName(sysUser.getNickName());
            }
            reportPersons.add(reportPerson);
        }
        if (productWorkOrderDto != null) {
            productWorkOrderDto.setReportPersons(reportPersons);
        }
        return productWorkOrderDto;
    }
    @Override
src/main/java/com/ruoyi/production/service/impl/ProductionProductMainServiceImpl.java
@@ -90,6 +90,8 @@
    private SalesLedgerProductionAccountingMapper salesLedgerProductionAccountingMapper;
    private ProductionReportPersonMapper productionReportPersonMapper;
    private StockUtils stockUtils;
@@ -176,6 +178,26 @@
        productionProductMain.setAuditUserId(SecurityUtils.getUserId());
        productionProductMainMapper.insert(productionProductMain);
        /*保存报工班组人员产量时长明细*/
        List<ProductionProductMainDto.Person> personList = dto.getPersonList();
        BigDecimal personSum = BigDecimal.ZERO;
        if (CollUtil.isNotEmpty(personList)) {
            for (ProductionProductMainDto.Person person : personList) {
                BigDecimal personQty = person.getQuantity() == null ? BigDecimal.ZERO : person.getQuantity();
                personSum = personSum.add(personQty);
                ProductionReportPerson reportPerson = new ProductionReportPerson();
                reportPerson.setProductMainId(productionProductMain.getId());
                reportPerson.setUserId(person.getUserId());
                reportPerson.setUserName(person.getUserName());
                reportPerson.setQuantity(personQty);
                reportPerson.setWorkHour(person.getWorkHour() != null ? person.getWorkHour() : 0);
                productionReportPersonMapper.insert(reportPerson);
            }
        }
        BigDecimal totalQuantity = dto.getQuantity() == null ? BigDecimal.ZERO : dto.getQuantity();
        if (CollUtil.isNotEmpty(personList) && personSum.compareTo(totalQuantity) != 0) {
            throw new ServiceException("各班组产量之和必须等于报工总产量");
        }
        /*新增报工投入表*/
        List<ProductStructureDto> productStructureDtos = productStructureMapper.listBybomAndProcess(Optional.ofNullable(productProcessRoute.getBomId()).orElse(0), productProcess.getId());
        if (productStructureDtos.isEmpty()) {
@@ -202,9 +224,10 @@
        productionProductOutput.setReplenishQty(dto.getReplenishQty());
        productionProductOutput.setAddQty(dto.getAddQty() != null ? dto.getAddQty() : BigDecimal.ZERO);
        productionProductOutputMapper.insert(productionProductOutput);
        //合格数量=报工数量-报废数量
        BigDecimal productQty = productionProductOutput.getQuantity().subtract(productionProductOutput.getScrapQty());
        //只有合格数量>0才能增加相应数据
        //工序流转数量=产出数量+加放数(报废仅做数据记录,不扣减工序流转基数,各工序总产量保持连贯)
        BigDecimal addQty = productionProductOutput.getAddQty() == null ? BigDecimal.ZERO : productionProductOutput.getAddQty();
        BigDecimal productQty = productionProductOutput.getQuantity().add(addQty);
        //只有工序流转数量>0才能增加相应数据
        if (productQty.compareTo(BigDecimal.ZERO) > 0) {
            List<ProductProcessRouteItem> productProcessRouteItems = productProcessRouteItemMapper.selectList(Wrappers.<ProductProcessRouteItem>lambdaQuery().eq(ProductProcessRouteItem::getProductRouteId, productProcessRouteItem.getProductRouteId()));
            /*更新工单和生产订单*/
@@ -219,10 +242,18 @@
            if (!machineRecords.isEmpty()) {
                productWorkOrder.setActualStartTime(machineRecords.get(0).getMachineStartTime());
            }
            // å®žé™…结束时间 = æŠ¥å·¥ç»“束时间
            productWorkOrder.setActualEndTime(now);//实际结束时间为报工结束时间
            productWorkOrder.setEndProductTime(now);
            productWorkOrderMapper.updateById(productWorkOrder);
            // ä»…完成数量达到计划数量时才填充实际结束时间,未完成则清空已填的结束时间(允许继续分批报工)
            if (productWorkOrder.getCompleteQuantity().compareTo(productWorkOrder.getPlanQuantity()) >= 0) {
                productWorkOrder.setActualEndTime(now);
                productWorkOrder.setEndProductTime(now);
                productWorkOrderMapper.updateById(productWorkOrder);
            } else {
                // updateById é»˜è®¤å¿½ç•¥ null å­—段,需用 UpdateWrapper æ˜¾å¼æ¸…空(自愈历史脏数据)
                productWorkOrderMapper.update(null, Wrappers.<ProductWorkOrder>lambdaUpdate()
                        .eq(ProductWorkOrder::getId, productWorkOrder.getId())
                        .set(ProductWorkOrder::getActualEndTime, null)
                        .set(ProductWorkOrder::getEndProductTime, null));
            }
            //生产订单
            ProductOrder productOrder = productOrderMapper.selectById(productWorkOrder.getProductOrderId());
            if (ObjectUtils.isNull(productOrder.getStartTime())) {
@@ -281,10 +312,13 @@
                // å‡å°‘ æ›´æ–°å·¥å•和生产订单
                ProductWorkOrder productWorkOrder = productWorkOrderMapper.selectById(productionProductMain.getWorkOrderId());
                ProductOrder productOrder = productOrderMapper.selectById(productWorkOrder.getProductOrderId());
                //工序流转数量=产出数量+加放数(与报工累加口径一致,报废不扣减流转基数)
                BigDecimal processQty = productionProductOutput.getQuantity().add(
                        productionProductOutput.getAddQty() == null ? BigDecimal.ZERO : productionProductOutput.getAddQty());
                productWorkOrder.setCompleteQuantity(
                        productWorkOrder.getCompleteQuantity()
                                .subtract(productionProductOutput.getQuantity())
                                .subtract(processQty)
                                .max(BigDecimal.ZERO)
                );
                List<ProductProcessRouteItem> productProcessRouteItems = productProcessRouteItemMapper.selectList(Wrappers.<ProductProcessRouteItem>lambdaQuery().eq(ProductProcessRouteItem::getProductRouteId, productProcessRouteItem.getProductRouteId()));
@@ -294,16 +328,16 @@
                    //如果是最后一道工序报工之后生产订单 éœ€è¦æ‰£é™¤
                    productOrder.setCompleteQuantity(
                            productOrder.getCompleteQuantity()
                                    .subtract(productionProductOutput.getQuantity())
                                    .subtract(processQty)
                                    .max(BigDecimal.ZERO)
                    );
                    if (productOrder.getCompleteQuantity().compareTo(productionProductOutput.getQuantity()) > 0) {
                    if (productOrder.getCompleteQuantity().compareTo(processQty) > 0) {
                        productOrder.setEndTime(null);
                    }
                    productOrderMapper.updateById(productOrder);
                }
                if (productWorkOrder.getCompleteQuantity().compareTo(productionProductOutput.getQuantity()) > 0) {
                if (productWorkOrder.getCompleteQuantity().compareTo(processQty) > 0) {
                    productWorkOrder.setActualEndTime(null);
                }
@@ -326,9 +360,10 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void nextAddProductMain(@NotNull ProductionProductOutput productionProductOutput) {
        //合格数量=报工数量-报废数量
        //实际入库数量=产出数量+加放数-报废数量
        ProductionProductMain productionProductMain = productionProductMainMapper.selectById(productionProductOutput.getProductMainId());
        BigDecimal productQty = productionProductOutput.getQuantity().subtract(productionProductOutput.getScrapQty());
        BigDecimal addQty = productionProductOutput.getAddQty() == null ? BigDecimal.ZERO : productionProductOutput.getAddQty();
        BigDecimal productQty = productionProductOutput.getQuantity().add(addQty).subtract(productionProductOutput.getScrapQty());
        ProductProcessRouteItem productProcessRouteItem = productProcessRouteItemMapper.selectById(productionProductMain.getProductProcessRouteItemId());
        ProductProcess productProcess = productProcessMapper.selectById(productProcessRouteItem.getProcessId());
        //工艺路线中当前工序对应的产出规格型号
@@ -348,32 +383,24 @@
                    inspectType = 2;
                    process = null;
                }
                Product product = productMapper.selectById(productModel.getProductId());
                QualityInspect qualityInspect = new QualityInspect();
                qualityInspect.setProductId(product.getId());
                qualityInspect.setProductName(product.getProductName());
                qualityInspect.setModel(productModel.getModel());
                qualityInspect.setUnit(productModel.getUnit());
                qualityInspect.setQuantity(productQty);
                qualityInspect.setProcess(process);
                qualityInspect.setInspectState(0);
                qualityInspect.setInspectType(inspectType);
                qualityInspect.setProductMainId(productionProductMain.getId());
                qualityInspect.setProductModelId(productModel.getId());
                qualityInspectMapper.insert(qualityInspect);
                List<QualityTestStandard> qualityTestStandard = qualityTestStandardMapper.getQualityTestStandardByProductId(product.getId(), inspectType, process);
                if (!qualityTestStandard.isEmpty()) {
                    qualityInspect.setTestStandardId(qualityTestStandard.get(0).getId());
                    qualityInspectMapper.updateById(qualityInspect);
                    qualityTestStandardParamMapper.selectList(Wrappers.<QualityTestStandardParam>lambdaQuery()
                                    .eq(QualityTestStandardParam::getTestStandardId, qualityTestStandard.get(0).getId()))//默认获取最新的
                            .forEach(qualityTestStandardParam -> {
                                QualityInspectParam param = new QualityInspectParam();
                                BeanUtils.copyProperties(qualityTestStandardParam, param);
                                param.setId(null);
                                param.setInspectId(qualityInspect.getId());
                                qualityInspectParamMapper.insert(param);
                            });
                // æŽ’产下达时已为该工单预生成质检单(product_main_id IS NULL),报工审核时认领并更新为实际数量
                QualityInspect preInspect = qualityInspectMapper.selectOne(
                        Wrappers.<QualityInspect>lambdaQuery()
                                .eq(QualityInspect::getWorkOrderId, productionProductMain.getWorkOrderId())
                                .isNull(QualityInspect::getProductMainId)
                                .last("limit 1"));
                if (preInspect != null) {
                    preInspect.setProductMainId(productionProductMain.getId());
                    preInspect.setQuantity(productQty);
                    if (preInspect.getInspectState() != null && preInspect.getInspectState() == 1
                            && preInspect.getInspectType() != null && preInspect.getInspectType() == 2) {
                        // å·²æäº¤ä¸”为出厂检:删除按计划数量入库的记录,按实际数量重新入库
                        stockUtils.deleteStockInRecord(preInspect.getId(), StockInQualifiedRecordTypeEnum.QUALITYINSPECT_STOCK_IN.getCode());
                        stockUtils.addStock(productModel.getId(), productQty, StockInQualifiedRecordTypeEnum.QUALITYINSPECT_STOCK_IN.getCode(), preInspect.getId(), "-", "-", "-");
                    }
                    qualityInspectMapper.updateById(preInspect);
                } else {
                    insertQualityInspect(productProcessRouteItem, inspectType, process, productionProductMain.getId(), productionProductMain.getWorkOrderId(), productQty);
                }
            } else {
                //直接入库
@@ -386,7 +413,49 @@
            /*添加生产核算        åŒºåˆ†å·¥åºæ˜¯è®¡ä»¶è¿˜æ˜¯è®¡æ—¶*/
            BigDecimal workHours = productProcess.getSalaryQuota();
            List<ProductionMachineRecord> productionMachineRecords = productionMachineRecordMapper.selectList(Wrappers.<ProductionMachineRecord>lambdaQuery().eq(ProductionMachineRecord::getWorkOrderId, productionProductMain.getWorkOrderId()));
            if (ObjectUtils.isNotEmpty(productionMachineRecords)) {
            // æŠ¥å·¥ç­ç»„人员产量时长明细(有明细则按人生成核算,无明细则回退机台操作员逻辑)
            List<ProductionReportPerson> personList = productionReportPersonMapper.selectList(Wrappers.<ProductionReportPerson>lambdaQuery().eq(ProductionReportPerson::getProductMainId, productionProductMain.getId()));
            // è®¾å¤‡å–报工单设备,未填写时回退取该工单任一台机
            Long reportDeviceId = productionProductMain.getDeviceId() != null
                    ? productionProductMain.getDeviceId()
                    : productionMachineRecords.stream().map(ProductionMachineRecord::getMachineId).filter(Objects::nonNull).findFirst().orElse(null);
            if (CollUtil.isNotEmpty(personList)) {
                // æ¯æ¬¡æŠ¥å·¥ç‹¬ç«‹æŒ‰äººç”Ÿæˆæ ¸ç®—(支持分批报工直至完成),不受上机记录报工状态影响
                BigDecimal netQty = productQty;
                BigDecimal rawQuantity = productionProductOutput.getQuantity() == null ? BigDecimal.ZERO : productionProductOutput.getQuantity();
                for (ProductionReportPerson person : personList) {
                    BigDecimal personQty = person.getQuantity() == null ? BigDecimal.ZERO : person.getQuantity();
                    BigDecimal finishedNum;
                    if (netQty.compareTo(rawQuantity) == 0 || rawQuantity.compareTo(BigDecimal.ZERO) == 0) {
                        // æ— æŠ¥åºŸï¼ˆæˆ–毛产量为0)时净值即毛产量,直接取个人产量
                        finishedNum = personQty;
                    } else {
                        // æœ‰æŠ¥åºŸæ—¶æŒ‰å„人毛产量占比分摊净值,四舍五入保留2位
                        finishedNum = personQty.multiply(netQty).divide(rawQuantity, 2, BigDecimal.ROUND_HALF_UP);
                    }
                    SalesLedgerProductionAccounting salesLedgerProductionAccounting = SalesLedgerProductionAccounting.builder()
                            .productMainId(productionProductMain.getId())
                            .schedulingUserId(person.getUserId())
                            .schedulingUserName(person.getUserName())
                            .finishedNum(finishedNum)
                            .workHours(workHours)
                            .process(productProcess.getName())
                            .schedulingDate(LocalDate.now())
                            .tenantId(productionProductOutput.getTenantId())
                            .deviceId(reportDeviceId)
                            .workHour(person.getWorkHour() == null ? 0L : person.getWorkHour().longValue())
                            .build();
                    salesLedgerProductionAccountingMapper.insert(salesLedgerProductionAccounting);
                }
                // è¯¥å·¥å•上机记录本次已报工
                for (ProductionMachineRecord productionMachineRecord : productionMachineRecords) {
                    if (!productionMachineRecord.getReportStatus()) {
                        productionMachineRecord.setReportStatus(true);
                        productionMachineRecordMapper.updateById(productionMachineRecord);
                    }
                }
            } else if (ObjectUtils.isNotEmpty(productionMachineRecords)) {
                // å…¼å®¹æ—§æ•°æ®ï¼šæœªä¼ äººå‘˜æ˜Žç»†åˆ™å›žé€€æŒ‰æœªæŠ¥å·¥çš„æœºå°ä¸Šæœºæ“ä½œå‘˜ç”Ÿæˆæ ¸ç®—
                for (ProductionMachineRecord productionMachineRecord : productionMachineRecords) {
                    //说明已经添加过了,不添加了
                    if (productionMachineRecord.getReportStatus()) {
@@ -418,6 +487,115 @@
        }
    }
    /**
     * ç”Ÿæˆè´¨æ£€å•并回填检测标准/检验参数
     */
    private void insertQualityInspect(ProductProcessRouteItem routeItem, Integer inspectType, String process,
                                      Long productMainId, Long workOrderId, BigDecimal quantity) {
        ProductModel productModel = productModelMapper.selectById(routeItem.getProductModelId());
        Product product = productMapper.selectById(productModel.getProductId());
        QualityInspect qualityInspect = new QualityInspect();
        qualityInspect.setProductId(product.getId());
        qualityInspect.setProductName(product.getProductName());
        qualityInspect.setModel(productModel.getModel());
        qualityInspect.setUnit(productModel.getUnit());
        qualityInspect.setQuantity(quantity);
        qualityInspect.setProcess(process);
        qualityInspect.setInspectState(0);
        qualityInspect.setInspectType(inspectType);
        qualityInspect.setProductMainId(productMainId);
        qualityInspect.setWorkOrderId(workOrderId);
        qualityInspect.setProductModelId(productModel.getId());
        qualityInspectMapper.insert(qualityInspect);
        List<QualityTestStandard> qualityTestStandard = qualityTestStandardMapper.getQualityTestStandardByProductId(product.getId(), inspectType, process);
        if (!qualityTestStandard.isEmpty()) {
            qualityInspect.setTestStandardId(qualityTestStandard.get(0).getId());
            qualityInspectMapper.updateById(qualityInspect);
            qualityTestStandardParamMapper.selectList(Wrappers.<QualityTestStandardParam>lambdaQuery()
                            .eq(QualityTestStandardParam::getTestStandardId, qualityTestStandard.get(0).getId()))//默认获取最新的
                    .forEach(qualityTestStandardParam -> {
                        QualityInspectParam param = new QualityInspectParam();
                        BeanUtils.copyProperties(qualityTestStandardParam, param);
                        param.setId(null);
                        param.setInspectId(qualityInspect.getId());
                        qualityInspectParamMapper.insert(param);
                    });
        }
    }
    @Override
    public void preGenerateQualityInspectForOrder(Long productOrderId) {
        if (productOrderId == null) {
            return;
        }
        ProductProcessRoute productProcessRoute = productProcessRouteMapper.selectOne(
                Wrappers.<ProductProcessRoute>lambdaQuery()
                        .eq(ProductProcessRoute::getProductOrderId, productOrderId));
        if (productProcessRoute == null) {
            return;
        }
        List<ProductProcessRouteItem> items = productProcessRouteItemMapper.selectList(
                Wrappers.<ProductProcessRouteItem>lambdaQuery()
                        .eq(ProductProcessRouteItem::getProductRouteId, productProcessRoute.getId())
                        .orderByAsc(ProductProcessRouteItem::getDragSort)
                        .orderByAsc(ProductProcessRouteItem::getId));
        if (items.isEmpty()) {
            return;
        }
        // ç¬¬ä¸€é“工序需质检才预生成
        ProductProcessRouteItem first = items.get(0);
        if (!Boolean.TRUE.equals(first.getIsQuality())) {
            return;
        }
        ProductWorkOrder workOrder = productWorkOrderMapper.selectOne(
                Wrappers.<ProductWorkOrder>lambdaQuery()
                        .eq(ProductWorkOrder::getProductProcessRouteItemId, first.getId()));
        if (workOrder == null) {
            return;
        }
        // è¯¥å·¥å•已存在预生成质检单则不重复生成
        Long existCount = qualityInspectMapper.selectCount(
                Wrappers.<QualityInspect>lambdaQuery()
                        .eq(QualityInspect::getWorkOrderId, workOrder.getId())
                        .isNull(QualityInspect::getProductMainId));
        if (existCount != null && existCount > 0) {
            return;
        }
        boolean onlyOne = items.size() == 1;
        int inspectType = onlyOne ? 2 : 1;
        String process = null;
        if (!onlyOne) {
            ProductProcess processEntity = productProcessMapper.selectById(first.getProcessId());
            if (processEntity != null) {
                process = processEntity.getName();
            }
        }
        insertQualityInspect(first, inspectType, process, null, workOrder.getId(), workOrder.getPlanQuantity());
    }
    @Override
    public void cleanPreGeneratedQualityInspects(List<Long> workOrderIds) {
        if (CollUtil.isEmpty(workOrderIds)) {
            return;
        }
        List<QualityInspect> preInspects = qualityInspectMapper.selectList(
                Wrappers.<QualityInspect>lambdaQuery()
                        .in(QualityInspect::getWorkOrderId, workOrderIds)
                        .isNull(QualityInspect::getProductMainId));
        if (CollUtil.isEmpty(preInspects)) {
            return;
        }
        for (QualityInspect pre : preInspects) {
            if (pre.getInspectState() != null && pre.getInspectState() == 1) {
                throw new ServiceException("已提交的检验单不能删除");
            }
        }
        List<Long> inspectIds = preInspects.stream().map(QualityInspect::getId).collect(Collectors.toList());
        qualityInspectParamMapper.delete(
                Wrappers.<QualityInspectParam>lambdaQuery().in(QualityInspectParam::getInspectId, inspectIds));
        qualityInspectMapper.deleteBatchIds(inspectIds);
    }
    @Override
    public Boolean removeProductMain(Long id) {
        //判断该条报工是否不合格处理,如果不合格处理了,则不允许删除
@@ -438,17 +616,22 @@
                new LambdaQueryWrapper<SalesLedgerProductionAccounting>()
                        .eq(SalesLedgerProductionAccounting::getProductMainId, productionProductMain.getId())
        );
        /*删除报工班组人员产量时长明细*/
        productionReportPersonMapper.delete(
                new LambdaQueryWrapper<ProductionReportPerson>()
                        .eq(ProductionReportPerson::getProductMainId, productionProductMain.getId())
        );
        /*更新工单和生产订单*/
        ProductWorkOrder productWorkOrder = productWorkOrderMapper.selectById(productionProductMain.getWorkOrderId());
        if (productWorkOrder != null && productionProductOutput != null) {
            BigDecimal outputQty = productionProductOutput.getQuantity() == null ? BigDecimal.ZERO : productionProductOutput.getQuantity();
            BigDecimal scrapQty = productionProductOutput.getScrapQty() == null ? BigDecimal.ZERO : productionProductOutput.getScrapQty();
            BigDecimal addQty = productionProductOutput.getAddQty() == null ? BigDecimal.ZERO : productionProductOutput.getAddQty();
            BigDecimal completeQty = productWorkOrder.getCompleteQuantity() == null ? BigDecimal.ZERO : productWorkOrder.getCompleteQuantity();
            // å¿…须为审核通过的才会减少数量
            BigDecimal validQuantity = outputQty.subtract(scrapQty);
            BigDecimal validQuantity = outputQty.add(addQty);
            if (productionProductMain.getAuditStatus() != 2) {
                productWorkOrder.setCompleteQuantity(completeQty.subtract(validQuantity).max(BigDecimal.ZERO));
            }
@@ -464,8 +647,8 @@
            if (productOrder != null) {
                BigDecimal orderCompleteQty = productOrder.getCompleteQuantity() == null ? BigDecimal.ZERO : productOrder.getCompleteQuantity();
                BigDecimal totalQty = productionProductOutput.getQuantity() != null ? productionProductOutput.getQuantity() : BigDecimal.ZERO;
                BigDecimal scrapQty = productionProductOutput.getScrapQty() != null ? productionProductOutput.getScrapQty() : BigDecimal.ZERO;
                BigDecimal actualQualifiedQty = totalQty.subtract(scrapQty);
                BigDecimal addQty = productionProductOutput.getAddQty() != null ? productionProductOutput.getAddQty() : BigDecimal.ZERO;
                BigDecimal actualQualifiedQty = totalQty.add(addQty);
                BigDecimal newCompleteQty = orderCompleteQty.subtract(actualQualifiedQty);
                productOrder.setCompleteQuantity(newCompleteQty.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : newCompleteQty);
                productOrder.setEndTime(null);
src/main/java/com/ruoyi/project/common/CommonController.java
@@ -2,7 +2,9 @@
import java.io.File;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -153,6 +155,40 @@
    }
    /**
     * é€šç”¨ä¸Šä¼ è¯·æ±‚(多个,返回文件列表,供附件上传组件使用)
     */
    @PostMapping("/uploadsList")
    @ApiOperation(value = "通用上传请求(多个,返回文件列表)")
    public AjaxResult uploadsList(List<MultipartFile> files) throws Exception
    {
        try
        {
            if (files == null || files.isEmpty())
            {
                return AjaxResult.error("未选择文件");
            }
            // ä¸Šä¼ æ–‡ä»¶è·¯å¾„
            String filePath = RuoYiConfig.getUploadPath();
            List<Map<String, String>> result = new ArrayList<>();
            for (MultipartFile file : files)
            {
                // ä¸Šä¼ å¹¶è¿”回新文件名称
                String fileName = FileUploadUtils.upload(filePath, file);
                String url = serverConfig.getUrl() + fileName;
                Map<String, String> item = new LinkedHashMap<>();
                item.put("name", file.getOriginalFilename());
                item.put("url", url);
                result.add(item);
            }
            return AjaxResult.success(result);
        }
        catch (Exception e)
        {
            return AjaxResult.error(e.getMessage());
        }
    }
    /**
     * æœ¬åœ°èµ„源通用下载
     */
    @GetMapping("/download/resource")
src/main/java/com/ruoyi/quality/pojo/QualityInspect.java
@@ -145,6 +145,11 @@
     */
    private Long productMainId;
    /**
     * ç”Ÿäº§å·¥å•id(排产下达预生成质检单用,此时无报工id)
     */
    private Long workOrderId;
    private Long productModelId;
    //不合格现象
    private String defectivePhenomena;
src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java
@@ -163,18 +163,22 @@
            } else {
                // è¿‡ç¨‹æ£€éªŒ/出厂检验:通过关联关系查询销售台账的客户
                customerValue = inspect.getCustomer(); // é»˜è®¤ä½¿ç”¨ç›´æŽ¥å­˜å‚¨çš„客户
                Long workOrderId = inspect.getWorkOrderId();
                if (inspect.getProductMainId() != null) {
                    // é€šè¿‡æŠ¥å·¥ID查询工单
                    ProductionProductMain productMain = productionProductMainMapper.selectById(inspect.getProductMainId());
                    if (productMain != null && productMain.getWorkOrderId() != null) {
                        ProductWorkOrder workOrder = productWorkOrderMapper.selectById(productMain.getWorkOrderId());
                        if (workOrder != null && workOrder.getProductOrderId() != null) {
                            ProductOrder productOrder = productOrderMapper.selectById(workOrder.getProductOrderId());
                            if (productOrder != null && productOrder.getSalesLedgerId() != null) {
                                SalesLedger salesLedger = salesLedgerMapper.selectById(productOrder.getSalesLedgerId());
                                if (salesLedger != null && salesLedger.getCustomerName() != null) {
                                    customerValue = salesLedger.getCustomerName();
                                }
                    if (productMain != null) {
                        workOrderId = productMain.getWorkOrderId();
                    }
                }
                if (workOrderId != null) {
                    ProductWorkOrder workOrder = productWorkOrderMapper.selectById(workOrderId);
                    if (workOrder != null && workOrder.getProductOrderId() != null) {
                        ProductOrder productOrder = productOrderMapper.selectById(workOrder.getProductOrderId());
                        if (productOrder != null && productOrder.getSalesLedgerId() != null) {
                            SalesLedger salesLedger = salesLedgerMapper.selectById(productOrder.getSalesLedgerId());
                            if (salesLedger != null && salesLedger.getCustomerName() != null) {
                                customerValue = salesLedger.getCustomerName();
                            }
                        }
                    }
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerProductServiceImpl.java
@@ -18,6 +18,7 @@
import com.ruoyi.production.pojo.*;
import com.ruoyi.production.service.ProductProcessService;
import com.ruoyi.production.service.ProductWorkOrderService;
import com.ruoyi.production.service.ProductionProductMainService;
import com.ruoyi.production.service.impl.ProductOrderServiceImpl;
import com.ruoyi.purchase.mapper.PurchaseLedgerMapper;
import com.ruoyi.purchase.pojo.PurchaseLedger;
@@ -63,8 +64,10 @@
    private final ProductWorkOrderService productWorkOrderService;
    private final ProductProcessService productProcessService;
    private ProductionProductMainService productionProductMainService;
    private SalesLedgerProductMapper salesLedgerProductMapper;
    private SalesLedgerProductionAccountingMapper salesLedgerProductionAccountingMapper;
    private ProductionReportPersonMapper productionReportPersonMapper;
    private SalesLedgerMapper salesLedgerMapper;
    private PurchaseLedgerMapper purchaseLedgerMapper;
    private ProductOrderMapper productOrderMapper;
@@ -316,6 +319,7 @@
                productProcessRouteItem.setProductOrderId(productOrder.getId());
                productProcessRouteItem.setProductRouteId(productProcessRoute.getId());
                productProcessRouteItem.setDragSort(processRouteItem.getDragSort());
                productProcessRouteItem.setIsQuality(processRouteItem.getIsQuality());
                int insert = productProcessRouteItemMapper.insert(productProcessRouteItem);
                if (insert > 0) {
                    ProductWorkOrder productWorkOrder = new ProductWorkOrder();
@@ -329,6 +333,8 @@
                }
            }
            // æŽ’产下达后自动触发第一道需质检工序的过程检验
            productionProductMainService.preGenerateQualityInspectForOrder(productOrder.getId());
            productOrder.setRouteId(processRoute.getId());
            productOrderMapper.updateById(productOrder);
        }
@@ -400,9 +406,13 @@
                    qualityInspectMapper.deleteByProductMainIds(productMainIds);
                    salesLedgerProductionAccountingMapper.delete(new LambdaQueryWrapper<SalesLedgerProductionAccounting>()
                            .in(SalesLedgerProductionAccounting::getProductMainId, productMainIds));
                    productionReportPersonMapper.delete(new LambdaQueryWrapper<ProductionReportPerson>()
                            .in(ProductionReportPerson::getProductMainId, productMainIds));
                }
                productionProductMainMapper.deleteByWorkOrderIds(workOrderIds);
                // æ¸…理排产预生成的质检单
                productionProductMainService.cleanPreGeneratedQualityInspects(workOrderIds);
                productWorkOrderMapper.delete(new LambdaQueryWrapper<ProductWorkOrder>()
                        .in(ProductWorkOrder::getProductOrderId, orderIds));
src/main/resources/application-dev.yml
@@ -74,9 +74,9 @@
    druid:
      # ä¸»åº“数据源
      master:
        url: jdbc:mysql://localhost:3300/product-inventory-management-ygcy?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        url: jdbc:mysql://localhost:3306/product-inventory-management-ygcy?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: root
        password: root
        password: 123456
      # ä»Žåº“数据源
      slave:
        # ä»Žæ•°æ®æºå¼€å…³/默认关闭
@@ -154,7 +154,7 @@
    database: 0
    # å¯†ç 
    #    password: root2022!
    password: 123456
#    password:
    # è¿žæŽ¥è¶…æ—¶æ—¶é—´
    timeout: 10s
src/main/resources/mapper/production/ProductProcessMapper.xml
@@ -25,7 +25,7 @@
        pp.name AS processName,
        SUM(pi.quantity) AS totalInput,
        SUM(distinct ppo.scrap_qty) AS totalScrap,
        SUM(distinct (ppo.quantity - ppo.scrap_qty)) AS totalOutput
        SUM(distinct (ppo.quantity + IFNULL(ppo.add_qty, 0))) AS totalOutput
        FROM
        production_product_output ppo
        INNER JOIN production_product_main ppm ON ppo.product_main_id = ppm.id
src/main/resources/mapper/production/ProductWorkOrderMapper.xml
@@ -39,7 +39,8 @@
                END AS work_order_type,
            pp.device_name,
            pp.user_ids as process_user_ids,
            t8.product_name as final_product_model
            t8.product_name as final_product_model,
            ppri.process_route_add_num as processRouteAddNum
        FROM
            product_work_order pwo
                LEFT JOIN product_process_route_item ppri ON ppri.id = pwo.product_process_route_item_id
@@ -68,7 +69,8 @@
            sum(ppo.scrap_qty) scrapQty,
            pp.device_id,
            pp.device_name,
            t8.product_name as final_product_model
            t8.product_name as final_product_model,
            ppri.process_route_add_num as processRouteAddNum
        FROM
            product_work_order pwo
                LEFT JOIN product_process_route_item ppri ON ppri.id = pwo.product_process_route_item_id
@@ -88,7 +90,8 @@
                 pm.model,
                 pm.unit,
                 p.product_name,
                 po.nps_no
                 po.nps_no,
                 ppri.process_route_add_num
    </select>
    <select id="selectWorkOrderStartStats" resultType="com.ruoyi.production.dto.ProductWorkOrderDto">
        SELECT
src/main/resources/mapper/production/ProductionProductMainMapper.xml
@@ -76,11 +76,11 @@
        pm.model as product_model_name,
        pm.unit,
        slpa.process,
        ppo.quantity,
        COALESCE(prp.quantity, slpa.finished_num) AS quantity,
        slpa.work_hours,
        slpa.work_hours * slpa.finished_num AS wages,
        dl.device_name,
        slpa.work_hour as work_minutes,
        slpa.work_hour as person_work_minutes,
        IFNULL(
        TIMESTAMPDIFF(MINUTE, ppm.start_time, ppm.end_time),
        0
@@ -91,6 +91,7 @@
        FROM
        production_product_main ppm
        LEFT JOIN sales_ledger_production_accounting slpa ON slpa.product_main_id = ppm.id
        LEFT JOIN production_report_person prp ON prp.product_main_id = ppm.id AND prp.user_id = slpa.scheduling_user_id
        LEFT JOIN production_product_output ppo ON ppm.id = ppo.product_main_id
        LEFT JOIN product_work_order pwo ON pwo.id = ppm.work_order_id
        LEFT JOIN product_order po ON po.id = pwo.product_order_id
src/main/resources/mapper/production/ProductionProductOutputMapper.xml
@@ -49,7 +49,7 @@
    <select id="selectDailyOutputStats" resultType="java.util.Map">
        SELECT
            DATE_FORMAT(create_time, '%Y-%m-%d') as date,
            SUM(quantity-scrap_qty) as quantity
            SUM(quantity + IFNULL(add_qty, 0)) as quantity
        FROM
            production_product_output
        WHERE
src/main/resources/mapper/production/SalesLedgerProductionAccountingMapper.xml
@@ -54,7 +54,7 @@
        SELECT
        slpa.scheduling_user_name,
        SUM(ppout.quantity) AS output_num,
        SUM(COALESCE(prp.quantity, slpa.finished_num)) AS output_num,
        CONCAT(
        ROUND(
        CASE
@@ -75,6 +75,8 @@
        FROM sales_ledger_production_accounting slpa
        LEFT JOIN production_product_main ppm
        ON slpa.product_main_id = ppm.id
        LEFT JOIN production_report_person prp
        ON prp.product_main_id = ppm.id AND prp.user_id = slpa.scheduling_user_id
        LEFT JOIN production_product_output ppout
        ON ppm.id = ppout.product_main_id
        left join device_ledger dl on dl.id = slpa.device_id
@@ -96,10 +98,10 @@
    <select id="selectDailyWagesStats" resultType="java.util.Map">
        SELECT DATE(ppout.create_time)                                                         AS dateStr,
               SUM(ppout.quantity - IFNULL(ppout.scrap_qty, 0))                                AS numberOfCompleted,
               SUM((ppout.quantity - IFNULL(ppout.scrap_qty, 0)) * IFNULL(pp.salary_quota, 0)) AS amount,
               SUM(ppout.quantity + IFNULL(ppout.add_qty, 0) - IFNULL(ppout.scrap_qty, 0))                                AS numberOfCompleted,
               SUM((ppout.quantity + IFNULL(ppout.add_qty, 0) - IFNULL(ppout.scrap_qty, 0)) * IFNULL(pp.salary_quota, 0)) AS amount,
               ROUND(
                       SUM(ppout.quantity - IFNULL(ppout.scrap_qty, 0)) * 100 / NULLIF(SUM(ppout.quantity), 0),
                       SUM(ppout.quantity + IFNULL(ppout.add_qty, 0) - IFNULL(ppout.scrap_qty, 0)) * 100 / NULLIF(SUM(ppout.quantity), 0),
                       2
               )                                                                               AS passRate
        FROM production_product_output ppout
src/main/resources/mapper/quality/QualityInspectMapper.xml
@@ -20,7 +20,7 @@
            </when>
            <otherwise>
                LEFT JOIN production_product_main ppm ON qi.product_main_id = ppm.id
                LEFT JOIN product_work_order pwo ON ppm.work_order_id = pwo.id
                LEFT JOIN product_work_order pwo ON pwo.id = COALESCE(qi.work_order_id, ppm.work_order_id)
            </otherwise>
        </choose>
        WHERE
@@ -88,7 +88,7 @@
            </when>
            <otherwise>
                LEFT JOIN production_product_main ppm ON qi.product_main_id = ppm.id
                LEFT JOIN product_work_order pwo ON ppm.work_order_id = pwo.id
                LEFT JOIN product_work_order pwo ON pwo.id = COALESCE(qi.work_order_id, ppm.work_order_id)
            </otherwise>
        </choose>
        WHERE qi.inspect_type=#{qualityInspect.inspectType}