1.计量器具台账上传附件报错
2.质量拉的数据不对(未明确)
3.计量器具台账逾期的做标红提醒
4.设备保养定时任务和记录要加上具体的保养内容
5.质量要区分质检规则抽检还是全检,抽检的话是抽多少百分比
6.供应商管理东西太少了,没有资质文件啊这些东西(是不是可以参考pro)
7.采购审批把人从李莹莹改成龙红星
已添加5个文件
已修改12个文件
1432 ■■■■■ 文件已修改
doc/20260617_quality_inspect_rule.sql 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/20260618_git_commits_summary.md 316 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/20260618_home_quality_query_fix.md 194 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/20260618_quick_inspect_sample.md 393 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/quality_inspect_rule.md 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/approve/service/impl/ApproveBusinessStatusService.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/basic/dto/StorageBlobDTO.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/home/dto/QualityQualifiedAnalysisDto.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/home/service/impl/HomeServiceImpl.java 146 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionProductMainServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/purchase/service/impl/PurchaseLedgerServiceImpl.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/dto/BatchQuickInspectRequest.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/pojo/QualityInspect.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/pojo/QualityTestStandard.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/utils/QualityInspectHelper.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/SalesQuotationMapper.xml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260617_quality_inspect_rule.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,11 @@
-- è´¨é‡æ£€éªŒè§„则:全检/抽检 + æŠ½æ£€æ¯”例
-- 2026-06-17
-- æ£€æµ‹æ ‡å‡†ä¸»è¡¨ï¼šæ–°å¢žæ£€éªŒè§„则和抽检比例
ALTER TABLE quality_test_standard ADD COLUMN inspect_rule INT DEFAULT 0 COMMENT '检验规则: 0=全检, 1=抽检';
ALTER TABLE quality_test_standard ADD COLUMN sample_ratio DECIMAL(5,2) DEFAULT NULL COMMENT '抽检比例(百分比), ä»…inspect_rule=1时有效, å¦‚10表示10%';
-- è´¨æ£€è®°å½•表:新增检验规则、抽检比例、抽检数量
ALTER TABLE quality_inspect ADD COLUMN inspect_rule INT DEFAULT NULL COMMENT '检验规则: 0=全检, 1=抽检';
ALTER TABLE quality_inspect ADD COLUMN sample_ratio DECIMAL(5,2) DEFAULT NULL COMMENT '抽检比例(百分比), ä»…inspectRule=1时有效';
ALTER TABLE quality_inspect ADD COLUMN sample_quantity DECIMAL(16,2) DEFAULT NULL COMMENT '抽检数量, ä»…inspectRule=1时有效';
docs/20260618_git_commits_summary.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,316 @@
# dev_新疆马铃薯pro åˆ†æ”¯ Git æäº¤è®°å½•整理
> ç›¸å¯¹äºŽ master åˆ†æ”¯ï¼Œå…±æœ‰ **1260 æ¡æäº¤**(含合并提交)
> ä»¥ä¸‹æŒ‰åŠŸèƒ½æ¨¡å—åˆ†ç±»æ•´ç†ä¸»è¦æäº¤å†…å®¹
---
## ä¸€ã€æäº¤ç»Ÿè®¡æ¦‚览
| æ¨¡å— | æäº¤æ•° | è¯´æ˜Ž |
|------|--------|------|
| production | 58 | ç”Ÿäº§ç®¡ç†æ¨¡å— |
| sales | 43 | é”€å”®ç®¡ç†æ¨¡å— |
| stock | 31 | åº“存管理模块 |
| quality | 18 | è´¨é‡ç®¡ç†æ¨¡å— |
| purchase | 17 | é‡‡è´­ç®¡ç†æ¨¡å— |
| account | 17 | è´¢åŠ¡ç®¡ç†æ¨¡å— |
| approve | 15 | å®¡æ‰¹æµç¨‹æ¨¡å— |
| home | 12 | é¦–页数据看板 |
| ai | 12 | AI助手功能 |
| technology | 11 | å·¥è‰ºè·¯çº¿æ¨¡å— |
| staff | 8 | å‘˜å·¥ç®¡ç†æ¨¡å— |
| device | 4 | è®¾å¤‡ç®¡ç†æ¨¡å— |
| config | 4 | é…ç½®ç›¸å…³ |
| other | - | å…¶ä»–杂项 |
---
## äºŒã€è¿‘期主要功能提交(最新50条)
### 2026å¹´6月
| æ—¥æœŸ | æäº¤è¯´æ˜Ž |
|------|----------|
| æœ€æ–° | `e772b3d8c` è®¾å¤‡ä¿å…»å†…容字段增加 |
| æœ€æ–° | `50c67ada0` feat(config): æ·»åŠ  Pinecone å‘量数据库配置 |
| æœ€æ–° | `eb832a81f` refactor(ai): é‡æž„ Pinecone å‘量存储配置和服务实现 |
| æœ€æ–° | `a61d5a200` refactor(knowledge-base): é‡æž„RAG向量检索功能的文件关联和异步处理 |
| æœ€æ–° | `29ccd9919` feat(financial): æ–°å¢žå‡­è¯åˆ†å½•科目明细字段 |
| æœ€æ–° | `4b91241e4` feat(database): æ·»åŠ é™„ä»¶æ•°æ®è¿ç§»è„šæœ¬ä»Žcommon_file到storage_blob_storage_attachment |
| æœ€æ–° | `feb86efc4` feat(iot): ç‰©è”设备接口新增存放位置字段 |
| æœ€æ–° | `789ee31d8` feat(vehicle): æ·»åŠ è½¦è¾†ç®¡ç†æ¨¡å—åŠå½’è¿˜é‡Œç¨‹åŠŸèƒ½ |
| æœ€æ–° | `a217dbfc7` feat(device): æ·»åŠ è®¾å¤‡ä¿å…»éªŒæ”¶åŠŸèƒ½å’Œå¹´åº¦å®šæ—¶ä»»åŠ¡æ”¯æŒ |
| æœ€æ–° | `284034e85` feat(stock): æ·»åŠ åº“å­˜ç‰©è”è®¾å¤‡ç»‘å®šå’Œå®žæ—¶æ•°é‡‡åŠŸèƒ½ |
| æœ€æ–° | `f80eee649` feat(home): æ–°å¢žä¸Šæœˆé”€å”®é‡‡è´­é‡‘额统计功能 |
| æœ€æ–° | `3d048a0ff` feat(shipping): å‘货填出库批号,按批号批量入库,销售退货改出库批号 |
| æœ€æ–° | `48c1deafd` feat(quality): ä¼˜åŒ–批量快速检验功能 |
| æœ€æ–° | `1baf55456` feat(quality): å¢žåŠ æ£€éªŒç®¡ç†åŠŸèƒ½å¹¶å®Œå–„åº“å­˜é¢„è­¦ |
| æœ€æ–° | `a0f6e4436` feat(ai): æ·»åŠ AI会话清理任务并优化财务分析功能 |
| æœ€æ–° | `261a7d264` fix(ai): ä¼˜åŒ–财务AI助手的工作规则和数据分析功能 |
---
## ä¸‰ã€æŒ‰åŠŸèƒ½æ¨¡å—è¯¦ç»†è¯´æ˜Ž
### 1. ç”Ÿäº§ç®¡ç†æ¨¡å— (production) - 58条
**核心功能:**
- ç”Ÿäº§è®¢å•管理与工艺路线
- ç”Ÿäº§æŠ¥å·¥ä¸Žäº§å‡ºå…¥åº“
- ç”Ÿäº§æ ¸ç®—功能
- ç”Ÿäº§çœ‹æ¿æ•°æ®ç»Ÿè®¡
**主要提交:**
```
feat(production): æ·»åŠ ç”Ÿäº§æ ¸ç®—åŠŸèƒ½å’Œç›¸å…³æŽ¥å£
feat(production): æ–°å¢žç”Ÿäº§è®¢å•功能并优化工单号生成逻辑
feat(production): ç”Ÿäº§æŠ¥å·¥ç›¸å…³åŠŸèƒ½
feat(production): æ·»åŠ ç”Ÿäº§è®¢å•å·¥è‰ºè·¯çº¿å…³è”
refactor(production): ä¼˜åŒ–生产订单工艺路线操作服务接口
fix(production): ä¿®å¤ç”Ÿäº§äº§å“æ•°é‡æ›´æ–°é€»è¾‘
```
---
### 2. é”€å”®ç®¡ç†æ¨¡å— (sales) - 43条
**核心功能:**
- é”€å”®å°è´¦ä¸Žé”€å”®æŠ¥ä»·
- é”€å”®å‘货与出库
- é”€å”®é€€è´§ç®¡ç†
- é”€å”®åˆåŒç®¡ç†
**主要提交:**
```
feat(sales): é”€å”®é€€è´§æ¨¡å—接口开发
feat(sales): æ·»åŠ é”€å”®æŠ¥ä»·å•ID查询条件支持
feat(sales): å‘货审批流程
feat(sales): å®¢æˆ·æ¡£æ¡ˆç®¡ç†ä¼˜åŒ–
fix(sales): é”€å”®å°è´¦ç¼–辑逻辑修复
```
---
### 3. åº“存管理模块 (stock) - 31条
**核心功能:**
- åº“存盘点与预警
- å…¥åº“/出库记录管理
- æ‰¹æ¬¡å·ç®¡ç†
- åº“存导出功能
**主要提交:**
```
feat(stock): æ·»åŠ åº“å­˜ç‰©è”è®¾å¤‡ç»‘å®šå’Œå®žæ—¶æ•°é‡‡åŠŸèƒ½
feat(stock): åº“存导出接口开发
feat(stock): æ·»åŠ æ‰¹å·å­—æ®µå¹¶ä¼˜åŒ–å‡ºå…¥åº“è®°å½•å¯¼å‡ºåŠŸèƒ½
feat(stock): ä¸åˆæ ¼åº“存导出
refactor(stock): é‡æž„库存相关枚举类以区分出入库类型
```
---
### 4. è´¨é‡ç®¡ç†æ¨¡å— (quality) - 18条
**核心功能:**
- æ£€éªŒç®¡ç†ï¼ˆåŽŸææ–™/过程/出厂检验)
- æ‰¹é‡å¿«é€Ÿæ£€éªŒ
- ä¸åˆæ ¼å“ç®¡ç†
- æ£€éªŒæ ‡å‡†é…ç½®
**主要提交:**
```
feat(quality): å¢žåŠ æ£€éªŒç®¡ç†åŠŸèƒ½å¹¶å®Œå–„åº“å­˜é¢„è­¦
feat(quality): ä¼˜åŒ–批量快速检验功能
feat(quality): æ·»åŠ ä¸åˆæ ¼å“å¤„ç†æ ‡è¯†å­—æ®µ
fix(quality): è´¨æ£€æäº¤æ•°æ®éªŒè¯
fix(quality): é¦–页质量数据统计错误修复
```
---
### 5. è´¢åŠ¡ç®¡ç†æ¨¡å— (account) - 17条
**核心功能:**
- åº”收应付管理
- è´¢åŠ¡æŠ¥è¡¨ç»Ÿè®¡
- å‡­è¯ç®¡ç†
- æœˆåº¦è¥æ”¶æ•°æ®
**主要提交:**
```
feat(account): ä¼˜åŒ–应收应付金额及月度统计数据计算
feat(account): é‡å†™è´¢åŠ¡æŠ¥è¡¨ç»Ÿè®¡åŠŸèƒ½
feat(account): æ·»åŠ æ˜¯å¦ç”Ÿæˆå¯¹è´¦å•å­—æ®µ
refactor(account): é‡æž„财务报表相关代码
feat(financial): æ–°å¢žå‡­è¯åˆ†å½•科目明细字段
```
---
### 6. å®¡æ‰¹æµç¨‹æ¨¡å— (approve) - 15条
**核心功能:**
- ååŒå®¡æ‰¹æµç¨‹
- å®¡æ‰¹å®žä¾‹ç®¡ç†
- å‡ºå·®ç”³è¯·
- æŠ¥é”€å•模块
**主要提交:**
```
refactor(approve): é‡æž„审批业务状态同步逻辑
feat(approve): æ·»åŠ å®¡æ‰¹å®žä¾‹æŒ‰å½“å‰ç”¨æˆ·è¿‡æ»¤åŠŸèƒ½
feat(approve): å®Œå–„审批实例管理功能并新增报销单模块
feat(approve): æ·»åŠ æ“ä½œæ—¥å¿—è®°å½•
fix(approve): ä¿®å¤å®¡æ‰¹å®žä¾‹æŸ¥è¯¢æ¡ä»¶åŠæµç¨‹æŽ§åˆ¶é—®é¢˜
```
---
### 7. é¦–页数据看板 (home) - 12条
**核心功能:**
- ç”Ÿäº§çœ‹æ¿
- è´¨é‡ç»Ÿè®¡
- è´¢åŠ¡ç»Ÿè®¡
- ä¸Šæœˆæ•°æ®ç»Ÿè®¡
**主要提交:**
```
feat(home): æ·»åŠ ç”Ÿäº§çœ‹æ¿åŠŸèƒ½å¹¶ä¼˜åŒ–è®¢å•æŸ¥è¯¢æ€§èƒ½
feat(home): æ–°å¢žä¸Šæœˆé”€å”®é‡‡è´­é‡‘额统计功能
refactor(home): é‡æž„首页财务统计功能实现
fix(home): é¦–页质量数据统计错误修复
```
---
### 8. AI助手功能 (ai) - 12条
**核心功能:**
- å®¡æ‰¹å¾…办助手
- è´¢åŠ¡AI助手
- é‡‡è´­AI助手
- RAG向量检索
**主要提交:**
```
feat(ai): æ·»åŠ å®¡æ‰¹å¾…åŠžåŠ©æ‰‹åŠŸèƒ½
feat(ai): æ›´æ–°é‡‡è´­å’Œé”€å”®AI工具的数据查询功能
feat(ai): æ·»åŠ AI会话清理任务并优化财务分析功能
refactor(ai): é‡æž„ Pinecone å‘量存储配置和服务实现
fix(ai): ä¼˜åŒ–财务AI助手的工作规则和数据分析功能
```
---
### 9. è®¾å¤‡ç®¡ç†æ¨¡å— (device) - 4条
**主要提交:**
```
feat(device): æ·»åŠ è®¾å¤‡ä¿å…»éªŒæ”¶åŠŸèƒ½å’Œå¹´åº¦å®šæ—¶ä»»åŠ¡æ”¯æŒ
feat(device): è®¾å¤‡å°è´¦æ–°å¢žé™„件图片上传
feat(iot): ç‰©è”设备接口新增存放位置字段
```
---
### 10. å·¥è‰ºè·¯çº¿æ¨¡å— (technology) - 11条
**主要提交:**
```
refactor(technology): ä½¿ç”¨DTO和VO重构工艺路线模块
refactor(technology): ä¼˜åŒ–工艺路线服务实现
feat(technology): å·¥åºç®¡ç†ç›¸å…³åŠŸèƒ½
```
---
### 11. å‘˜å·¥ç®¡ç†æ¨¡å— (staff) - 8条
**主要提交:**
```
feat(staff): ä¿®æ”¹å‘˜å·¥å…¥èŒæœåŠ¡æŽ¥å£å¢žåŠ ç”¨æˆ·æ·»åŠ æ ‡è¯†å‚æ•°
feat(staff): æ·»åŠ åœ¨èŒå‘˜å·¥å¯¼å…¥åŠŸèƒ½
fix(staff): è§£å†³å¯¼å‡ºæ•°æ®åˆ—数不一致问题
```
---
### 12. å…¶ä»–重要功能
**车辆管理:**
```
feat(vehicle): æ·»åŠ è½¦è¾†ç®¡ç†æ¨¡å—åŠå½’è¿˜é‡Œç¨‹åŠŸèƒ½
```
**知识库/向量检索:**
```
refactor(knowledge-base): é‡æž„RAG向量检索功能的文件关联和异步处理
feat(config): æ·»åŠ  Pinecone å‘量数据库配置
```
**数据迁移:**
```
feat(database): æ·»åŠ é™„ä»¶æ•°æ®è¿ç§»è„šæœ¬ä»Žcommon_file到storage_blob_storage_attachment
```
---
## å››ã€é…ç½®æ–‡ä»¶æ›´æ–°
多个客户环境配置文件更新:
- `dev_新疆马铃薯pro` (当前分支)
- `dev_山西_晋和园_pro`
- `dev_河南_鹤壁`
- `dev_天津_*` ç³»åˆ—
- `dev_宁夏_*` ç³»åˆ—
- å…¶ä»–客户定制环境
---
## äº”、依赖更新
```
chore(deps): æ›´æ–° mybatis-plus ç‰ˆæœ¬åˆ° 3.5.15
chore(deps): æ›´æ–° MyBatis-Plus ä¾èµ–版本
```
---
## å…­ã€é‡æž„与优化
| ç±»åž‹ | è¯´æ˜Ž |
|------|------|
| refactor | é‡æž„审批业务状态同步逻辑 |
| refactor | é‡æž„首页财务统计功能实现 |
| refactor | é‡æž„库存相关枚举类 |
| refactor | é‡æž„采购AI控制器 |
| refactor | é‡æž„工艺路线模块使用DTO/VO |
| refactor | è¿ç§»Swagger注解到OpenAPI 3.0 |
| refactor | ä¼˜åŒ–订单编号生成逻辑 |
| refactor | ç®€åŒ–库存盘点服务重复代码 |
---
## ä¸ƒã€Bug修复精选
| é—®é¢˜ | ä¿®å¤å†…容 |
|------|----------|
| è´¨é‡ç»Ÿè®¡é”™è¯¯ | é¦–页质量数据合格/不合格率统计错误 |
| å®¡æ‰¹æµç¨‹å¼‚常 | ä¿®å¤å®¡æ‰¹èŠ‚ç‚¹å®¡æ‰¹äººä¸ºç©ºæ—¶çš„å¼‚å¸¸å¤„ç† |
| ç”Ÿäº§æ•°é‡æ›´æ–° | ä¿®å¤ç”Ÿäº§äº§å“æ•°é‡æ›´æ–°é€»è¾‘ |
| è®¢å•编号生成 | ä¿®å¤å­—段名不一致问题 |
| åº“存查询SQL | ä¿®å¤å¤šä½™å­—符导致SQL语法错误 |
| å¯¼å‡ºæ•°æ® | è§£å†³å¯¼å‡ºæ•°æ®åˆ—数不一致问题 |
---
## å…«ã€å¤‡æ³¨
- åˆ†æ”¯åç§°ï¼š`dev_新疆马铃薯pro`
- åŸºå‡†åˆ†æ”¯ï¼š`master`
- æäº¤æ€»æ•°ï¼š1260条(含合并)
- ä¸»è¦å¼€å‘者:yys åŠå›¢é˜Ÿæˆå‘˜
- æ¶‰åŠå®¢æˆ·ï¼šæ–°ç–†é©¬é“ƒè–¯é¡¹ç›®åŠå¤šä¸ªå®šåˆ¶åŒ–分支
docs/20260618_home_quality_query_fix.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,194 @@
# é¦–页质量类查询修复与优化前端联调文档
> ä¿®å¤ `home` æ¨¡å—下质量类接口的若干问题,并优化查询性能。
> æœ¬æ¬¡æ”¹åЍ**只涉及响应数值**,**接口路径、请求参数、响应字段名均未变化**。
## æ¶‰åŠé¡µé¢
- é¦–页 / æ•°æ®çœ‹æ¿ - è´¨é‡åˆ†æžæ¨¡å—
- é¦–页 / æ•°æ®çœ‹æ¿ - è´¨é‡æ£€éªŒæ•°é‡å¡ç‰‡
- é¦–页 / æ•°æ®çœ‹æ¿ - ä¸åˆæ ¼é¢„è­¦
- é¦–页 / æ•°æ®çœ‹æ¿ - å®Œæˆæ£€éªŒæ•°è¶‹åŠ¿
- é¦–页 / æ•°æ®çœ‹æ¿ - ä¸åˆæ ¼äº§å“æŽ’名
- é¦–页 / æ•°æ®çœ‹æ¿ - åŽŸææ–™/过程/出厂检测合格率
- é¦–页 / æ•°æ®çœ‹æ¿ - è´¨é‡ç»Ÿè®¡ (周/月/å­£)
- è´¨é‡ç®¡ç† - æ£€éªŒå•新增/编辑页 (检验规则字段)
## å—影响接口一览
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /home/qualityInspectionCount | è´¨é‡æ£€éªŒæ•°é‡ (语义有变更) |
| GET | /home/nonComplianceWarning | è¿‘七天不合格预警 |
| GET | /home/completedInspectionCount | è¿‘七天完成检验数 |
| GET | /home/unqualifiedProductRanking | ä¸åˆæ ¼äº§å“æŽ’名 (口径变更) |
| GET | /home/rawMaterialDetection | åŽŸææ–™æ£€æµ‹åˆæ ¼çŽ‡ |
| GET | /home/processDetection | è¿‡ç¨‹æ£€æµ‹åˆæ ¼çއ |
| GET | /home/factoryDetection | å‡ºåŽ‚æ£€æµ‹åˆæ ¼çŽ‡ |
| GET | /home/qualityInspectionStatistics | è´¨é‡ç»Ÿè®¡ (周/月/å­£) |
| POST | /qualityInspect | æ–°å¢žæ£€éªŒå• (inspectRule é»˜è®¤å€¼é€»è¾‘) |
## ä¸€ã€`/home/qualityInspectionCount` (语义变更,前端需关注)
### å˜æ›´åŽŸå› 
- åŽŸå®žçŽ°æŠŠå…¨è¡¨åŠ è½½åˆ°å†…å­˜è¿›è¡Œæ±‚å’Œï¼Œä¸” `totalCountGrowthRate` ç”¨ *截止今日累计 vs æˆªæ­¢æ˜¨æ—¥ç´¯è®¡* è®¡ç®—,得到的实质是 `today_only / yesterday_cumulative`,无业务意义。
- åŽŸå®žçŽ°å¯¹ `checkTime` ç”¨ `eq(checkTime, "yyyy-MM-dd")`,若数据库 `check_time` å«éž 0 æ—¶åˆ†ç§’会全部漏匹配,导致 `todayPendingCount`/`todayCompletedCount` å¶å‘为 0。
### æ–°å£å¾„
| å­—段 | æ—§å£å¾„ | æ–°å£å¾„ |
|------|--------|--------|
| `totalCount` | æˆªæ­¢ä»Šæ—¥æ‰€æœ‰ `quantity` ç´¯åŠ  | ä¸å˜ï¼Œä»æ˜¯åŽ†å²ç´¯è®¡ (改用 SQL `SUM` èšåˆ) |
| `totalCountGrowthRate` | (今日累计 - æ˜¨æ—¥ç´¯è®¡) / æ˜¨æ—¥ç´¯è®¡ Ã— 100 | **(今日单日量 - æ˜¨æ—¥å•日量) / æ˜¨æ—¥å•日量 Ã— 100** |
| `todayPendingCount` | `inspect_state=0` ä¸” `check_time = ä»Šå¤© 00:00:00` | `inspect_state=0` ä¸” `check_time` âˆˆ \[今天 00:00, æ˜Žå¤© 00:00) |
| `todayPendingCountGrowthRate` | (今日待完成 - æ˜¨æ—¥å¾…完成) / æ˜¨æ—¥å¾…完成 Ã— 100 | ä¸å˜ï¼Œä½†åˆ†å­åˆ†æ¯éƒ½æ˜¯çœŸæ­£"当日量" |
| `todayCompletedCount` | åŒä¸Š | åŒä¸Š |
| `todayCompletedCountGrowthRate` | åŒä¸Š | åŒä¸Š |
### å“åº”示例
```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "totalCount": 12345.00,
    "totalCountGrowthRate": 8.50,
    "todayPendingCount": 12.00,
    "todayPendingCountGrowthRate": -20.00,
    "todayCompletedCount": 80.00,
    "todayCompletedCountGrowthRate": 15.50
  }
}
```
### å‰ç«¯ä¿®æ”¹ç‚¹
`totalCountGrowthRate` çš„视觉提示文案建议从 "总检验数同比增长" æ”¹ä¸º "今日新增 vs æ˜¨æ—¥æ–°å¢ž",避免误解为累计同比。
```html
<el-tooltip content="今日单日检验数相对昨日单日检验数的增长率">
  <span>总检验数同比 {{ formatRate(data.totalCountGrowthRate) }}</span>
</el-tooltip>
```
```js
methods: {
  formatRate(rate) {
    if (rate === null || rate === undefined) return '0%';
    const sign = rate > 0 ? '+' : '';
    return `${sign}${rate}%`;
  }
}
```
## äºŒã€`/home/unqualifiedProductRanking` (口径变更,前端需关注)
### å˜æ›´åŽŸå› 
原实现拉取所有 `inspect_state=1` çš„记录到内存做聚合排序,数据量大时存在 OOM é£Žé™©ã€‚
### æ–°å£å¾„
| ç»´åº¦ | æ—§ | æ–° |
|------|----|----|
| æ—¶é—´èŒƒå›´ | å…¨åŽ†å² | **近 30 å¤© (含今天)** |
| æŽ’序 | åˆæ ¼çŽ‡å‡åº Top5 | ä¸å˜ |
| å­—段 | `productName / totalCount / completedCount / passRate` | ä¸å˜ |
### å‰ç«¯ä¿®æ”¹ç‚¹
如果当前页面有 "全历史" ä¹‹ç±»çš„æ–‡æ¡ˆï¼Œè¯·æ”¹æˆ "近 30 å¤©"。
```html
<div class="card-title">不合格产品排名 <span class="sub">(近 30 å¤©)</span></div>
```
## ä¸‰ã€æ—¥æœŸä¸Šç•Œç»Ÿä¸€æ”¹ä¸ºå³å¼€åŒºé—´ (语义未变,但补全数据)
`commonDetection` (rawMaterial / process / factoryDetection)、`nonComplianceWarning`、`completedInspectionCount`、`qualityInspectionStatistics` ä¹‹å‰éƒ½ç”¨ `le(checkTime, endDate)`。
当 `check_time` æ˜¯ `datetime` åˆ—时,`<= "yyyy-MM-dd"` ç­‰ä»·äºŽ `<= "yyyy-MM-dd 00:00:00"`,会漏掉当天 00:00:00 ä¹‹åŽçš„æ‰€æœ‰è®°å½•。
后端已统一改为 `lt(checkTime, endDate + 1 å¤©)`,**响应字段、字段语义不变**,只是当天数据不再丢失。
### å‰ç«¯ä¿®æ”¹ç‚¹
无需改动代码。但建议联调时核对:周/月/季视图最后一天是否出现以前缺失的检验记录。
## å››ã€å…¶ä»–内部优化 (前端无感)
- `qualityInspectionCount` æ”¹ç”¨ `SELECT COALESCE(SUM(quantity), 0)` èšåˆï¼Œå•次 SQL æ›¿ä»£ä¹‹å‰çš„ 6 æ¬¡å…¨è¡¨ `selectList`。
- `qualityInspectionStatistics` ä¸­ `i.getInspectType() == 0` ä¿®å¤ä¸º `Objects.equals(...)`,避免 Integer è‡ªåŠ¨è£…ç®±ç¼“å­˜èŒƒå›´å¤–çš„ç­‰å€¼è¯¯åˆ¤ã€‚
## æ³¨æ„äº‹é¡¹
- `qualityInspectionCount` çš„ `totalCountGrowthRate` çŽ°åœ¨åæ˜ çš„æ˜¯"日环比增长率",业务侧文案与图表说明需同步调整。
- `unqualifiedProductRanking` æ”¹ä¸ºè¿‘ 30 å¤©åŽï¼Œ**Top5 åˆ—表可能变化**,请通知业务确认。
- ä¸Šçº¿åŽå»ºè®®è”调以下场景:
  - `check_time` ä¸ºéž 00:00:00 çš„æ£€éªŒè®°å½•是否被正确统计 (周/月视图最后一天)。
  - ä»Šæ—¥æ‰€æœ‰æŠ¥æ£€çš„"待完成 / å·²å®Œæˆ"数据是否能实时反映到首页卡片。
- è‹¥æŸæŽ¥å£éœ€è¦ä¿ç•™æ—§çš„全历史口径 (例如导出、对账),请单独提需求增加新接口,避免改回首页接口。
---
## äº”、检验规则默认值逻辑变更 (新增检验单)
### ä¸šåŠ¡è§„åˆ™
当新建检验单时,后端会根据 `inspectRule` è‡ªåŠ¨è¡¥å…¨ `sampleRatio` å’Œ `sampleQuantity`:
| inspectRule | å«ä¹‰ | sampleRatio | sampleQuantity |
|-------------|------|-------------|----------------|
| 0 æˆ– null | å…¨æ£€ | 100 (固定) | `quantity` (全部数量) |
| 1 | æŠ½æ£€ | å– `sampleRatio` ä¼ å…¥å€¼ (若为空则 0) | `quantity Ã— sampleRatio / 100` (向上取整) |
### å‰ç«¯ä¿®æ”¹ç‚¹
新增检验单页面,检验规则下拉选择后:
```html
<el-form-item label="检验规则">
  <el-select v-model="form.inspectRule" @change="onInspectRuleChange">
    <el-option label="全检" :value="0" />
    <el-option label="抽检" :value="1" />
  </el-select>
</el-form-item>
<el-form-item label="抽检比例(%)" v-if="form.inspectRule === 1">
  <el-input-number v-model="form.sampleRatio" :min="0" :max="100" />
</el-form-item>
<el-form-item label="抽检数量">
  <el-input :value="computedSampleQuantity" disabled />
</el-form-item>
```
```js
computed: {
  computedSampleQuantity() {
    if (this.form.inspectRule === 0 || this.form.inspectRule === null) {
      // å…¨æ£€æ—¶ï¼ŒæŠ½æ£€æ•°é‡ = æ€»æ•°é‡
      return this.form.quantity || 0;
    }
    // æŠ½æ£€æ—¶ï¼ŒæŠ½æ£€æ•°é‡ = æ€»æ•°é‡ Ã— æ¯”例 / 100 (向上取整)
    const qty = this.form.quantity || 0;
    const ratio = this.form.sampleRatio || 0;
    return Math.ceil(qty * ratio / 100);
  }
},
methods: {
  onInspectRuleChange(val) {
    if (val === 0) {
      // å…¨æ£€æ—¶è‡ªåŠ¨æ¸…ç©ºæŠ½æ£€æ¯”ä¾‹è¾“å…¥æ¡†
      this.form.sampleRatio = 100;
    }
  }
}
```
### æ³¨æ„äº‹é¡¹
- åŽç«¯å·²åšå…œåº•处理,即使前端不传 `sampleRatio/sampleQuantity`,也会自动补全。
- å»ºè®®å‰ç«¯åœ¨æäº¤å‰åšé¢„计算展示,提升用户体验。
- é‡‡è´­å…¥åº“自动生成检验单、报工生成过程/出厂检验单时,同样会按此规则自动填充。
docs/20260618_quick_inspect_sample.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,393 @@
# è´¨æ£€å¿«é€Ÿæ£€éªŒæŠ½æ£€åŠŸèƒ½å‰ç«¯è”è°ƒæ–‡æ¡£
> æ‰©å±•批量快速检验接口,支持全检和抽检两种检验模式,自动填充抽检数量。
## æ¶‰åŠé¡µé¢
- è´¨é‡ç®¡ç† / æ£€éªŒå•列表 - æ‰¹é‡å¿«é€Ÿæ£€éªŒå¼¹çª—
- è´¨é‡ç®¡ç† / åŽŸææ–™æ£€éªŒ / è¿‡ç¨‹æ£€éªŒ / å‡ºåŽ‚æ£€éªŒ - å¿«é€Ÿæ£€éªŒæ“ä½œ
## API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /qualityInspect/batchQuickInspect | æ‰¹é‡å¿«é€Ÿæ£€éªŒ (扩展参数) |
**请求参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| ids | List\<Long\> | æ˜¯ | æ£€éªŒå•ID列表 |
| checkResult | String | æ˜¯ | æ£€æµ‹ç»“果:合格/不合格/部分合格 |
| testStandardId | Long | æ˜¯ | æŒ‡æ ‡æ ‡å‡†ID |
| checkCompany | String | å¦ | æ£€æµ‹å•位 |
| checkName | String | å¦ | æ£€éªŒå‘˜å§“名 |
| checkTime | String | å¦ | æ£€æµ‹æ—¥æœŸï¼Œæ ¼å¼ï¼šYYYY-MM-DD |
| paramList | List | å¦ | æ£€éªŒå‚数列表 |
| **sampleQuantity** | BigDecimal | å¦ | **抽检数量(新增)** |
| **qualifiedQuantity** | BigDecimal | å¦ | **合格数量(新增)** |
| **unqualifiedQuantity** | BigDecimal | å¦ | **不合格数量(新增)** |
**响应:**
```json
{
  "code": 200,
  "msg": "快速检验完成:成功 3 æ¡"
}
```
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
### æ£€éªŒæ¨¡å¼åˆ¤æ–­
根据检验单的 `inspectRule` å­—段判断检验模式:
| inspectRule | æ£€éªŒæ¨¡å¼ | å¤„理逻辑 |
|-------------|----------|----------|
| 0 æˆ– null | å…¨æ£€ | æ£€éªŒæ•°é‡ = æ€»æ•°é‡ï¼Œé»˜è®¤å…¨éƒ¨åˆæ ¼ |
| 1 | æŠ½æ£€ | æ£€éªŒæ•°é‡ = æŠ½æ£€æ ·æœ¬æ•°é‡ |
### æŠ½æ£€æ•°é‡æ¥æºä¼˜å…ˆçº§
当检验单为抽检模式时,抽检数量按以下优先级确定:
1. **前端传入** `sampleQuantity`(最高优先级)
2. **检验单已有** `sampleQuantity`
3. **按抽检比例计算**:`总数量 Ã— sampleRatio / 100`(向上取整)
4. æ ¡éªŒï¼šæŠ½æ£€æ•°é‡ä¸èƒ½è¶…过总数量,超过则自动修正为总数量
### åˆæ ¼/不合格数量
| åœºæ™¯ | å¤„理逻辑 |
|------|----------|
| å‰ç«¯ä¼ å…¥äº† qualifiedQuantity/unqualifiedQuantity | ä½¿ç”¨å‰ç«¯ä¼ å…¥å€¼ |
| å‰ç«¯æœªä¼ å…¥ | é»˜è®¤å…¨éƒ¨åˆæ ¼ï¼ˆåˆæ ¼æ•°=检验数量,不合格数=0) |
### å…¥åº“处理
- å…¥åº“数量 = åˆæ ¼æ•°é‡ Ã— å…¥åº“比例 / 100
- å…¥åº“比例默认 100%,可通过 `stockInRatio` å­—段调整
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. å¿«é€Ÿæ£€éªŒå¼¹çª—增加抽检字段
```html
<el-dialog title="批量快速检验" v-model="quickInspectVisible">
  <el-form :model="quickInspectForm" label-width="100px">
    <!-- åŸºç¡€å­—段 -->
    <el-form-item label="检测结果" required>
      <el-select v-model="quickInspectForm.checkResult">
        <el-option label="合格" value="合格" />
        <el-option label="不合格" value="不合格" />
        <el-option label="部分合格" value="部分合格" />
      </el-select>
    </el-form-item>
    <el-form-item label="指标标准" required>
      <el-select v-model="quickInspectForm.testStandardId">
        <!-- æŒ‡æ ‡æ ‡å‡†åˆ—表 -->
      </el-select>
    </el-form-item>
    <el-form-item label="检验员">
      <el-input v-model="quickInspectForm.checkName" />
    </el-form-item>
    <el-form-item label="检测日期">
      <el-date-picker v-model="quickInspectForm.checkTime" type="date" format="YYYY-MM-DD" value-format="YYYY-MM-DD" />
    </el-form-item>
    <!-- æŠ½æ£€ç›¸å…³å­—段(根据检验单模式动态显示) -->
    <el-form-item label="检验模式">
      <el-tag :type="inspectModeTag">{{ inspectModeLabel }}</el-tag>
    </el-form-item>
    <!-- æŠ½æ£€æ¨¡å¼ä¸‹æ˜¾ç¤º -->
    <el-form-item label="抽检数量" v-if="showSampleFields">
      <el-input-number
        v-model="quickInspectForm.sampleQuantity"
        :min="1"
        :max="maxSampleQuantity"
        :precision="0"
      />
      <span class="tip">最大可抽检数量:{{ maxSampleQuantity }}</span>
    </el-form-item>
    <!-- éƒ¨åˆ†åˆæ ¼æˆ–不合格时显示 -->
    <el-form-item label="合格数量" v-if="showQuantityFields">
      <el-input-number
        v-model="quickInspectForm.qualifiedQuantity"
        :min="0"
        :max="quickInspectForm.sampleQuantity || totalQuantity"
        :precision="0"
      />
    </el-form-item>
    <el-form-item label="不合格数量" v-if="showQuantityFields">
      <el-input-number
        v-model="quickInspectForm.unqualifiedQuantity"
        :min="0"
        :max="quickInspectForm.sampleQuantity || totalQuantity"
        :precision="0"
      />
    </el-form-item>
  </el-form>
  <template #footer>
    <el-button @click="quickInspectVisible = false">取消</el-button>
    <el-button type="primary" @click="submitQuickInspect">确认检验</el-button>
  </template>
</el-dialog>
```
### 2. data æ•°æ®
```js
data() {
  return {
    quickInspectVisible: false,
    quickInspectForm: {
      ids: [],
      checkResult: '合格',
      testStandardId: null,
      checkCompany: '',
      checkName: '',
      checkTime: '',
      paramList: [],
      sampleQuantity: null,
      qualifiedQuantity: null,
      unqualifiedQuantity: null,
    },
    // é€‰ä¸­çš„æ£€éªŒå•列表
    selectedInspects: [],
    // å½“前检验单信息(用于单条快速检验)
    currentInspect: null,
  }
}
```
### 3. computed è®¡ç®—属性
```js
computed: {
  // æ£€éªŒæ¨¡å¼æ ‡ç­¾
  inspectModeLabel() {
    if (!this.currentInspect) return '未知';
    return this.currentInspect.inspectRule === 1 ? '抽检' : '全检';
  },
  inspectModeTag() {
    if (!this.currentInspect) return 'info';
    return this.currentInspect.inspectRule === 1 ? 'warning' : 'success';
  },
  // æ˜¯å¦æ˜¾ç¤ºæŠ½æ£€å­—段
  showSampleFields() {
    return this.currentInspect && this.currentInspect.inspectRule === 1;
  },
  // æ˜¯å¦æ˜¾ç¤ºåˆæ ¼/不合格数量输入(部分合格或不合格时)
  showQuantityFields() {
    return this.quickInspectForm.checkResult === '部分合格' ||
           this.quickInspectForm.checkResult === '不合格';
  },
  // æ€»æ•°é‡
  totalQuantity() {
    return this.currentInspect?.quantity || 0;
  },
  // æœ€å¤§æŠ½æ£€æ•°é‡
  maxSampleQuantity() {
    return this.totalQuantity;
  },
  // é»˜è®¤æŠ½æ£€æ•°é‡ï¼ˆä»Žæ£€éªŒå•获取或按比例计算)
  defaultSampleQuantity() {
    if (!this.currentInspect) return 0;
    if (this.currentInspect.sampleQuantity) {
      return this.currentInspect.sampleQuantity;
    }
    if (this.currentInspect.sampleRatio) {
      return Math.ceil(this.totalQuantity * this.currentInspect.sampleRatio / 100);
    }
    return this.totalQuantity;
  },
}
```
### 4. methods æ–¹æ³•
```js
methods: {
  // æ‰“开快速检验弹窗
  openQuickInspectDialog(row) {
    // å•条快速检验
    this.currentInspect = row;
    this.quickInspectForm.ids = [row.id];
    // æŠ½æ£€æ¨¡å¼ä¸‹è‡ªåŠ¨å¡«å……æŠ½æ£€æ•°é‡
    if (row.inspectRule === 1) {
      this.quickInspectForm.sampleQuantity = this.defaultSampleQuantity;
    }
    // å…¶ä»–字段初始化
    this.quickInspectForm.checkResult = '合格';
    this.quickInspectForm.testStandardId = row.testStandardId;
    this.quickInspectForm.checkTime = new Date().toISOString().slice(0, 10);
    this.quickInspectForm.qualifiedQuantity = null;
    this.quickInspectForm.unqualifiedQuantity = null;
    this.quickInspectVisible = true;
  },
  // æ‰¹é‡å¿«é€Ÿæ£€éªŒ
  openBatchQuickInspectDialog(selection) {
    this.selectedInspects = selection;
    this.quickInspectForm.ids = selection.map(s => s.id);
    // æ‰¹é‡æ£€éªŒæ—¶ï¼Œå–第一条检验单的信息作为参考
    this.currentInspect = selection[0];
    // æŠ½æ£€æ¨¡å¼ä¸‹è‡ªåЍ填充
    if (this.currentInspect?.inspectRule === 1) {
      this.quickInspectForm.sampleQuantity = this.defaultSampleQuantity;
    }
    this.quickInspectVisible = true;
  },
  // æ£€æµ‹ç»“果变化时自动调整数量
  onCheckResultChange(val) {
    if (val === '合格') {
      // åˆæ ¼æ—¶ï¼Œæ¸…空手动输入的数量
      this.quickInspectForm.qualifiedQuantity = null;
      this.quickInspectForm.unqualifiedQuantity = null;
    } else if (val === '不合格') {
      // ä¸åˆæ ¼æ—¶ï¼Œé»˜è®¤å…¨éƒ¨ä¸åˆæ ¼
      const qty = this.showSampleFields ? this.quickInspectForm.sampleQuantity : this.totalQuantity;
      this.quickInspectForm.qualifiedQuantity = 0;
      this.quickInspectForm.unqualifiedQuantity = qty;
    } else if (val === '部分合格') {
      // éƒ¨åˆ†åˆæ ¼æ—¶ï¼Œéœ€è¦ç”¨æˆ·æ‰‹åŠ¨è¾“å…¥
      this.quickInspectForm.qualifiedQuantity = null;
      this.quickInspectForm.unqualifiedQuantity = null;
    }
  },
  // æäº¤å¿«é€Ÿæ£€éªŒ
  async submitQuickInspect() {
    // æ ¡éªŒå¿…填字段
    if (!this.quickInspectForm.checkResult) {
      this.$message.warning('请选择检测结果');
      return;
    }
    if (!this.quickInspectForm.testStandardId) {
      this.$message.warning('请选择指标标准');
      return;
    }
    // æŠ½æ£€æ¨¡å¼ä¸‹æ ¡éªŒæŠ½æ£€æ•°é‡
    if (this.showSampleFields && !this.quickInspectForm.sampleQuantity) {
      this.$message.warning('请输入抽检数量');
      return;
    }
    // éƒ¨åˆ†åˆæ ¼æ—¶æ ¡éªŒæ•°é‡
    if (this.quickInspectForm.checkResult === '部分合格') {
      if (!this.quickInspectForm.qualifiedQuantity && !this.quickInspectForm.unqualifiedQuantity) {
        this.$message.warning('请输入合格数量和不合格数量');
        return;
      }
      const total = this.showSampleFields
        ? this.quickInspectForm.sampleQuantity
        : this.totalQuantity;
      const sum = (this.quickInspectForm.qualifiedQuantity || 0) +
                  (this.quickInspectForm.unqualifiedQuantity || 0);
      if (sum > total) {
        this.$message.warning('合格数量+不合格数量不能超过检验数量');
        return;
      }
    }
    try {
      const res = await axios.post('/qualityInspect/batchQuickInspect', this.quickInspectForm);
      if (res.code === 200) {
        this.$message.success(res.msg);
        this.quickInspectVisible = false;
        this.refreshList();
      } else {
        this.$message.error(res.msg);
      }
    } catch (e) {
      this.$message.error('快速检验失败');
    }
  },
}
```
### 5. æ£€éªŒå•列表显示抽检信息
建议在检验单列表中增加以下列:
```html
<el-table-column label="检验规则" width="100">
  <template #default="{ row }">
    <el-tag :type="row.inspectRule === 1 ? 'warning' : 'success'" size="small">
      {{ row.inspectRule === 1 ? '抽检' : '全检' }}
    </el-tag>
  </template>
</el-table-column>
<el-table-column label="抽检比例" width="100">
  <template #default="{ row }">
    {{ row.inspectRule === 1 ? (row.sampleRatio || 0) + '%' : '-' }}
  </template>
</el-table-column>
<el-table-column label="抽检数量" width="100">
  <template #default="{ row }">
    {{ row.inspectRule === 1 ? (row.sampleQuantity || '-') : row.quantity }}
  </template>
</el-table-column>
```
## è¯·æ±‚示例
### å…¨æ£€æ¨¡å¼
```json
{
  "ids": [1, 2, 3],
  "checkResult": "合格",
  "testStandardId": 100,
  "checkName": "张三",
  "checkTime": "2026-06-18"
}
```
### æŠ½æ£€æ¨¡å¼ - å…¨éƒ¨åˆæ ¼
```json
{
  "ids": [4],
  "checkResult": "合格",
  "testStandardId": 100,
  "checkName": "张三",
  "checkTime": "2026-06-18",
  "sampleQuantity": 50
}
```
### æŠ½æ£€æ¨¡å¼ - éƒ¨åˆ†åˆæ ¼
```json
{
  "ids": [5],
  "checkResult": "部分合格",
  "testStandardId": 100,
  "checkName": "张三",
  "checkTime": "2026-06-18",
  "sampleQuantity": 50,
  "qualifiedQuantity": 45,
  "unqualifiedQuantity": 5
}
```
## æ³¨æ„äº‹é¡¹
- æ‰¹é‡å¿«é€Ÿæ£€éªŒæ—¶ï¼Œå¦‚果选中的检验单有不同的检验规则(全检/抽检),建议提示用户分开处理,或前端按第一条检验单的模式统一处理。
- æŠ½æ£€æ•°é‡ç”±å‰ç«¯ä¼ å…¥æ—¶ï¼ŒåŽç«¯ä¼šæ ¡éªŒä¸è¶…过总数量,超出自动修正。
- æŠ½æ£€æ¨¡å¼ä¸‹ï¼Œå…¥åº“数量按抽检样本的合格数量计算,而非推算总量。
- å¦‚需在抽检模式下根据样本合格率推算总量入库比例,请另行提出需求。
- å‰ç«¯åœ¨æ‰“开弹窗时,应自动填充检验单已有的 `sampleQuantity` æˆ–按比例计算的默认值,减少用户输入负担。
docs/quality_inspect_rule.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,108 @@
# è´¨é‡æ£€éªŒè§„则(全检/抽检)
## æ¶‰åŠé¡µé¢
- æ£€æµ‹æ ‡å‡†ç®¡ç†é¡µé¢ï¼ˆæ–°å¢ž/编辑弹窗、列表)
- åŽŸææ–™æ£€éªŒé¡µé¢ï¼ˆåˆ—è¡¨ã€æ–°å¢ž/编辑弹窗)
- è¿‡ç¨‹æ£€éªŒé¡µé¢ï¼ˆåˆ—表、新增/编辑弹窗)
- å‡ºåŽ‚æ£€éªŒé¡µé¢ï¼ˆåˆ—è¡¨ã€æ–°å¢ž/编辑弹窗)
## API
### æ£€æµ‹æ ‡å‡†
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /qualityTestStandard/add | æ–°å¢žæ£€æµ‹æ ‡å‡†ï¼ˆæ–°å¢ž inspectRule、sampleRatio å­—段) |
| POST | /qualityTestStandard/update | ä¿®æ”¹æ£€æµ‹æ ‡å‡†ï¼ˆæ–°å¢ž inspectRule、sampleRatio å­—段) |
| GET | /qualityTestStandard/listPage | åˆ—表查询(返回新增字段) |
**新增请求/响应参数:**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| inspectRule | Integer | å¦ | æ£€éªŒè§„则: 0=全检, 1=抽检,默认0 |
| sampleRatio | BigDecimal | å¦ | æŠ½æ£€æ¯”例(百分比),仅inspectRule=1时有效,如10表示10% |
### è´¨æ£€è®°å½•(原材料/过程/出厂检验)
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /qualityInspect/add | æ–°å¢žè´¨æ£€è®°å½•(新增 inspectRule、sampleRatio、sampleQuantity å­—段) |
| POST | /qualityInspect/update | ä¿®æ”¹è´¨æ£€è®°å½•(新增字段) |
| GET | /qualityInspect/listPage | åˆ—表查询(返回新增字段) |
| POST | /qualityInspect/export | å¯¼å‡ºExcel(新增"检验规则"、"抽检比例"、"抽检数量"列) |
**新增响应参数:**
| å‚æ•° | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| inspectRule | Integer | æ£€éªŒè§„则: 0=全检, 1=抽检 |
| sampleRatio | BigDecimal | æŠ½æ£€æ¯”例(%) |
| sampleQuantity | BigDecimal | æŠ½æ£€æ•°é‡ |
**说明:** ç”Ÿäº§æŠ¥å·¥è‡ªåŠ¨åˆ›å»ºè´¨æ£€è®°å½•æ—¶ï¼Œä¼šä»Žå…³è”çš„æ£€æµ‹æ ‡å‡†å¤åˆ¶ `inspectRule` å’Œ `sampleRatio`,并自动计算 `sampleQuantity = æ€»æ•°é‡ Ã— æŠ½æ£€æ¯”例 / 100`(向上取整)。
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. æ£€æµ‹æ ‡å‡† - æ–°å¢ž/编辑弹窗
```html
<el-form-item label="检验规则" prop="inspectRule">
  <el-radio-group v-model="form.inspectRule">
    <el-radio :label="0">全检</el-radio>
    <el-radio :label="1">抽检</el-radio>
  </el-radio-group>
</el-form-item>
<el-form-item label="抽检比例(%)" prop="sampleRatio" v-if="form.inspectRule === 1">
  <el-input-number v-model="form.sampleRatio" :min="0.01" :max="100" :precision="2" placeholder="请输入抽检比例" />
</el-form-item>
```
### 2. æ£€æµ‹æ ‡å‡† - åˆ—表
```html
<el-table-column label="检验规则" prop="inspectRule" min-width="100">
  <template #default="{ row }">
    {{ row.inspectRule === 0 ? '全检' : row.inspectRule === 1 ? '抽检' : '' }}
  </template>
</el-table-column>
<el-table-column label="抽检比例(%)" prop="sampleRatio" min-width="120" />
```
### 3. è´¨æ£€è®°å½• - åˆ—表(原材料/过程/出厂检验页面)
```html
<el-table-column label="检验规则" prop="inspectRule" min-width="100">
  <template #default="{ row }">
    {{ row.inspectRule === 0 ? '全检' : row.inspectRule === 1 ? '抽检' : '' }}
  </template>
</el-table-column>
<el-table-column label="抽检比例(%)" prop="sampleRatio" min-width="120" />
<el-table-column label="抽检数量" prop="sampleQuantity" min-width="100" />
```
### 4. è´¨æ£€è®°å½• - æ–°å¢ž/编辑弹窗(可选手动填写)
```html
<el-form-item label="检验规则" prop="inspectRule">
  <el-radio-group v-model="form.inspectRule">
    <el-radio :label="0">全检</el-radio>
    <el-radio :label="1">抽检</el-radio>
  </el-radio-group>
</el-form-item>
<el-form-item label="抽检比例(%)" prop="sampleRatio" v-if="form.inspectRule === 1">
  <el-input-number v-model="form.sampleRatio" :min="0.01" :max="100" :precision="2" placeholder="请输入抽检比例" />
</el-form-item>
```
### 5. å¯¼å‡º
导出Excel自动新增"检验规则"、"抽检比例(%)"、"抽检数量"列,无需前端额外处理。
## æ³¨æ„äº‹é¡¹
- æ‰§è¡Œæ•°æ®åº“迁移脚本 `doc/20260617_quality_inspect_rule.sql` åŽå†éƒ¨ç½²
- æ£€æµ‹æ ‡å‡†è®¾ç½®æ£€éªŒè§„则后,生产报工自动生成的质检记录会继承该规则
- æŠ½æ£€æ¯”例仅在检验规则为"抽检"时才需填写
- æŠ½æ£€æ•°é‡ç”±ç³»ç»Ÿè‡ªåŠ¨è®¡ç®—ï¼š`总数量 Ã— æŠ½æ£€æ¯”例 / 100`(向上取整),无需手动填写
src/main/java/com/ruoyi/approve/service/impl/ApproveBusinessStatusService.java
@@ -159,7 +159,26 @@
        qualityInspectMapper.insert(qualityInspect);
        List<QualityTestStandard> qualityTestStandard = qualityTestStandardMapper.getQualityTestStandardByProductId(saleProduct.getProductId(), 0, null);
        if (qualityTestStandard.size() > 0) {
            qualityInspect.setTestStandardId(qualityTestStandard.get(0).getId());
            QualityTestStandard std = qualityTestStandard.get(0);
            qualityInspect.setTestStandardId(std.getId());
            // æ ¹æ® inspectRule è®¾ç½®æŠ½æ£€æ¯”例和抽检数量默认值
            if (std.getInspectRule() == null || std.getInspectRule() == 0) {
                // å…¨æ£€
                qualityInspect.setInspectRule(0);
                qualityInspect.setSampleRatio(java.math.BigDecimal.valueOf(100));
                qualityInspect.setSampleQuantity(saleProduct.getQuantity() != null ? saleProduct.getQuantity() : java.math.BigDecimal.ZERO);
            } else {
                // æŠ½æ£€
                qualityInspect.setInspectRule(1);
                java.math.BigDecimal ratio = std.getSampleRatio() != null ? std.getSampleRatio() : java.math.BigDecimal.ZERO;
                qualityInspect.setSampleRatio(ratio);
                if (saleProduct.getQuantity() != null && ratio.compareTo(java.math.BigDecimal.ZERO) > 0) {
                    qualityInspect.setSampleQuantity(saleProduct.getQuantity().multiply(ratio)
                            .divide(java.math.BigDecimal.valueOf(100), 0, java.math.RoundingMode.CEILING));
                } else {
                    qualityInspect.setSampleQuantity(java.math.BigDecimal.ZERO);
                }
            }
            qualityInspectMapper.updateById(qualityInspect);
            qualityTestStandardParamMapper.selectList(Wrappers.<QualityTestStandardParam>lambdaQuery()
                            .eq(QualityTestStandardParam::getTestStandardId, qualityTestStandard.get(0).getId()))
src/main/java/com/ruoyi/basic/dto/StorageBlobDTO.java
@@ -5,6 +5,8 @@
import com.ruoyi.basic.pojo.StorageBlob;
import lombok.Data;
import java.util.Map;
@Data
public class StorageBlobDTO extends StorageBlob {
    /**
@@ -23,7 +25,7 @@
    private String application;
    /**
     * æ”¯æŒä»Žæ•°å­—ID反序列化(前端可能只传ID)
     * æ”¯æŒä»Žæ•°å­—ID或完整JSON对象反序列化
     */
    @JsonCreator
    public static StorageBlobDTO from(Object value) {
@@ -32,6 +34,47 @@
            dto.setId(((Number) value).longValue());
            return dto;
        }
        if (value instanceof Map) {
            @SuppressWarnings("unchecked")
            Map<String, Object> map = (Map<String, Object>) value;
            StorageBlobDTO dto = new StorageBlobDTO();
            if (map.get("id") instanceof Number) {
                dto.setId(((Number) map.get("id")).longValue());
            }
            if (map.get("resourceKey") instanceof String) {
                dto.setResourceKey((String) map.get("resourceKey"));
            }
            if (map.get("contentType") instanceof String) {
                dto.setContentType((String) map.get("contentType"));
            }
            if (map.get("originalFilename") instanceof String) {
                dto.setOriginalFilename((String) map.get("originalFilename"));
            }
            if (map.get("uidFilename") instanceof String) {
                dto.setUidFilename((String) map.get("uidFilename"));
            }
            if (map.get("byteSize") instanceof Number) {
                dto.setByteSize(((Number) map.get("byteSize")).longValue());
            }
            if (map.get("path") instanceof String) {
                dto.setPath((String) map.get("path"));
            }
            if (map.get("previewURL") instanceof String) {
                dto.setPreviewURL((String) map.get("previewURL"));
            }
            if (map.get("downloadURL") instanceof String) {
                dto.setDownloadURL((String) map.get("downloadURL"));
            }
            return dto;
        }
        if (value instanceof String) {
            try {
                StorageBlobDTO dto = new StorageBlobDTO();
                dto.setId(Long.parseLong((String) value));
                return dto;
            } catch (NumberFormatException ignored) {
            }
        }
        throw new IllegalArgumentException("无法反序列化 StorageBlobDTO: " + value);
    }
}
src/main/java/com/ruoyi/home/dto/QualityQualifiedAnalysisDto.java
@@ -20,12 +20,12 @@
    /**
     * åˆæ ¼æ•°
     */
    private int qualifiedCount;
    private BigDecimal qualifiedCount;
    /**
     * ä¸åˆæ ¼æ•°
     */
    private int unqualifiedCount;
    private BigDecimal unqualifiedCount;
    /**
     * åˆæ ¼æ¯”例
src/main/java/com/ruoyi/home/service/impl/HomeServiceImpl.java
@@ -1721,14 +1721,15 @@
        LocalDate endDate = range[1];
        String startStr = startDate.toString();
        String endStr = endDate.toString();
        // ä½¿ç”¨åŒºé—´å³å¼€ï¼ˆ< endDate + 1 å¤©ï¼‰ä¿è¯æ£€æµ‹æ—¶é—´å«æ—¶åˆ†ç§’时也能覆盖到 endDate å½“天
        String endExclusiveStr = endDate.plusDays(1).toString();
        List<QualityInspect> list = qualityInspectMapper.selectList(
                new LambdaQueryWrapper<QualityInspect>()
                        .eq(QualityInspect::getInspectType, inspectType)
                        .eq(QualityInspect::getInspectState, 1)
                        .ge(QualityInspect::getCheckTime, startStr)
                        .le(QualityInspect::getCheckTime, endStr));
                        .lt(QualityInspect::getCheckTime, endExclusiveStr));
        return buildQualifiedAnalysis(list);
    }
@@ -1767,8 +1768,8 @@
        QualityQualifiedAnalysisDto dto = new QualityQualifiedAnalysisDto();
        if (CollectionUtils.isEmpty(list)) {
            dto.setQualifiedCount(0);
            dto.setUnqualifiedCount(0);
            dto.setQualifiedCount(BigDecimal.valueOf(0));
            dto.setUnqualifiedCount(BigDecimal.valueOf(0));
            dto.setQualifiedRate(BigDecimal.ZERO.setScale(2));
            dto.setUnqualifiedRate(BigDecimal.ZERO.setScale(2));
            return dto;
@@ -1784,8 +1785,8 @@
        BigDecimal totalCount = qualifiedCount.add(unqualifiedCount);
        dto.setQualifiedCount(qualifiedCount.intValue());
        dto.setUnqualifiedCount(unqualifiedCount.intValue());
        dto.setQualifiedCount(qualifiedCount);
        dto.setUnqualifiedCount(unqualifiedCount);
        if (totalCount.compareTo(BigDecimal.ZERO) == 0) {
            dto.setQualifiedRate(BigDecimal.ZERO.setScale(2));
@@ -1808,74 +1809,64 @@
    @Override
    public QualityInspectionCountDto qualityInspectionCount() {
        String todayStr = LocalDate.now().toString();
        String prevDayStr = LocalDate.now().minusDays(1).toString();
        // æŸ¥è¯¢å‡ºæˆªæ­¢ä»Šæ—¥çš„æ€»æ£€éªŒæ•°
        List<QualityInspect> todayList = qualityInspectMapper.selectList(new LambdaQueryWrapper<QualityInspect>()
                .le(QualityInspect::getCheckTime, todayStr));
        // æŸ¥è¯¢å‡ºæˆªæ­¢å‰ä¸€å¤©çš„æ€»æ£€éªŒæ•°
        List<QualityInspect> prevList = qualityInspectMapper.selectList(new LambdaQueryWrapper<QualityInspect>()
                .le(QualityInspect::getCheckTime, prevDayStr));
        // è®¡ç®—今日的总检验数
        BigDecimal todayCount = todayList.stream()
                .map(QualityInspect::getQuantity)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        // è®¡ç®—前一天的总检验数
        BigDecimal prevCount = prevList.stream()
                .map(QualityInspect::getQuantity)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        LocalDate today = LocalDate.now();
        String todayStartStr = today.toString();
        String tomorrowStartStr = today.plusDays(1).toString();
        String yesterdayStartStr = today.minusDays(1).toString();
        // è®¡ç®—今日相对昨天的一个总比增长
        BigDecimal growthRate = calcGrowthRate(todayCount, prevCount);
        // ç´¯è®¡æ€»æ£€éªŒæ•°ï¼ˆæˆªæ­¢ä»Šæ—¥ 24:00 ä¹‹å‰çš„æ‰€æœ‰è®°å½•)
        BigDecimal totalCount = sumInspectQuantity(null, tomorrowStartStr, null);
        // ä»Šæ—¥å•日检验数
        BigDecimal todayCount = sumInspectQuantity(todayStartStr, tomorrowStartStr, null);
        // æ˜¨æ—¥å•日检验数
        BigDecimal yesterdayCount = sumInspectQuantity(yesterdayStartStr, todayStartStr, null);
        // æ€»æ£€éªŒæ•°åŒæ¯”增长 = (今日单日 - æ˜¨æ—¥å•æ—¥) / æ˜¨æ—¥å•æ—¥ Ã— 100%
        BigDecimal growthRate = calcGrowthRate(todayCount, yesterdayCount);
        // è®¡ç®—今天的待完成数量
        List<QualityInspect> todayPendingList = qualityInspectMapper.selectList(new LambdaQueryWrapper<QualityInspect>()
                .eq(QualityInspect::getInspectState, 0)
                .eq(QualityInspect::getCheckTime, todayStr));
        // ä»Šæ—¥å¾…完成(inspectState=0)
        BigDecimal todayPendingCount = sumInspectQuantity(todayStartStr, tomorrowStartStr, 0);
        BigDecimal yesterdayPendingCount = sumInspectQuantity(yesterdayStartStr, todayStartStr, 0);
        BigDecimal todayPendingCountGrowthRate = calcGrowthRate(todayPendingCount, yesterdayPendingCount);
        // è®¡ç®—前一天的待完成数量
        List<QualityInspect> prevPendingList = qualityInspectMapper.selectList(new LambdaQueryWrapper<QualityInspect>()
                .eq(QualityInspect::getInspectState, 0)
                .eq(QualityInspect::getCheckTime, prevDayStr));
        // è®¡ç®—今天的待完成数量
        BigDecimal todayPendingCount = todayPendingList.stream()
                .map(QualityInspect::getQuantity)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        // ä»Šæ—¥å·²å®Œæˆï¼ˆinspectState=1)
        BigDecimal todayCompletedCount = sumInspectQuantity(todayStartStr, tomorrowStartStr, 1);
        BigDecimal yesterdayCompletedCount = sumInspectQuantity(yesterdayStartStr, todayStartStr, 1);
        BigDecimal todayCompletedCountGrowthRate = calcGrowthRate(todayCompletedCount, yesterdayCompletedCount);
        // è®¡ç®—前一天的待完成数量
        BigDecimal prevPendingCount = prevPendingList.stream()
                .map(QualityInspect::getQuantity)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        // è®¡ç®—今天的待完成数量相对昨天的一个同比增长
        BigDecimal todayPendingCountGrowthRate = calcGrowthRate(todayPendingCount, prevPendingCount);
        // è®¡ç®—今天的已完成数量
        List<QualityInspect> todayCompletedList = qualityInspectMapper
                .selectList(new LambdaQueryWrapper<QualityInspect>()
                        .eq(QualityInspect::getInspectState, 1)
                        .eq(QualityInspect::getCheckTime, todayStr));
        // è®¡ç®—前一天的已完成数量
        List<QualityInspect> prevCompletedList = qualityInspectMapper
                .selectList(new LambdaQueryWrapper<QualityInspect>()
                        .eq(QualityInspect::getInspectState, 1)
                        .eq(QualityInspect::getCheckTime, prevDayStr));
        // è®¡ç®—今天的已完成数量
        BigDecimal todayCompletedCount = todayCompletedList.stream()
                .map(QualityInspect::getQuantity)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        // è®¡ç®—前一天的已完成数量
        BigDecimal prevCompletedCount = prevCompletedList.stream()
                .map(QualityInspect::getQuantity)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        // è®¡ç®—今天的已完成数量相对昨天的一个同比增长
        BigDecimal todayCompletedCountGrowthRate = calcGrowthRate(todayCompletedCount, prevCompletedCount);
        QualityInspectionCountDto dto = new QualityInspectionCountDto();
        dto.setTotalCount(todayCount);
        dto.setTotalCount(totalCount);
        dto.setTotalCountGrowthRate(growthRate);
        dto.setTodayPendingCount(todayPendingCount);
        dto.setTodayPendingCountGrowthRate(todayPendingCountGrowthRate);
        dto.setTodayCompletedCount(todayCompletedCount);
        dto.setTodayCompletedCountGrowthRate(todayCompletedCountGrowthRate);
        return dto;
    }
    /**
     * é€šè¿‡ SQL SUM èšåˆ quality_inspect.quantity,避免把全表加载到内存
     */
    private BigDecimal sumInspectQuantity(String startInclusive, String endExclusive, Integer inspectState) {
        QueryWrapper<QualityInspect> wrapper = new QueryWrapper<>();
        wrapper.select("COALESCE(SUM(quantity), 0) AS total");
        if (startInclusive != null) {
            wrapper.ge("check_time", startInclusive);
        }
        if (endExclusive != null) {
            wrapper.lt("check_time", endExclusive);
        }
        if (inspectState != null) {
            wrapper.eq("inspect_state", inspectState);
        }
        List<Map<String, Object>> rows = qualityInspectMapper.selectMaps(wrapper);
        if (rows == null || rows.isEmpty()) {
            return BigDecimal.ZERO;
        }
        Object total = rows.get(0).get("total");
        if (total == null) {
            return BigDecimal.ZERO;
        }
        return total instanceof BigDecimal ? (BigDecimal) total : new BigDecimal(total.toString());
    }
    private BigDecimal calcGrowthRate(BigDecimal today, BigDecimal prev) {
@@ -1892,14 +1883,14 @@
    public NonComplianceWarningDto nonComplianceWarning() {
        String[] range = lastSevenDaysDateRange();
        String startStr = range[0];
        String endStr = range[1];
        String endExclusiveStr = range[1];
        // æŸ¥è¯¢è¿‘七天已处理不合格数据
        List<QualityUnqualified> list = qualityUnqualifiedMapper.selectList(
                new LambdaQueryWrapper<QualityUnqualified>()
                        .eq(QualityUnqualified::getInspectState, 1)
                        .ge(QualityUnqualified::getCheckTime, startStr)
                        .le(QualityUnqualified::getCheckTime, endStr));
                        .lt(QualityUnqualified::getCheckTime, endExclusiveStr));
        NonComplianceWarningDto dto = new NonComplianceWarningDto();
@@ -1998,24 +1989,24 @@
    }
    /**
     * èŽ·å–è¿‘ä¸ƒå¤©çš„æ—¥æœŸåŒºé—´ï¼ˆä»…å«å¹´æœˆæ—¥ï¼‰
     * èŽ·å–è¿‘ä¸ƒå¤©çš„æ—¥æœŸåŒºé—´ï¼ˆä»…å«å¹´æœˆæ—¥ï¼‰ï¼šè¿”å›ž [start, endExclusive] â€”— endExclusive æ˜¯ä»Šå¤© + 1 å¤©ï¼Œä¾¿äºŽä½¿ç”¨ lt åŒ…含今天全部
     */
    public static String[] lastSevenDaysDateRange() {
        LocalDate today = LocalDate.now();
        return new String[]{today.minusDays(6).toString(), today.toString()};
        return new String[]{today.minusDays(6).toString(), today.plusDays(1).toString()};
    }
    @Override
    public List<CompletedInspectionCountDto> completedInspectionCount() {
        String[] range = lastSevenDaysDateRange();
        String startStr = range[0];
        String endStr = range[1];
        String endExclusiveStr = range[1];
        // æŸ¥è¯¢è¿‘七天已完成的检验数据
        List<QualityInspect> list = qualityInspectMapper.selectList(new LambdaQueryWrapper<QualityInspect>()
                .eq(QualityInspect::getInspectState, 1)
                .ge(QualityInspect::getCheckTime, startStr)
                .le(QualityInspect::getCheckTime, endStr));
                .lt(QualityInspect::getCheckTime, endExclusiveStr));
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM-dd");
@@ -2067,8 +2058,15 @@
    @Override
    public List<UnqualifiedProductRankDto> unqualifiedProductRanking() {
        // é™å®šè¿‘ 30 å¤©ï¼Œé¿å…å…¨è¡¨åŠ è½½
        LocalDate today = LocalDate.now();
        String startStr = today.minusDays(29).toString();
        String endExclusiveStr = today.plusDays(1).toString();
        List<QualityInspect> list = qualityInspectMapper.selectList(new LambdaQueryWrapper<QualityInspect>()
                .eq(QualityInspect::getInspectState, 1));
                .eq(QualityInspect::getInspectState, 1)
                .ge(QualityInspect::getCheckTime, startStr)
                .lt(QualityInspect::getCheckTime, endExclusiveStr));
        if (CollectionUtils.isEmpty(list)) {
            return new ArrayList<>();
@@ -2219,7 +2217,7 @@
        List<QualityInspect> qualityInspectList = qualityInspectMapper
                .selectList(new LambdaQueryWrapper<QualityInspect>()
                        .ge(QualityInspect::getCheckTime, startDate.toString())
                        .le(QualityInspect::getCheckTime, endDate.toString())
                        .lt(QualityInspect::getCheckTime, endDate.plusDays(1).toString())
                        .eq(QualityInspect::getInspectState, 1));
        QualityStatisticsDto dto = new QualityStatisticsDto();
@@ -2294,15 +2292,15 @@
        // ç»Ÿè®¡æ¯ç§æ£€éªŒç±»åž‹çš„不合格数量
        item.setSupplierNum(list.stream()
                .filter(i -> i.getInspectType() == 0)
                .filter(i -> Objects.equals(i.getInspectType(), 0))
                .map(i -> i.getUnqualifiedQuantity() != null ? i.getUnqualifiedQuantity() : BigDecimal.ZERO)
                .reduce(BigDecimal.ZERO, BigDecimal::add));
        item.setProcessNum(list.stream()
                .filter(i -> i.getInspectType() == 1)
                .filter(i -> Objects.equals(i.getInspectType(), 1))
                .map(i -> i.getUnqualifiedQuantity() != null ? i.getUnqualifiedQuantity() : BigDecimal.ZERO)
                .reduce(BigDecimal.ZERO, BigDecimal::add));
        item.setFactoryNum(list.stream()
                .filter(i -> i.getInspectType() == 2)
                .filter(i -> Objects.equals(i.getInspectType(), 2))
                .map(i -> i.getUnqualifiedQuantity() != null ? i.getUnqualifiedQuantity() : BigDecimal.ZERO)
                .reduce(BigDecimal.ZERO, BigDecimal::add));
src/main/java/com/ruoyi/production/service/impl/ProductionProductMainServiceImpl.java
@@ -336,7 +336,25 @@
                qualityInspectMapper.insert(qualityInspect);
                List<QualityTestStandard> qualityTestStandard = qualityTestStandardMapper.getQualityTestStandardByProductId(product.getId(), inspectType, process);
                if (!qualityTestStandard.isEmpty()) {
                    qualityInspect.setTestStandardId(qualityTestStandard.get(0).getId());
                    QualityTestStandard std = qualityTestStandard.get(0);
                    qualityInspect.setTestStandardId(std.getId());
                    qualityInspect.setInspectRule(std.getInspectRule());
                    // æ ¹æ® inspectRule è®¾ç½®æŠ½æ£€æ¯”例和抽检数量默认值
                    if (std.getInspectRule() == null || std.getInspectRule() == 0) {
                        // å…¨æ£€: sampleRatio=100, sampleQuantity=全部数量
                        qualityInspect.setSampleRatio(java.math.BigDecimal.valueOf(100));
                        qualityInspect.setSampleQuantity(productQty != null ? productQty : java.math.BigDecimal.ZERO);
                    } else {
                        // æŠ½æ£€: sampleRatio å–标准配置值
                        java.math.BigDecimal ratio = std.getSampleRatio() != null ? std.getSampleRatio() : java.math.BigDecimal.ZERO;
                        qualityInspect.setSampleRatio(ratio);
                        if (productQty != null && ratio.compareTo(java.math.BigDecimal.ZERO) > 0) {
                            qualityInspect.setSampleQuantity(productQty.multiply(ratio)
                                    .divide(java.math.BigDecimal.valueOf(100), 0, java.math.RoundingMode.CEILING));
                        } else {
                            qualityInspect.setSampleQuantity(java.math.BigDecimal.ZERO);
                        }
                    }
                    qualityInspectMapper.updateById(qualityInspect);
                    qualityTestStandardParamMapper.selectList(Wrappers.<QualityTestStandardParam>lambdaQuery()
                                    .eq(QualityTestStandardParam::getTestStandardId, qualityTestStandard.get(0).getId()))
src/main/java/com/ruoyi/purchase/service/impl/PurchaseLedgerServiceImpl.java
@@ -424,7 +424,26 @@
        qualityInspectMapper.insert(qualityInspect);
        List<QualityTestStandard> qualityTestStandard = qualityTestStandardMapper.getQualityTestStandardByProductId(saleProduct.getProductId(), 0,null);
        if (qualityTestStandard.size()>0){
            qualityInspect.setTestStandardId(qualityTestStandard.get(0).getId());
            QualityTestStandard std = qualityTestStandard.get(0);
            qualityInspect.setTestStandardId(std.getId());
            // æ ¹æ® inspectRule è®¾ç½®æŠ½æ£€æ¯”例和抽检数量默认值
            if (std.getInspectRule() == null || std.getInspectRule() == 0) {
                // å…¨æ£€
                qualityInspect.setInspectRule(0);
                qualityInspect.setSampleRatio(java.math.BigDecimal.valueOf(100));
                qualityInspect.setSampleQuantity(saleProduct.getQuantity() != null ? saleProduct.getQuantity() : java.math.BigDecimal.ZERO);
            } else {
                // æŠ½æ£€
                qualityInspect.setInspectRule(1);
                java.math.BigDecimal ratio = std.getSampleRatio() != null ? std.getSampleRatio() : java.math.BigDecimal.ZERO;
                qualityInspect.setSampleRatio(ratio);
                if (saleProduct.getQuantity() != null && ratio.compareTo(java.math.BigDecimal.ZERO) > 0) {
                    qualityInspect.setSampleQuantity(saleProduct.getQuantity().multiply(ratio)
                            .divide(java.math.BigDecimal.valueOf(100), 0, java.math.RoundingMode.CEILING));
                } else {
                    qualityInspect.setSampleQuantity(java.math.BigDecimal.ZERO);
                }
            }
            qualityInspectMapper.updateById(qualityInspect);
            qualityTestStandardParamMapper.selectList(Wrappers.<QualityTestStandardParam>lambdaQuery()
                    .eq(QualityTestStandardParam::getTestStandardId,qualityTestStandard.get(0).getId()))
src/main/java/com/ruoyi/quality/dto/BatchQuickInspectRequest.java
@@ -4,6 +4,7 @@
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Data
@@ -29,4 +30,13 @@
    @Schema(description = "检验参数列表")
    private List<QualityInspectParam> paramList;
    @Schema(description = "抽检数量,用于抽检模式时填写实际抽检样本数量")
    private BigDecimal sampleQuantity;
    @Schema(description = "合格数量(抽检样本中的合格数)")
    private BigDecimal qualifiedQuantity;
    @Schema(description = "不合格数量(抽检样本中的不合格数)")
    private BigDecimal unqualifiedQuantity;
}
src/main/java/com/ruoyi/quality/pojo/QualityInspect.java
@@ -171,6 +171,17 @@
    @Schema(description = "入库比例(百分比),默认100")
    private BigDecimal stockInRatio;
    @Schema(description = "检验规则: 0=全检, 1=抽检")
    @Excel(name = "检验规则", readConverterExp = "0=全检,1=抽检")
    private Integer inspectRule;
    @Schema(description = "抽检比例(百分比), ä»…inspectRule=1时有效")
    @Excel(name = "抽检比例(%)")
    private BigDecimal sampleRatio;
    @Schema(description = "抽检数量, ä»…inspectRule=1时有效")
    private BigDecimal sampleQuantity;
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
}
src/main/java/com/ruoyi/quality/pojo/QualityTestStandard.java
@@ -69,6 +69,12 @@
    @Schema(description = "工序id")
    private Integer processId;
    @Schema(description = "检验规则: 0=全检, 1=抽检")
    private Integer inspectRule;
    @Schema(description = "抽检比例(百分比), ä»…inspectRule=1时有效, å¦‚10表示10%")
    private java.math.BigDecimal sampleRatio;
    @TableField(fill = FieldFill.INSERT)
    private Long deptId;
}
src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java
@@ -80,12 +80,39 @@
        QualityInspect qualityInspect = new QualityInspect();
        BeanUtils.copyProperties(qualityInspectDto, qualityInspect);
        qualityInspect.setInspectState(0);//默认未提交
        // æ ¹æ® inspectRule è¡¥å…¨æŠ½æ£€æ¯”例和抽检数量默认值
        applyInspectRuleDefaults(qualityInspect);
        qualityInspectMapper.insert(qualityInspect);
        for (QualityInspectParam qualityInspectParam : qualityInspectDto.getQualityInspectParams()) {
            qualityInspectParam.setInspectId(qualityInspect.getId());
        }
        qualityInspectParamService.saveBatch(qualityInspectDto.getQualityInspectParams());
        return 0;
    }
    /**
     * æ ¹æ® inspectRule è®¾ç½®æŠ½æ£€æ¯”例和抽检数量默认值
     * - inspectRule=0 (全检): sampleRatio=100, sampleQuantity=全部数量
     * - inspectRule=1 (抽检): sampleRatio å–传入值或0, sampleQuantity=数量×比例/100
     */
    private void applyInspectRuleDefaults(QualityInspect inspect) {
        Integer rule = inspect.getInspectRule();
        java.math.BigDecimal quantity = inspect.getQuantity();
        if (rule == null || rule == 0) {
            // å…¨æ£€
            inspect.setSampleRatio(java.math.BigDecimal.valueOf(100));
            inspect.setSampleQuantity(quantity != null ? quantity : java.math.BigDecimal.ZERO);
        } else {
            // æŠ½æ£€
            java.math.BigDecimal ratio = inspect.getSampleRatio() != null ? inspect.getSampleRatio() : java.math.BigDecimal.ZERO;
            inspect.setSampleRatio(ratio);
            if (quantity != null && ratio.compareTo(java.math.BigDecimal.ZERO) > 0) {
                inspect.setSampleQuantity(quantity.multiply(ratio)
                        .divide(java.math.BigDecimal.valueOf(100), 0, java.math.RoundingMode.CEILING));
            } else {
                inspect.setSampleQuantity(java.math.BigDecimal.ZERO);
            }
        }
    }
    @Override
@@ -244,7 +271,9 @@
    /**
     * åœ¨ç‹¬ç«‹äº‹åŠ¡ä¸­å¤„ç†å•ä¸ªæ£€éªŒå•
     * æ•°é‡ã€åˆæ ¼æ•°é‡é»˜è®¤ä½¿ç”¨æ£€éªŒå•自身的数量,不合格数量为0
     * æ”¯æŒå…¨æ£€å’ŒæŠ½æ£€æ¨¡å¼ï¼š
     * - å…¨æ£€ï¼šæ•°é‡ã€åˆæ ¼æ•°é‡é»˜è®¤ä½¿ç”¨æ£€éªŒå•自身的数量,不合格数量为0
     * - æŠ½æ£€ï¼šä½¿ç”¨å‰ç«¯ä¼ å…¥çš„ sampleQuantity/qualifiedQuantity/unqualifiedQuantity
     */
    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
    public void processSingleInspect(Long id, BatchQuickInspectRequest request,
@@ -257,17 +286,66 @@
            throw new RuntimeException("检验单已提交");
        }
        // æ•°é‡é»˜è®¤å–检验单自身的数量,不合格数量为0
        BigDecimal qty = qualityInspect.getQuantity() != null ? qualityInspect.getQuantity() : BigDecimal.ZERO;
        BigDecimal qualified = qty;
        BigDecimal unqualified = BigDecimal.ZERO;
        // åˆ¤æ–­æ£€éªŒè§„则:全检(0/null) æˆ– æŠ½æ£€(1)
        Integer inspectRule = qualityInspect.getInspectRule();
        boolean isFullInspect = inspectRule == null || inspectRule == 0;
        BigDecimal totalQty = qualityInspect.getQuantity() != null ? qualityInspect.getQuantity() : BigDecimal.ZERO;
        BigDecimal sampleQty;
        BigDecimal qualified;
        BigDecimal unqualified;
        if (isFullInspect) {
            // å…¨æ£€æ¨¡å¼ï¼šæ£€éªŒæ•°é‡ = æ€»æ•°é‡
            sampleQty = totalQty;
            // å¦‚果前端传入了合格/不合格数量则使用,否则默认全部合格
            if (request.getQualifiedQuantity() != null && request.getUnqualifiedQuantity() != null) {
                qualified = request.getQualifiedQuantity();
                unqualified = request.getUnqualifiedQuantity();
            } else {
                qualified = totalQty;
                unqualified = BigDecimal.ZERO;
            }
        } else {
            // æŠ½æ£€æ¨¡å¼
            // æŠ½æ£€æ•°é‡ï¼šä¼˜å…ˆä½¿ç”¨å‰ç«¯ä¼ å…¥å€¼ï¼Œå…¶æ¬¡ä½¿ç”¨æ£€éªŒå•已有的 sampleQuantity,最后按比例计算
            if (request.getSampleQuantity() != null && request.getSampleQuantity().compareTo(BigDecimal.ZERO) > 0) {
                sampleQty = request.getSampleQuantity();
            } else if (qualityInspect.getSampleQuantity() != null && qualityInspect.getSampleQuantity().compareTo(BigDecimal.ZERO) > 0) {
                sampleQty = qualityInspect.getSampleQuantity();
            } else {
                // æŒ‰æŠ½æ£€æ¯”例计算
                BigDecimal ratio = qualityInspect.getSampleRatio() != null ? qualityInspect.getSampleRatio() : BigDecimal.ZERO;
                sampleQty = totalQty.multiply(ratio)
                        .divide(BigDecimal.valueOf(100), 0, BigDecimal.ROUND_CEILING);
            }
            // æ ¡éªŒæŠ½æ£€æ•°é‡ä¸èƒ½è¶…过总数量
            if (sampleQty.compareTo(totalQty) > 0) {
                sampleQty = totalQty;
            }
            // åˆæ ¼/不合格数量:优先使用前端传入值
            if (request.getQualifiedQuantity() != null || request.getUnqualifiedQuantity() != null) {
                qualified = request.getQualifiedQuantity() != null ? request.getQualifiedQuantity() : BigDecimal.ZERO;
                unqualified = request.getUnqualifiedQuantity() != null ? request.getUnqualifiedQuantity() : BigDecimal.ZERO;
            } else {
                // é»˜è®¤æŠ½æ£€æ ·æœ¬å…¨éƒ¨åˆæ ¼
                qualified = sampleQty;
                unqualified = BigDecimal.ZERO;
            }
        }
        // 2. æ›´æ–°æ£€éªŒå•字段
        qualityInspect.setCheckResult(checkResult);
        qualityInspect.setTestStandardId(request.getTestStandardId());
        qualityInspect.setQuantity(qty);
        // è®°å½•实际检验数量(抽检时为样本数量,全检时为总数量)
        qualityInspect.setQuantity(sampleQty);
        qualityInspect.setQualifiedQuantity(qualified);
        qualityInspect.setUnqualifiedQuantity(unqualified);
        // æ›´æ–°æŠ½æ£€æ•°é‡å­—段
        if (!isFullInspect) {
            qualityInspect.setSampleQuantity(sampleQty);
        }
        if (request.getCheckCompany() != null) {
            qualityInspect.setCheckCompany(request.getCheckCompany());
        }
src/main/java/com/ruoyi/quality/utils/QualityInspectHelper.java
@@ -15,6 +15,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.util.List;
/**
@@ -56,6 +57,8 @@
        QualityTestStandard firstStandard = qualityTestStandardList.get(0);
        qualityInspect.setTestStandardId(firstStandard.getId());
        // æ ¹æ® inspectRule è®¾ç½®é»˜è®¤æŠ½æ£€æ¯”例和抽检数量
        applyInspectRuleDefaults(qualityInspect, firstStandard, saleProduct.getQuantity());
        qualityInspectMapper.updateById(qualityInspect);
        List<QualityTestStandardParam> standardParams = qualityTestStandardParamMapper.selectList(
@@ -70,4 +73,35 @@
            qualityInspectParamMapper.insert(param);
        }
    }
    /**
     * æ ¹æ® inspectRule è®¾ç½®æŠ½æ£€æ¯”例和抽检数量默认值
     * - inspectRule=0 (全检): sampleRatio=100, sampleQuantity=全部数量
     * - inspectRule=1 (抽检): sampleRatio å–标准配置值, sampleQuantity=数量×比例/100
     */
    private void applyInspectRuleDefaults(QualityInspect inspect, QualityTestStandard standard, BigDecimal quantity) {
        Integer rule = standard.getInspectRule();
        inspect.setInspectRule(rule);
        if (rule == null || rule == 0) {
            // å…¨æ£€
            inspect.setSampleRatio(BigDecimal.valueOf(100));
            inspect.setSampleQuantity(quantity != null ? quantity : BigDecimal.ZERO);
        } else {
            // æŠ½æ£€
            BigDecimal ratio = standard.getSampleRatio();
            if (ratio == null) {
                ratio = BigDecimal.ZERO;
            }
            inspect.setSampleRatio(ratio);
            if (quantity != null && ratio.compareTo(BigDecimal.ZERO) > 0) {
                BigDecimal sampleQty = quantity.multiply(ratio)
                        .divide(BigDecimal.valueOf(100), 4, BigDecimal.ROUND_HALF_UP)
                        .setScale(0, BigDecimal.ROUND_HALF_UP);
                inspect.setSampleQuantity(sampleQty);
            } else {
                inspect.setSampleQuantity(BigDecimal.ZERO);
            }
        }
    }
}
src/main/resources/mapper/sales/SalesQuotationMapper.xml
@@ -11,7 +11,7 @@
                AND t1.quotation_no LIKE CONCAT('%',#{salesQuotationDto.quotationNo},'%')
            </if>
            <if test="salesQuotationDto.customer != null and salesQuotationDto.customer != '' ">
                AND t1.customer = #{salesQuotationDto.customer}
                AND t1.customer like concat("%",#{salesQuotationDto.customer},"%")
            </if>
            <if test="salesQuotationDto.status != null and salesQuotationDto.status != '' ">
                AND t1.status = #{salesQuotationDto.status}