diff --git a/src/main/java/digital/laboratory/platform/entrustment/service/impl/EntrustMaterialCheckoutResultServiceImpl.java b/src/main/java/digital/laboratory/platform/entrustment/service/impl/EntrustMaterialCheckoutResultServiceImpl.java index 5af51a7..e720acd 100644 --- a/src/main/java/digital/laboratory/platform/entrustment/service/impl/EntrustMaterialCheckoutResultServiceImpl.java +++ b/src/main/java/digital/laboratory/platform/entrustment/service/impl/EntrustMaterialCheckoutResultServiceImpl.java @@ -72,6 +72,11 @@ import java.util.stream.Collectors; public class EntrustMaterialCheckoutResultServiceImpl extends ServiceImpl implements EntrustMaterialCheckoutResultService { + /** + * Excel 工作表名最大长度(OOXML 规范:1~31 字符) + */ + private static final int MAX_SHEET_NAME_LENGTH = 31; + @Resource private EntrustmentService entrustmentService; @@ -688,6 +693,9 @@ public class EntrustMaterialCheckoutResultServiceImpl extends ServiceImpl getOriginalIdentificationList(List oldResults) { List allResults = new ArrayList<>(); + if (CollUtil.isEmpty(oldResults)) { + return allResults; + } for (String oldResult : oldResults) { if (StringUtils.isNotBlank(oldResult)) { if ("委托".equals(oldResult)) { @@ -751,54 +759,55 @@ public class EntrustMaterialCheckoutResultServiceImpl extends ServiceImpl drugNames = new HashSet<>(); + // 4. 提取所有涉及的化合物名称,并按"归一化名称"归并分组 + // 字典里用 "氢可酮*" 标注管制药品,业务上与 "氢可酮" 是同一化合物,这里合并为一组 + // key = 归一化名称(统计表展示、工作表名都用它),value = 库里的各种原始写法(查询要用原始写法) + Map> groupedDrugNames = new LinkedHashMap<>(); for (EntrustMaterialCheckoutResult result : newResultList) { - List drugLites = DrugLiteConvert.getDrugLites(result.getQualitativeResult()); - for (DrugLite drugLite : drugLites) { - drugNames.add(drugLite.getName()); - } - if (isMetabolite && StringUtils.isNotBlank(result.getMetaboliteResult())) { - List drugLites1 = DrugLiteConvert.getDrugLites(result.getMetaboliteResult()); - for (DrugLite drugLite : drugLites1) { - if (!drugNames.contains(drugLite.getName())) { - drugNames.add(drugLite.getName()); - } - } + collectDrugNames(groupedDrugNames, result.getQualitativeResult()); + if (isMetabolite) { + collectDrugNames(groupedDrugNames, result.getMetaboliteResult()); } } // 5. 处理不同毒品名称的数据 Map> resultMap = new HashMap<>(); - for (String drugName : drugNames) { - // 5.1 查询所有含有该毒品的检验结果 - List resultList = this.list(Wrappers.lambdaQuery() - .and(queryWrapper -> queryWrapper - .like(EntrustMaterialCheckoutResult::getMetaboliteResult, drugName) - .or() - .like(EntrustMaterialCheckoutResult::getQualitativeResult, drugName)) - .in(EntrustMaterialCheckoutResult::getEntrustId, entrustIdList)); + for (Map.Entry> drugGroup : groupedDrugNames.entrySet()) { + String drugName = drugGroup.getKey(); + Set rawDrugNames = drugGroup.getValue(); - if (resultList.isEmpty()) { + // 5.1 查询所有含有该毒品的检验结果(组内每种原始写法各查一次,再按检材 id 去重合并) + Map checkoutResultMap = new LinkedHashMap<>(); + for (String rawDrugName : rawDrugNames) { + List resultList = this.list(Wrappers.lambdaQuery() + .and(queryWrapper -> queryWrapper + .like(EntrustMaterialCheckoutResult::getMetaboliteResult, rawDrugName) + .or() + .like(EntrustMaterialCheckoutResult::getQualitativeResult, rawDrugName)) + .in(EntrustMaterialCheckoutResult::getEntrustId, entrustIdList)); + + if (isMetabolite) { + resultList.forEach(item -> { + if (StringUtils.isNotBlank(item.getMetaboliteResult())) { + List drugLites = DrugLiteConvert.getDrugLites(item.getMetaboliteResult()); + List drugLites1 = DrugLiteConvert.getDrugLites(item.getQualitativeResult()); + drugLites.addAll(drugLites1); + item.setQualitativeResult(JSONArray.toJSONString(drugLites)); + } + }); + } + + // 同一检材可能同时命中组内的多个原始写法,按检材 id 去重 + for (EntrustMaterialCheckoutResult item : resultList) { + checkoutResultMap.putIfAbsent(item.getId(), item); + } + } + + if (checkoutResultMap.isEmpty()) { continue; } - if (isMetabolite) { - resultList.forEach(item -> { - if (StringUtils.isNotBlank(item.getMetaboliteResult())) { - List drugLites = DrugLiteConvert.getDrugLites(item.getMetaboliteResult()); - List drugLites1 = DrugLiteConvert.getDrugLites(item.getQualitativeResult()); - drugLites.addAll(drugLites1); - item.setQualitativeResult(JSONArray.toJSONString(drugLites)); - } - }); - } - - // 转换为 Map,方便后续查找 - Map checkoutResultMap = resultList.stream() - .collect(Collectors.toMap(EntrustMaterialCheckoutResult::getId, Function.identity())); - // 5.2 获取相关的鉴定材料 Set materialIdSet = checkoutResultMap.keySet(); List materialList = entrustmentIdentificationMaterialService.list(Wrappers.lambdaQuery() @@ -862,6 +871,46 @@ public class EntrustMaterialCheckoutResultServiceImpl extends ServiceImpl> grouped, String drugResultJson) { + if (StringUtils.isBlank(drugResultJson)) { + return; + } + for (DrugLite drugLite : DrugLiteConvert.getDrugLites(drugResultJson)) { + String rawName = drugLite.getName(); + if (StringUtils.isBlank(rawName)) { + continue; + } + grouped.computeIfAbsent(normalizeDrugName(rawName), key -> new LinkedHashSet<>()).add(rawName); + } + } + + /** + * 化合物名称归一化。 + *

+ * 化合物字典里用星号标注管制药品(如 "氢可酮*",共 52 条),业务上它与不带星号的 + * "氢可酮" 是同一化合物,所以导出时按下述规则归并:去掉星号 + 去首尾空白。 + * 归并后同一个化合物在统计表中只占一行、在工作簿中只占一张工作表。 + *

+ * 注意:只影响"化合物归并/工作表名",明细行"检出结果"列仍按原始名称输出。 + * + * @param drugName 原始化合物名称 + * @return 归一化后的名称 + */ + private static String normalizeDrugName(String drugName) { + if (StringUtils.isBlank(drugName)) { + return drugName; + } + String normalized = drugName.replace("*", "").replace("*", "").trim(); + // 名称本身全是星号时归一化会变空,退回原始名称,避免出现空的分组键 + return normalized.isEmpty() ? drugName : normalized; + } + /** * 导出Excel * @@ -897,9 +946,17 @@ public class EntrustMaterialCheckoutResultServiceImpl extends ServiceImpl resultData) { // 确定表名 - String sheetName = (entrustType == 0) ? "常规毒品" : "生物样本"; - Sheet sheet = workbook.createSheet(sheetName); + String sheetName = isBiologySample(entrustType) ? "生物样本" : "常规毒品"; + Sheet sheet = workbook.createSheet(safeSheetName(workbook, sheetName)); // 定义表头 String[] inVivoHeaders = {"检材类型", "检材编号", "检材名称", "检材性状", "检材质量", "年龄", "性别", "委托单位", "受理日期", "检出结果"}; String[] inVitroHeaders = {"检材类型", "检材编号", "检材名称", "检材性状", "检材质量", "委托单位", "受理日期", "检出结果"}; - String[] headers = (entrustType == 0) ? inVitroHeaders : inVivoHeaders; + String[] headers = isBiologySample(entrustType) ? inVivoHeaders : inVitroHeaders; // 创建表头行 Row headerRow = sheet.createRow(0); @@ -942,7 +999,7 @@ public class EntrustMaterialCheckoutResultServiceImpl extends ServiceImpl> resultDataMap, List oldResults, Integer entrustType, LocalDateTime startTime, LocalDateTime endTime) { - Sheet sheet = workbook.createSheet("统计"); + Sheet sheet = workbook.createSheet(safeSheetName(workbook, "统计")); String[] headers = {"化合物种类", "检出数量", "总数量", "检出率"}; @@ -965,19 +1022,24 @@ public class EntrustMaterialCheckoutResultServiceImpl extends ServiceImpl> resultDataMap, Integer entrustType) { for (Map.Entry> entry : resultDataMap.entrySet()) { -// String drugName = entry.getKey().replaceAll("[^\u4e00-\u9fa5a-zA-Z0-9]", ""); - String drugName = entry.getKey(); - Sheet sheet = workbook.createSheet(drugName); + // 工作表名必须净化:化合物字典里存在 "氢可酮*" 这类带 * 的名称(共 52 条)以及含 [ ] 的 + // IUPAC 名称,直接建表会抛 IllegalArgumentException 导致整份报表导出失败。 + // 注意只清洗"工作表名",单元格里的检出结果仍保留原始名称。 + String sheetName = safeSheetName(workbook, entry.getKey()); + Sheet sheet = workbook.createSheet(sheetName); // 定义表头 - String[] headers = (entrustType == 0) ? new String[]{"检材类型", "检材编号", "检材名称", "检材性状", "检材质量", "委托单位", "受理日期", "检出结果"} - : new String[]{"检材类型", "检材编号", "检材名称", "检材性状", "检材质量", "年龄", "性别", "委托单位", "受理日期", "检出结果"}; + String[] headers = isBiologySample(entrustType) + ? new String[]{"检材类型", "检材编号", "检材名称", "检材性状", "检材质量", "年龄", "性别", "委托单位", "受理日期", "检出结果"} + : new String[]{"检材类型", "检材编号", "检材名称", "检材性状", "检材质量", "委托单位", "受理日期", "检出结果"}; // 创建表头行 Row headerRow = sheet.createRow(0); @@ -995,6 +1057,50 @@ public class EntrustMaterialCheckoutResultServiceImpl extends ServiceImpl + * Excel/OOXML 规范要求工作表名:不能包含 {@code : \ / ? * [ ]} 这 7 个字符、长度 1~31、不能为空、不能重名 + * (POI 判重不区分大小写,校验不通过会抛 IllegalArgumentException)。 + * 化合物字典里本身就有带 * 的名称(如 "氢可酮*",共 52 条)和含 [ ] 的 IUPAC 名称, + * 所以这里统一做"过滤非法字符 -> 截断 31 字符 -> 空名兜底 -> 重名加后缀(2)(3)…"。 + * + * @param workbook 目标工作簿 + * @param rawName 原始名称(化合物名称) + * @return 可直接用于 createSheet 的合法且唯一的工作表名 + */ + private String safeSheetName(Workbook workbook, String rawName) { + String base = StrUtil.isBlank(rawName) + ? "" + : rawName.replaceAll("[\\[\\]:*?/\\\\\\x00-\\x1f\\x7f]", "").trim(); + if (base.isEmpty()) { + base = "未命名"; + } + if (base.length() > MAX_SHEET_NAME_LENGTH) { + base = base.substring(0, MAX_SHEET_NAME_LENGTH); + } + String name = base; + int index = 2; + while (workbook.getSheet(name) != null) { + String suffix = "(" + index++ + ")"; + String cut = base.length() + suffix.length() > MAX_SHEET_NAME_LENGTH + ? base.substring(0, MAX_SHEET_NAME_LENGTH - suffix.length()) + : base; + name = cut + suffix; + } + return name; + } + private void fillRowData(Row row, EntrustmentIdentificationMaterialVO vo, Integer entrustType) { row.createCell(0).setCellValue(vo.getOldIdentificationResult()); row.createCell(1).setCellValue(vo.getAcceptNo()); @@ -1002,8 +1108,9 @@ public class EntrustMaterialCheckoutResultServiceImpl extends ServiceImpl results = new ArrayList<>(); - for (String oldResult : oldResults) { - if (oldResult.equals("委托")) { - results.add("首次鉴定"); - results.add("补充鉴定"); - results.add("重新鉴定"); - } else results.add(oldResult); - } + // 鉴定情况过滤条件("委托" 等价于首次/补充/重新鉴定) + List results = getOriginalIdentificationList(oldResults); // 查询符合条件的 Entrustment 记录 - List entrusts = entrustmentService.list(wrapper + LambdaQueryWrapper entrustWrapper = wrapper .eq(Entrustment::getEntrustmentType, entrustType) - .ge(Entrustment::getStatus, EntrustStatusConstants.ENTRUST_STATUS_ACCEPTED.getStatus()) - .in(Entrustment::getOldIdentificationResult, results) - ); + .ge(Entrustment::getStatus, EntrustStatusConstants.ENTRUST_STATUS_ACCEPTED.getStatus()); + // 注意:鉴定情况为空时不能拼 in 条件,否则 MyBatis-Plus 会生成 "IN ()" 造成 SQL 语法错误 + if (CollUtil.isNotEmpty(results)) { + entrustWrapper.in(Entrustment::getOldIdentificationResult, results); + } + List entrusts = entrustmentService.list(entrustWrapper); List entrustIds = entrusts.stream().map(Entrustment::getId).collect(Collectors.toList()); + // 没有委托时直接返回 0,同样避免 "IN ()" + if (CollUtil.isEmpty(entrustIds)) { + return 0; + } return entrustmentIdentificationMaterialService.lambdaQuery() .in(EntrustmentIdentificationMaterial::getEntrustmentId, entrustIds) diff --git a/src/main/java/digital/laboratory/platform/entrustment/service/impl/EntrustmentIdentificationMaterialServiceImpl.java b/src/main/java/digital/laboratory/platform/entrustment/service/impl/EntrustmentIdentificationMaterialServiceImpl.java index d34a593..bbd5328 100644 --- a/src/main/java/digital/laboratory/platform/entrustment/service/impl/EntrustmentIdentificationMaterialServiceImpl.java +++ b/src/main/java/digital/laboratory/platform/entrustment/service/impl/EntrustmentIdentificationMaterialServiceImpl.java @@ -1661,15 +1661,43 @@ public class EntrustmentIdentificationMaterialServiceImpl extends ServiceImpl sortVoByAcceptNo(List materials) { - materials.sort(Comparator.comparing((EntrustmentIdentificationMaterial m) -> { - String[] parts = m.getAcceptNo().split("-"); - return new int[]{Integer.parseInt(parts[0]), Integer.parseInt(parts[1]), Integer.parseInt(parts[2])}; - }, Comparator.comparingInt((int[] arr) -> arr[0]) // 按年份排序 - .thenComparingInt(arr -> arr[1]) // 按流水号排序 - .thenComparingInt(arr -> arr[2]))); // 按子流水号排序 + if (materials == null || materials.isEmpty()) { + return materials; + } + materials.sort(Comparator.comparing((EntrustmentIdentificationMaterial m) -> parseAcceptNoSortKey(m.getAcceptNo()), + Comparator.comparingInt((int[] arr) -> arr[0]) // 按年份排序 + .thenComparingInt(arr -> arr[1]) // 按流水号排序 + .thenComparingInt(arr -> arr[2]))); // 按子流水号排序 return materials; } + /** + * 把受理编号(形如 "2025-3-7")解析成排序键 {年, 流水号, 子流水号}。 + *

+ * 编号为空或格式不符合预期时返回最大值排到最后,避免排序过程中抛 + * NullPointerException / NumberFormatException 导致接口报错。 + * + * @param acceptNo 受理编号 + * @return 排序键 + */ + private static int[] parseAcceptNoSortKey(String acceptNo) { + if (StrUtil.isNotBlank(acceptNo)) { + String[] parts = acceptNo.trim().split("-"); + if (parts.length == 3) { + try { + return new int[]{ + Integer.parseInt(parts[0].trim()), + Integer.parseInt(parts[1].trim()), + Integer.parseInt(parts[2].trim()) + }; + } catch (NumberFormatException ignored) { + // 走下面的兜底分支 + } + } + } + return new int[]{Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE}; + } + @Override /** * 根据嫌疑人信息生成委托检材信息 diff --git a/src/main/java/digital/laboratory/platform/entrustment/service/impl/ExcelOperationServiceImpl.java b/src/main/java/digital/laboratory/platform/entrustment/service/impl/ExcelOperationServiceImpl.java index 4531b5e..4a602b2 100644 --- a/src/main/java/digital/laboratory/platform/entrustment/service/impl/ExcelOperationServiceImpl.java +++ b/src/main/java/digital/laboratory/platform/entrustment/service/impl/ExcelOperationServiceImpl.java @@ -362,48 +362,101 @@ public class ExcelOperationServiceImpl implements ExcelOperationService { */ private List processHairExcelDataToMaterialEntity(List> data, Entrustment entrustment, Map drugLiteMap, int orderNo, CaseEvent cj) { List entrustmentIdentificationMaterialList = new ArrayList<>(); - for (Map datum : data) { - // 构建检材对象 - EntrustmentIdentificationMaterial material = new EntrustmentIdentificationMaterial(); - material.setEntrustmentId(entrustment.getId()); - material.setCaseId(entrustment.getCaseId()); - material.setName(datum.get("姓名") + "毛发"); - material.setRemark(datum.get("身份证号")); + List errorMessages = new ArrayList<>(); + for (int i = 0; i < data.size(); i++) { + Map datum = data.get(i); + int rowNum = i + 2; // Excel行号(数据从第2行开始) + String idNum = null; + String name = null; + try { + name = datum.get("姓名"); + // ---------- 1. 姓名必填校验 ---------- + if (StrUtil.isBlank(name)) { + throw new IllegalArgumentException("姓名不能为空"); + } - // 解析身份证号 - String idNum = datum.get("身份证号"); - if (idNum != null && idNum.length() == 18) { - material.setBiologyGender(this.getSexByIdNum(idNum)); - material.setMaterialAge(Integer.valueOf(this.getAgeByIdNum(idNum))); + // ---------- 2. 身份证号校验 ---------- + idNum = datum.get("身份证号"); + if (StrUtil.isBlank(idNum)) { + throw new IllegalArgumentException("身份证号不能为空"); + } + if (idNum.length() != 18) { + throw new IllegalArgumentException(String.format( + "身份证号[%s]长度不正确(当前%d位,应为18位),请检查身份证号是否完整", + idNum, idNum.length())); + } + // 性别解析 + String gender = this.getSexByIdNum(idNum); + if (StrUtil.isBlank(gender)) { + throw new IllegalArgumentException(String.format( + "身份证号[%s]性别解析失败,请检查第17位是否为数字0~9", idNum)); + } + // 年龄解析 + String ageStr = this.getAgeByIdNum(idNum); + if (StrUtil.isBlank(ageStr)) { + throw new IllegalArgumentException(String.format( + "身份证号[%s]出生日期解析失败,请检查第7~14位是否为合法日期(yyyyMMdd格式),如19900520", + idNum)); + } + + // ---------- 3. 检测结果必填校验 ---------- + String testResult = datum.get("检测结果"); + if (StrUtil.isBlank(testResult)) { + throw new IllegalArgumentException("检测结果不能为空"); + } + + // ---------- 4. 采样单位校验 ---------- + String drawPlace = datum.get("采样单位"); + if (StrUtil.isBlank(drawPlace)) { + throw new IllegalArgumentException("采样单位不能为空"); + } + + // ---------- 构建检材对象 ---------- + EntrustmentIdentificationMaterial material = new EntrustmentIdentificationMaterial(); + material.setEntrustmentId(entrustment.getId()); + material.setCaseId(entrustment.getCaseId()); + material.setName(name + "毛发"); + material.setRemark(idNum); + material.setBiologyGender(gender); + material.setMaterialAge(Integer.valueOf(ageStr)); + material.setDrawPlace(drawPlace); + material.setId(IdWorker.get32UUID().toUpperCase()); + + // 检测结果解析 + String[] drugs = testResult.split("、"); + buildMatchedDrugs(drugLiteMap, material, drugs); + + // 设置检材默认属性 + material.setBiologyType(EntrustBiologyType.HAIR.getDesc()); + material.setType("1"); + material.setTypeName("生物样本"); + material.setQuantity(new BigDecimal(50)); + material.setUnit("mg"); + material.setForm("黑色头发"); + material.setFormName("黑色头发"); + material.setOrderNo(orderNo); + material.setImEntrustNumber(String.valueOf(orderNo)); + orderNo++; + material.setPackComplete(true); + material.setRtSampleQuantity(0); + material.setDrawTime(LocalDateTime.now()); + material.setAnalysisOption(AnalysisOptionEnums.QUALITATIVE.getCode()); + + entrustmentIdentificationMaterialService.setMaterialIdentificationNo(material, cj); + material.setSample1No(sampleService.getNewSampleNo(material.getImNo(), 1)); + + entrustmentIdentificationMaterialList.add(material); + + } catch (Exception e) { + String idInfo = StrUtil.isNotBlank(idNum) ? (" 证件号[" + idNum + "]") : ""; + String nameInfo = StrUtil.isNotBlank(name) ? name : "未知"; + errorMessages.add(String.format("第%d行 姓名[%s]%s: %s", rowNum, nameInfo, idInfo, e.getMessage())); } + } - material.setDrawPlace(datum.get("采样单位")); - material.setId(IdWorker.get32UUID().toUpperCase()); - - // 检测结果解析 - String[] drugs = datum.get("检测结果").split("、"); - buildMatchedDrugs(drugLiteMap, material, drugs); - - // 设置检材默认属性 - material.setBiologyType(EntrustBiologyType.HAIR.getDesc()); - material.setType("1"); - material.setTypeName("生物样本"); - material.setQuantity(new BigDecimal(50)); - material.setUnit("mg"); - material.setForm("黑色头发"); - material.setFormName("黑色头发"); - material.setOrderNo(orderNo); - material.setImEntrustNumber(String.valueOf(orderNo)); - orderNo++; - material.setPackComplete(true); - material.setRtSampleQuantity(0); - material.setDrawTime(LocalDateTime.now()); - material.setAnalysisOption(AnalysisOptionEnums.QUALITATIVE.getCode()); - - entrustmentIdentificationMaterialService.setMaterialIdentificationNo(material, cj); - material.setSample1No(sampleService.getNewSampleNo(material.getImNo(), 1)); - - entrustmentIdentificationMaterialList.add(material); + // 一次性汇总所有错误返回给用户 + if (!errorMessages.isEmpty()) { + throw new CheckedException("Excel导入校验失败,共" + errorMessages.size() + "处错误:\n" + String.join("\n", errorMessages)); } return entrustmentIdentificationMaterialList; } @@ -451,7 +504,7 @@ public class ExcelOperationServiceImpl implements ExcelOperationService { // 3. 身份证号业务校验 // 条件:生物样本(Type=1) 且 属于指定的鉴定结果类别 -// 3. 身份证号业务校验及一致性比对 + // 3. 身份证号业务校验及一致性比对 if (entrustment.getEntrustmentType() == 1 && idCardRequiredResults.contains(entrustment.getOldIdentificationResult())) { String idCard = datum.get("身份证号"); if (StrUtil.isBlank(idCard)) { @@ -708,7 +761,7 @@ public class ExcelOperationServiceImpl implements ExcelOperationService { int genderCode = Integer.parseInt(idNumber.substring(16, 17)); return genderCode % 2 == 0 ? "女" : "男"; } catch (Exception e) { - + log.warn("身份证号[{}] 性别解析失败: {}", idNumber, e.getMessage()); } } return ""; @@ -726,6 +779,8 @@ public class ExcelOperationServiceImpl implements ExcelOperationService { return String.valueOf(age); } catch (Exception e) { + log.warn("身份证号[{}] 年龄解析失败(出生日期[{}-{}]): {}", idNumber, + idNumber.substring(6, 10), idNumber.substring(10, 14), e.getMessage()); } } return "";