gaoluyang
昨天 8c65f898ed6e5b0c7a94c792c4562f6ec831dccd
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
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
package cn.iocoder.yudao.module.mes.service.pro.feedback;
 
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.exception.ServiceException;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.pro.feedback.vo.*;
import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants;
import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodeRuleCodeEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProFeedbackTypeEnum;
import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackAuxDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.feedback.MesProFeedbackDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderBomDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderProcessDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemconsume.MesWmItemConsumeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.wm.productproduce.MesWmProductProduceLineDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.ipqc.MesQcIpqcDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.feedback.MesProFeedbackAuxMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.feedback.MesProFeedbackMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.ipqc.MesQcIpqcMapper;
import cn.iocoder.yudao.module.mes.enums.pro.MesProFeedbackStatusEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProProcessBillingTypeEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProTaskStatusEnum;
import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum;
import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum;
import cn.iocoder.yudao.module.mes.service.qc.ipqc.MesQcIpqcService;
import cn.iocoder.yudao.module.mes.service.qc.ipqc.MesQcIpqcWageFieldService;
import cn.iocoder.yudao.module.mes.service.pro.process.MesProProcessService;
import cn.iocoder.yudao.module.mes.service.pro.route.MesProRouteProcessService;
import cn.iocoder.yudao.module.mes.service.pro.task.MesProTaskService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderBomService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderProcessService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderParamRecordService;
import cn.iocoder.yudao.module.mes.service.pro.wage.MesProFeedbackQualityResult;
import cn.iocoder.yudao.module.mes.service.pro.wage.MesProFeedbackWageBreakdown;
import cn.iocoder.yudao.module.mes.service.pro.wage.MesProFeedbackWageCalculator;
import cn.iocoder.yudao.module.mes.service.pro.wage.MesProProcessWageRuleService;
import cn.iocoder.yudao.module.mes.service.pro.worktime.MesProWorktimeService;
import cn.iocoder.yudao.module.mes.service.wm.itemconsume.MesWmItemConsumeService;
import cn.iocoder.yudao.module.mes.service.wm.productproduce.MesWmProductProduceLineService;
import cn.iocoder.yudao.module.mes.service.wm.productproduce.MesWmProductProduceService;
import cn.iocoder.yudao.module.mes.service.mdm.MesMdmItemService;
import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemBatchConfigApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemBatchConfigRespDTO;
import cn.iocoder.yudao.module.mes.api.mps.MesProMpsApi;
import cn.iocoder.yudao.module.mes.api.mps.dto.MesProMpsRespDTO;
import cn.iocoder.yudao.module.mes.api.event.ProductionFinishedEvent;
import jakarta.annotation.Resource;
import org.springframework.context.ApplicationEventPublisher;
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.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
 
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
 
/**
 * MES 生产报工 Service 实现类
 *
 * @author 超级管理员
 */
@Service
@Validated
@lombok.extern.slf4j.Slf4j
public class MesProFeedbackServiceImpl implements MesProFeedbackService {
 
    @Resource
    private MesProFeedbackEquipmentService equipmentService;
 
    @Resource
    private MesProFeedbackMapper feedbackMapper;
    @Resource
    private MesProFeedbackAuxMapper feedbackAuxMapper;
    @Resource
    private MesProWorktimeService worktimeService;
    @Resource
    private MesProFeedbackWageCalculator wageCalculator;
    @Resource
    private MesProProcessWageRuleService wageRuleService;
    @Resource
    private MesQcIpqcMapper ipqcMapper;
    @Resource
    @Lazy // IPQC 完成回写报工,避免循环依赖
    private MesQcIpqcService ipqcService;
    @Resource
    @Lazy // 过程检验单工资字段值(质检填写),避免与 IPQC 主服务循环依赖
    private MesQcIpqcWageFieldService ipqcWageFieldService;
 
    @Resource
    private MesProWorkOrderService workOrderService;
    @Resource
    @Lazy
    private MesProWorkOrderParamRecordService paramRecordService;
    @Resource
    private MesProRouteProcessService routeProcessService;
    @Resource
    private MesProProcessService processService;
    @Resource
    private MesProWorkOrderBomService workOrderBomService;
    @Resource
    private MesProWorkOrderProcessService workOrderProcessService;
    @Resource
    @Lazy // 避免循环依赖
    private MesProTaskService taskService;
    @Resource
    private MesWmItemConsumeService itemConsumeService;
    @Resource
    private MesWmProductProduceService productProduceService;
    @Resource
    private MesWmProductProduceLineService produceLineService;
    @Resource
    private MesProMpsApi mpsApi;
    @Resource
    private MesMdAutoCodeRecordService autoCodeRecordService;
    @Resource
    private ApplicationEventPublisher eventPublisher;
    @Resource
    private MesMdmItemService mdmItemService;
    @Resource
    private MdmItemBatchConfigApi itemBatchConfigApi;
    @Resource
    private cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi processInstanceApi;
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createFeedback(MesProFeedbackSaveReqVO createReqVO) {
        // 1. 校验
        MesProTaskDO task = validateFeedbackData(createReqVO);
 
        // 2. 插入(主键由数据库自增,忽略请求中可能残留的 id;自动填充 itemId)
        MesProFeedbackDO feedback = BeanUtils.toBean(createReqVO, MesProFeedbackDO.class)
                .setId(null)
                .setStatus(MesProFeedbackStatusEnum.PREPARE.getStatus())
                .setItemId(task.getItemId());
        // 2.1 计算标准工时快照与辅助工时明细(只用于考核对比,参数缺失不阻断报工)
        List<MesProFeedbackAuxDO> auxList = worktimeService.calculateForFeedback(feedback, createReqVO.getAuxList());
        // 2.2 按工序计费类型、单价规则、辅助计薪与工资字段快照工资,谁报的工就记在谁名下
        applyProcessBilling(feedback, processService.getProcess(task.getProcessId()), auxList);
        feedbackMapper.insert(feedback);
        saveFeedbackAux(feedback.getId(), auxList);
        // 3. 保存参数值;必填参数校验失败时整体回滚,避免残留草稿
        saveParamValues(task.getId(), createReqVO.getParamValues());
        return feedback.getId();
    }
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateFeedback(MesProFeedbackSaveReqVO updateReqVO) {
        equipmentService.lock();
        // 1.1 校验存在 + 草稿状态
        validateFeedbackStatusPrepare(updateReqVO.getId());
        // 1.2 校验业务数据
        MesProTaskDO task = validateFeedbackData(updateReqVO);
 
        // 2. 更新(自动填充 itemId)
        MesProFeedbackDO updateObj = BeanUtils.toBean(updateReqVO, MesProFeedbackDO.class)
                .setItemId(task.getItemId());
        // 2.1 重新计算标准工时快照与辅助工时明细
        List<MesProFeedbackAuxDO> auxList = worktimeService.calculateForFeedback(updateObj, updateReqVO.getAuxList());
        // 2.2 按工序当前的计费类型/单价规则/辅助计薪/工资字段重算工资(草稿阶段允许改工时/数量)
        applyProcessBilling(updateObj, processService.getProcess(task.getProcessId()), auxList);
        feedbackMapper.updateById(updateObj);
        feedbackAuxMapper.deleteByFeedbackId(updateReqVO.getId());
        saveFeedbackAux(updateReqVO.getId(), auxList);
        equipmentService.resetSnapshot(updateReqVO.getId());
        // 3. 保存参数值;必填参数校验失败时整体回滚
        saveParamValues(task.getId(), updateReqVO.getParamValues());
    }
 
    private void saveFeedbackAux(Long feedbackId, List<MesProFeedbackAuxDO> auxList) {
        if (CollUtil.isEmpty(auxList)) {
            return;
        }
        auxList.forEach(aux -> {
            aux.setId(null).setFeedbackId(feedbackId);
            feedbackAuxMapper.insert(aux);
        });
    }
 
    private void saveParamValues(Long taskId, List<MesProFeedbackSaveReqVO.ParamValue> paramValues) {
        // 即使参数为空也调用更新,以便触发必填参数校验(任务绑定了参数时必须填写)
        if (CollUtil.isEmpty(paramValues)) {
            paramRecordService.updateTaskParamValues(taskId, null, null);
            return;
        }
        paramRecordService.updateTaskParamValues(taskId,
                paramValues.stream().map(MesProFeedbackSaveReqVO.ParamValue::getDetailId).toList(),
                paramValues.stream().map(MesProFeedbackSaveReqVO.ParamValue::getValue).toList());
    }
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteFeedback(Long id) {
        equipmentService.lock();
        // 1. 校验存在 + 草稿状态
        validateFeedbackStatusPrepare(id);
 
        // 2. 删除(辅助工时明细一并删除)
        feedbackMapper.deleteById(id);
        feedbackAuxMapper.deleteByFeedbackId(id);
    }
 
    @Override
    public MesProFeedbackDO getFeedback(Long id) {
        return feedbackMapper.selectById(id);
    }
 
    @Override
    public List<MesProFeedbackDO> getFeedbackListByTaskIds(Collection<Long> taskIds) {
        if (CollUtil.isEmpty(taskIds)) {
            return Collections.emptyList();
        }
        return feedbackMapper.selectListByTaskIds(taskIds);
    }
 
    @Override
    public List<MesProFeedbackDO> getFeedbackListByTaskId(Long taskId) {
        return feedbackMapper.selectListByTaskId(taskId);
    }
 
    @Override
    public PageResult<MesProFeedbackDO> getFeedbackPage(MesProFeedbackPageReqVO pageReqVO) {
        return feedbackMapper.selectPage(pageReqVO);
    }
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void submitFeedback(MesProFeedbackSubmitReqVO reqVO) {
        equipmentService.lock();
        Long id = reqVO.getId();
        // 1. 校验存在 + 草稿状态
        MesProFeedbackDO feedback = validateFeedbackStatusPrepare(id);
        equipmentService.validateSubmission(feedback);
        // 1.1 顺序报工校验:同一生产工单内,上一道工序必须已报工完成,否则不允许提交
        if (feedback.getTaskId() != null) {
            MesProTaskDO submitTask = taskService.getTask(feedback.getTaskId());
            if (submitTask != null) {
                validatePreviousProcessFinished(submitTask);
            }
        }
 
        MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(feedback.getWorkOrderId());
 
        // 1.2 报工与质检分离:提交报工不再自动生成过程检验单,质检由质检部门手工建单
        if (!processInstanceApi.isGlobalAutoApprove() && workOrder != null
                && Boolean.TRUE.equals(workOrder.getFeedbackApprove())
                && cn.hutool.core.util.StrUtil.isNotBlank(workOrder.getProcessDefinitionKey())) {
            // 2. 更新状态为审批中,并启动流程
            String processInstanceId = processInstanceApi.createProcessInstance(cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId(),
                    new cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO()
                            .setProcessDefinitionKey(workOrder.getProcessDefinitionKey())
                            .setBusinessKey(String.valueOf(id)));
 
            feedbackMapper.updateById(new MesProFeedbackDO().setId(id)
                    .setStatus(MesProFeedbackStatusEnum.APPROVING.getStatus())
                    .setProcessDefinitionKey(workOrder.getProcessDefinitionKey())
                    .setProcessInstanceId(processInstanceId));
        } else {
            // 2. 判断这个报工需不需要批次号
            MesProFeedbackBatchCheckRespVO mesProFeedbackBatchCheckRespVO = checkBatchRequirements(id);
            if (mesProFeedbackBatchCheckRespVO.getNeedBatchInput()) {
                if (StrUtil.isBlank(reqVO.getLotNumber())) {
                    throw exception(MD_ITEM_BATCH_CONFIG_AT_LEAST_ONE_APPROVE_FLAG);
                }
            }
 
            // 3. 更新状态为审批中,然后直接通过审批
            feedbackMapper.updateById(new MesProFeedbackDO().setId(id)
                    .setStatus(MesProFeedbackStatusEnum.APPROVING.getStatus()));
            MesProFeedbackApproveReqVO approveReqVO = cn.iocoder.yudao.framework.common.util.object.BeanUtils.toBean(reqVO, MesProFeedbackApproveReqVO.class);
            // 自动审批:过程检验单刚生成尚未填写,不做质检工资前置校验(质量调整留空)
            approveFeedback(approveReqVO, false);
        }
    }
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void handleBpmProcessResult(Long id, Integer bpmResult,String reason) {
        // 1. 柔性校验:获取报工单并判断状态
        MesProFeedbackDO feedback = feedbackMapper.selectById(id);
        if (feedback == null) {
            log.warn("[handleBpmProcessResult] 生产报工({}) 不存在", id);
            return;
        }
        if (!MesProFeedbackStatusEnum.APPROVING.getStatus().equals(feedback.getStatus())) {
            log.warn("[handleBpmProcessResult] 生产报工({}) 不处于审批中状态,当前状态:{}", id, feedback.getStatus());
            return;
        }
 
        // 2. 根据审批结果进行处理
        if (cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum.APPROVE.getStatus().equals(bpmResult)) {
            MesProFeedbackApproveReqVO  reqVO = new MesProFeedbackApproveReqVO();
            reqVO.setId(id);
            //  判断这个报工需不需要批次号
            MesProFeedbackBatchCheckRespVO mesProFeedbackBatchCheckRespVO = checkBatchRequirements(id);
            if (mesProFeedbackBatchCheckRespVO.getNeedBatchInput()){
               if (StrUtil.isBlank(reason)){
                   //  报错 请在审批意见里填写批次号
                   throw  exception(MD_ITEM_BATCH_CONFIG_AT_LEAST_ONE_APPROVE_FLAG);
               }else {
                 BeanUtils.copyProperties(mesProFeedbackBatchCheckRespVO,reqVO);
                 reqVO.setLotNumber(reason);
               }
            }
            approveFeedback(reqVO);
        } else if (cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum.REJECT.getStatus().equals(bpmResult) ||
                   cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum.CANCEL.getStatus().equals(bpmResult)) {
            rejectFeedback(id);
        }
    }
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void rejectFeedback(Long id) {
        equipmentService.lock();
        // 1. 校验存在 + 审批中状态
        validateFeedbackStatusApproving(id);
 
        // 2. 更新状态为草稿
        feedbackMapper.updateById(new MesProFeedbackDO().setId(id)
                .setStatus(MesProFeedbackStatusEnum.PREPARE.getStatus()));
    }
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean approveFeedback(MesProFeedbackApproveReqVO reqVO) {
        return approveFeedback(reqVO, true);
    }
 
    private boolean approveFeedback(MesProFeedbackApproveReqVO reqVO, boolean enforceQualityGate) {
        equipmentService.lock();
        // 1.1 校验存在 + 审批中状态
        MesProFeedbackDO feedback = validateFeedbackStatusApproving(reqVO.getId());
        equipmentService.validateApproval(feedback);
        try (var equipmentScope = equipmentService.beginApproval(feedback)) {
            return approveValidatedFeedback(reqVO, feedback, enforceQualityGate);
        }
    }
 
    private boolean approveValidatedFeedback(MesProFeedbackApproveReqVO reqVO, MesProFeedbackDO feedback,
                                             boolean enforceQualityGate) {
        // 1.2 校验报工数量 > 0
        if (feedback.getFeedbackQuantity() == null
                || feedback.getFeedbackQuantity().compareTo(BigDecimal.ZERO) <= 0) {
            throw exception(PRO_FEEDBACK_QUANTITY_MUST_POSITIVE);
        }
        // 1.3 校验任务未完成
        taskService.validateTaskNotFinished(feedback.getTaskId());
 
        // 报工与质检分离:报工审批不再依赖「是否质检」开关,质量数据由质检单回填。
        // 1.4 工资按报工时点的快照重算,避免工序定额事后调整影响已提交的报工
        recalculateWageFromSnapshot(feedback, null);
 
        // 3. 物料消耗:只要本工序配置了生产 BOM 就自动扣料,不再依赖倒冲开关;
        //    未配置 BOM 的工序跳过(物料走手工领料/投料),不阻断报工审批
        List<MesProWorkOrderBomDO> boms = workOrderBomService.getWorkOrderBomListByWorkOrderIdAndProcessId(
                feedback.getWorkOrderId(), feedback.getProcessId());
        if (CollUtil.isNotEmpty(boms)) {
            // 3.1 生成消耗记录并执行扣减(本工序投料用量比例 × 报工数量,FIFO 扣线边库)
            MesWmItemConsumeDO itemConsume = itemConsumeService.generateItemConsume(feedback);
            if (itemConsume != null) {
                itemConsumeService.finishItemConsume(itemConsume.getId());
            }
        }
 
        // 报工与质检分开:报工审批完成即累计产量,质量数量由质检回填。
        feedbackMapper.updateById(new MesProFeedbackDO().setId(reqVO.getId())
                .setStatus(MesProFeedbackStatusEnum.UNCHECK.getStatus())
                .setQualifiedQuantity(BigDecimal.ZERO)
                .setUnqualifiedQuantity(BigDecimal.ZERO)
                .setUncheckQuantity(feedback.getFeedbackQuantity())
                .setBillingType(feedback.getBillingType())
                .setWageRate(feedback.getWageRate())
                .setWageRuleId(feedback.getWageRuleId())
                .setWorkHours(feedback.getWorkHours())
                .setWageAmount(feedback.getWageAmount())
                .setAuxPaidHours(feedback.getAuxPaidHours())
                .setAuxWageAmount(feedback.getAuxWageAmount())
                .setQualityPassRate(feedback.getQualityPassRate())
                .setQualityCoefficient(feedback.getQualityCoefficient())
                .setQualityDeduction(feedback.getQualityDeduction())
                .setWageBreakdown(feedback.getWageBreakdown()));
        feedback.setQualifiedQuantity(BigDecimal.ZERO).setUnqualifiedQuantity(BigDecimal.ZERO);
        productProduceService.generateProductProduce(feedback, true, reqVO);
 
        // 每道工序只累计本任务;工单与 MPS 由最后一道工序回写,与关键工序标记无关。
        updateTaskAndWorkOrderByFeedback(feedback);
        equipmentService.afterApproved(feedback);
        return false; // 产出等待质检,报工产量已累计
    }
 
    /**
     * 根据当前报工单的最终结果,更新生产任务和生产工单的进度
     *
     * <ul>
     *   <li>使用报工数量(feedbackQuantity),累加任务和工单的已生产数量</li>
     *   <li>使用产出单行按 qualityStatus 聚合的合格品/不合格品数量,累加任务的合格品/不合格品数量
     *       (不直接用 feedback 上的数量,确保质检回调场景下数量来源正确)</li>
     *      isPush 是否推送生产完成事件
     * </ul>
     *
     * @param feedback 报工单
     */
    void updateTaskAndWorkOrderByFeedback(MesProFeedbackDO feedback) {
        // 报工审批只累计产量;合格与不合格数量在质检完成时累计。
        taskService.updateProducedQuantity(feedback.getTaskId(),
                feedback.getFeedbackQuantity(), BigDecimal.ZERO, BigDecimal.ZERO);
        // 只有最后一道工序的报工计入工单与 MPS,避免中间工序重复累计。
        MesProWorkOrderProcessDO lastProcess = workOrderProcessService.getLastWorkOrderProcessByWorkOrderId(feedback.getWorkOrderId());
        if (lastProcess != null && ObjUtil.equal(lastProcess.getProcessId(), feedback.getProcessId())) {
            workOrderService.updateProducedQuantity(feedback.getWorkOrderId(),
                    feedback.getFeedbackQuantity());
            // MPS 只记录销售订单需求量;实际超产仍由工单和产出入库记录完整保留。
            allocateMpsProducedQuantity(feedback.getWorkOrderId(), feedback.getFeedbackQuantity());
        }
 
 
        // 5. 检查并发布生产完成事件
        checkAndPublishProductionFinished(feedback.getWorkOrderId());
    }
 
    /**
     * 检查并发布生产完成事件
     */
    void checkAndPublishProductionFinished(Long workOrderId) {
        Set<Long> newlyCompletedSaleOrderIds = new LinkedHashSet<>();
        for (MesProMpsRespDTO mps : mpsApi.getMpsListByWorkOrderId(workOrderId)) {
            if (mps.getSaleOrderId() == null
                    || ObjUtil.equal(mps.getStatus(), cn.iocoder.yudao.module.mes.enums.pro.MesProMpsStatusEnum.FINISHED.getStatus())
                    || ObjUtil.equal(mps.getStatus(), cn.iocoder.yudao.module.mes.enums.pro.MesProMpsStatusEnum.CANCELED.getStatus())) {
                continue;
            }
            if (Boolean.TRUE.equals(mpsApi.checkAndFinishMps(mps.getId()))) {
                newlyCompletedSaleOrderIds.add(mps.getSaleOrderId());
            }
        }
        for (Long saleOrderId : newlyCompletedSaleOrderIds) {
            if (mpsApi.checkSaleOrderProductionFinished(saleOrderId)) {
                eventPublisher.publishEvent(new ProductionFinishedEvent(saleOrderId, java.time.LocalDateTime.now()));
            }
        }
    }
 
    void allocateMpsProducedQuantity(Long workOrderId, BigDecimal actualQuantity) {
        BigDecimal remaining = actualQuantity;
        for (MesProMpsRespDTO mps : mpsApi.getMpsListByWorkOrderId(workOrderId)) {
            if (remaining.signum() <= 0) {
                break;
            }
            if (ObjUtil.equal(mps.getStatus(), cn.iocoder.yudao.module.mes.enums.pro.MesProMpsStatusEnum.CANCELED.getStatus())) {
                continue;
            }
            BigDecimal planned = mps.getQuantity() == null ? BigDecimal.ZERO : mps.getQuantity();
            BigDecimal produced = mps.getQuantityProduced() == null ? BigDecimal.ZERO : mps.getQuantityProduced();
            BigDecimal need = planned.subtract(produced).max(BigDecimal.ZERO);
            BigDecimal assigned = remaining.min(need);
            if (assigned.signum() > 0) {
                mpsApi.updateMpsProducedQuantity(mps.getId(), assigned);
                remaining = remaining.subtract(assigned);
            }
        }
        if (remaining.signum() > 0) {
            log.info("[allocateMpsProducedQuantity] 工单({}) 超产数量({})保留在产出库存,不分配到销售订单", workOrderId, remaining);
        }
    }
 
    // ==================== 校验方法 ====================
 
    @Override
    public MesProFeedbackDO validateFeedbackExists(Long id) {
        MesProFeedbackDO feedback = feedbackMapper.selectById(id);
        if (feedback == null) {
            throw exception(PRO_FEEDBACK_NOT_EXISTS);
        }
        return feedback;
    }
 
    private MesProFeedbackDO validateFeedbackStatusPrepare(Long id) {
        MesProFeedbackDO feedback = validateFeedbackExists(id);
        if (ObjUtil.notEqual(feedback.getStatus(), MesProFeedbackStatusEnum.PREPARE.getStatus())) {
            throw exception(PRO_FEEDBACK_NOT_PREPARE);
        }
        return feedback;
    }
 
    private MesProFeedbackDO validateFeedbackStatusApproving(Long id) {
        MesProFeedbackDO feedback = validateFeedbackExists(id);
        if (ObjUtil.notEqual(feedback.getStatus(), MesProFeedbackStatusEnum.APPROVING.getStatus())) {
            throw exception(PRO_FEEDBACK_NOT_APPROVING);
        }
        return feedback;
    }
 
    /**
     * 校验报工单的业务数据(创建 & 修改共用)
     *
     * <p>以生产任务为核心:前端只需传 taskId,workOrderId、routeId、processId、itemId、workstationId
     * 从任务自动填充,无需前端重复传入。
     *
     * @param reqVO 报工请求
     * @return 关联的生产任务
     */
    private MesProTaskDO validateFeedbackData(MesProFeedbackSaveReqVO reqVO) {
        // 1. 校验任务存在且未终态
        MesProTaskDO task = taskService.validateTaskNotFinished(reqVO.getTaskId());
 
        // 校验停工状态不能报工
        if (!MesProTaskStatusEnum.canFeedback(task.getStatus())) {
            throw exception(PRO_FEEDBACK_CANNOT_CREATE_WHEN_PAUSED);
        }
 
        // 2. 从任务自动填充关联字段(前端无需传入,后端统一从任务获取)
        reqVO.setWorkOrderId(task.getWorkOrderId());
        reqVO.setRouteId(task.getRouteId());
        reqVO.setProcessId(task.getProcessId());
        reqVO.setItemId(task.getItemId());
        reqVO.setWorkstationId(task.getWorkstationId());
 
        // 3. 校验工艺路线 + 工序配置有效
        MesProRouteProcessDO routeProcess = routeProcessService.getRouteProcessByRouteIdAndProcessId(
                task.getRouteId(), task.getProcessId());
        if (routeProcess == null) {
            throw exception(PRO_FEEDBACK_ROUTE_PROCESS_INVALID);
        }
        // 4.1 校验数量:无论是否需要检验,报工只需填报工数量,且必须 > 0
        // 合格品/不良品数量由质检确定。
        if (reqVO.getFeedbackQuantity() == null
                || reqVO.getFeedbackQuantity().compareTo(BigDecimal.ZERO) <= 0) {
            throw exception(PRO_FEEDBACK_QUANTITY_MUST_POSITIVE);
        }
 
        return task;
    }
 
    /**
     * 按工序配置把计费类型、单价规则、辅助计薪与工资字段快照到报工单,并计算本次报工工资。
     *
     * <p>口径:
     * <ul>
     *   <li>计时:主工资 = 本次报工工时(小时)× 单价(元/小时);单价大于 0 时必须填写工时</li>
     *   <li>计件:主工资 = 合格数量(未填时取报工数量)× 单价(元/件)</li>
     *   <li>辅助:辅助明细分钟数 ÷ 60 × 辅助小时单价;工序辅助小时工资兜底</li>
     *   <li>字段:系数连乘作用于(主工资 + 辅助工资),再叠加加减金额与计价数量</li>
     * </ul>
     * 前端传入的 billingType/wageRate/wageAmount 一律以后端计算覆盖,防止被篡改。
     * 单价为 0 表示该工序尚未定价,按 0 元计薪,不阻断报工(核算时可按人调整)。
     */
    private void applyProcessBilling(MesProFeedbackDO feedback, MesProProcessDO process,
                                     List<MesProFeedbackAuxDO> auxList) {
        if (process == null) {
            throw exception(PRO_FEEDBACK_PROCESS_NOT_EXISTS, feedback.getProcessId());
        }
        MesProFeedbackWageBreakdown breakdown = wageCalculator.calculate(process,
                feedback.getRouteId(), feedback.getMachineryId(), feedback.getWorkHours(),
                feedback.getFeedbackQuantity(), feedback.getQualifiedQuantity(), auxList,
                feedback.getFeedbackTime() != null ? feedback.getFeedbackTime().toLocalDate() : null,
                getWageFieldValues(feedback.getId()));
        applyWageBreakdown(feedback, breakdown);
    }
 
    /**
     * 把工资计算结果写回报工单
     */
    private void applyWageBreakdown(MesProFeedbackDO feedback, MesProFeedbackWageBreakdown breakdown) {
        if (CollUtil.isNotEmpty(breakdown.getWarnings())) {
            log.warn("报工工资计算提示:报工单[{}] {}", feedback.getCode(), String.join(";", breakdown.getWarnings()));
        }
        feedback.setBillingType(breakdown.getBillingType())
                .setWageRate(breakdown.getWageRate())
                .setWageRuleId(breakdown.getWageRuleId())
                .setWorkHours(breakdown.getWorkHours())
                .setAuxPaidHours(breakdown.getAuxPaidHours())
                .setAuxWageAmount(breakdown.getAuxWage())
                .setWageAmount(breakdown.getFinalWage())
                .setQualityPassRate(breakdown.getQuality() != null ? breakdown.getQuality().getPassRate() : null)
                .setQualityCoefficient(breakdown.getQuality() != null ? breakdown.getQuality().getCoefficient() : null)
                .setQualityDeduction(breakdown.getQuality() != null && breakdown.getQuality().getDeduction() != null
                        ? breakdown.getQuality().getDeduction() : BigDecimal.ZERO)
                .setWageBreakdown(JsonUtils.toJsonString(breakdown));
    }
 
    /**
     * 取第一个设备编号作为主计价设备(快捷报工的 machineryIds 为逗号分隔)
     */
    private static Long firstMachineryId(String machineryIds) {
        if (StrUtil.isBlank(machineryIds)) {
            return null;
        }
        for (String part : machineryIds.split(",")) {
            String trimmed = part.trim();
            if (StrUtil.isBlank(trimmed)) {
                continue;
            }
            try {
                return Long.parseLong(trimmed);
            } catch (NumberFormatException ex) {
                return null;
            }
        }
        return null;
    }
 
    /**
     * 审批时按报工时点的快照重算工资,避免工序配置事后调整影响已提交的报工。
     * 历史数据(无工资拆分快照)则按当前工序配置补齐。
     *
     * @param qualityResult 过程检验汇总;空=非质检工序或自动审批,不做质量调整
     */
    private void recalculateWageFromSnapshot(MesProFeedbackDO feedback, MesProFeedbackQualityResult qualityResult) {
        BigDecimal qualifiedQuantity = qualityResult != null
                ? qualityResult.qualifiedQuantity() : feedback.getQualifiedQuantity();
        // 质检部门在过程检验单上填写的工资字段值,优先于工序固定值
        Map<String, String> wageFieldValues = getWageFieldValues(feedback.getId());
        MesProFeedbackWageBreakdown breakdown;
        if (StrUtil.isBlank(feedback.getWageBreakdown())) {
            breakdown = wageCalculator.calculate(processService.getProcess(feedback.getProcessId()),
                    feedback.getRouteId(), feedback.getMachineryId(), feedback.getWorkHours(),
                    feedback.getFeedbackQuantity(), qualifiedQuantity,
                    feedback.getId() != null ? feedbackAuxMapper.selectListByFeedbackId(feedback.getId())
                            : Collections.emptyList(),
                    feedback.getFeedbackTime() != null ? feedback.getFeedbackTime().toLocalDate() : null,
                    wageFieldValues);
        } else {
            breakdown = wageCalculator.fromSnapshot(feedback, qualifiedQuantity, wageFieldValues);
        }
        if (qualityResult != null) {
            wageCalculator.applyQuality(breakdown, feedback.getProcessId(), feedback.getRouteId(),
                    qualityResult.passRate(), qualityResult.unqualifiedQuantity());
        }
        applyWageBreakdown(feedback, breakdown);
    }
 
    /**
     * 取报工单关联的过程检验单上质检填写的工资字段值
     *
     * <p>同一报工单存在多张检验单时,已完成的优先;字段未填写时不放入 Map,计算时回退工序固定值。
     */
    private Map<String, String> getWageFieldValues(Long feedbackId) {
        if (feedbackId == null) {
            return Collections.emptyMap();
        }
        List<MesQcIpqcDO> ipqcList = ipqcMapper.selectListBySourceDoc(
                MesBizTypeConstants.PRO_FEEDBACK, feedbackId);
        if (CollUtil.isEmpty(ipqcList)) {
            return Collections.emptyMap();
        }
        List<Long> ipqcIds = new ArrayList<>(ipqcList.size());
        ipqcList.stream()
                .filter(ipqc -> ObjUtil.equal(ipqc.getStatus(), MesQcStatusEnum.FINISHED.getStatus()))
                .map(MesQcIpqcDO::getId).forEach(ipqcIds::add);
        ipqcList.stream()
                .filter(ipqc -> !ObjUtil.equal(ipqc.getStatus(), MesQcStatusEnum.FINISHED.getStatus()))
                .map(MesQcIpqcDO::getId).forEach(ipqcIds::add);
        return ipqcWageFieldService.getValueMapByIpqcIds(ipqcIds);
    }
 
    /**
     * 校验质检工序的过程检验已完成,并汇总合格率与不合格数量
     */
    private MesProFeedbackQualityResult validateAndGetQualityResult(MesProFeedbackDO feedback) {
        MesProProcessDO process = processService.getProcess(feedback.getProcessId());
        String processName = process != null ? process.getName() : "ID=" + feedback.getProcessId();
        List<MesQcIpqcDO> finishedList = ipqcMapper.selectListBySourceDoc(
                        MesBizTypeConstants.PRO_FEEDBACK, feedback.getId()).stream()
                .filter(ipqc -> ObjUtil.equal(ipqc.getStatus(), MesQcStatusEnum.FINISHED.getStatus()))
                .toList();
        if (CollUtil.isEmpty(finishedList)) {
            throw new ServiceException(PRO_FEEDBACK_QUALITY_INSPECTION_NOT_FINISHED.getCode(),
                    StrUtil.format("工序[{}]为质检工序,报工单[{}]尚未完成过程检验;"
                                    + "请到「质量管理-过程检验」完成检验后再审批",
                            processName, feedback.getCode()));
        }
        BigDecimal checkQuantity = BigDecimal.ZERO;
        BigDecimal qualifiedQuantity = BigDecimal.ZERO;
        BigDecimal unqualifiedQuantity = BigDecimal.ZERO;
        for (MesQcIpqcDO ipqc : finishedList) {
            checkQuantity = checkQuantity.add(ObjectUtil.defaultIfNull(ipqc.getCheckQuantity(), BigDecimal.ZERO));
            qualifiedQuantity = qualifiedQuantity.add(ObjectUtil.defaultIfNull(ipqc.getQualifiedQuantity(), BigDecimal.ZERO));
            unqualifiedQuantity = unqualifiedQuantity.add(
                    ObjectUtil.defaultIfNull(ipqc.getUnqualifiedQuantity(), BigDecimal.ZERO));
        }
        if (checkQuantity.signum() <= 0) {
            throw new ServiceException(PRO_FEEDBACK_QUALITY_INSPECTION_NOT_FINISHED.getCode(),
                    StrUtil.format("工序[{}]的过程检验单[{}]检测数量为 0,无法计算合格率;"
                                    + "请在「质量管理-过程检验」补录检测数量后再审批",
                            processName, finishedList.get(0).getCode()));
        }
        BigDecimal passRate = qualifiedQuantity.multiply(BigDecimal.valueOf(100))
                .divide(checkQuantity, 2, RoundingMode.HALF_UP);
        return new MesProFeedbackQualityResult(passRate, checkQuantity, qualifiedQuantity, unqualifiedQuantity);
    }
 
    /**
     * 顺序报工校验:同一生产工单内,上一道工序必须已报工完成,否则不允许报本道工序
     *
     * <p>判定口径:上一道工序的有效任务(未取消)累计审批通过的产量已达到其排产数量,
     * 即任一有效任务 produced_quantity &lt; quantity 都视为未完成并拦截。
     * 之所以不依赖任务 status:任务状态只有整单完工/取消时才置终态
     * (finishTaskByOrderId / cancelTaskByOrderId),无法作为"单道工序已完成"的依据,
     * produced_quantity 是唯一实时可靠的进度信号。
     *
     * @param task 当前待报工任务
     */
    private void validatePreviousProcessFinished(MesProTaskDO task) {
        if (task == null || task.getWorkOrderId() == null || task.getProcessId() == null) {
            return; // 数据不完整,无法推导顺序,放行
        }
        // 1. 当前任务所属工序在工单工序链(按 sort 升序)中的位置
        List<MesProWorkOrderProcessDO> processes = workOrderProcessService
                .getWorkOrderProcessListByWorkOrderId(task.getWorkOrderId());
        if (CollUtil.isEmpty(processes)) {
            return; // 工单未配置工序,放行
        }
        // 拷贝后再排序,避免修改调用方返回的(可能是不可变)集合
        processes = new ArrayList<>(processes);
        processes.sort(Comparator.comparing(MesProWorkOrderProcessDO::getSort,
                Comparator.nullsLast(Comparator.naturalOrder())));
        int index = -1;
        for (int i = 0; i < processes.size(); i++) {
            if (ObjUtil.equal(processes.get(i).getProcessId(), task.getProcessId())) {
                index = i;
                break;
            }
        }
        if (index <= 0) {
            return; // 当前工序不在链中(数据异常,放行)或本身就是第一道工序,无需前置
        }
        Long prevProcessId = processes.get(index - 1).getProcessId();
        if (prevProcessId == null) {
            return;
        }
        // 2. 上一道工序的有效任务(排除已取消)累计待完成量
        List<MesProTaskDO> prevTasks = taskService.getTaskListByWorkOrderIds(
                        Collections.singletonList(task.getWorkOrderId())).stream()
                .filter(t -> ObjUtil.equal(t.getProcessId(), prevProcessId))
                .filter(t -> !ObjUtil.equal(t.getStatus(), MesProTaskStatusEnum.CANCELED.getStatus()))
                .toList();
        if (CollUtil.isEmpty(prevTasks)) {
            return; // 上一道工序无有效生产任务(含全部取消),视为无前置要求,放行
        }
        BigDecimal remaining = BigDecimal.ZERO;
        for (MesProTaskDO prevTask : prevTasks) {
            BigDecimal qty = prevTask.getQuantity() == null ? BigDecimal.ZERO : prevTask.getQuantity();
            BigDecimal produced = prevTask.getProducedQuantity() == null ? BigDecimal.ZERO : prevTask.getProducedQuantity();
            if (produced.compareTo(qty) < 0) {
                remaining = remaining.add(qty.subtract(produced));
            }
        }
        if (remaining.compareTo(BigDecimal.ZERO) <= 0) {
            return; // 上一道工序已全部报满,放行
        }
        // 3. 拦截并提示上一道工序名称与剩余待报数量
        MesProProcessDO prevProcess = processService.getProcess(prevProcessId);
        String prevProcessName = prevProcess != null ? prevProcess.getName() : String.valueOf(prevProcessId);
        throw exception(PRO_FEEDBACK_PREV_PROCESS_NOT_FINISHED, prevProcessName, remaining);
    }
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public MesProFeedbackQuickCreateRespVO quickCreateFeedback(MesProFeedbackQuickCreateReqVO reqVO) {
        equipmentService.lock();
        // 1. 校验任务存在且非终态
        MesProTaskDO task = taskService.validateTaskNotFinished(reqVO.getTaskId());
 
        // 校验停工状态不能报工
        if (!MesProTaskStatusEnum.canFeedback(task.getStatus())) {
            throw exception(PRO_FEEDBACK_CANNOT_CREATE_WHEN_PAUSED);
        }
        // 1.1 顺序报工校验:同一生产工单内,上一道工序必须已报工完成,否则不允许报工
        validatePreviousProcessFinished(task);
 
        // 2. 校验工艺路线工序配置
        MesProRouteProcessDO routeProcess = routeProcessService.getRouteProcessByRouteIdAndProcessId(
                task.getRouteId(), task.getProcessId());
        if (routeProcess == null) {
            throw exception(PRO_FEEDBACK_ROUTE_PROCESS_INVALID);
        }
        // 3. 校验数量:只填报工数量,且必须 > 0(合格品/不良品数量由质检确定)
        if (reqVO.getFeedbackQuantity() == null
                || reqVO.getFeedbackQuantity().compareTo(BigDecimal.ZERO) <= 0) {
            throw exception(PRO_FEEDBACK_QUANTITY_MUST_POSITIVE);
        }
        // 4. 构建报工DO
        MesProFeedbackDO feedback = new MesProFeedbackDO()
                .setCode(autoCodeRecordService.generateAutoCode(MesMdAutoCodeRuleCodeEnum.PRO_FEEDBACK_CODE.getCode()))
                .setType(MesProFeedbackTypeEnum.SELF.getType())
                .setWorkstationId(task.getWorkstationId())
                .setRouteId(task.getRouteId())
                .setProcessId(task.getProcessId())
                .setWorkOrderId(task.getWorkOrderId())
                .setTaskId(reqVO.getTaskId())
                .setItemId(task.getItemId())
                .setFeedbackQuantity(reqVO.getFeedbackQuantity())
                .setWorkHours(reqVO.getWorkHours())
                .setScheduledQuantity(task.getQuantity())
                .setFeedbackTime(reqVO.getFeedbackTime() != null ? reqVO.getFeedbackTime() : LocalDateTime.now())
                .setMachineryIds(reqVO.getMachineryIds())
                .setMachineryId(firstMachineryId(reqVO.getMachineryIds()))
                .setStatus(MesProFeedbackStatusEnum.APPROVING.getStatus())
                .setRemark(reqVO.getRemark());
        // 4.1 快捷报工默认没有工时录入入口:计时计薪且已定价的工序,必须显式传工时;
        //     批量报工等不传工时的入口会得到可执行的提示,引导到报工页面填写
        MesProProcessDO process = processService.getProcess(task.getProcessId());
        if (process == null) {
            throw exception(PRO_FEEDBACK_PROCESS_NOT_EXISTS, task.getProcessId());
        }
        BigDecimal wageRate = ObjectUtil.defaultIfNull(wageRuleService.resolve(task.getProcessId(), task.getRouteId(),
                feedback.getMachineryId(), process.getBillingType(), LocalDate.now()).getWageRate(), BigDecimal.ZERO);
        if (MesProProcessBillingTypeEnum.TIME == MesProProcessBillingTypeEnum.valueOfType(process.getBillingType())
                && wageRate.signum() > 0 && reqVO.getWorkHours() == null) {
            throw exception(PRO_FEEDBACK_TIME_BILLING_QUICK_UNSUPPORTED, process.getName());
        }
        // 4.2 快捷报工没有辅助项入口,仅按生产长度(报工单位为米时)计算标准工时快照
        List<MesProFeedbackAuxDO> auxList = worktimeService.calculateForFeedback(feedback, null);
        // 4.3 按工序计费类型、单价规则与工资字段快照并计算本次报工工资
        applyProcessBilling(feedback, process, auxList);
        equipmentService.validateSubmission(feedback);
        feedbackMapper.insert(feedback);
        saveFeedbackAux(feedback.getId(), auxList);
        saveParamValues(task.getId(), reqVO.getParamValues());
        // 5. 返回结果
        MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(task.getWorkOrderId());
        // 5.1 报工后剩余可报工数量 = 排产 - 已生产 - 在途(含本次报工)
        BigDecimal reportable = task.getQuantity() == null ? BigDecimal.ZERO : task.getQuantity();
        BigDecimal produced = task.getProducedQuantity() == null ? BigDecimal.ZERO : task.getProducedQuantity();
        BigDecimal inTransit = BigDecimal.ZERO;
        for (MesProFeedbackDO fb : getFeedbackListByTaskId(task.getId())) {
            if (!ObjUtil.equal(fb.getStatus(), MesProFeedbackStatusEnum.FINISHED.getStatus())
                    && !ObjUtil.equal(fb.getStatus(), MesProFeedbackStatusEnum.UNCHECK.getStatus())) {
                inTransit = inTransit.add(fb.getFeedbackQuantity() == null ? BigDecimal.ZERO : fb.getFeedbackQuantity());
            }
        }
        BigDecimal reportableQuantity = reportable.subtract(produced).subtract(inTransit).max(BigDecimal.ZERO);
        return new MesProFeedbackQuickCreateRespVO()
                .setFeedbackId(feedback.getId())
                .setTaskStatus(task.getStatus())
                .setWorkOrderStatus(workOrder != null ? workOrder.getStatus() : null)
                .setReportableQuantity(reportableQuantity);
    }
 
    @Override
    public MesProFeedbackWageBreakdown previewWage(MesProFeedbackWagePreviewReqVO reqVO) {
        MesProTaskDO task = taskService.getTask(reqVO.getTaskId());
        if (task == null) {
            throw exception(PRO_TASK_NOT_EXISTS);
        }
        MesProProcessDO process = processService.getProcess(task.getProcessId());
        if (process == null) {
            throw exception(PRO_FEEDBACK_PROCESS_NOT_EXISTS, task.getProcessId());
        }
        // 借用一个临时报工单对象复用标准工时/辅助工时计算
        MesProFeedbackDO temp = new MesProFeedbackDO()
                .setTaskId(task.getId())
                .setRouteId(task.getRouteId())
                .setProcessId(task.getProcessId())
                .setWorkstationId(task.getWorkstationId())
                .setMachineryId(reqVO.getMachineryId())
                .setWorkHours(reqVO.getWorkHours())
                .setFeedbackQuantity(reqVO.getFeedbackQuantity())
                .setQualifiedQuantity(reqVO.getQualifiedQuantity())
                .setProductionLength(reqVO.getProductionLength());
        List<MesProFeedbackAuxDO> auxList = worktimeService.calculateForFeedback(temp, reqVO.getAuxList());
        return wageCalculator.calculate(process, task.getRouteId(), reqVO.getMachineryId(),
                reqVO.getWorkHours(), reqVO.getFeedbackQuantity(), reqVO.getQualifiedQuantity(), auxList,
                reqVO.getFeedbackDate());
    }
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public MesProFeedbackBatchCreateRespVO batchCreateFeedback(MesProFeedbackBatchCreateReqVO reqVO) {
        List<MesProFeedbackBatchCreateRespVO.BatchResult> results = new java.util.ArrayList<>();
        int successCount = 0;
        int failCount = 0;
        for (Long taskId : reqVO.getTaskIds()) {
            try {
                MesProFeedbackQuickCreateReqVO singleReq = new MesProFeedbackQuickCreateReqVO();
                singleReq.setTaskId(taskId);
                singleReq.setFeedbackQuantity(reqVO.getFeedbackQuantity());
                singleReq.setFeedbackTime(reqVO.getFeedbackTime());
                singleReq.setRemark(reqVO.getRemark());
                MesProFeedbackQuickCreateRespVO resp = quickCreateFeedback(singleReq);
                results.add(new MesProFeedbackBatchCreateRespVO.BatchResult()
                        .setTaskId(taskId)
                        .setSuccess(true)
                        .setFeedbackId(resp.getFeedbackId()));
                successCount++;
            } catch (Exception e) {
                results.add(new MesProFeedbackBatchCreateRespVO.BatchResult()
                        .setTaskId(taskId)
                        .setSuccess(false)
                        .setErrorMsg(e.getMessage()));
                failCount++;
            }
        }
        return new MesProFeedbackBatchCreateRespVO()
                .setSuccessCount(successCount)
                .setFailCount(failCount)
                .setResults(results);
    }
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateProFeedbackWhenIpqcFinish(Long feedbackId, Long sourceLineId,
                                                BigDecimal qualifiedQty, BigDecimal unqualifiedQty,
                                                BigDecimal laborScrapQty, BigDecimal materialScrapQty, BigDecimal otherScrapQty) {
        // 1. 校验报工单存在且为待检验状态
        MesProFeedbackDO feedback = feedbackMapper.selectByIdForUpdate(feedbackId);
        if (feedback == null) {
            throw exception(PRO_FEEDBACK_NOT_EXISTS);
        }
        if (ObjUtil.notEqual(feedback.getStatus(), MesProFeedbackStatusEnum.UNCHECK.getStatus())) {
            throw exception(PRO_FEEDBACK_NOT_UNCHECK);
        }
 
        // 2. 拆分待检产出行(合格/不合格),生成明细,完成产出入库
        productProduceService.splitPendingAndFinishProduce(feedbackId, sourceLineId, qualifiedQty, unqualifiedQty);
 
        // 3. 回写合格/不合格/废品数量,更新状态为已完成
        feedbackMapper.updateById(new MesProFeedbackDO().setId(feedbackId)
                .setQualifiedQuantity(qualifiedQty)
                .setUnqualifiedQuantity(unqualifiedQty)
                .setUncheckQuantity(BigDecimal.ZERO)
                .setLaborScrapQuantity(laborScrapQty)
                .setMaterialScrapQuantity(materialScrapQty)
                .setOtherScrapQuantity(otherScrapQty)
                .setStatus(MesProFeedbackStatusEnum.FINISHED.getStatus()));
 
        // 4. 产量已在报工审批时累计,此处只回填质量数量。
        feedback.setQualifiedQuantity(qualifiedQty).setUnqualifiedQuantity(unqualifiedQty);
        taskService.updateQualityQuantity(feedback.getTaskId(),
                ObjectUtil.defaultIfNull(qualifiedQty, BigDecimal.ZERO),
                ObjectUtil.defaultIfNull(unqualifiedQty, BigDecimal.ZERO));
        //5.检查并发布生产完成事件
        checkAndPublishProductionFinished(feedback.getWorkOrderId());
        checkAndAutoFinishAfterLastQc(feedback);
    }
 
    void checkAndAutoFinishAfterLastQc(MesProFeedbackDO feedback) {
        MesProWorkOrderProcessDO lastProcess = workOrderProcessService
                .getLastWorkOrderProcessByWorkOrderId(feedback.getWorkOrderId());
        if (lastProcess == null || !ObjUtil.equal(lastProcess.getProcessId(), feedback.getProcessId())) {
            return;
        }
        // 同一工单的末道报工全部完成质检后,才允许级联完成工单任务。
        Long pendingCount = feedbackMapper.selectCount(new LambdaQueryWrapperX<MesProFeedbackDO>()
                .eq(MesProFeedbackDO::getWorkOrderId, feedback.getWorkOrderId())
                .eq(MesProFeedbackDO::getProcessId, feedback.getProcessId())
                .ne(MesProFeedbackDO::getStatus, MesProFeedbackStatusEnum.FINISHED.getStatus()));
        if (pendingCount == 0) {
            workOrderService.checkAndAutoFinishWorkOrder(feedback.getWorkOrderId());
        }
    }
 
    @Override
    public MesProFeedbackBatchCheckRespVO checkBatchRequirements(Long feedbackId) {
        MesProFeedbackDO feedback = validateFeedbackExists(feedbackId);
        MesProFeedbackBatchCheckRespVO respVO = new MesProFeedbackBatchCheckRespVO();
 
        // 1. 检查物料是否启用批次管理
        MesMdmItemDO item = mdmItemService.getItem(feedback.getItemId());
        if (item == null || Boolean.FALSE.equals(item.getIsBatchManaged())) {
            respVO.setNeedBatchInput(false);
            respVO.setIsBatchManaged(false);
            return respVO;
        }
        respVO.setIsBatchManaged(true);
 
        // 2. 获取批次配置
        MdmItemBatchConfigRespDTO config = itemBatchConfigApi.getItemBatchConfig(feedback.getItemId());
        if (config == null) {
            respVO.setNeedBatchInput(false);
            return respVO;
        }
 
        // 3. 填充配置标志
        respVO.setProduceDateFlag(config.getProduceDateFlag());
        respVO.setExpireDateFlag(config.getExpireDateFlag());
        respVO.setReceiptDateFlag(config.getReceiptDateFlag());
        respVO.setVendorFlag(config.getVendorFlag());
        respVO.setClientFlag(config.getClientFlag());
        respVO.setSalesOrderCodeFlag(config.getSalesOrderCodeFlag());
        respVO.setPurchaseOrderCodeFlag(config.getPurchaseOrderCodeFlag());
        respVO.setWorkOrderFlag(config.getWorkOrderFlag());
        respVO.setTaskFlag(config.getTaskFlag());
        respVO.setWorkstationFlag(config.getWorkstationFlag());
        respVO.setToolFlag(config.getToolFlag());
        respVO.setMoldFlag(config.getMoldFlag());
        respVO.setLotNumberFlag(config.getLotNumberFlag());
        respVO.setQualityStatusFlag(config.getQualityStatusFlag());
 
        // 4. 判断哪些字段已自动填充(报工和生产过程中必然有值的字段)
        List<String> autoFilled = new ArrayList<>();
        // 生产日期:审批时自动取当天
        autoFilled.add("produceDate");
        // 生产工单:从报工记录获取
        autoFilled.add("workOrderId");
        // 生产任务:从报工记录获取
        autoFilled.add("taskId");
        // 工作站:从报工记录获取
        autoFilled.add("workstationId");
        respVO.setAutoFilledFields(autoFilled);
 
        // 5. 判断是否需要弹窗:存在必填但无法自动填充的字段
        boolean needBatchInput = false;
        if (Boolean.TRUE.equals(config.getExpireDateFlag())) needBatchInput = true;
        if (Boolean.TRUE.equals(config.getReceiptDateFlag())) needBatchInput = true;
        if (Boolean.TRUE.equals(config.getVendorFlag())) needBatchInput = true;
        if (Boolean.TRUE.equals(config.getClientFlag())) needBatchInput = true;
        if (Boolean.TRUE.equals(config.getSalesOrderCodeFlag())) needBatchInput = true;
        if (Boolean.TRUE.equals(config.getPurchaseOrderCodeFlag())) needBatchInput = true;
        if (Boolean.TRUE.equals(config.getToolFlag())) needBatchInput = true;
        if (Boolean.TRUE.equals(config.getMoldFlag())) needBatchInput = true;
        if (Boolean.TRUE.equals(config.getLotNumberFlag())) needBatchInput = true;
        if (Boolean.TRUE.equals(config.getQualityStatusFlag())) needBatchInput = true;
        respVO.setNeedBatchInput(needBatchInput);
 
        return respVO;
    }
 
}