1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
package cn.iocoder.yudao.module.mes.service.qc.ipqc;
 
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceLineDO;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.qc.ipqc.vo.MesQcIpqcImportExcelVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.ipqc.vo.MesQcIpqcPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.ipqc.vo.MesQcIpqcSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.vo.MesQcImportRespVO;
import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodeRuleCodeEnum;
import cn.iocoder.yudao.module.mes.service.qc.imports.MesQcImportHelper;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkstationDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.defectrecord.MesQcDefectRecordDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.ipqc.MesQcIpqcDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.template.MesQcTemplateDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.template.MesQcTemplateItemDO;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.ipqc.MesQcIpqcMapper;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcDefectLevelEnum;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcTypeEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProFeedbackStatusEnum;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationService;
import cn.iocoder.yudao.module.mes.service.pro.feedback.MesProFeedbackService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProcessService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProductService;
import cn.iocoder.yudao.module.mes.service.pro.task.MesProTaskService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
import cn.iocoder.yudao.module.mes.service.qc.defectrecord.MesQcDefectRecordService;
import cn.iocoder.yudao.module.mes.service.qc.indicatorresult.MesQcIndicatorResultService;
import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateItemService;
import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateService;
import cn.iocoder.yudao.module.mes.controller.admin.qc.ncr.vo.MesQcNcrSaveReqVO;
import cn.iocoder.yudao.module.mes.service.qc.ncr.MesQcNcrService;
import cn.iocoder.yudao.module.mes.service.wm.productproduce.MesWmProductProduceLineService;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
 
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
 
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
 
/**
 * MES 过程检验单(IPQC) Service 实现类
 *
 * @author 超级管理员
 */
@Service
@Validated
@Slf4j
public class MesQcIpqcServiceImpl implements MesQcIpqcService {
 
    /**
     * IPQC 保存数据校验上下文
     *
     * @param workstation 工位(可选,未指定 workstationId 时为空)
     * @param task        生产任务(可选,指定 taskId 时非空)
     * @param workOrder   生产工单(可选,未指定 workOrderId 时为空)
     */
    private record SaveDataContext(MesMdWorkstationDO workstation, MesProTaskDO task, MesProWorkOrderDO workOrder) {}
 
    @Resource
    private MesQcIpqcMapper ipqcMapper;
 
    @Resource
    private MesQcTemplateItemService templateItemService;
    @Resource
    private MesQcTemplateService templateService;
    @Resource
    private MesQcIpqcLineService ipqcLineService;
    @Resource
    @Lazy
    private MesProWorkOrderService workOrderService;
    @Resource
    @Lazy
    private MesMdWorkstationService workstationService;
    @Resource
    @Lazy
    private MesMdItemService itemService;
    @Resource
    @Lazy
    private MesQcDefectRecordService defectRecordService;
    @Resource
    @Lazy
    private MesProRouteProductService routeProductService;
    @Resource
    @Lazy
    private MesProRouteProcessService routeProcessService;
    @Resource
    @Lazy
    private MesProTaskService taskService;
    @Resource
    @Lazy
    private MesProFeedbackService feedbackService;
    @Resource
    @Lazy
    private MesQcIndicatorResultService indicatorResultService;
    @Resource
    @Lazy
    private MesWmProductProduceLineService productProduceLineService;
 
    @Resource
    @Lazy
    private MesQcNcrService ncrService;
 
    @Resource
    private AdminUserApi adminUserApi;
 
    @Resource
    private MesQcImportHelper qcImportHelper;
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createIpqc(MesQcIpqcSaveReqVO createReqVO) {
        // 1.1 校验数据(生产订单、工位均非必填)
        SaveDataContext ctx = validateIpqcSaveData(createReqVO);
        // 1.2 确定检验物料:有任务时取任务的物料,有工单时取工单的产品,都没有时取请求指定的产品物料
        Long itemId = resolveItemId(createReqVO, ctx);
        // 1.3 确定质检方案:显式指定时校验方案本身可用,未指定时按产品物料已绑定方案兜底
        Long templateId = resolveTemplateId(itemId, createReqVO.getTemplateId());
        // 1.4 获取来源单据编号
        String sourceDocCode = validateAndGetSourceDocCode(
                createReqVO.getSourceDocType(), createReqVO.getSourceDocId(), createReqVO.getSourceLineId());
 
        // 2. 插入主表
        MesQcIpqcDO ipqc = BeanUtils.toBean(createReqVO, MesQcIpqcDO.class)
                .setItemId(itemId)
                .setProcessId(getProcessId(itemId,
                        ctx.workstation() != null ? ctx.workstation().getProcessId() : null))
                .setTemplateId(templateId)
                .setSourceDocCode(sourceDocCode)
                .setStatus(MesQcStatusEnum.DRAFT.getStatus());
        ipqcMapper.insert(ipqc);
 
        // 3. 从模板指标自动生成检验行
        ipqcLineService.createLinesFromTemplate(ipqc.getId(), templateId);
        return ipqc.getId();
    }
 
    @Override
    public void updateIpqc(MesQcIpqcSaveReqVO updateReqVO) {
        // 1.1 校验存在 + 草稿状态
        validateIpqcStatusPrepare(updateReqVO.getId());
        // 1.2 校验数据
        SaveDataContext ctx = validateIpqcSaveData(updateReqVO);
 
        // 2. 更新主表
        MesQcIpqcDO existIpqc = validateIpqcExists(updateReqVO.getId());
        MesQcIpqcDO updateObj = BeanUtils.toBean(updateReqVO, MesQcIpqcDO.class);
        updateObj.setSourceDocType(null).setSourceDocId(null).setSourceLineId(null); // 不允许修改来源单据
        updateObj.setTemplateId(null); // 不允许修改模板
        updateObj.setItemId(null); // 不允许修改物料
        updateObj.setWorkOrderId(null); // 不允许修改工单
        updateObj.setProcessId(getProcessId(existIpqc.getItemId(),
                ctx.workstation() != null ? ctx.workstation().getProcessId() : null));
        ipqcMapper.updateById(updateObj);
    }
 
    private SaveDataContext validateIpqcSaveData(MesQcIpqcSaveReqVO reqVO) {
        // 校验编号唯一
        validateIpqcCodeUnique(reqVO.getId(), reqVO.getCode());
        // 校验工位存在(工位非必填,未选择时跳过)
        MesMdWorkstationDO workstation = reqVO.getWorkstationId() != null
                ? workstationService.validateWorkstationExists(reqVO.getWorkstationId()) : null;
        adminUserApi.validateUser(reqVO.getInspectorUserId());
        // 校验工单存在且已确认(生产订单非必填,未选择时跳过)
        MesProWorkOrderDO workOrder = reqVO.getWorkOrderId() != null
                ? workOrderService.validateWorkOrderConfirmed(reqVO.getWorkOrderId()) : null;
        if (workstation != null && reqVO.getProcessId() != null
                && ObjUtil.notEqual(workstation.getProcessId(), reqVO.getProcessId())) {
            throw exception(PRO_WORKSTATION_PROCESS_MISMATCH);
        }
        // 校验任务关系(如果指定了任务)
        MesProTaskDO task = null;
        if (reqVO.getTaskId() != null) {
            task = taskService.validateTaskNotFinished(reqVO.getTaskId());
            validateTaskRelation(task, workstation, workOrder, reqVO);
        } else if (workOrder != null && reqVO.getItemId() != null
                && ObjUtil.notEqual(reqVO.getItemId(), workOrder.getProductId())) {
            // 无任务时,itemId 仍需与工单产品一致
            throw exception(PRO_WORK_ORDER_PRODUCT_MISMATCH);
        }
        return new SaveDataContext(workstation, task, workOrder);
    }
 
    /**
     * 确定检验物料:任务优先,其次工单产品,都没有时取请求指定的产品物料(都未指定则为空)
     */
    private Long resolveItemId(MesQcIpqcSaveReqVO reqVO, SaveDataContext ctx) {
        if (ctx.task() != null) {
            return ctx.task().getItemId();
        }
        if (ctx.workOrder() != null) {
            return ctx.workOrder().getProductId();
        }
        return reqVO.getItemId();
    }
 
    /**
     * 确定质检方案
     *
     * <p>产品物料已绑定 IPQC 方案时沿用原逻辑:未显式指定取绑定列表首个,显式指定则必须命中绑定列表;
     * <p>产品物料未匹配到任何方案(未选生产订单/生产任务/产品物料)时,显式指定的方案改为校验方案本身:
     * 存在、已启用、检测种类包含过程检验,不再要求与产品物料绑定。
     */
    private Long resolveTemplateId(Long itemId, Long explicitTemplateId) {
        List<MesQcTemplateItemDO> templateItems = templateItemService.getTemplateItemList(
                itemId, MesQcTypeEnum.IPQC.getType(), null, null);
        if (CollUtil.isNotEmpty(templateItems)) {
            if (explicitTemplateId == null) {
                return templateItems.get(0).getTemplateId();
            }
            return templateItems.stream()
                    .filter(available -> Objects.equals(available.getTemplateId(), explicitTemplateId))
                    .findFirst()
                    .orElseThrow(() -> exception(QC_NO_TEMPLATE)) // 指定了方案但不在匹配列表中,拒绝
                    .getTemplateId();
        }
        // 产品物料未匹配到任何 IPQC 方案
        if (explicitTemplateId != null) {
            return validateTemplateUsable(explicitTemplateId);
        }
        throw new ServiceException(QC_IPQC_NO_TEMPLATE.getCode(), buildNoTemplateMessage(itemId));
    }
 
    /**
     * 校验质检方案本身可用:存在、已启用、检测种类包含过程检验,返回方案编号
     */
    private Long validateTemplateUsable(Long templateId) {
        MesQcTemplateDO template = templateService.getTemplate(templateId);
        if (template == null) {
            throw new ServiceException(QC_TEMPLATE_NOT_EXISTS.getCode(), StrUtil.format(
                    "所选质检方案不存在(ID={}),可能已被删除,请刷新后重新选择质检方案", templateId));
        }
        if (!CommonStatusEnum.isEnable(template.getStatus())) {
            throw exception(QC_IPQC_TEMPLATE_IS_DISABLE, template.getName());
        }
        if (!CollUtil.contains(template.getTypes(), MesQcTypeEnum.IPQC.getType())) {
            throw exception(QC_IPQC_TEMPLATE_TYPE_NOT_MATCH, template.getName());
        }
        return template.getId();
    }
 
    /**
     * 构建「无可用质检方案」的提示:区分「有产品物料但未绑定方案」与「未选工单/任务/物料」
     */
    private String buildNoTemplateMessage(Long itemId) {
        if (itemId == null) {
            return "未选择生产订单/生产任务/产品物料,无法自动匹配质检方案:"
                    + "请直接选择「质检方案」后重试,或先选择生产订单/生产任务以自动带出该产品已绑定的方案。";
        }
        return StrUtil.format("产品物料[{}]未绑定「过程检验」质检方案:请在【质量管理-质检方案】中为该物料"
                + "配置检测种类包含过程检验的方案,或在本单手动选择质检方案。", describeItem(itemId));
    }
 
    /**
     * 描述产品物料,用于异常提示:优先给出「编码 名称」,查不到时回退为物料编号
     */
    private String describeItem(Long itemId) {
        MesMdItemDO item = itemService.getItem(itemId);
        return item != null ? item.getCode() + " " + item.getName() : "物料编号 ID=" + itemId;
    }
 
    private void validateTaskRelation(MesProTaskDO task, MesMdWorkstationDO workstation,
                                      MesProWorkOrderDO workOrder, MesQcIpqcSaveReqVO reqVO) {
        // 生产订单、工位均非必填,未选择时跳过对应比对
        if (workOrder != null && ObjUtil.notEqual(task.getWorkOrderId(), workOrder.getId())) {
            throw exception(PRO_TASK_WORK_ORDER_MISMATCH);
        }
        if (workstation != null && ObjUtil.notEqual(task.getWorkstationId(), workstation.getId())) {
            throw exception(PRO_TASK_WORKSTATION_MISMATCH);
        }
        Long expectedProcessId = reqVO.getProcessId() != null ? reqVO.getProcessId()
                : (workstation != null ? workstation.getProcessId() : null);
        if (expectedProcessId != null && ObjUtil.notEqual(task.getProcessId(), expectedProcessId)) {
            throw exception(PRO_TASK_ROUTE_PROCESS_MISMATCH);
        }
        // 校验检验物料与任务物料一致(有任务时,IPQC 检验的物料应与任务加工的物料一致)
        if (reqVO.getItemId() != null && ObjUtil.notEqual(reqVO.getItemId(), task.getItemId())) {
            throw exception(PRO_TASK_ITEM_MISMATCH);
        }
    }
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void finishIpqc(Long id) {
        // 1.1 校验存在 + 草稿状态
        MesQcIpqcDO ipqc = validateIpqcStatusPrepare(id);
        // 1.2 校验检测结论必填
        if (ipqc.getCheckResult() == null) {
            throw exception(QC_IPQC_CHECK_RESULT_EMPTY);
        }
        // 1.3 校验至少存在一条检测结果
        indicatorResultService.validateIndicatorResultExistsByQcIdAndType(id, MesQcTypeEnum.IPQC.getType());
 
        // 2. 更新状态为已完成
        ipqcMapper.updateById(new MesQcIpqcDO()
                .setId(id).setStatus(MesQcStatusEnum.FINISHED.getStatus()));
 
        // 3. 不合格时自动创建 NCR
        generateNcrIfNeeded(ipqc);
 
        // 4. 回写来源单据
        writeBackSourceDoc(ipqc);
    }
 
    /**
     * 不合格时自动创建 NCR(不合格品处理单)
     *
     * @param ipqc 过程检验单
     */
    private void generateNcrIfNeeded(MesQcIpqcDO ipqc) {
        BigDecimal unqualifiedQty = ipqc.getUnqualifiedQuantity();
        if (unqualifiedQty == null || unqualifiedQty.compareTo(BigDecimal.ZERO) <= 0) {
            return;
        }
        String ncrCode = "NCR" + java.time.LocalDateTime.now()
                .format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"));
        MesQcNcrSaveReqVO saveReqVO = new MesQcNcrSaveReqVO();
        saveReqVO.setCode(ncrCode);
        saveReqVO.setName("不合格品处理单");
        saveReqVO.setQcType(MesQcTypeEnum.IPQC.getType());
        saveReqVO.setQcId(ipqc.getId());
        saveReqVO.setItemId(ipqc.getItemId());
        saveReqVO.setUnqualifiedQuantity(unqualifiedQty);
        saveReqVO.setDefectDescription("由过程检验(" + ipqc.getCode() + ")自动生成");
        ncrService.createNcr(saveReqVO);
        log.info("[generateNcrIfNeeded] IPQC({}) 不合格,自动创建 NCR", ipqc.getId());
    }
 
    /**
     * 回写来源单据(IPQC 完成后)
     *
     * <p>当来源为报工时,拆分待检产出行、完成产出入库、回写报工状态和任务/工单进度
     *
     * @param ipqc 过程检验单
     */
    private void writeBackSourceDoc(MesQcIpqcDO ipqc) {
        // 1. 确定 sourceDocType/sourceDocId
        Long sourceDocId = ipqc.getSourceDocId();
        Long sourceLineId = ipqc.getSourceLineId();
 
        // 如果 IPQC 单没有设置来源,尝试通过 taskId 查找关联的待检验报工
        if (sourceDocId == null && ipqc.getTaskId() != null) {
            MesProFeedbackDO feedback = feedbackService.getFeedbackListByTaskId(ipqc.getTaskId()).stream()
                    .filter(f -> MesProFeedbackStatusEnum.UNCHECK.getStatus().equals(f.getStatus()))
                    .findFirst().orElse(null);
            if (feedback != null) {
                sourceDocId = feedback.getId();
                log.info("[writeBackSourceDoc] IPQC({}) 未设置来源单据,通过 taskId({}) 找到待检验报工({})",
                        ipqc.getId(), ipqc.getTaskId(), feedback.getId());
            }
        }
 
        if (sourceDocId == null) {
            log.warn("[writeBackSourceDoc] IPQC({}) 无法确定来源报工,跳过回写", ipqc.getId());
            return;
        }
 
        // 2. 回写报工
        feedbackService.updateProFeedbackWhenIpqcFinish(sourceDocId, sourceLineId,
                ObjectUtil.defaultIfNull(ipqc.getQualifiedQuantity(), BigDecimal.ZERO),
                ObjectUtil.defaultIfNull(ipqc.getUnqualifiedQuantity(), BigDecimal.ZERO),
                ObjectUtil.defaultIfNull(ipqc.getLaborScrapQuantity(), BigDecimal.ZERO),
                ObjectUtil.defaultIfNull(ipqc.getMaterialScrapQuantity(), BigDecimal.ZERO),
                ObjectUtil.defaultIfNull(ipqc.getOtherScrapQuantity(), BigDecimal.ZERO));
    }
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteIpqc(Long id) {
        // 1. 校验存在 + 草稿状态
        validateIpqcStatusPrepare(id);
 
        // 2.1 删除主表
        ipqcMapper.deleteById(id);
        // 2.2 级联删除行
        ipqcLineService.deleteListByIpqcId(id);
        // 2.3 级联删除缺陷记录
        defectRecordService.deleteListByQcTypeAndQcId(MesQcTypeEnum.IPQC.getType(), id);
    }
 
    @Override
    public MesQcIpqcDO validateIpqcExists(Long id) {
        MesQcIpqcDO ipqc = ipqcMapper.selectById(id);
        if (ipqc == null) {
            throw exception(QC_IPQC_NOT_EXISTS);
        }
        return ipqc;
    }
 
    /**
     * 校验过程检验单存在且为草稿状态
     *
     * @param id 过程检验单 ID
     * @return 过程检验单
     */
    private MesQcIpqcDO validateIpqcStatusPrepare(Long id) {
        MesQcIpqcDO ipqc = validateIpqcExists(id);
        if (ObjUtil.notEqual(ipqc.getStatus(), MesQcStatusEnum.DRAFT.getStatus())) {
            throw exception(QC_IPQC_NOT_PREPARE);
        }
        return ipqc;
    }
 
    private void validateIpqcCodeUnique(Long id, String code) {
        MesQcIpqcDO ipqc = ipqcMapper.selectByCode(code);
        if (ipqc == null) {
            return;
        }
        if (ObjUtil.notEqual(ipqc.getId(), id)) {
            throw exception(QC_IPQC_CODE_DUPLICATE);
        }
    }
 
    private String validateAndGetSourceDocCode(Integer sourceDocType, Long sourceDocId, Long sourceLineId) {
        if (sourceDocType == null || sourceDocId == null) {
            return null;
        }
        if (Objects.equals(sourceDocType, MesBizTypeConstants.PRO_FEEDBACK)) {
            MesProFeedbackDO feedback = feedbackService.validateFeedbackExists(sourceDocId);
            // 校验 sourceLineId 必填
            if (sourceLineId == null) {
                throw exception(QC_IPQC_SOURCE_LINE_REQUIRED);
            }
            // 校验 sourceLineId 存在,且属于该报工的产出行
            MesWmProductProduceLineDO targetLine = productProduceLineService
                    .validateProductProduceLineExists(sourceLineId);
            if (ObjUtil.notEqual(targetLine.getFeedbackId(), sourceDocId)) {
                throw exception(QC_IPQC_SOURCE_LINE_NOT_BELONG);
            }
            // 校验该产出行为待检验状态
            if (ObjUtil.notEqual(targetLine.getQualityStatus(), MesWmQualityStatusEnum.PENDING.getStatus())) {
                throw exception(QC_IPQC_SOURCE_LINE_NOT_PENDING);
            }
            return feedback.getCode();
        }
        // 未知来源类型应报错,而不是静默忽略
        throw exception(QC_IPQC_SOURCE_DOC_TYPE_UNKNOWN);
    }
 
    /**
     * 推导工序 ID
     *
     * <p>通过产品 → 工艺路线产品 → 工艺路线 → 校验工位工序是否在该路线中
     * <p>如果工位的工序在路线中,则返回该 processId;否则返回 null
     *
     * @param productId 产品 ID(工单产品)
     * @param workstationProcessId 工位关联的工序 ID
     * @return 推导出的工序 ID,无法推导时返回 null
     */
    private Long getProcessId(Long productId, Long workstationProcessId) {
        if (productId == null || workstationProcessId == null) {
            return null;
        }
        // 1. 产品 → 工艺路线
        MesProRouteProductDO routeProduct = routeProductService.getRouteProductByItemId(productId);
        if (routeProduct == null) {
            return null;
        }
        // 2. 校验工位的工序是否在该路线中
        if (routeProcessService.getRouteProcessByRouteIdAndProcessId(
                routeProduct.getRouteId(), workstationProcessId) == null) {
            return null;
        }
        return workstationProcessId;
    }
 
    @Override
    public MesQcIpqcDO getIpqc(Long id) {
        return ipqcMapper.selectById(id);
    }
 
    @Override
    public PageResult<MesQcIpqcDO> getIpqcPage(MesQcIpqcPageReqVO pageReqVO) {
        return ipqcMapper.selectPage(pageReqVO);
    }
 
    @Override
    public void recalculateDefectStats(Long ipqcId, List<MesQcDefectRecordDO> records) {
        MesQcIpqcDO ipqc = validateIpqcExists(ipqcId);
        // 1. 行级缺陷统计
        ipqcLineService.recalculateLineDefectStats(ipqcId, records);
 
        // 2.1 汇总主表的缺陷数量
        int totalCritical = 0;
        int totalMajor = 0;
        int totalMinor = 0;
        for (MesQcDefectRecordDO record : records) {
            int quantity = ObjUtil.defaultIfNull(record.getQuantity(), 1);
            if (Objects.equals(record.getLevel(), MesQcDefectLevelEnum.CRITICAL.getType())) {
                totalCritical += quantity;
            } else if (Objects.equals(record.getLevel(), MesQcDefectLevelEnum.MAJOR.getType())) {
                totalMajor += quantity;
            } else if (Objects.equals(record.getLevel(), MesQcDefectLevelEnum.MINOR.getType())) {
                totalMinor += quantity;
            } else {
                throw exception(QC_DEFECT_RECORD_LEVEL_UNKNOWN);
            }
        }
        // 2.2 计算缺陷率
        BigDecimal criticalRate = BigDecimal.ZERO;
        BigDecimal majorRate = BigDecimal.ZERO;
        BigDecimal minorRate = BigDecimal.ZERO;
        if (ipqc.getCheckQuantity() != null && ipqc.getCheckQuantity().compareTo(BigDecimal.ZERO) > 0) {
            BigDecimal checkQty = ipqc.getCheckQuantity();
            criticalRate = BigDecimal.valueOf(totalCritical).multiply(BigDecimal.valueOf(100))
                    .divide(checkQty, 2, RoundingMode.HALF_UP);
            majorRate = BigDecimal.valueOf(totalMajor).multiply(BigDecimal.valueOf(100))
                    .divide(checkQty, 2, RoundingMode.HALF_UP);
            minorRate = BigDecimal.valueOf(totalMinor).multiply(BigDecimal.valueOf(100))
                    .divide(checkQty, 2, RoundingMode.HALF_UP);
        }
        // 2.3 更新主表
        MesQcIpqcDO updateIpqc = new MesQcIpqcDO().setId(ipqcId)
                .setCriticalQuantity(totalCritical).setMajorQuantity(totalMajor).setMinorQuantity(totalMinor)
                .setCriticalRate(criticalRate).setMajorRate(majorRate).setMinorRate(minorRate);
        ipqcMapper.updateById(updateIpqc);
    }
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public MesQcImportRespVO importIpqcList(List<MesQcIpqcImportExcelVO> importList) {
        MesQcImportRespVO respVO = MesQcImportRespVO.builder()
                .createList(new ArrayList<>()).failureList(new LinkedHashMap<>()).build();
        if (CollUtil.isEmpty(importList)) {
            return respVO;
        }
        qcImportHelper.startImport();
        Map<String, Integer> firstRowNoByKey = new HashMap<>();
        for (int i = 0; i < importList.size(); i++) {
            MesQcIpqcImportExcelVO row = importList.get(i);
            if (isImportRowEmpty(row)) {
                continue; // 跳过空行
            }
            String label = StrUtil.isNotBlank(row.getCode()) ? "检验单编号 " + row.getCode().trim()
                    : "第 " + (i + 2) + " 行";
            String importKey = buildImportKey(row);
            try {
                Integer firstRowNo = firstRowNoByKey.get(importKey);
                if (firstRowNo != null) {
                    throw MesQcImportHelper.duplicateRowException(firstRowNo, buildImportKeyDesc(row));
                }
                respVO.getCreateList().add(createIpqcByImport(row));
                firstRowNoByKey.putIfAbsent(importKey, i + 2);
            } catch (Exception ex) {
                respVO.getFailureList().put(label, MesQcImportHelper.resolveImportErrorMessage(ex));
            }
        }
        return respVO;
    }
 
    private boolean isImportRowEmpty(MesQcIpqcImportExcelVO row) {
        return row == null || (StrUtil.isBlank(row.getCode()) && StrUtil.isBlank(row.getName())
                && StrUtil.isBlank(row.getType()) && StrUtil.isBlank(row.getWorkOrderCode())
                && StrUtil.isBlank(row.getWorkstationName()) && StrUtil.isBlank(row.getTemplateName())
                && row.getCheckQuantity() == null && row.getQualifiedQuantity() == null
                && row.getUnqualifiedQuantity() == null && row.getLaborScrapQuantity() == null
                && row.getMaterialScrapQuantity() == null && row.getOtherScrapQuantity() == null
                && StrUtil.isBlank(row.getCheckResult()) && StrUtil.isBlank(row.getInspectorName())
                && StrUtil.isBlank(row.getInspectDate()) && StrUtil.isBlank(row.getRemark()));
    }
 
    /**
     * 依据导入行创建一张过程检验单:物料由工单推导,状态为「待检验」,检验行由质检方案自动生成
     *
     * @return 检验单编号
     */
    private String createIpqcByImport(MesQcIpqcImportExcelVO row) {
        MesQcIpqcSaveReqVO reqVO = new MesQcIpqcSaveReqVO();
        // 1. 检验单编号:填写则使用,留空自动生成
        String code = StrUtil.isNotBlank(row.getCode()) ? row.getCode().trim()
                : qcImportHelper.generateCode(MesMdAutoCodeRuleCodeEnum.QC_IPQC_CODE.getCode());
        if (ipqcMapper.selectByCode(code) != null) {
            throw new IllegalArgumentException("检验单编号已存在:" + code
                    + "。请修改模板中的「检验单编号」,或先在系统中删除该编号的检验单后重新导入");
        }
        reqVO.setCode(code);
        // 2. 名称、检验类型、工单、工位、质检方案(生产工单号、工位名称留空即不关联,不再拦截)
        if (StrUtil.isBlank(row.getName())) {
            throw new IllegalArgumentException("检验单名称不能为空:请填写模板中该行的「检验单名称」列");
        }
        reqVO.setName(row.getName().trim());
        reqVO.setType(qcImportHelper.resolveDictValue(row.getType(),
                DictTypeConstants.MES_IPQC_TYPE, "检验类型", true));
        reqVO.setWorkOrderId(qcImportHelper.resolveWorkOrderId(row.getWorkOrderCode()));
        reqVO.setWorkstationId(qcImportHelper.resolveWorkstationId(row.getWorkstationName()));
        reqVO.setTemplateId(qcImportHelper.resolveTemplateId(row.getTemplateName()));
        // 3. 数量:合格品 + 不合格品 必须等于检测数量(检测数量留空则自动合计);工废/料废/其他废留空按 0
        BigDecimal qualified = qcImportHelper.resolveQuantity(row.getQualifiedQuantity(), "合格品数量", true);
        BigDecimal unqualified = qcImportHelper.resolveQuantity(row.getUnqualifiedQuantity(), "不合格品数量", true);
        reqVO.setQualifiedQuantity(qualified);
        reqVO.setUnqualifiedQuantity(unqualified);
        reqVO.setCheckQuantity(qcImportHelper.resolveCheckQuantity(
                row.getCheckQuantity(), qualified, unqualified, "检测数量"));
        BigDecimal laborScrap = qcImportHelper.resolveQuantity(row.getLaborScrapQuantity(), "工废数量", false);
        BigDecimal materialScrap = qcImportHelper.resolveQuantity(row.getMaterialScrapQuantity(), "料废数量", false);
        BigDecimal otherScrap = qcImportHelper.resolveQuantity(row.getOtherScrapQuantity(), "其他废品数量", false);
        laborScrap = laborScrap != null ? laborScrap : BigDecimal.ZERO;
        materialScrap = materialScrap != null ? materialScrap : BigDecimal.ZERO;
        otherScrap = otherScrap != null ? otherScrap : BigDecimal.ZERO;
        validateScrapQuantity(unqualified, laborScrap, materialScrap, otherScrap);
        reqVO.setLaborScrapQuantity(laborScrap);
        reqVO.setMaterialScrapQuantity(materialScrap);
        reqVO.setOtherScrapQuantity(otherScrap);
        // 4. 检验信息
        reqVO.setCheckResult(qcImportHelper.resolveDictValue(row.getCheckResult(),
                DictTypeConstants.MES_QC_CHECK_RESULT, "检测结果", false));
        reqVO.setInspectorUserId(qcImportHelper.resolveInspectorUserId(row.getInspectorName()));
        reqVO.setInspectDate(qcImportHelper.parseDateTime(row.getInspectDate(), "检测日期"));
        reqVO.setRemark(StrUtil.trimToNull(row.getRemark()));
        // 5. 去重:同一 生产工单 + 检验类型 + 工位 + 检测日期 的过程检验单不可重复导入
        MesQcIpqcDO duplicate = CollUtil.getFirst(ipqcMapper.selectListByImportKey(reqVO.getWorkOrderId(),
                reqVO.getType(), reqVO.getWorkstationId(), reqVO.getInspectDate()));
        if (duplicate != null) {
            throw MesQcImportHelper.duplicateExistingException(buildImportKeyDesc(row), "过程检验单",
                    duplicate.getCode(), "质量管理-过程检验");
        }
        // 6. 复用创建逻辑:校验工单/工位/人员、按物料匹配质检方案、生成检验行
        try {
            createIpqc(reqVO);
        } catch (ServiceException ex) {
            MesQcImportHelper.rethrowTemplateError(ex, row.getWorkOrderCode(), "过程检验(IPQC)");
        }
        return code;
    }
 
    /**
     * 校验 工废 + 料废 + 其他废品 不超过不合格品数量
     */
    private void validateScrapQuantity(BigDecimal unqualified, BigDecimal laborScrap,
                                       BigDecimal materialScrap, BigDecimal otherScrap) {
        BigDecimal scrapSum = laborScrap.add(materialScrap).add(otherScrap);
        if (scrapSum.compareTo(unqualified) <= 0) {
            return;
        }
        throw new IllegalArgumentException("工废、料废、其他废品数量之和不能超过不合格品数量:不合格品数量为 "
                + unqualified.stripTrailingZeros().toPlainString() + ",而 工废 " + laborScrap.stripTrailingZeros().toPlainString()
                + " + 料废 " + materialScrap.stripTrailingZeros().toPlainString() + " + 其他废品 "
                + otherScrap.stripTrailingZeros().toPlainString() + " = " + scrapSum.stripTrailingZeros().toPlainString()
                + "。请修改模板中的废品数量或不合格品数量后重新导入");
    }
 
    /**
     * 导入去重键:同一 生产工单 + 检验类型 + 工位 + 检测日期 视为同一批过程检验数据
     */
    private String buildImportKey(MesQcIpqcImportExcelVO row) {
        return String.join("|", StrUtil.trimToEmpty(row.getWorkOrderCode()), StrUtil.trimToEmpty(row.getType()),
                StrUtil.trimToEmpty(row.getWorkstationName()), StrUtil.trimToEmpty(row.getInspectDate()));
    }
 
    private String buildImportKeyDesc(MesQcIpqcImportExcelVO row) {
        return "生产工单[" + MesQcImportHelper.describeImportValue(row.getWorkOrderCode())
                + "]、检验类型[" + MesQcImportHelper.describeImportValue(row.getType())
                + "]、工位[" + MesQcImportHelper.describeImportValue(row.getWorkstationName())
                + "]、检测日期[" + MesQcImportHelper.describeImportValue(row.getInspectDate()) + "]";
    }
 
}