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
package cn.iocoder.yudao.module.mes.service.qc.imports;
 
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils;
import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
import cn.iocoder.yudao.module.crm.api.customer.dto.CrmCustomerRespDTO;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkstationDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.qc.template.MesQcTemplateDO;
import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService;
import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateService;
import cn.iocoder.yudao.module.srm.api.supplier.SrmSupplierApi;
import cn.iocoder.yudao.module.srm.api.supplier.dto.SrmSupplierRespDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;
 
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
 
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.QC_NO_TEMPLATE;
 
/**
 * MES 质检单导入辅助类
 *
 * 负责把 Excel 中的人类可读值(物料编码、供应商名称、客户名称、质检方案名称、检测人员姓名、工单号、工位名称等)
 * 解析为系统编号,并统一处理字典、日期、错误文案。
 * 各物料/供应商/客户/方案映射在 {@link #startImport()} 后首次使用时一次性加载,避免逐行查询。
 */
@Component
public class MesQcImportHelper {
 
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private MdmItemApi mdmItemApi;
    @Resource
    private SrmSupplierApi srmSupplierApi;
    @Resource
    private CrmCustomerApi crmCustomerApi;
    @Resource
    private MesQcTemplateService templateService;
    @Resource
    private MesMdAutoCodeRecordService autoCodeRecordService;
    @Resource
    private MesProWorkOrderService workOrderService;
    @Resource
    private MesMdWorkstationService workstationService;
 
    /** 质检方案名称 -> 编号(可能重名,命中多个时取第一个) */
    private Map<String, Long> templateNameMap;
    /** 供应商名称 -> 编号 */
    private Map<String, Long> supplierNameMap;
    /** 客户名称 -> 编号 */
    private Map<String, Long> customerNameMap;
    /** 当前登录用户可见的客户编号;null 表示不限制 */
    private Set<Long> permittedClientIds;
    private boolean clientPermissionLoaded;
 
    /**
     * 开始一次导入:清空上一次导入的缓存,保证新建的供应商/客户/方案本次即可命中
     */
    public void startImport() {
        templateNameMap = null;
        supplierNameMap = null;
        customerNameMap = null;
        permittedClientIds = null;
        clientPermissionLoaded = false;
    }
 
    /**
     * 生成检验单编号,如 IQC20260101000001
     */
    public String generateCode(String ruleCode) {
        return autoCodeRecordService.generateAutoCode(ruleCode);
    }
 
    /**
     * 解析检测人员:留空取当前登录用户;填写时按姓名(昵称)精确匹配,取不到则用模糊匹配的第一条
     */
    public Long resolveInspectorUserId(String nickname) {
        if (StrUtil.isBlank(nickname)) {
            return getLoginUserId();
        }
        List<AdminUserRespDTO> users = adminUserApi.getUserListByNickname(nickname.trim());
        if (CollUtil.isEmpty(users)) {
            throw new IllegalArgumentException("检测人员不存在:" + nickname.trim()
                    + "。请在「检测人员」列填写系统内的用户姓名(可在「系统管理-用户管理」中核对),或留空使用当前登录用户");
        }
        return users.stream().filter(user -> nickname.trim().equals(user.getNickname()))
                .map(AdminUserRespDTO::getId).findFirst().orElseGet(() -> users.get(0).getId());
    }
 
    /**
     * 解析产品物料编号,基于物料编码
     */
    public Long resolveItemId(String itemCode) {
        if (StrUtil.isBlank(itemCode)) {
            throw new IllegalArgumentException("产品物料编码不能为空:请填写模板中该行的「产品物料编码」列");
        }
        MdmItemRespDTO item = mdmItemApi.getItemByCode(itemCode.trim()).getCheckedData();
        if (item == null) {
            throw new IllegalArgumentException("物料不存在:" + itemCode.trim()
                    + "。请先在「物料管理」中维护该物料(编码需与模板完全一致),或修改模板中的产品物料编码后重新导入");
        }
        return item.getId();
    }
 
    /**
     * 解析质检方案编号,基于方案名称;留空返回 null(由系统按物料+检验类型自动匹配)
     */
    public Long resolveTemplateId(String templateName) {
        if (StrUtil.isBlank(templateName)) {
            return null;
        }
        if (templateNameMap == null) {
            templateNameMap = new HashMap<>();
            List<MesQcTemplateDO> templates = templateService.getTemplateList();
            if (CollUtil.isNotEmpty(templates)) {
                templates.forEach(template -> {
                    if (StrUtil.isNotBlank(template.getName())) {
                        templateNameMap.putIfAbsent(template.getName().trim(), template.getId());
                    }
                });
            }
        }
        Long templateId = templateNameMap.get(templateName.trim());
        if (templateId == null) {
            throw new IllegalArgumentException("质检方案不存在:" + templateName.trim()
                    + "。请在「质检管理-质检方案」中核对方案名称,或留空该列由系统按物料自动匹配方案后重新导入");
        }
        return templateId;
    }
 
    /**
     * 解析供应商编号,基于供应商名称;留空返回 null
     */
    public Long resolveVendorId(String vendorName) {
        if (StrUtil.isBlank(vendorName)) {
            return null;
        }
        if (supplierNameMap == null) {
            supplierNameMap = new HashMap<>();
            List<SrmSupplierRespDTO> suppliers = srmSupplierApi.getSupplierSimpleList().getCheckedData();
            if (CollUtil.isNotEmpty(suppliers)) {
                suppliers.forEach(supplier -> {
                    if (StrUtil.isNotBlank(supplier.getName())) {
                        supplierNameMap.putIfAbsent(supplier.getName().trim(), supplier.getId());
                    }
                });
            }
        }
        Long vendorId = supplierNameMap.get(vendorName.trim());
        if (vendorId == null) {
            throw new IllegalArgumentException("供应商不存在或未通过准入审批:" + vendorName.trim()
                    + "。请在「供应商管理」中核对供应商名称(名称需与模板完全一致),或留空该列后重新导入");
        }
        return vendorId;
    }
 
    /**
     * 解析客户编号,基于客户名称
     */
    public Long resolveClientId(String clientName) {
        if (StrUtil.isBlank(clientName)) {
            throw new IllegalArgumentException("客户名称不能为空:请填写模板中该行的「客户名称」列");
        }
        if (customerNameMap == null) {
            customerNameMap = new HashMap<>();
            List<CrmCustomerRespDTO> customers = crmCustomerApi.getCustomerSimpleList();
            if (CollUtil.isNotEmpty(customers)) {
                customers.forEach(customer -> {
                    if (StrUtil.isNotBlank(customer.getName())) {
                        customerNameMap.putIfAbsent(customer.getName().trim(), customer.getId());
                    }
                });
            }
        }
        Long clientId = customerNameMap.get(clientName.trim());
        if (clientId == null) {
            throw new IllegalArgumentException("客户不存在:" + clientName.trim()
                    + "。请先在「CRM 客户管理」中创建该客户(名称需与模板完全一致),或修改模板中的客户名称后重新导入");
        }
        return clientId;
    }
 
    /**
     * 校验客户在当前登录用户的可见范围内
     *
     * 出货检验单列表按客户数据权限过滤,权限外的客户导入后本人在列表中看不到该单据,因此提前拦截。
     */
    public void validateClientPermission(Long clientId, String clientName) {
        if (!clientPermissionLoaded) {
            List<Long> permittedIds = crmCustomerApi.getPermittedCustomerIds();
            permittedClientIds = permittedIds == null ? null : new HashSet<>(permittedIds);
            clientPermissionLoaded = true;
        }
        if (permittedClientIds == null || permittedClientIds.contains(clientId)) {
            return;
        }
        throw new IllegalArgumentException("客户[" + clientName + "]不在您的数据权限范围内:"
                + "导入后您将无法在「出货检验」列表中看到该检验单。请改用您负责的客户(可在「CRM 客户管理-我负责的」中查看),"
                + "或联系管理员为您开通该客户的权限后重新导入");
    }
 
    /**
     * 解析生产工单编号,基于工单号
     */
    public Long resolveWorkOrderId(String workOrderCode) {
        if (StrUtil.isBlank(workOrderCode)) {
            throw new IllegalArgumentException("生产工单号不能为空:请填写模板中该行的「生产工单号」列");
        }
        MesProWorkOrderDO workOrder = workOrderService.getWorkOrder(workOrderCode.trim());
        if (workOrder == null) {
            throw new IllegalArgumentException("生产工单不存在:" + workOrderCode.trim()
                    + "。请在「生产管理-生产工单」中核对该工单号(需与模板完全一致),或修改模板后重新导入");
        }
        return workOrder.getId();
    }
 
    /**
     * 解析工位编号,基于工位名称
     */
    public Long resolveWorkstationId(String workstationName) {
        if (StrUtil.isBlank(workstationName)) {
            throw new IllegalArgumentException("工位名称不能为空:请填写模板中该行的「工位名称」列");
        }
        MesMdWorkstationDO workstation = workstationService.getWorkstationByName(workstationName.trim());
        if (workstation == null) {
            throw new IllegalArgumentException("工位不存在:" + workstationName.trim()
                    + "。请在「基础数据-工位管理」中核对该工位名称(需与模板完全一致),或修改模板后重新导入");
        }
        return workstation.getId();
    }
 
    /**
     * 解析字典值:支持填写字典名称(如 校验通过)或字典值(如 1)
     *
     * @param text       Excel 单元格内容
     * @param dictType   字典类型
     * @param fieldLabel 列名,用于错误提示
     * @param required   是否必填
     */
    public Integer resolveDictValue(String text, String dictType, String fieldLabel, boolean required) {
        List<String> labelList = DictFrameworkUtils.getDictDataLabelList(dictType);
        if (StrUtil.isBlank(text)) {
            if (required) {
                throw new IllegalArgumentException(fieldLabel + "不能为空:请填写模板中该行的「" + fieldLabel
                        + "」列,可选值:" + String.join("、", labelList));
            }
            return null;
        }
        String value = text.trim();
        if (value.matches("\\d+") && DictFrameworkUtils.getDictDataValueList(dictType).contains(value)) {
            return Integer.valueOf(value);
        }
        String parsedValue = DictFrameworkUtils.parseDictDataValue(dictType, value);
        if (parsedValue == null) {
            throw new IllegalArgumentException(fieldLabel + "不存在:" + value + "。模板中「" + fieldLabel
                    + "」请填写以下之一:" + String.join("、", labelList));
        }
        return Integer.valueOf(parsedValue);
    }
 
    /**
     * 解析数量:必填时不能为空,且不能小于 0
     */
    public BigDecimal resolveQuantity(BigDecimal value, String fieldLabel, boolean required) {
        if (value == null) {
            if (required) {
                throw new IllegalArgumentException(fieldLabel + "不能为空:请填写模板中该行的「" + fieldLabel + "」列");
            }
            return null;
        }
        if (value.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException(fieldLabel + "不能小于 0,当前为 " + value.stripTrailingZeros().toPlainString()
                    + "。请修改模板后重新导入");
        }
        return value;
    }
 
    /**
     * 校验「合计数量 = 合格品数量 + 不合格品数量」
     *
     * @param total            合计数量(合计列留空时传 null,由合格 + 不合格 自动推算)
     * @param qualified        合格品数量
     * @param unqualified      不合格品数量
     * @param totalFieldLabel  合计列的列名,如「本次接收数量」
     */
    public BigDecimal resolveCheckQuantity(BigDecimal total, BigDecimal qualified, BigDecimal unqualified,
                                           String totalFieldLabel) {
        BigDecimal sum = qualified.add(unqualified);
        if (total == null) {
            return sum;
        }
        if (total.compareTo(sum) != 0) {
            throw new IllegalArgumentException(totalFieldLabel + "必须等于合格品数量与不合格品数量之和:合格品数量 "
                    + qualified.stripTrailingZeros().toPlainString() + " + 不合格品数量 "
                    + unqualified.stripTrailingZeros().toPlainString() + " = " + sum.stripTrailingZeros().toPlainString()
                    + ",而「" + totalFieldLabel + "」填写的是 " + total.stripTrailingZeros().toPlainString()
                    + "。请修改模板中的数量列,或将该列留空由系统自动合计后重新导入");
        }
        return total;
    }
 
    /**
     * 解析日期时间:支持 2026-01-01 10:00:00 / 2026-01-01 10:00 / 2026-01-01 / 2026/1/1;留空返回 null
     */
    public LocalDateTime parseDateTime(String text, String fieldLabel) {
        if (StrUtil.isBlank(text)) {
            return null;
        }
        String value = text.trim();
        for (String pattern : new String[]{"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy/MM/dd HH:mm:ss"}) {
            try {
                return LocalDateTime.parse(value, DateTimeFormatter.ofPattern(pattern));
            } catch (Exception ignored) {
                // 继续尝试下一个格式
            }
        }
        for (String pattern : new String[]{"yyyy-MM-dd", "yyyy/M/d"}) {
            try {
                return java.time.LocalDate.parse(value, DateTimeFormatter.ofPattern(pattern)).atStartOfDay();
            } catch (Exception ignored) {
                // 继续尝试下一个格式
            }
        }
        throw new IllegalArgumentException(fieldLabel + "格式不正确:" + text + ",示例 2026-01-01 10:00:00");
    }
 
    /**
     * 解析必填的日期时间:留空报错
     */
    public LocalDateTime parseRequiredDateTime(String text, String fieldLabel) {
        LocalDateTime value = parseDateTime(text, fieldLabel);
        if (value == null) {
            throw new IllegalArgumentException(fieldLabel + "不能为空:请填写模板中该行的「" + fieldLabel
                    + "」列,示例 2026-01-01 10:00:00");
        }
        return value;
    }
 
    /**
     * 物料未配置质检方案时,补充物料与检验类型上下文再抛出;其余异常原样抛出
     */
    public static void rethrowTemplateError(ServiceException ex, String itemCode, String qcTypeLabel) {
        if (Objects.equals(ex.getCode(), QC_NO_TEMPLATE.getCode())) {
            throw new IllegalArgumentException("物料[" + StrUtil.blankToDefault(itemCode, "未知") + "]未配置"
                    + qcTypeLabel + "的质检方案。请在「质检管理-质检方案」中为该物料配置" + qcTypeLabel
                    + "方案,或在模板的「质检方案」列填写该物料已配置的方案名称后重新导入");
        }
        throw ex;
    }
 
    /**
     * 单行导入失败时的提示文案
     */
    public static String resolveImportErrorMessage(Exception ex) {
        if (ex instanceof cn.iocoder.yudao.framework.common.exception.ServiceException serviceException) {
            return serviceException.getMessage();
        }
        return StrUtil.blankToDefault(ex.getMessage(), "导入失败");
    }
 
    /**
     * 重复数据提示中的取值展示:留空显示为「未填写」
     */
    public static String describeImportValue(String value) {
        return StrUtil.isBlank(value) ? "未填写" : value.trim();
    }
 
    /**
     * 同一份模板内出现重复数据时的提示
     */
    public static IllegalArgumentException duplicateRowException(int firstRowNo, String keyDesc) {
        return new IllegalArgumentException("检测到重复导入:本次导入的第 " + firstRowNo
                + " 行与当前行的数据完全相同(" + keyDesc + ")。请删除模板中重复的行后重新导入");
    }
 
    /**
     * 与系统中已有单据重复时的提示
     */
    public static IllegalArgumentException duplicateExistingException(String keyDesc, String docLabel,
                                                                    String existingCode, String menuPath) {
        return new IllegalArgumentException("检测到重复导入:" + keyDesc + " 与已有" + docLabel + "「" + existingCode
                + "」完全相同(可能是本次导入前面的行,也可能是系统中已有单据)。请勿重复导入;如确需重新导入,请先在「"
                + menuPath + "」中删除该" + docLabel + "后重试");
    }
 
}