diff --git a/dlp-drugtesting-biz/src/main/java/digital/laboratory/platform/inspection/service/impl/EntrustInfoServiceImpl.java b/dlp-drugtesting-biz/src/main/java/digital/laboratory/platform/inspection/service/impl/EntrustInfoServiceImpl.java index 25492b5..577608a 100644 --- a/dlp-drugtesting-biz/src/main/java/digital/laboratory/platform/inspection/service/impl/EntrustInfoServiceImpl.java +++ b/dlp-drugtesting-biz/src/main/java/digital/laboratory/platform/inspection/service/impl/EntrustInfoServiceImpl.java @@ -110,7 +110,7 @@ public class EntrustInfoServiceImpl extends ServiceImpl records = ret.getRecords(); for (EntrustInfo info : records) { List list = assignmentInfoService.list(Wrappers.lambdaQuery().eq(AssignmentInfo::getBusinessId, info.getId())); diff --git a/dlp-drugtesting-biz/src/main/java/digital/laboratory/platform/inspection/service/impl/InspectRecordServiceImpl.java b/dlp-drugtesting-biz/src/main/java/digital/laboratory/platform/inspection/service/impl/InspectRecordServiceImpl.java index 6254cd0..de00c5e 100644 --- a/dlp-drugtesting-biz/src/main/java/digital/laboratory/platform/inspection/service/impl/InspectRecordServiceImpl.java +++ b/dlp-drugtesting-biz/src/main/java/digital/laboratory/platform/inspection/service/impl/InspectRecordServiceImpl.java @@ -26,6 +26,7 @@ import digital.laboratory.platform.inspection.vo.IonPairAndCEVO; import digital.laboratory.platform.inspection.vo.TestRecordReagentVO; import digital.laboratory.platform.inspetion.api.entity.EntrustInfo; import digital.laboratory.platform.inspetion.api.entity.SampleInfo; +import digital.laboratory.platform.inspetion.api.entity.TargetObject; import digital.laboratory.platform.inspetion.api.entity.TestRecord; import digital.laboratory.platform.sys.entity.Drug; import digital.laboratory.platform.sys.enums.entrust.EntrustBiologyType; @@ -127,22 +128,32 @@ public class InspectRecordServiceImpl implements InspectRecordService { System.out.println(String.format("转换为 PDF 结束")); } + /** + * 构建体内检验记录数据 + * + * @param entrustInfo 委托信息对象 + * @param materialType 材料类型(如血液、尿液等) + * @return 包含体内检验记录的Map数据 + * @throws Exception 如果未找到检验记录,则抛出异常 + */ public Map buildInVivoRecordData(EntrustInfo entrustInfo, String materialType) throws Exception { - // 获取检验记录信息 + // 1️⃣ 获取当前委托业务对应的检验记录信息 TestRecord testRecord = testRecordService.getTestRecordByBusinessId(entrustInfo.getId()); + // 2️⃣ 如果找不到对应的检验记录,则抛出异常 if (testRecord == null) { throw new Exception("未找到检验记录信息"); } - // 生成基础数据 + // 3️⃣ 生成基础数据,构建通用的检验记录文档数据 HashMap data = buildCommonInspectRecordDocMap(entrustInfo, testRecord, materialType); - // 获取样品数据 + // 4️⃣ 查询该检验记录下的所有样品数据 List dataList = testRecordSampleDataService.list( Wrappers.lambdaQuery().eq(TestRecordSampleData::getTestId, testRecord.getId())); + // 5️⃣ 如果没有样品数据,则直接返回空数据 if (CollectionUtils.isEmpty(dataList)) { data.put("dataDtos", Collections.emptyList()); data.put("sampleSize", 0); @@ -150,41 +161,48 @@ public class InspectRecordServiceImpl implements InspectRecordService { return data; } - // 批量查询所有样品扩展数据,避免多次查询数据库 + // 6️⃣ 批量查询所有样品的扩展数据,避免多次查询数据库,提高效率 Map> dataExpandMap = testRecordSampledataExpandService.list( Wrappers.lambdaQuery() .in(TestRecordSampleDataExpand::getTestDataId, dataList.stream().map(TestRecordSampleData::getId).collect(Collectors.toList())) ).stream().collect(Collectors.groupingBy(TestRecordSampleDataExpand::getTestDataId)); - // 根据化合物名称分组 + // 7️⃣ 按化合物名称进行分组,确保同一化合物的数据在一起处理 Map> dataMap = dataList.stream() .collect(Collectors.groupingBy(TestRecordSampleData::getCompoundCnName)); + // 8️⃣ 获取标准品列表,并按照其中的化合物顺序进行排序 + String referenceMaterialName = (String) data.get("referenceMaterialName"); + List collect = Arrays.stream(referenceMaterialName.split("\n")) // 按换行拆分 + .map(line -> line.replaceFirst("^\u3000\u3000\u2611 ", "") // 去掉前缀 "  ✔ " + .replaceFirst("^\u2611 ", "")) // 去掉前缀 "✔ " + .collect(Collectors.toList()); // 转换为列表 + + // 9️⃣ 用于存储最终的检验数据 List dataDtos = new ArrayList<>(); - // 遍历化合物组 - for (Map.Entry> entry : dataMap.entrySet()) { - String compoundName = entry.getKey(); - List list = entry.getValue(); + // 🔟 遍历化合物列表,按照标准品顺序构建数据 + for (String compoundName : collect) { + List list = dataMap.get(compoundName); - // 添加空白数据 + // 10.1️⃣ 先添加一个空白样品数据 TestRecordSampleDataDocDTO blankVo = new TestRecordSampleDataDocDTO(); blankVo.setName("空白" + materialType); blankVo.setCompoundName(compoundName); - blankVo.setPTargetRtTime("/"); // 设置空值为"/" - blankVo.setPRtTimeError("/"); // 设置空值为"/" - blankVo.setPRtTimeWithinError("否"); // 默认"否" - blankVo.setPIonAbundanceRatio("/"); // 设置空值为"/" - blankVo.setPIonAbundanceRatioError("/"); // 设置空值为"/" - blankVo.setPIonAbundanceRatioWithinError("否"); // 默认"否" - blankVo.setPIsDetected("否"); // 默认"否" + blankVo.setPTargetRtTime("/"); + blankVo.setPRtTimeError("/"); + blankVo.setPRtTimeWithinError("否"); + blankVo.setPIonAbundanceRatio("/"); + blankVo.setPIonAbundanceRatioError("/"); + blankVo.setPIonAbundanceRatioWithinError("否"); + blankVo.setPIsDetected("否"); dataDtos.add(blankVo); - // 根据样品类型分组 + // 10.2️⃣ 按样品类型(STD:标准样品,Analyte:检材样品)分组 Map> map = list.stream() .collect(Collectors.groupingBy(TestRecordSampleData::getSampleType)); - // 处理标准样品(STD) + // 11️⃣ 处理标准样品(STD) if (map.containsKey("STD")) { TestRecordSampleData std = map.get("STD").get(0); TestRecordSampleDataDocDTO stdVo = new TestRecordSampleDataDocDTO(); @@ -195,22 +213,22 @@ public class InspectRecordServiceImpl implements InspectRecordService { for (TestRecordSampleDataExpand expand : expandList) { if (!expand.getBasePeak()) { stdVo.setPIonAbundanceRatio(expand.getIonAbundanceRatio() != null ? expand.getIonAbundanceRatio().setScale(2, RoundingMode.HALF_UP).toString() : "/"); - stdVo.setPIonAbundanceRatioError("/"); // 设置空值为"/" - stdVo.setPIonAbundanceRatioWithinError("/"); // 设置空值为"/" + stdVo.setPIonAbundanceRatioError("/"); + stdVo.setPIonAbundanceRatioWithinError("/"); break; // 只取第一个符合条件的扩展数据 } } } stdVo.setName("空白" + materialType + "加标"); stdVo.setPTargetRtTime(std.getTargetRtTime() != null ? String.format("%.2f", std.getTargetRtTime()) : "/"); - stdVo.setPRtTimeError("/"); // 设置空值为"/" - stdVo.setPRtTimeWithinError("/"); // 设置空值为"/" - stdVo.setPIsDetected("是"); // 默认"是" + stdVo.setPRtTimeError("/"); + stdVo.setPRtTimeWithinError("/"); + stdVo.setPIsDetected("是"); stdVo.setCompoundName(compoundName); dataDtos.add(stdVo); } - // 处理检材样品(Analyte) + // 12️⃣ 处理检材样品(Analyte) if (map.containsKey("Analyte")) { List analyte = map.get("Analyte"); analyte.sort(testRecordSampleDataService.getSortBySampleNo()); @@ -226,24 +244,7 @@ public class InspectRecordServiceImpl implements InspectRecordService { vo.setPRtTimeWithinError(item.getRtTimeWithinError() != null ? item.getRtTimeWithinError().toString() : "/"); vo.setPIsDetected(item.getIsDetected() != null && item.getIsDetected() == 1 ? "是" : "否"); - List expandList = dataExpandMap.get(item.getId()); - if (expandList != null) { - for (TestRecordSampleDataExpand expand : expandList) { - if (!expand.getBasePeak()) { - vo.setPIonAbundanceRatio(expand.getIonAbundanceRatio() != null ? expand.getIonAbundanceRatio().setScale(2, RoundingMode.HALF_UP).toString() : "/"); - vo.setPIonAbundanceRatioError(expand.getIonAbundanceRatioError() != null ? expand.getIonAbundanceRatioError().setScale(2, RoundingMode.HALF_UP).toString() : "/"); - vo.setPIonAbundanceRatioWithinError(expand.getIonAbundanceRatioWithinError() != null ? expand.getIonAbundanceRatioWithinError() : "/"); - break; // 只取第一个符合条件的扩展数据 - } - } - } - - // 重新命名样品 - vo.setName((dataList - .stream() - .collect(Collectors.groupingBy(TestRecordSampleData::getSampleNo)) - .keySet() - .size() == 1) ? "检材样品" : (i + 1) + "号检材样品"); + vo.setName((dataList.stream().collect(Collectors.groupingBy(TestRecordSampleData::getSampleNo)).keySet().size() == 1) ? "检材样品" : (i + 1) + "号检材样品"); vo.setCompoundName(compoundName); dataVOS.add(vo); } @@ -251,28 +252,25 @@ public class InspectRecordServiceImpl implements InspectRecordService { } } - // 加入图谱序号值 + // 13️⃣ 为所有数据项添加序号 int indexNum = 1; - for (int i = 0; i < dataDtos.size(); i++) { - dataDtos.get(i).setIndexNum(String.valueOf(indexNum++)); + for (TestRecordSampleDataDocDTO dto : dataDtos) { + dto.setIndexNum(String.valueOf(indexNum++)); } - this.buildIonPairAndCE(data, testRecordReagentService - .list(Wrappers.lambdaQuery() - .in(TestRecordReagent::getId, testRecord.getReagentConsumablesList()) - .eq(TestRecordReagent::getCategory, "标准物质")) - .stream() - .map(item -> item.getId()) - .collect(Collectors.toList())); + // 14️⃣ 构建离子对和CE值数据 + this.buildIonPairAndCE(data, testRecordReagentService.list( + Wrappers.lambdaQuery() + .in(TestRecordReagent::getId, testRecord.getReagentConsumablesList()) + .eq(TestRecordReagent::getCategory, "标准物质")) + .stream().map(TestRecordReagent::getId).collect(Collectors.toList())); - // 返回处理后的数据 + // 15️⃣ 返回最终处理的数据 data.put("dataDtos", dataDtos); - data.put("type", "inVivo"); - data.put("inspectOpinion", this.buildInspectOpinion(testRecordSampleDataService - .lambdaQuery() + data.put("type", materialType); + data.put("inspectOpinion", this.buildInspectOpinion(testRecordSampleDataService.lambdaQuery() .eq(TestRecordSampleData::getTestId, testRecord.getId()) - .eq(TestRecordSampleData::getSampleType, "Analyte") - .list())); + .eq(TestRecordSampleData::getSampleType, "Analyte").list())); return data; } @@ -424,116 +422,163 @@ public class InspectRecordServiceImpl implements InspectRecordService { // 9️⃣ 返回处理后的数据 data.put("dataDtos", dataDtos); data.put("sampleSize", dataDtos.size()); - data.put("type", "inVitro"); - - try { - Map> isDetectedMap = testRecordSampleDataService - .lambdaQuery() - .eq(TestRecordSampleData::getTestId, testRecord.getId()) - .eq(TestRecordSampleData::getSampleType, "Analyte") - .list() - .stream() - .filter(Objects::nonNull) // 避免空值 - .collect(Collectors.groupingBy(TestRecordSampleData::getIsDetected)); - - List isDetectedStrs = new ArrayList<>(); - List notDetectedStrs = new ArrayList<>(); - - for (Map.Entry> entry : isDetectedMap.entrySet()) { - if (entry.getKey() != null && entry.getValue() != null && !entry.getValue().isEmpty()) { - List value = entry.getValue(); - Map> map = value.stream() - .filter(Objects::nonNull) - .filter(item -> item.getCompoundName() != null && item.getSampleNo() != null) - .collect(Collectors.groupingBy(TestRecordSampleData::getCompoundCnName)); - - for (Map.Entry> listEntry : map.entrySet()) { - if (listEntry.getKey() != null && listEntry.getValue() != null && !listEntry.getValue().isEmpty()) { - List entryValue = listEntry.getValue(); - entryValue.sort(testRecordSampleDataService.getSortBySampleNo()); - - String collect = entryValue.stream() - .map(item -> { - String[] parts = item.getSampleNo().split("-"); - return (parts.length > 2) ? parts[2] + "号" : item.getSampleNo(); // 预防数组越界 - }) - .collect(Collectors.joining("、")); - - String description; - if (entry.getKey() == 1) { - description = "☑ " + collect + "样品空白对照无干扰,在相同条件下进行测定时,样品溶液中目标物与浓度相近的标准工作溶液中的目标物相比,色谱峰保留时间一致、质谱特征离子一致,各离子丰度比相对偏差符合规定范围,判定样品中检出" + listEntry.getKey() + "。"; - isDetectedStrs.add(description); - } else { - description = "☑ " + collect + "样品空白对照无干扰,样品溶液中未检出符合阳性结果评价要求的色谱峰,标准溶液空白无干扰,判定样品中为未检出" + listEntry.getKey() + "。"; - notDetectedStrs.add(description); - } - } - } - } - } - - if (!isDetectedStrs.isEmpty()) { - data.put("isDetectedStrs", "阳性结果:" + "\n" + isDetectedStrs.stream() - .map(item -> "\u3000\u3000" + item) - .collect(Collectors.joining("\n"))); - } - if (!notDetectedStrs.isEmpty()) { - data.put("notDetectedStrs", "阴性结果:" + "\n" + notDetectedStrs.stream() - .map(item -> "\u3000\u3000" + item) - .collect(Collectors.joining("\n"))); - } - } catch (Exception e) { - } + // 1️⃣0 添加检测结果与分析结果判定 List sampleDataList = testRecordSampleDataService .lambdaQuery() .eq(TestRecordSampleData::getTestId, testRecord.getId()) .eq(TestRecordSampleData::getSampleType, "Analyte") .list(); -// 添加非空校验,防止传入 `null` 进入 `buildInspectOpinion` + data.put("type", "常规毒品"); data.put("inspectOpinion", sampleDataList.size() > 0 ? this.buildInspectOpinion(sampleDataList) : ""); + this.processDetectionResults(data, testRecord); return data; } + /** + * 📌 处理检测结果并格式化 + * + * @param data 存储检测结果的 Map + * @param testRecord 当前检测记录 + */ + public void processDetectionResults(Map data, TestRecord testRecord) { + // 查询所有 "Analyte" 类型的样本数据 + List analyteDataList = testRecordSampleDataService + .lambdaQuery() + .eq(TestRecordSampleData::getTestId, testRecord.getId()) + .eq(TestRecordSampleData::getSampleType, "Analyte") + .list(); + if (analyteDataList.isEmpty()) { + return; // 如果没有数据,直接返回 + } + + // 按 `isDetected` 分组,1 为检出,0 为未检出 + Map> isDetectedMap = analyteDataList.stream() + .filter(Objects::nonNull) + .collect(Collectors.groupingBy(TestRecordSampleData::getIsDetected)); + + List isDetectedStr = new ArrayList<>(); + List notDetectedStr = new ArrayList<>(); + + // 遍历检测结果,生成描述 + for (Map.Entry> entry : isDetectedMap.entrySet()) { + List samples = entry.getValue(); + if (samples == null || samples.isEmpty()) { + continue; // 如果样本数据为空,跳过 + } + + // 按化合物名称分组样本 + Map> compoundSampleMap = samples.stream() + .filter(sample -> sample.getCompoundCnName() != null && sample.getSampleNo() != null) + .collect(Collectors.groupingBy(TestRecordSampleData::getCompoundCnName)); + + for (Map.Entry> compoundEntry : compoundSampleMap.entrySet()) { + String compoundName = compoundEntry.getKey(); + List compoundSamples = compoundEntry.getValue(); + + if (compoundSamples == null || compoundSamples.isEmpty()) { + continue; // 如果化合物名称或样本为空,跳过 + } + + // 按 `sampleNo` 排序样本 + compoundSamples.sort(testRecordSampleDataService.getSortBySampleNo()); + + // 拼接样本编号 + String sampleNos = compoundSamples.stream() + .map(sample -> { + String[] parts = sample.getSampleNo().split("-"); + return (parts.length > 2) ? parts[2] + "号" : sample.getSampleNo(); // 防止数组越界 + }) + .collect(Collectors.joining("、")); + + // 生成描述 + String description; + if (entry.getKey() == 1) { + description = String.format( + "☑ %s样品空白对照无干扰,在相同条件下进行测定时,样品溶液中目标物与浓度相近的标准工作溶液中的目标物相比," + + "色谱峰保留时间一致、质谱特征离子一致,各离子丰度比相对偏差符合规定范围,判定样品中检出%s。", + sampleNos, compoundName); + isDetectedStr.add(description); + } else { + description = String.format( + "☑ %s样品空白对照无干扰,样品溶液中未检出符合阳性结果评价要求的色谱峰,标准溶液空白无干扰,判定样品中未检出%s。", + sampleNos, compoundName); + notDetectedStr.add(description); + } + } + } + + // 将阳性结果和阴性结果放入数据 Map 中 + if (!isDetectedStr.isEmpty()) { + data.put("isDetectedStrs", "阳性结果:" + "\n" + isDetectedStr.stream() + .map(item -> "  " + item) // 使用全角空格缩进 + .collect(Collectors.joining("\n"))); + } + if (!notDetectedStr.isEmpty()) { + data.put("notDetectedStrs", "阴性结果:" + "\n" + notDetectedStr.stream() + .map(item -> "  " + item) // 使用全角空格缩进 + .collect(Collectors.joining("\n"))); + } + } + + + /** + * 📌 构建离子对和碰撞能量信息,并存入数据 Map + * + * @param data 存储结果的 Map + * @param reagentIdList 试剂 ID 列表 + */ public void buildIonPairAndCE(Map data, List reagentIdList) { List ionPairAndCEVOS = new ArrayList<>(); - // 遍历 reagentIdList + // 🔄 遍历 reagentIdList 获取试剂信息 for (String id : reagentIdList) { TestRecordReagentVO vo = testRecordReagentService.getVOById(id); if (vo == null) { - continue; // 如果 vo 为空,跳过当前循环 + continue; // ⏭️ 如果 vo 为空,跳过当前循环 } Drug drug = vo.getDrug(); if (drug == null) { - continue; // 如果 drug 为空,跳过当前循环 + continue; // ⏭️ 如果 drug 为空,跳过当前循环 } - // 添加 ionPairAndCEVO1 和 ionPairAndCEVO2 + // 📌 创建主产物离子信息并添加到列表 ionPairAndCEVOS.add(createIonPairAndCEVO(drug.getName(), drug.getMainProductIon(), drug.getMainDeclusteringPotential(), drug.getMainCollisionEnergy())); + + // 📌 创建次产物离子信息并添加到列表 ionPairAndCEVOS.add(createIonPairAndCEVO(drug.getName(), drug.getMinorProductIon(), drug.getMinorDeclusteringPotential(), drug.getMinorCollisionEnergy())); } - // 将结果放入数据 map 中 + // 📌 将结果放入数据 map 中,供后续使用 data.put("ionPairAndCEVOS", ionPairAndCEVOS); - data.put("ionPairAndCEVOSize", ionPairAndCEVOS.size() / 2); } + // 创建 IonPairAndCEVO 对象的辅助方法 + + /** + * 📌 创建 IonPairAndCEVO 对象并初始化属性 + * + * @param compoundName 化合物名称 + * @param productIon 产物离子 + * @param declusteringPotential 去簇电位(DP) + * @param collisionEnergy 碰撞能量(CE) + * @return 初始化后的 IonPairAndCEVO 对象 + */ private IonPairAndCEVO createIonPairAndCEVO(String compoundName, String productIon, Integer declusteringPotential, Integer collisionEnergy) { IonPairAndCEVO ionPairAndCEVO = new IonPairAndCEVO(); - ionPairAndCEVO.setCompoundName(compoundName); - ionPairAndCEVO.setProductIon(productIon); - ionPairAndCEVO.setDeclusteringPotential(declusteringPotential); - ionPairAndCEVO.setCollisionEnergy(collisionEnergy); - return ionPairAndCEVO; + ionPairAndCEVO.setCompoundName(compoundName); // 设置化合物名称 + ionPairAndCEVO.setProductIon(productIon); // 设置产物离子 + ionPairAndCEVO.setDeclusteringPotential(declusteringPotential); // 设置去簇电位(DP) + ionPairAndCEVO.setCollisionEnergy(collisionEnergy); // 设置碰撞能量(CE) + return ionPairAndCEVO; // 返回创建的对象 } @@ -555,7 +600,6 @@ public class InspectRecordServiceImpl implements InspectRecordService { } else { templatePath = "/template" + "/贵阳生物样本尿液检验记录模板.docx"; } - System.out.println(data); LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy(); Configure config = Configure.builder(). bind("dataDtos", policy) @@ -564,106 +608,134 @@ public class InspectRecordServiceImpl implements InspectRecordService { } /** - * 根据委托信息、模板路径、配置和数据构建文档文件并上传到OSS + * 📄 构建 Word 文档并上传至 OSS * - * @param entrustId 委托id + * @param entrustId 委托 ID * @param templatePath 模板文件路径 - * @param config 配置信息 - * @param data 要填充到模板中的数据 - * @return 上传后的文件路径 + * @param config 配置对象 + * @param data 数据映射 + * @return 上传到 OSS 的文件路径 * @throws Exception 可能抛出的异常 */ private String buildDocFileAndUploadToOss(String entrustId, String templatePath, Configure config, Map data) throws Exception { + // 📌 读取模板文件 ByteArrayOutputStream bos = new ByteArrayOutputStream(); ossFile.fileGet(templatePath, bos); byte[] templateArray = bos.toByteArray(); - ByteArrayInputStream bis = new ByteArrayInputStream(templateArray); bos.close(); - XWPFTemplate template = XWPFTemplate.compile(bis, config).render(data); - NiceXWPFDocument document = template.getXWPFDocument(); - List tables = document.getTables(); - XWPFTable table = tables.get(tables.size() - 1); // 获取最后一个表格 + try (ByteArrayInputStream bis = new ByteArrayInputStream(templateArray); + ByteArrayOutputStream fosWord = new ByteArrayOutputStream()) { + // 📌 渲染模板数据 + XWPFTemplate template = XWPFTemplate.compile(bis, config).render(data); + NiceXWPFDocument document = template.getXWPFDocument(); + List tables = document.getTables(); - String type = (String) data.get("type"); + // 📌 读取数据类型并安全处理 + String type = (String) data.getOrDefault("type", ""); // 避免空指针 + XWPFTable table = tables.get(tables.size() - 1); // 获取最后一个表格 - if (StringUtils.isNotBlank(type) && type.equals("inVitro")) { - int startRow = 1; // 从第二行开始(表头是第一行,索引从0开始) - // **处理合并单元格** - int mergeStep = 4; // 每4行合并一次 - int[] mergeColumns = {0, 1, 2, 3, 4, 9}; // 合并的列:第1、2、3、4、5、10列,索引分别是 0, 1, 2, 3, 4, 9 + if ("常规毒品".equals(type)) { + mergeRegularDrugTable(table); + } else if ("尿液".equals(type) || "毛发".equals(type)) { + mergeNonRegularDrugTable(table); + mergeAdditionalTable(type, tables); + } - // 遍历表格数据行 - for (int rowIndex = startRow; rowIndex < table.getNumberOfRows(); rowIndex += mergeStep) { - int endRow = Math.min(rowIndex + mergeStep - 1, table.getNumberOfRows() - 1); // 计算合并的终止行,防止越界 - for (int col : mergeColumns) { - TableTools.mergeCellsVertically(table, col, rowIndex, endRow); - } + // 📌 写入文档流 + template.write(fosWord); + template.close(); + document.close(); + + // 📌 构建文件存储路径 + ByteArrayInputStream fisWord = new ByteArrayInputStream(fosWord.toByteArray()); + String path = TestRecordFileUrl.TEST_RECORD_CATALOGUE.getFileUrl() + "/" + entrustId + "/" + "贵阳生物样本检验记录.docx"; + ossFile.fileSave(path, fisWord); + + return path; // 📌 返回 OSS 文件路径 + } + } + + /** + * 📌 合并常规毒品检验记录检测数据部分表格的单元格 + */ + private void mergeRegularDrugTable(XWPFTable table) { + int startRow = 1; // 🔹 从第二行开始(索引从0开始) + int mergeStep = 4; // 🔹 每 4 行合并一次 + int[] mergeColumns = {0, 1, 2, 3, 4, 9}; // 🔹 需要合并的列 + + for (int rowIndex = startRow; rowIndex < table.getNumberOfRows(); rowIndex += mergeStep) { + int endRow = Math.min(rowIndex + mergeStep - 1, table.getNumberOfRows() - 1); // 防止越界 + for (int col : mergeColumns) { + TableTools.mergeCellsVertically(table, col, rowIndex, endRow); } - } else { - // **处理合并单元格** - int startRow = 1; // 从第二行开始(表头是第一行,索引从0开始) - String currentCompoundName = null; - int mergeStartRow = -1; - - for (int rowIndex = startRow; rowIndex < table.getNumberOfRows(); rowIndex++) { - XWPFTableRow row = table.getRow(rowIndex); - XWPFTableCell cell = row.getCell(1); // 获取第二列的单元格(索引1) - String compoundName = cell.getText().trim(); - - if (currentCompoundName == null || !currentCompoundName.equals(compoundName)) { - // 如果当前化合物名称与上一行不同,则结束之前的合并 - if (mergeStartRow >= 0 && rowIndex > mergeStartRow + 1) { - TableTools.mergeCellsVertically(table, 1, mergeStartRow, rowIndex - 1); // 合并第二列(列索引1) - } - currentCompoundName = compoundName; - mergeStartRow = rowIndex; + } + } + + /** + * 📌 合并生物样本检验记录检测数据部分表格的单元格 + */ + private void mergeNonRegularDrugTable(XWPFTable table) { + int startRow = 1; + String currentCompoundName = null; + int mergeStartRow = -1; + + for (int rowIndex = startRow; rowIndex < table.getNumberOfRows(); rowIndex++) { + XWPFTableRow row = table.getRow(rowIndex); + XWPFTableCell cell = row.getCell(1); // 🔹 第二列(索引1) + String compoundName = cell.getText().trim(); + + if (!compoundName.equals(currentCompoundName)) { + if (mergeStartRow >= 0 && rowIndex > mergeStartRow + 1) { + TableTools.mergeCellsVertically(table, 1, mergeStartRow, rowIndex - 1); } + currentCompoundName = compoundName; + mergeStartRow = rowIndex; } + } - // 处理最后一组合并(循环结束后可能残留未合并的区域) - if (mergeStartRow >= 0 && mergeStartRow < table.getNumberOfRows() - 1) { - TableTools.mergeCellsVertically(table, 1, mergeStartRow, table.getNumberOfRows() - 1); - } + // 📌 处理最后一组合并 + if (mergeStartRow >= 0 && mergeStartRow < table.getNumberOfRows() - 1) { + TableTools.mergeCellsVertically(table, 1, mergeStartRow, table.getNumberOfRows() - 1); + } + } + + /** + * 📌 处理毛发或尿液表格目标物定性离子对、碰撞能量 (CE)等参数表格的合并逻辑 + * 📌 因为毛发和尿液的配置表格数量不同,因此下标不同 + */ + private void mergeAdditionalTable(String type, List tables) { + if (!"毛发".equals(type) && !"尿液".equals(type)) { + return; // 如果不是毛发或尿液类型,则不处理 } - System.out.println("总共的表格数量:" + tables.size()); - XWPFTable xwpfTable = tables.get(2); + int tableIndex = "毛发".equals(type) ? 2 : 1; + if (tables.size() <= tableIndex) { + return; // 避免索引越界 + } + XWPFTable xwpfTable = tables.get(tableIndex); List rows = xwpfTable.getRows(); - // 跳过第一行(表头),从第二行开始处理 - for (int i = 1; i < rows.size() - 1; i++) { // 注意:i 从 1 开始 + for (int i = 1; i < rows.size() - 1; i++) { // 🔹 从第二行开始 XWPFTableRow currentRow = rows.get(i); XWPFTableRow nextRow = rows.get(i + 1); - // 获取当前行和下一行的化合物名称 String currentCompoundName = currentRow.getCell(0).getText().trim(); String nextCompoundName = nextRow.getCell(0).getText().trim(); - // 如果当前行和下一行的化合物名称相同,则合并它们的第一列 if (currentCompoundName.equals(nextCompoundName)) { - // 合并单元格 - currentRow.getCell(0).getCTTc().addNewTcPr().addNewVMerge().setVal(org.openxmlformats.schemas.wordprocessingml.x2006.main.STMerge.RESTART); - nextRow.getCell(0).getCTTc().addNewTcPr().addNewVMerge().setVal(org.openxmlformats.schemas.wordprocessingml.x2006.main.STMerge.CONTINUE); + // 📌 合并单元格 + currentRow.getCell(0).getCTTc().addNewTcPr().addNewVMerge() + .setVal(org.openxmlformats.schemas.wordprocessingml.x2006.main.STMerge.RESTART); + nextRow.getCell(0).getCTTc().addNewTcPr().addNewVMerge() + .setVal(org.openxmlformats.schemas.wordprocessingml.x2006.main.STMerge.CONTINUE); - // 删除下一行的第一个单元格的内容(可选) + // 📌 清空被合并行的内容 nextRow.getCell(0).setText(""); } } - - bis.close(); - ByteArrayOutputStream fosWord = new ByteArrayOutputStream(); - template.write(fosWord); - template.close(); - ByteArrayInputStream fisWord = new ByteArrayInputStream(fosWord.toByteArray()); - fosWord.close(); - document.close(); - - String path = TestRecordFileUrl.TEST_RECORD_CATALOGUE.getFileUrl() + "/" + entrustId + "/" + "贵阳生物样本检验记录.docx"; - ossFile.fileSave(path, fisWord); - return path; } @@ -768,22 +840,23 @@ public class InspectRecordServiceImpl implements InspectRecordService { .in(TestRecordReagent::getId, testRecord.getReagentConsumablesList()) .eq(TestRecordReagent::getCategory, "标准物质")); + if (references == null || references.isEmpty()) { data.put("referenceMaterialName", "未找到标准物质数据!"); } else { - String referenceMaterialName = ""; if (references.size() == 1) { referenceMaterialName = "\u2611" + " " + references.get(0).getReagentConsumableName(); } else { + TestRecordReagent firstReagent = references.get(0); String firstStr = "\u2611" + " " + references.get(0).getReagentConsumableName() + "\n"; references.remove(references.get(0)); referenceMaterialName = firstStr + references.stream() .map(reagent -> "\u3000\u3000\u2611" + " " + reagent.getReagentConsumableName()) .collect(Collectors.joining("\n")); + references.add(0, firstReagent); } data.put("referenceMaterialName", referenceMaterialName); - data.put("materialIngredient", references.stream() .map(reagent -> reagent.getReagentConsumableName()) .collect(Collectors.joining("、"))); @@ -845,8 +918,7 @@ public class InspectRecordServiceImpl implements InspectRecordService { * @param sampleInfoList 样本信息列表 * @return 物料特性描述(如果列表为空,则返回空字符串) */ - @Override - public String buildMaterialCharacterDesc(List sampleInfoList) { + private String buildMaterialCharacterDesc(List sampleInfoList) { if (CollUtil.isEmpty(sampleInfoList)) { return StrUtil.EMPTY; } @@ -896,66 +968,104 @@ public class InspectRecordServiceImpl implements InspectRecordService { */ private int extractTailNumber(String orderNo) { String[] parts = orderNo.split("-"); - if (parts.length > 0) { - return Integer.parseInt(parts[parts.length - 1]); // 取最后一段 - } - return 0; + return (parts.length > 0) ? Integer.parseInt(parts[parts.length - 1]) : 0; } /** - * 格式化编号范围: - * - 单个编号:返回 "1号" - * - 连续编号:返回 "1号至3号" + * 对样品编号进行排序 * - * @param start 起始编号 - * @param end 结束编号 - * @return 格式化的编号字符串(如果 start == end,返回单号) + * @param sampleNumbers 样品编号列表,例如 ["3号", "1号", "2号"] + * @return 排序后的样品编号列表,例如 ["1号", "2号", "3号"] */ - private String formatRange(int start, int end) { - return (start == end) ? start + "号" : start + "号至" + end + "号"; + private List sortSampleNumbers(List sampleNumbers) { + return sampleNumbers.stream() + .sorted(Comparator.comparingInt(this::extractNumber)) + .collect(Collectors.toList()); } + /** + * 从样品编号中提取数值部分 + * + * @param sampleNumber 样品编号,例如 "12号" + * @return 提取出的数字,例如 12 + */ + private int extractNumber(String sampleNumber) { + return sampleNumber.matches("\\d+号") ? Integer.parseInt(sampleNumber.replace("号", "")) : Integer.MAX_VALUE; + } - private String formatSampleNumbers(List sampleNumbers) { - List numbers = sampleNumbers.stream() + /** + * 格式化编号范围: + * - 单个编号:返回 "1号" + * - 连续编号:返回 "1号至3号" + * + * @param sortedSampleNumbers 已排序的样品编号列表 + * @return 格式化的编号字符串 + */ + private String formatSampleNumbers(List sortedSampleNumbers) { + List numbers = sortedSampleNumbers.stream() .map(s -> s.replace("号", "")) // 移除"号"字 .map(Integer::parseInt) // 转换为整数 .sorted() // 排序 .collect(Collectors.toList()); - if (numbers.size() > 1 && numbers.get(numbers.size() - 1) - numbers.get(0) == numbers.size() - 1) { - return numbers.get(0) + "至" + numbers.get(numbers.size() - 1) + "号"; + List formattedList = new ArrayList<>(); + int start = numbers.get(0), prev = start; + + for (int i = 1; i < numbers.size(); i++) { + int current = numbers.get(i); + if (current != prev + 1) { + formattedList.add(formatRange(start, prev)); + start = current; + } + prev = current; } + formattedList.add(formatRange(start, prev)); - return String.join("、", sampleNumbers); + return String.join("、", formattedList); } /** - * 根据传入的TestRecordSampleData列表生成鉴定意见 + * 生成编号范围的字符串 * - * @param dataList 包含检验数据的列表 - * @return 检测意见字符串,以“;”分隔 + * @param start 起始编号 + * @param end 结束编号 + * @return 生成的范围字符串,例如 "1号至3号" 或 "5号" + */ + private String formatRange(int start, int end) { + return (start == end) ? start + "号" : start + "号至" + end + "号"; + } + + /** + * 构建检测意见,根据样品的检出与未检出情况,生成描述性文本。 + * + * @param dataList 检测记录数据列表,每个对象包含样品编号、化合物名称及是否检出标识 + * @return 格式化的检测意见字符串 */ - @Override public String buildInspectOpinion(List dataList) { + // 1. **处理空数据情况** if (dataList == null || dataList.isEmpty()) { return ""; } - Map> detectedMap = new LinkedHashMap<>(); - Map> notDetectedMap = new LinkedHashMap<>(); - Set allCompoundNames = new HashSet<>(); + // 2. **定义存储结构** + Map> detectedMap = new LinkedHashMap<>(); // 记录检出化合物的样品编号映射(样品编号 -> 该样品检出的化合物列表) + Map> notDetectedMap = new LinkedHashMap<>(); // 记录未检出化合物的样品编号映射(样品编号 -> 该样品未检出的化合物列表) + Set allCompoundNames = new HashSet<>(); // 存储所有涉及的化合物名称(用于后续分析) + // 3. **遍历数据列表,整理样品的检出与未检出情况** for (TestRecordSampleData record : dataList) { - String sampleNo = record.getSampleNo(); - String compoundName = record.getCompoundCnName(); + String sampleNo = record.getSampleNo(); // 样品编号 + String compoundName = record.getCompoundCnName(); // 化合物名称 + // **跳过无效数据** if (sampleNo == null || compoundName == null || sampleNo.trim().isEmpty()) { continue; } + // **记录化合物名称** allCompoundNames.add(compoundName); + // **分类存储检出与未检出数据** if (record.getIsDetected() != null && record.getIsDetected() == 1) { detectedMap.computeIfAbsent(sampleNo, k -> new ArrayList<>()).add(compoundName); } else { @@ -963,56 +1073,70 @@ public class InspectRecordServiceImpl implements InspectRecordService { } } + // 4. **定义样品编号格式化方法** + // 目标是将形如 "A12"、"B34" 这样的样品编号转换成 "12号"、"34号" Function extractSampleNumber = sampleNo -> sampleNo.matches(".*\\d+") - ? sampleNo.replaceAll(".*?(\\d+)$", "$1号") + ? sampleNo.replaceAll(".*?(\\d+)$", "$1号") // 只提取最后的数字并添加 "号" : sampleNo; + // 5. **构建检出化合物的描述** List detectedSentences = new ArrayList<>(); - Map, List> groupedDetectedSamples = new LinkedHashMap<>(); + Map, List> groupedDetectedSamples = new LinkedHashMap<>(); // 用于按相同化合物组合分组样品 + // **分组整理检出样品** for (Map.Entry> entry : detectedMap.entrySet()) { - Set compoundSet = new HashSet<>(entry.getValue()); + Set compoundSet = new HashSet<>(entry.getValue()); // 将检出的化合物集合化(去重) groupedDetectedSamples.computeIfAbsent(compoundSet, k -> new ArrayList<>()).add(extractSampleNumber.apply(entry.getKey())); } + // **遍历分组后的数据,生成描述文本** for (Map.Entry, List> entry : groupedDetectedSamples.entrySet()) { - String sampleNumbers = formatSampleNumbers(entry.getValue()); // 这里也应用连续编号格式化 - String compounds = String.join("、", entry.getKey()); + List sortedSampleNumbers = sortSampleNumbers(entry.getValue()); // 对样品编号排序 + String sampleNumbers = formatSampleNumbers(sortedSampleNumbers); // 处理编号连续情况 + String compounds = String.join("、", entry.getKey()); // 多个化合物用 "、" 连接 - if (entry.getValue().size() > 1) { + if (sortedSampleNumbers.size() > 1) { detectedSentences.add(String.format("从%s检材样品中均检出%s成分", sampleNumbers, compounds)); } else { detectedSentences.add(String.format("从%s检材样品中检出%s成分", sampleNumbers, compounds)); } } + // 6. **构建未检出化合物的描述** List notDetectedSentences = new ArrayList<>(); - Map, List> groupedNotDetectedSamples = new LinkedHashMap<>(); + Map, List> groupedNotDetectedSamples = new LinkedHashMap<>(); // 用于按相同化合物组合分组未检出样品 + // **分组整理未检出样品** for (Map.Entry> entry : notDetectedMap.entrySet()) { String sampleNo = entry.getKey(); Set compoundSet = new HashSet<>(entry.getValue()); + // 只有当该样品编号未出现在 detectedMap(即没有检出任何化合物)时,才记录未检出的情况 if (!detectedMap.containsKey(sampleNo)) { groupedNotDetectedSamples.computeIfAbsent(compoundSet, k -> new ArrayList<>()).add(extractSampleNumber.apply(sampleNo)); } } + // **遍历分组后的数据,生成描述文本** for (Map.Entry, List> entry : groupedNotDetectedSamples.entrySet()) { - String sampleNumbers = formatSampleNumbers(entry.getValue()); // 这里也应用连续编号格式化 - String compounds = String.join("、", entry.getKey()); + List sortedSampleNumbers = sortSampleNumbers(entry.getValue()); // 对样品编号排序 + String sampleNumbers = formatSampleNumbers(sortedSampleNumbers); // 处理编号连续情况 + String compounds = String.join("、", entry.getKey()); // 多个化合物用 "、" 连接 - if (entry.getValue().size() > 1) { + if (sortedSampleNumbers.size() > 1) { notDetectedSentences.add(String.format("从%s检材样品中均未检出%s成分", sampleNumbers, compounds)); } else { notDetectedSentences.add(String.format("从%s检材样品中未检出%s成分", sampleNumbers, compounds)); } } - List finalSentences = new ArrayList<>(detectedSentences); - finalSentences.addAll(notDetectedSentences); + // 7. **最终结果合并** + List finalSentences = new ArrayList<>(detectedSentences); // 先添加检出的描述 + finalSentences.addAll(notDetectedSentences); // 再添加未检出的描述 + // 使用 ";" 连接所有描述,并返回最终的检测意见字符串 return String.join(";", finalSentences); } + }