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
package cn.iocoder.yudao.module.mes.service.qc.retentionsample;
 
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.erp.api.sale.ErpSaleOrderApi;
import cn.iocoder.yudao.module.erp.api.sale.dto.ErpSaleOrderRespDTO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.retentionsample.vo.MesQcRetentionSamplePageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.qc.retentionsample.vo.MesQcRetentionSampleSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.mps.MesProMpsDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.retentionsample.MesQcRetentionSampleDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.mps.MesProMpsMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.qc.retentionsample.MesQcRetentionSampleMapper;
import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodeRuleCodeEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProWorkOrderSourceTypeEnum;
import cn.iocoder.yudao.module.mes.enums.pro.MesProWorkOrderStatusEnum;
import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
 
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
 
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
 
/**
 * MES 留样记录 Service 实现类
 *
 * @author 超级管理员
 */
@Service
@Validated
public class MesQcRetentionSampleServiceImpl implements MesQcRetentionSampleService {
 
    /**
     * 默认留样保存时长(月)
     */
    private static final int DEFAULT_RETENTION_MONTHS = 6;
 
    @Resource
    private MesQcRetentionSampleMapper retentionSampleMapper;
 
    @Resource
    private MesProWorkOrderService workOrderService;
    @Resource
    private MesProMpsMapper mpsMapper;
    @Resource
    private MesMdAutoCodeRecordService autoCodeRecordService;
    @Resource
    private ErpSaleOrderApi saleOrderApi;
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createRetentionSample(MesQcRetentionSampleSaveReqVO createReqVO) {
        // 1. 校验工单
        MesProWorkOrderDO workOrder = validateWorkOrder(createReqVO.getWorkOrderId());
        // 2. 校验该工单是否已登记
        MesQcRetentionSampleDO existing = retentionSampleMapper.selectByWorkOrderId(workOrder.getId());
        if (existing != null) {
            throw exception(QC_RETENTION_SAMPLE_WORK_ORDER_DUPLICATE,
                    workOrder.getCode(), existing.getCode());
        }
 
        // 3. 组装留样记录(工单相关字段以后端查询结果为准)
        MesQcRetentionSampleDO sample = BeanUtils.toBean(createReqVO, MesQcRetentionSampleDO.class);
        sample.setId(null);
        sample.setCode(autoCodeRecordService.generateAutoCode(
                MesMdAutoCodeRuleCodeEnum.QC_RETENTION_SAMPLE_CODE.getCode()));
        sample.setWorkOrderCode(workOrder.getCode());
        sample.setProductId(workOrder.getProductId());
        sample.setBatchCode(workOrder.getBatchCode());
        sample.setExpiryDate(resolveExpiryDate(sample.getSampleDate(), createReqVO.getExpiryDate()));
        if (sample.getKeeperUserId() == null) {
            sample.setKeeperUserId(getLoginUserId());
        }
        fillSaleOrder(workOrder, sample, createReqVO.getSaleOrderId());
 
        // 4. 插入
        retentionSampleMapper.insert(sample);
        return sample.getId();
    }
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateRetentionSample(MesQcRetentionSampleSaveReqVO updateReqVO) {
        // 1. 校验存在
        MesQcRetentionSampleDO oldSample = validateRetentionSampleExists(updateReqVO.getId());
        // 2.1 不允许修改所属工单
        if (ObjUtil.notEqual(oldSample.getWorkOrderId(), updateReqVO.getWorkOrderId())) {
            throw exception(QC_RETENTION_SAMPLE_WORK_ORDER_CHANGED);
        }
        // 2.2 校验工单
        MesProWorkOrderDO workOrder = validateWorkOrder(oldSample.getWorkOrderId());
 
        // 3. 组装更新对象(编码、工单、产品、批次不允许修改)
        MesQcRetentionSampleDO updateObj = BeanUtils.toBean(updateReqVO, MesQcRetentionSampleDO.class);
        updateObj.setCode(null);
        updateObj.setWorkOrderId(null);
        updateObj.setWorkOrderCode(null);
        updateObj.setProductId(null);
        updateObj.setBatchCode(null);
        updateObj.setExpiryDate(resolveExpiryDate(updateReqVO.getSampleDate(), updateReqVO.getExpiryDate()));
        fillSaleOrder(workOrder, updateObj, updateReqVO.getSaleOrderId());
 
        // 4. 更新
        retentionSampleMapper.updateById(updateObj);
    }
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteRetentionSample(Long id) {
        // 1. 校验存在
        MesQcRetentionSampleDO sample = validateRetentionSampleExists(id);
        // 2. 工单已完成时不允许删除
        MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(sample.getWorkOrderId());
        if (workOrder != null
                && ObjUtil.equal(workOrder.getStatus(), MesProWorkOrderStatusEnum.FINISHED.getStatus())) {
            throw exception(QC_RETENTION_SAMPLE_FINISHED_DELETE, workOrder.getCode());
        }
 
        retentionSampleMapper.deleteById(id);
    }
 
    // ==================== 校验与填充方法 ====================
 
    private MesProWorkOrderDO validateWorkOrder(Long workOrderId) {
        MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(workOrderId);
        if (workOrder == null) {
            throw exception(QC_RETENTION_SAMPLE_WORK_ORDER_NOT_EXISTS);
        }
        if (ObjUtil.equal(workOrder.getStatus(), MesProWorkOrderStatusEnum.CANCELED.getStatus())) {
            throw exception(QC_RETENTION_SAMPLE_WORK_ORDER_CANCELED, workOrder.getCode());
        }
        return workOrder;
    }
 
    private MesQcRetentionSampleDO validateRetentionSampleExists(Long id) {
        MesQcRetentionSampleDO sample = retentionSampleMapper.selectById(id);
        if (sample == null) {
            throw exception(QC_RETENTION_SAMPLE_NOT_EXISTS);
        }
        return sample;
    }
 
    private LocalDateTime resolveExpiryDate(LocalDateTime sampleDate, LocalDateTime expiryDate) {
        if (expiryDate != null) {
            return expiryDate;
        }
        return sampleDate.plusMonths(DEFAULT_RETENTION_MONTHS);
    }
 
    /**
     * 解析并校验销售订单:
     * 1. 由 MPS 下发的工单,销售订单以 MPS 为准;合并下发时由用户指定本次留样对应的销售订单;
     * 2. 客户订单来源、但无 MPS 的工单,按工单来源单号校验所选销售订单;
     * 3. 库存备货工单可不绑定销售订单。
     */
    private void fillSaleOrder(MesProWorkOrderDO workOrder, MesQcRetentionSampleDO sample,
                               Long submittedSaleOrderId) {
        // 1. 优先使用 MPS 记录的销售订单(合并下发时一个工单对应多张销售订单)
        List<MesProMpsDO> mpsList = mpsMapper.selectListByWorkOrderId(workOrder.getId());
        Map<Long, String> saleOrderMap = new LinkedHashMap<>();
        for (MesProMpsDO mps : mpsList) {
            if (mps.getSaleOrderId() != null) {
                saleOrderMap.putIfAbsent(mps.getSaleOrderId(), mps.getSaleOrderNo());
            }
        }
        if (saleOrderMap.size() == 1) {
            Map.Entry<Long, String> entry = saleOrderMap.entrySet().iterator().next();
            if (submittedSaleOrderId != null
                    && ObjUtil.notEqual(submittedSaleOrderId, entry.getKey())) {
                throw exception(QC_RETENTION_SAMPLE_SALE_ORDER_MISMATCH,
                        workOrder.getCode(), entry.getValue(),
                        resolveSaleOrderNoOrId(submittedSaleOrderId));
            }
            // 校验 MPS 记录的销售订单真实存在,避免把不存在的订单静默带入留样记录
            validateMpsSaleOrderExists(workOrder, entry.getKey(), entry.getValue());
            sample.setSaleOrderId(entry.getKey());
            sample.setSaleOrderNo(entry.getValue());
            return;
        }
        if (saleOrderMap.size() > 1) {
            String saleOrderNos = String.join("、", saleOrderMap.values());
            if (submittedSaleOrderId == null) {
                throw exception(QC_RETENTION_SAMPLE_SALE_ORDER_AMBIGUOUS,
                        workOrder.getCode(), saleOrderNos);
            }
            if (!saleOrderMap.containsKey(submittedSaleOrderId)) {
                throw exception(QC_RETENTION_SAMPLE_SALE_ORDER_MISMATCH,
                        workOrder.getCode(), saleOrderNos,
                        resolveSaleOrderNoOrId(submittedSaleOrderId));
            }
            validateMpsSaleOrderExists(workOrder, submittedSaleOrderId,
                    saleOrderMap.get(submittedSaleOrderId));
            sample.setSaleOrderId(submittedSaleOrderId);
            sample.setSaleOrderNo(saleOrderMap.get(submittedSaleOrderId));
            return;
        }
 
        // 2. 客户订单来源工单:必须选择与来源单号一致的销售订单
        if (ObjUtil.equal(workOrder.getOrderSourceType(), MesProWorkOrderSourceTypeEnum.ORDER.getType())
                && StrUtil.isNotEmpty(workOrder.getOrderSourceCode())) {
            if (submittedSaleOrderId == null) {
                throw exception(QC_RETENTION_SAMPLE_SALE_ORDER_REQUIRED,
                        workOrder.getCode(), workOrder.getOrderSourceCode());
            }
            ErpSaleOrderRespDTO saleOrder = getSaleOrderOrThrow(submittedSaleOrderId);
            if (StrUtil.isNotEmpty(saleOrder.getNo())
                    && ObjUtil.notEqual(saleOrder.getNo(), workOrder.getOrderSourceCode())) {
                throw exception(QC_RETENTION_SAMPLE_SALE_ORDER_MISMATCH,
                        workOrder.getCode(), workOrder.getOrderSourceCode(), saleOrder.getNo());
            }
            sample.setSaleOrderId(saleOrder.getId());
            sample.setSaleOrderNo(saleOrder.getNo());
            return;
        }
 
        // 3. 库存备货工单:可选绑定
        if (submittedSaleOrderId != null) {
            ErpSaleOrderRespDTO saleOrder = getSaleOrderOrThrow(submittedSaleOrderId);
            sample.setSaleOrderId(saleOrder.getId());
            sample.setSaleOrderNo(saleOrder.getNo());
        }
    }
 
    private void validateMpsSaleOrderExists(MesProWorkOrderDO workOrder, Long saleOrderId,
                                            String saleOrderNo) {
        CommonResult<ErpSaleOrderRespDTO> result = saleOrderApi.getSaleOrder(saleOrderId);
        if (result.isError() || result.getData() == null) {
            throw exception(QC_RETENTION_SAMPLE_MPS_SALE_ORDER_NOT_EXISTS,
                    workOrder.getCode(), StrUtil.blankToDefault(saleOrderNo, "未记录"), saleOrderId);
        }
    }
 
    private ErpSaleOrderRespDTO getSaleOrderOrThrow(Long saleOrderId) {
        CommonResult<ErpSaleOrderRespDTO> result = saleOrderApi.getSaleOrder(saleOrderId);
        if (result.isError() || result.getData() == null) {
            throw exception(QC_RETENTION_SAMPLE_SALE_ORDER_NOT_EXISTS, "ID=" + saleOrderId);
        }
        return result.getData();
    }
 
    private String resolveSaleOrderNoOrId(Long saleOrderId) {
        CommonResult<ErpSaleOrderRespDTO> result = saleOrderApi.getSaleOrder(saleOrderId);
        if (result.isSuccess() && result.getData() != null) {
            return result.getData().getNo();
        }
        return "ID=" + saleOrderId;
    }
 
    // ==================== 查询方法 ====================
 
    @Override
    public MesQcRetentionSampleDO getRetentionSample(Long id) {
        return retentionSampleMapper.selectById(id);
    }
 
    @Override
    public PageResult<MesQcRetentionSampleDO> getRetentionSamplePage(
            MesQcRetentionSamplePageReqVO pageReqVO) {
        return retentionSampleMapper.selectPage(pageReqVO);
    }
 
    @Override
    public MesQcRetentionSampleDO getRetentionSampleByWorkOrderId(Long workOrderId) {
        return retentionSampleMapper.selectByWorkOrderId(workOrderId);
    }
 
}