From f259dbe1d49c1829177369bde2f1c7519d6a8c9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=A8=E6=B5=B7=E8=88=AA?= <11918452+yang-haihang@user.noreply.gitee.com> Date: Fri, 28 Mar 2025 16:21:21 +0800 Subject: [PATCH] update --- .../impl/InspectRecordServiceImpl.java | 435 ++++++++++++------ 1 file changed, 292 insertions(+), 143 deletions(-) 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 22c6eba..506cf17 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 @@ -3,12 +3,13 @@ package digital.laboratory.platform.inspection.service.impl; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.io.IoUtil; import cn.hutool.core.util.StrUtil; -import com.alibaba.fastjson.JSONArray; import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.deepoove.poi.XWPFTemplate; import com.deepoove.poi.config.Configure; import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy; +import com.deepoove.poi.util.TableTools; import com.deepoove.poi.xwpf.NiceXWPFDocument; import digital.laboratory.platform.common.core.util.R; import digital.laboratory.platform.common.feign.RemoteWord2PDFService; @@ -31,6 +32,7 @@ import digital.laboratory.platform.sys.entity.Drug; import digital.laboratory.platform.sys.enums.entrust.EntrustBiologyType; import feign.Response; import org.apache.commons.io.output.ByteArrayOutputStream; +import org.apache.poi.xwpf.usermodel.XWPFTable; import org.springframework.beans.BeanUtils; import org.springframework.mock.web.MockMultipartFile; import org.springframework.stereotype.Service; @@ -39,6 +41,7 @@ import javax.annotation.Resource; import javax.servlet.http.HttpServletResponse; import java.io.ByteArrayInputStream; import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; @@ -163,7 +166,7 @@ public class InspectRecordServiceImpl implements InspectRecordService { // 根据化合物名称分组 Map> dataMap = dataList.stream() - .collect(Collectors.groupingBy(TestRecordSampleData::getCompoundName)); + .collect(Collectors.groupingBy(TestRecordSampleData::getCompoundCnName)); List dataDtos = new ArrayList<>(); @@ -265,20 +268,28 @@ public class InspectRecordServiceImpl implements InspectRecordService { // 返回处理后的数据 data.put("dataDtos", dataDtos); data.put("sampleSize", dataDtos.size() / 2); - data.put("inspectOpinion", this.buildInspectOpinion(dataList)); + data.put("inspectOpinion", this.buildInspectOpinion(testRecordSampleDataService + .lambdaQuery() + .eq(TestRecordSampleData::getTestId, testRecord.getId()) + .eq(TestRecordSampleData::getSampleType, "Analyte") + .list())); return data; } - public Map buildInVitroRecordData(EntrustInfo entrustInfo, TestRecord testRecord) { - Map data = new HashMap<>(); + public Map buildInVitroRecordData(EntrustInfo entrustInfo, TestRecord testRecord, Map data) { + // 1️⃣ 按化合物名称分组 TestRecordSampleData - Map> compoundSampleDataMap = testRecordSampleDataService + + List dataList = testRecordSampleDataService .lambdaQuery() .eq(TestRecordSampleData::getTestId, testRecord.getId()) - .list() + .list(); + + Map> compoundSampleDataMap = dataList .stream() - .collect(Collectors.groupingBy(TestRecordSampleData::getCompoundName)); + .collect(Collectors.groupingBy(TestRecordSampleData::getCompoundCnName)); + // 2️⃣ 过滤出 "标准物质" 类别的试剂,并转换为 Map (key=药品名称, value=试剂VO) List reagentConsumablesList = testRecord.getReagentConsumablesList(); @@ -306,35 +317,44 @@ public class InspectRecordServiceImpl implements InspectRecordService { TestRecordSampleData stdSample = stdList.get(0); // 4️⃣ 获取 STD 样本的扩展数据 (以 MassToChargeRatio 为 key) - Map expandMap = testRecordSampledataExpandService.lambdaQuery() + Map expandMap = testRecordSampledataExpandService.lambdaQuery() .eq(TestRecordSampleDataExpand::getTestDataId, stdSample.getId()) .list() .stream() - .collect(Collectors.toMap(TestRecordSampleDataExpand::getMassToChargeRatio, Function.identity())); + .collect(Collectors.toMap( + e -> e.getMassToChargeRatio().setScale(0, RoundingMode.FLOOR).toString(), // 保留整数部分 + Function.identity() + )); // 5️⃣ 获取当前化合物对应的试剂,提取特征离子 TestRecordReagentVO reagentVO = reagentMap.get(compoundName); - if (reagentVO == null || reagentVO.getDrug().getCharacteristicIons() == null) { + if (reagentVO == null || StringUtils.isBlank(reagentVO.getDrug().getCharacteristicIons())) { continue; // 没有对应试剂或特征离子数据,跳过 } String[] peaks = reagentVO.getDrug().getCharacteristicIons().split("、"); + // 6️⃣ 处理 "标样" 数据 (STD) for (int i = 0; i < peaks.length; i++) { String peak = (i == 0) ? peaks[i].substring(0, peaks[i].length() - 1) : peaks[i]; - TestRecordSampleDataExpand expand = expandMap.get(new BigDecimal(peak)); - + TestRecordSampleDataExpand expand = expandMap.get(peak); if (expand != null) { TestRecordSampleDataDocDTO dto = new TestRecordSampleDataDocDTO(); BeanUtils.copyProperties(stdSample, dto); dto.setName("标样"); - dto.setPTargetRtTime(dto.getTargetRtTime().toString()); + dto.setCompoundName(compoundName); + dto.setPTargetRtTime(Optional.ofNullable(dto.getTargetRtTime()).map(Object::toString).orElse("/")); dto.setPRtTimeError("±1"); dto.setPRtTimeWithinError("/"); dto.setPMassToChargeRatio(i == 0 ? peak + "(基峰)" : peak); - dto.setPIonAbundanceRatio(expand.getIonAbundanceRatio().toString()); - dto.setPIonAbundanceRatioError(expand.getIonAbundanceRatioError().toString()); - dto.setPIonAbundanceRatioWithinError(expand.getIonAbundanceRatioWithinError()); + dto.setPIonAbundanceRatio(Optional.ofNullable(expand.getIonAbundanceRatio()) + .map(value -> value.setScale(2, RoundingMode.HALF_UP).toString()) + .orElse("/")); + + dto.setPIonAbundanceRatioError(Optional.ofNullable(expand.getIonAbundanceRatioError()) + .map(value -> value.setScale(2, RoundingMode.HALF_UP).toString()) + .orElse("/")); + dto.setPIonAbundanceRatioWithinError(Optional.ofNullable(expand.getIonAbundanceRatioWithinError()).filter(s -> !s.isEmpty()).orElse("/")); dto.setPIsDetected("/"); dataDtos.add(dto); } @@ -348,7 +368,7 @@ public class InspectRecordServiceImpl implements InspectRecordService { dto.setPTargetRtTime("/"); dto.setPRtTimeError("/"); dto.setPRtTimeWithinError("/"); - dto.setPMassToChargeRatio(peak.equals(peaks[0]) ? peak + "(基峰)" : peak); + dto.setPMassToChargeRatio(peak.equals(peaks[0]) ? peaks[0].substring(0, peaks[0].length() - 1) + "(基峰)" : peak); dto.setPIonAbundanceRatio("/"); dto.setPIonAbundanceRatioError("/"); dto.setPIonAbundanceRatioWithinError(peak.equals(peaks[0]) ? "/" : "否"); @@ -362,27 +382,37 @@ public class InspectRecordServiceImpl implements InspectRecordService { analyteList.sort(testRecordSampleDataService.getSortBySampleNo()); // 按样本编号排序 for (TestRecordSampleData analyte : analyteList) { - Map analyteExpandMap = testRecordSampledataExpandService.lambdaQuery() + Map analyteExpandMap = testRecordSampledataExpandService.lambdaQuery() .eq(TestRecordSampleDataExpand::getTestDataId, analyte.getId()) .list() .stream() - .collect(Collectors.toMap(TestRecordSampleDataExpand::getMassToChargeRatio, Function.identity())); + .collect(Collectors.toMap( + e -> e.getMassToChargeRatio().setScale(0, RoundingMode.FLOOR).toString(), // 保留整数部分 + Function.identity() + )); for (int i = 0; i < peaks.length; i++) { String peak = (i == 0) ? peaks[i].substring(0, peaks[i].length() - 1) : peaks[i]; - TestRecordSampleDataExpand expand = analyteExpandMap.get(new BigDecimal(peak)); - + TestRecordSampleDataExpand expand = analyteExpandMap.get(peak); if (expand != null) { TestRecordSampleDataDocDTO dto = new TestRecordSampleDataDocDTO(); BeanUtils.copyProperties(analyte, dto); - dto.setPTargetRtTime(dto.getTargetRtTime().toString()); - dto.setPRtTimeError(dto.getRtTimeWithinError()); - dto.setPRtTimeWithinError(dto.getRtTimeWithinError()); + dto.setPTargetRtTime(Optional.ofNullable(dto.getTargetRtTime()).map(Object::toString).orElse("/")); + dto.setPRtTimeError(Optional.ofNullable(dto.getRtTimeError()) + .map(value -> new BigDecimal(value.toString()).setScale(2, RoundingMode.HALF_UP).toString()) + .orElse("/")); + dto.setPRtTimeWithinError(Optional.ofNullable(dto.getRtTimeWithinError()).orElse("/")); dto.setPMassToChargeRatio(i == 0 ? peak + "(基峰)" : peak); - dto.setPIonAbundanceRatio(expand.getIonAbundanceRatio().toString()); - dto.setPIonAbundanceRatioError(expand.getIonAbundanceRatioError().toString()); - dto.setPIonAbundanceRatioWithinError(expand.getIonAbundanceRatioWithinError()); - dto.setPIsDetected(dto.getIsDetected() == 1 ? "是" : "否"); + dto.setPIonAbundanceRatio(Optional.ofNullable(expand.getIonAbundanceRatio()) + .map(value -> value.setScale(2, RoundingMode.HALF_UP).toString()) + .orElse("/")); + + dto.setPIonAbundanceRatioError(Optional.ofNullable(expand.getIonAbundanceRatioError()) + .map(value -> value.setScale(2, RoundingMode.HALF_UP).toString()) + .orElse("/")); + dto.setPIonAbundanceRatioWithinError(Optional.ofNullable(expand.getIonAbundanceRatioWithinError()).orElse("/")); + dto.setPIsDetected(dto.getIsDetected() != null && dto.getIsDetected() == 1 ? "是" : "否"); + dto.setCompoundName(compoundName); dataDtos.add(dto); } } @@ -392,6 +422,76 @@ public class InspectRecordServiceImpl implements InspectRecordService { // 9️⃣ 返回处理后的数据 data.put("dataDtos", dataDtos); + data.put("sampleSize", dataDtos.size()); + + 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("\u2611"))); + } + if (!notDetectedStrs.isEmpty()) { + data.put("notDetectedStrs", "阴性结果:" + "\n" + notDetectedStrs.stream() + .map(item -> "\u3000\u3000" + item) + .collect(Collectors.joining("\u2611"))); + } + } catch (Exception e) { + } + + List sampleDataList = testRecordSampleDataService + .lambdaQuery() + .eq(TestRecordSampleData::getTestId, testRecord.getId()) + .eq(TestRecordSampleData::getSampleType, "Analyte") + .list(); + +// 添加非空校验,防止传入 `null` 进入 `buildInspectOpinion` + data.put("inspectOpinion", sampleDataList.size() > 0 ? this.buildInspectOpinion(sampleDataList) : ""); + return data; } @@ -470,8 +570,7 @@ public class InspectRecordServiceImpl implements InspectRecordService { * @return 上传后的文件路径 * @throws Exception 可能抛出的异常 */ - private String buildDocFileAndUploadToOss(String entrustId, String templatePath, Configure - config, Map data) 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(); @@ -481,11 +580,26 @@ public class InspectRecordServiceImpl implements InspectRecordService { XWPFTemplate template = XWPFTemplate.compile(bis, config).render(data); NiceXWPFDocument document = template.getXWPFDocument(); -// XWPFTable table = document.getTables().get(3); -// int sampleSize = (int) data.get("sampleSize"); -// //合并单元格 -// -// TableTools.mergeCellsVertically(table, 1, 1, sampleSize); + List tables = document.getTables(); + XWPFTable table = tables.get(tables.size() - 1); // 获取最后一个表格 + + int sampleSize = (int) data.get("sampleSize"); + + // **处理合并单元格** + int startRow = 1; // 从第二行开始(索引从0开始,所以第二行是索引1) + int mergeStep = 4; // 每4行合并一次 + + // 指定需要合并的列(注意索引从0开始) + int[] mergeColumns = {0, 1, 2, 3, 4, 9}; // 1、2、3、4、5、10列的索引分别是 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); + } + } bis.close(); ByteArrayOutputStream fosWord = new ByteArrayOutputStream(); @@ -494,12 +608,14 @@ public class InspectRecordServiceImpl implements InspectRecordService { 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; } + /** * 生成常规毒品的检验记录(贵阳禁毒) */ @@ -509,8 +625,7 @@ public class InspectRecordServiceImpl implements InspectRecordService { TestRecord testRecord = testRecordService.lambdaQuery() .eq(TestRecord::getBusinessId, businessId) .one(); - Map data = this.buildInVitroRecordData(entrustInfo, testRecord); - docMap.putAll(data); + Map data = this.buildInVitroRecordData(entrustInfo, testRecord, docMap); String templatePath = "/template" + "/贵阳禁毒常规毒品检验记录模板.docx"; LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy(); Configure config = Configure.builder(). @@ -528,7 +643,9 @@ public class InspectRecordServiceImpl implements InspectRecordService { EntrustInfo entrustInfo = entrustInfoService.getById(businessId); // 这里因为检验记录实体并未关联业务id, 只能先查询实验数据,在通过实验数据获取到检验记录信息 - TestRecord testRecord = testRecordService.getTestRecordByBusinessId(entrustInfo.getId()); + TestRecord testRecord = testRecordService.lambdaQuery() + .eq(TestRecord::getBusinessId, businessId) + .one(); // 封装数据到map中 Map docMap = buildCommonInspectRecordDocMap(entrustInfo, testRecord, ""); @@ -561,28 +678,17 @@ public class InspectRecordServiceImpl implements InspectRecordService { data.put("inspectYear", testRecord.getTestStartDate().getYear()); data.put("inspectMonth", testRecord.getTestStartDate().getMonthValue()); data.put("inspectDay", testRecord.getTestStartDate().getDayOfMonth()); + String[] split = entrustInfo.getAcceptNo().split("-"); + if (split.length == 2) { + data.put("acceptNo", "[" + split[0] + "]" + split[1] + "号"); + } - data.put("acceptNo", entrustInfo.getAcceptNo()); - - // 检材性状描述和检验要求成分 - List sampleInfoList = getSampleInfosByMaterialType(entrustInfo, materialType); - String materialCharacterDesc = buildMaterialCharacterDesc(sampleInfoList); - String targetObjectStr = sampleInfoList - .stream() - .flatMap( - sampleInfo -> { - String toJSONString = JSONArray.toJSONString(sampleInfo.getTargetObject()); - List drugLiteList = JSONArray.parseArray(toJSONString, TargetObject.class); - return drugLiteList.stream(); - }) - .collect(Collectors.toList()) // 得到转换好的筛查物对象 - .stream() - .map(TargetObject::getName) - .collect(Collectors.toSet()) // 区筛查物对象名称并去重 - .stream() - .collect(Collectors.joining("、")); // 拼接筛查物名称 + // 检材性状描述 + String materialCharacterDesc = buildMaterialCharacterDesc(sampleInfoService + .lambdaQuery() + .in(SampleInfo::getId, testRecord.getSampleTestList()) + .list()); data.put("materialCharacterDesc", materialCharacterDesc); - data.put("materialIngredient", targetObjectStr); // 获取仪器设备数据(批量查询,减少数据库压力) List deviceIdList = testRecord.getDeviceIdList(); @@ -591,11 +697,19 @@ public class InspectRecordServiceImpl implements InspectRecordService { : testRecordInstrumentService.list(Wrappers.lambdaQuery() .in(TestRecordInstrument::getId, deviceIdList)); - data.put("instrumentName", CollectionUtils.isEmpty(instruments) - ? "未找到仪器设备数据!" - : instruments.stream() - .map(i -> "\u3000\u3000" + i.getInstrumentName()) // 使用 Unicode 全角空格 - .collect(Collectors.joining("\n"))); + if (materialType.isEmpty()) { + data.put("instrumentName", CollectionUtils.isEmpty(instruments) + ? "未找到仪器设备数据!" + : instruments.stream() + .map(i -> "\u3000\u3000\u2611" + i.getInstrumentName()) // 使用 Unicode 全角空格 + .collect(Collectors.joining("\n"))); + } else { + data.put("instrumentName", CollectionUtils.isEmpty(instruments) + ? "未找到仪器设备数据!" + : instruments.stream() + .map(i -> "\u3000\u3000" + i.getInstrumentName()) // 使用 Unicode 全角空格 + .collect(Collectors.joining("\n"))); + } // 设置使用的标准物质和试剂 @@ -606,14 +720,18 @@ public class InspectRecordServiceImpl implements InspectRecordService { if (references == null || references.isEmpty()) { data.put("referenceMaterialName", "未找到试剂耗材数据!"); } else { - + String firstStr = "\u2611" + " " + references.get(0).getReagentConsumableName() + "\n"; + references.remove(references.get(0)); String referenceMaterialName = references.stream() - .map(reagent -> "\u2611" + " " + reagent.getReagentConsumableName()) + .map(reagent -> "\u3000\u3000\u2611" + " " + reagent.getReagentConsumableName()) .collect(Collectors.joining("\n")); - data.put("referenceMaterialName", referenceMaterialName); - } + data.put("referenceMaterialName", firstStr + referenceMaterialName); + data.put("materialIngredient", references.stream() + .map(reagent -> reagent.getReagentConsumableName()) + .collect(Collectors.joining("、"))); + } List reagents = testRecordReagentService.list(Wrappers.lambdaQuery() .in(TestRecordReagent::getId, testRecord.getReagentConsumablesList()) .eq(TestRecordReagent::getCategory, "试剂")); @@ -657,136 +775,167 @@ public class InspectRecordServiceImpl implements InspectRecordService { /** - * 构建检材性状描述 + * 构建检材性状描述(使用检材编号尾号,并合并连续编号) + *

+ * 规则: + * 1. **提取编号尾号**(如 `2025007001` → `1`) + * 2. **按尾号排序** + * 3. **连续编号 & 物质性状相同时合并** + * 4. **只有一个检材时,省略编号,直接描述** + * 5. **输出格式示例:** + * - `"1号至4号检材为粉状"` + * - `"检材为粉状"`(仅有一个检材时) * * @param sampleInfoList 样本信息列表 - * @return 物料特性描述,如果无法构建则返回null + * @return 物料特性描述(如果列表为空,则返回空字符串) */ private String buildMaterialCharacterDesc(List sampleInfoList) { if (CollUtil.isEmpty(sampleInfoList)) { return StrUtil.EMPTY; } - // 仅有一个样本时,直接返回 - if (sampleInfoList.size() == 1) { - return "检材为" + sampleInfoList.get(0).getForm() + "。"; - } + // 按尾号排序(提取编号尾号进行比较) + sampleInfoList.sort(Comparator.comparing(sample -> extractTailNumber(sample.getAcceptNo()))); - // 按 form 分组 - Map> sampleMapGroupByForm = sampleInfoList.stream() - .collect(Collectors.groupingBy(SampleInfo::getForm)); + List descriptions = new ArrayList<>(); + int start = -1, prev = -1; + String prevForm = null; + List mergedOrderNos = new ArrayList<>(); + + // 遍历排序后的检材编号,合并相邻且性状相同的编号 + for (SampleInfo sample : sampleInfoList) { + int current = extractTailNumber(sample.getAcceptNo()); // 提取尾号 + String form = sample.getForm(); + + if (start == -1) { // 初始化 + start = prev = current; + prevForm = form; + } else if (current == prev + 1 && form.equals(prevForm)) { + // 只有连续尾号 + 相同性状才能合并 + prev = current; + } else { + // 遇到新性状或不连续尾号,存储前一段描述 + mergedOrderNos.add(formatRange(start, prev) + "检材为" + prevForm); + start = prev = current; + prevForm = form; + } + } + // 处理最后一组数据 + mergedOrderNos.add(formatRange(start, prev) + "检材为" + prevForm); - // 所有样本 form 相同 - if (sampleMapGroupByForm.size() == 1) { - String form = sampleMapGroupByForm.keySet().iterator().next(); // 取唯一的 key - String orderNoStr = sampleInfoList.stream() - .map(sample -> sample.getOrderNo() + "号") - .collect(Collectors.joining("、")); - return orderNoStr + "检材均为" + form + "。"; + // **如果只有一个检材,省略编号** + if (mergedOrderNos.size() == 1 && sampleInfoList.size() == 1) { + return "检材为" + prevForm + "。"; } - // 存在多个 form 的情况 - List descriptions = new ArrayList<>(); - sampleMapGroupByForm.forEach((form, value) -> { - String orderNoStr = value.stream() - .map(sample -> sample.getOrderNo() + "号") - .collect(Collectors.joining("、")); - descriptions.add(orderNoStr + "检材均为" + form); - }); + return String.join(";", mergedOrderNos) + "。"; + } + + /** + * 提取检材编号的尾号(即最后一个 `-` 之后的数值) + * + * @param orderNo 检材编号(格式:2025-89-13) + * @return 尾号(示例:返回 13) + */ + private int extractTailNumber(String orderNo) { + String[] parts = orderNo.split("-"); + if (parts.length > 0) { + return Integer.parseInt(parts[parts.length - 1]); // 取最后一段 + } + return 0; + } - return String.join(";", descriptions) + "。"; // 用分号连接不同类型的描述 + /** + * 格式化编号范围: + * - 单个编号:返回 "1号" + * - 连续编号:返回 "1号至3号" + * + * @param start 起始编号 + * @param end 结束编号 + * @return 格式化的编号字符串(如果 start == end,返回单号) + */ + private String formatRange(int start, int end) { + return (start == end) ? start + "号" : start + "号至" + end + "号"; } + public String buildInspectOpinion(List dataList) { - // 1. 检查输入的dataList是否为空,如果为空直接返回当前数据。 if (dataList == null || dataList.isEmpty()) { return ""; } - // 2. 初始化数据结构,用于分类和记录检出与未检出的物质 - // - detectedMap 用来存储检出物质的样本信息,key为样本编号,value为检出的物质列表 - // - notDetectedMap 用来存储未检出物质的样本信息,key为样本编号,value为未检出的物质列表 - // - allCompoundNames 用来存储所有物质名称的集合,方便后续处理 Map> detectedMap = new LinkedHashMap<>(); Map> notDetectedMap = new LinkedHashMap<>(); Set allCompoundNames = new HashSet<>(); - // 3. 遍历dataList,按每个样本的检出状态(是否检出)对物质进行分类 for (TestRecordSampleData record : dataList) { - String sampleNo = record.getSampleNo(); // 获取样本编号 - String compoundName = record.getCompoundName(); // 获取化合物名称 - allCompoundNames.add(compoundName); // 将化合物名称添加到所有化合物名称集合中 + String sampleNo = record.getSampleNo(); + String compoundName = record.getCompoundCnName(); - // 根据isDetected值将物质添加到相应的Map中 - if (record.getIsDetected() == 1) { - detectedMap.computeIfAbsent(sampleNo, k -> new ArrayList<>()).add(compoundName); // 如果检出,将物质加入到检测的Map中 + // 确保样本编号有效 + 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 { - notDetectedMap.computeIfAbsent(sampleNo, k -> new ArrayList<>()).add(compoundName); // 如果未检出,将物质加入到未检测的Map中 + notDetectedMap.computeIfAbsent(sampleNo, k -> new ArrayList<>()).add(compoundName); } } - // 4. 定义一个lambda函数,提取样本编号中的流水号(例如:"S001" -> "1号") - // 目的是生成以“号”结尾的样本编号格式。 - Function extractSampleNumber = sampleNo -> sampleNo.substring(sampleNo.lastIndexOf('-') + 1) + "号"; + Function extractSampleNumber = sampleNo -> sampleNo.matches(".*\\d+") + ? sampleNo.replaceAll(".*?(\\d+)$", "$1号") + : sampleNo; - // 5. 处理“检出”数据 - // 先根据样本编号分组相同的检出物质,之后生成相应的语句描述 - List detectedSentences = new ArrayList<>(); // 存储“检出”物质的描述 - Map, List> groupedDetectedSamples = new LinkedHashMap<>(); // 用来存储按物质分组的样本编号 + List detectedSentences = new ArrayList<>(); + Map, List> groupedDetectedSamples = new LinkedHashMap<>(); - // 遍历检测到的物质,将相同物质的样本编号进行分组 for (Map.Entry> entry : detectedMap.entrySet()) { - Set compoundSet = new HashSet<>(entry.getValue()); // 去重,保证每个物质只记录一次 - groupedDetectedSamples.computeIfAbsent(compoundSet, k -> new ArrayList<>()).add(extractSampleNumber.apply(entry.getKey())); // 根据物质的组合来分组样本编号 + 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 = String.join("、", entry.getValue()); // 将所有样本编号合并为字符串 - String compounds = String.join("、", entry.getKey()); // 将所有检出的物质合并为字符串 + String sampleNumbers = String.join("、", entry.getValue()); + String compounds = String.join("、", entry.getKey()); - // 如果多个样本检出相同的物质,描述中使用“均检出”,否则使用“检出” if (entry.getValue().size() > 1) { - detectedSentences.add(String.format("从%s检材样品中均检出%s成分", sampleNumbers, compounds)); // 多个样本检出 + detectedSentences.add(String.format("从%s检材样品中均检出%s成分", sampleNumbers, compounds)); } else { - detectedSentences.add(String.format("从%s检材样品中检出%s成分", sampleNumbers, compounds)); // 单个样本检出 + detectedSentences.add(String.format("从%s检材样品中检出%s成分", sampleNumbers, compounds)); } } - // 6. 处理“未检出”数据 - // 先根据未检出的物质及样本编号进行分组,然后生成相应的描述 - List notDetectedSentences = new ArrayList<>(); // 存储“未检出”物质的描述 - Map, List> groupedNotDetectedSamples = new LinkedHashMap<>(); // 用来存储按物质分组的样本编号 + List notDetectedSentences = new ArrayList<>(); + Map, List> groupedNotDetectedSamples = new LinkedHashMap<>(); - // 遍历未检出的物质,找到未检出的样本并将其分组 for (Map.Entry> entry : notDetectedMap.entrySet()) { - String sampleNo = entry.getKey(); // 获取样本编号 - if (detectedMap.containsKey(sampleNo)) { - continue; // 如果该样本已经出现在检出组中,跳过,不再处理 - } + String sampleNo = entry.getKey(); + Set compoundSet = new HashSet<>(entry.getValue()); - Set compoundSet = new HashSet<>(entry.getValue()); // 获取未检出的物质,去重 - groupedNotDetectedSamples.computeIfAbsent(compoundSet, k -> new ArrayList<>()).add(extractSampleNumber.apply(sampleNo)); // 将未检出的物质分组 + // 仅当该样本没有任何检出的物质时,才记录未检出 + if (!detectedMap.containsKey(sampleNo)) { + groupedNotDetectedSamples.computeIfAbsent(compoundSet, k -> new ArrayList<>()).add(extractSampleNumber.apply(sampleNo)); + } } - // 遍历每个未检出物质的分组,生成描述 for (Map.Entry, List> entry : groupedNotDetectedSamples.entrySet()) { - String sampleNumbers = String.join("、", entry.getValue()); // 将未检出的样本编号合并为字符串 - String compounds = String.join("、", entry.getKey()); // 将未检出的物质合并为字符串 + String sampleNumbers = String.join("、", entry.getValue()); + String compounds = String.join("、", entry.getKey()); - // 如果多个样本未检出相同的物质,描述中使用“均未检出”,否则使用“未检出” if (entry.getValue().size() > 1) { - notDetectedSentences.add(String.format("从%s检材样品中均未检出%s成分", sampleNumbers, compounds)); // 多个样本未检出 + notDetectedSentences.add(String.format("从%s检材样品中均未检出%s成分", sampleNumbers, compounds)); } else { - notDetectedSentences.add(String.format("从%s检材样品中未检出%s成分", sampleNumbers, compounds)); // 单个样本未检出 + notDetectedSentences.add(String.format("从%s检材样品中未检出%s成分", sampleNumbers, compounds)); } } - // 7. 将“检出”和“未检出”描述合并到最终结果中 - List finalSentences = new ArrayList<>(detectedSentences); // 先将检出物质的描述加入 - finalSentences.addAll(notDetectedSentences); // 将未检出的描述添加到末尾 + List finalSentences = new ArrayList<>(detectedSentences); + finalSentences.addAll(notDetectedSentences); - // 8. 将最终的描述字符串添加到结果数据Map中,字段名为"detectionStr"返回结果 return String.join(";", finalSentences); } }