diff --git a/detection/src/main/java/com/njcn/gather/device/service/impl/PqDevServiceImpl.java b/detection/src/main/java/com/njcn/gather/device/service/impl/PqDevServiceImpl.java index c52c4a30..0db441ff 100644 --- a/detection/src/main/java/com/njcn/gather/device/service/impl/PqDevServiceImpl.java +++ b/detection/src/main/java/com/njcn/gather/device/service/impl/PqDevServiceImpl.java @@ -44,6 +44,7 @@ import com.njcn.gather.system.dictionary.service.IDictDataService; import com.njcn.gather.system.dictionary.service.IDictTypeService; import com.njcn.gather.type.pojo.po.DevType; import com.njcn.gather.type.service.IDevTypeService; +import com.njcn.gather.user.user.pojo.po.SysUser; import com.njcn.gather.user.user.service.ISysUserService; import com.njcn.web.factory.PageFactory; import com.njcn.web.utils.ExcelUtil; @@ -353,9 +354,13 @@ public class PqDevServiceImpl extends ServiceImpl implements pqDevVO.setDevKey(EncryptionUtil.decoderString(1, pqDevVO.getDevKey())); } if (StrUtil.isNotBlank(pqDevVO.getCheckBy())) { - pqDevVO.setCheckBy(userService.getById(pqDevVO.getCheckBy()).getName()); + SysUser sysUser = userService.getById(pqDevVO.getCheckBy()); + if (ObjectUtil.isNotNull(sysUser)) { + pqDevVO.setCheckBy(sysUser.getName()); + } else { + pqDevVO.setCheckBy(pqDevVO.getCheckBy()); + } } - DevType devType = devTypeService.getById(pqDevVO.getDevType()); if (ObjectUtil.isNotNull(devType)) { pqDevVO.setDevChns(devType.getDevChns()); diff --git a/detection/src/main/java/com/njcn/gather/monitor/service/IPqMonitorService.java b/detection/src/main/java/com/njcn/gather/monitor/service/IPqMonitorService.java index b7056340..3e603ec9 100644 --- a/detection/src/main/java/com/njcn/gather/monitor/service/IPqMonitorService.java +++ b/detection/src/main/java/com/njcn/gather/monitor/service/IPqMonitorService.java @@ -111,4 +111,5 @@ public interface IPqMonitorService extends IService { * @return */ Integer getDevCheckResult(String devId); + } diff --git a/detection/src/main/java/com/njcn/gather/plan/pojo/enums/DataSourceEnum.java b/detection/src/main/java/com/njcn/gather/plan/pojo/enums/DataSourceEnum.java index 151831f9..b5d2f2e1 100644 --- a/detection/src/main/java/com/njcn/gather/plan/pojo/enums/DataSourceEnum.java +++ b/detection/src/main/java/com/njcn/gather/plan/pojo/enums/DataSourceEnum.java @@ -10,13 +10,13 @@ import java.util.Objects; */ @Getter public enum DataSourceEnum { - REAL_DATA("real", "3s实时数据"), + REAL_DATA("real", "3s数据(150周波数据)"), - MINUTE_STATISTICS_MAX("max", "分钟统计数据-最大"), - MINUTE_STATISTICS_MIN("min", "分钟统计数据-最小"), - MINUTE_STATISTICS_AVG("avg", "分钟统计数据-平均"), - MINUTE_STATISTICS_CP95("cp95", "分钟统计数据-CP95"), - WAVE_DATA("wave_data", "录波"); + MINUTE_STATISTICS_MAX("max", "分钟统计数据-最大值"), + MINUTE_STATISTICS_MIN("min", "分钟统计数据-最小值"), + MINUTE_STATISTICS_AVG("avg", "分钟统计数据-平均值"), + MINUTE_STATISTICS_CP95("cp95", "分钟统计数据-CP95值"), + WAVE_DATA("wave_data", "录波数据"); private String value; private String msg; diff --git a/detection/src/main/java/com/njcn/gather/plan/service/IAdPlanService.java b/detection/src/main/java/com/njcn/gather/plan/service/IAdPlanService.java index 68fcde3f..b6fdde4d 100644 --- a/detection/src/main/java/com/njcn/gather/plan/service/IAdPlanService.java +++ b/detection/src/main/java/com/njcn/gather/plan/service/IAdPlanService.java @@ -189,4 +189,20 @@ public interface IAdPlanService extends IService { */ boolean importSubPlanDataZip(MultipartFile file, String patternId, HttpServletResponse response); + /** + * 导出计划检测结果数据 + * + * @param planId + * @param devIds + * @param report + * @param response + */ + void exportPlanCheckDataZip(String planId, List devIds, Integer report, HttpServletResponse response); + + /** + * 比对模式下计划的检测大项获取 + * @param planId 计划ID + * @return 检测项集合 + */ + List getScriptListContrast(String planId); } diff --git a/detection/src/main/java/com/njcn/gather/plan/service/impl/AdPlanServiceImpl.java b/detection/src/main/java/com/njcn/gather/plan/service/impl/AdPlanServiceImpl.java index 3046cd4c..c9107d07 100644 --- a/detection/src/main/java/com/njcn/gather/plan/service/impl/AdPlanServiceImpl.java +++ b/detection/src/main/java/com/njcn/gather/plan/service/impl/AdPlanServiceImpl.java @@ -12,6 +12,7 @@ import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.ZipUtil; import cn.hutool.extra.spring.SpringUtil; +import cn.hutool.json.JSONConfig; import cn.hutool.json.JSONUtil; import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; @@ -22,6 +23,7 @@ import com.njcn.common.pojo.enums.common.DataStateEnum; import com.njcn.common.pojo.enums.response.CommonResponseEnum; import com.njcn.common.pojo.exception.BusinessException; import com.njcn.common.pojo.poi.PullDown; +import com.njcn.gather.detection.pojo.po.AdPair; import com.njcn.gather.detection.service.IAdPariService; import com.njcn.gather.device.mapper.PqDevMapper; import com.njcn.gather.device.pojo.enums.*; @@ -78,6 +80,7 @@ import com.njcn.gather.system.dictionary.pojo.po.DictType; import com.njcn.gather.system.dictionary.service.IDictDataService; import com.njcn.gather.system.dictionary.service.IDictTreeService; import com.njcn.gather.system.dictionary.service.IDictTypeService; +import com.njcn.gather.tools.report.model.constant.ReportConstant; import com.njcn.gather.type.pojo.po.DevType; import com.njcn.gather.type.service.IDevTypeService; import com.njcn.gather.user.user.pojo.po.SysUser; @@ -1880,6 +1883,186 @@ public class AdPlanServiceImpl extends ServiceImpl impleme } } + @Override + public void exportPlanCheckDataZip(String planId, List devIds, Integer report, HttpServletResponse response) { + AdPlanCheckDataVO planCheckDataVO = new AdPlanCheckDataVO(); + // 获取检测计划基本数据 + AdPlan plan = this.getById(planId); + planCheckDataVO.setPlan(plan); + // 获取检测计划绑定的被检设备数据 + List devList = pqDevService.list(new LambdaQueryWrapper().eq(PqDev::getPlanId, planId).in(PqDev::getId, devIds)); + if (CollUtil.isEmpty(devList)) { + throw new BusinessException(CommonResponseEnum.FAIL, "选择的被检设备不存在"); + } + planCheckDataVO.setDevList(devList); + List devIdList = devList.stream().map(PqDev::getId).collect(Collectors.toList()); + // 被检设备状态统计 + List devSubList = pqDevSubService.list(new LambdaQueryWrapper().in(PqDevSub::getDevId, devIdList)); + planCheckDataVO.setDevSubList(devSubList); + // 被检设备监测点信息 + List monitorList = pqMonitorService.list(new LambdaQueryWrapper().in(PqMonitor::getDevId, devIdList)); + planCheckDataVO.setMonitorList(monitorList); + // devMonitorId = 被检设备ID+通道号 + List devMonitorIds = new ArrayList<>(); + for (PqDev dev : devList) { + List channelNoList = StrUtil.split(dev.getInspectChannel(), StrUtil.COMMA); + for (String channelNo : channelNoList) { + devMonitorIds.add(dev.getId() + StrUtil.UNDERLINE + channelNo); + } + } + planCheckDataVO.setDevMonitorIds(devMonitorIds); + // 设备通道匹对关系 + List pairList = adPairService.list(new LambdaQueryWrapper().eq(AdPair::getPlanId, planId).in(AdPair::getDevMonitorId, devMonitorIds)); + planCheckDataVO.setPairList(pairList); + // 获取计划检测结果数据表以及数据 + Integer code = plan.getCode(); + List dataTableNames = CollUtil.newArrayList("ad_harmonic_" + code, "ad_non_harmonic_" + code, "ad_harmonic_result_" + code, "ad_non_harmonic_result_" + code); + + // 创建临时目录用于存储txt文件 + File tempDataDir = FileUtil.mkdir(FileUtil.getTmpDirPath() + "plan_data_" + System.currentTimeMillis() + "/"); + List dataFiles = new ArrayList<>(); + int dataBatch = 0; + if (CollUtil.isNotEmpty(pairList)) { + for (String dataTableName : dataTableNames) { + // 创建数据文件 + String fileName = dataTableName.replace("_" + code, "") + ".txt"; + File dataFile = FileUtil.file(tempDataDir, fileName); + // 确保文件存在 + FileUtil.touch(dataFile); + + // 初始化写入标志,用于判断是否已写入字段名 + boolean isFirstWrite = true; + + // 分页查询,避免一次性加载大量数据 + int pageSize = 10000; // 每页查询10000条记录 + int offset = 0; + List> pageData; + do { + dataBatch += 1; + String paginatedSql = buildPaginatedQuery(dataTableName, devMonitorIds, pageSize, offset); + pageData = jdbcTemplate.queryForList(paginatedSql); + + // 将当前页数据追加到文件中 + if (CollUtil.isNotEmpty(pageData)) { + StringBuilder content = new StringBuilder(); + + // 如果是第一次写入,先写入字段名 + if (isFirstWrite) { + // 获取字段名 + Map firstRow = pageData.get(0); + List fieldNames = new ArrayList<>(firstRow.keySet()); + + // 写入字段名作为第一行 + content.append(StrUtil.join("\t", fieldNames)).append(System.lineSeparator()); + isFirstWrite = false; + } + + // 写入数据行 + for (Map data : pageData) { + List values = new ArrayList<>(data.values()); + content.append(StrUtil.join("\t", values)).append(System.lineSeparator()); + } + + // 追加内容到文件 + FileUtil.appendUtf8String(content.toString(), dataFile); + } + offset += pageSize; + } while (pageData.size() == pageSize); // 如果查询结果少于pageSize,说明已经查询完所有数据 + + // 如果文件存在且不为空,则添加到数据文件列表中 + if (FileUtil.exist(dataFile) && FileUtil.size(dataFile) > 0) { + dataFiles.add(dataFile); + } + } + } + planCheckDataVO.setDataBatch(dataBatch); + + // 导出数据.zip文件 + String jsonStr = JSONUtil.toJsonStr(planCheckDataVO, new JSONConfig().setIgnoreNullValue(false)); + try { + // 创建临时目录 + File tempDir = FileUtil.mkdir(FileUtil.getTmpDirPath() + "export_" + System.currentTimeMillis() + "/"); + + // 创建 JSON 文件 + String jsonFileName = plan.getName() + ".json"; + File jsonFile = FileUtil.file(tempDir, jsonFileName); + FileUtil.writeUtf8String(jsonStr, jsonFile); + + // 创建 ZIP 文件 + String zipFileName = URLEncoder.encode(plan.getName() + "_检测数据.zip", "UTF-8"); + File zipFile = FileUtil.file(tempDir, zipFileName); + + // 创建一个临时目录存放所有文件 + File tempZipDir = FileUtil.mkdir(FileUtil.getTmpDirPath() + "temp_plan_check_data_" + System.currentTimeMillis() + "/"); + // 复制json文件到临时目录 + FileUtil.copy(jsonFile, tempZipDir, true); + + // 复制数据txt文件到临时目录 + for (File dataFile : dataFiles) { + FileUtil.copy(dataFile, tempZipDir, true); + } + + // 添加检测报告文件 + if (ObjectUtil.isNotNull(report) && report.equals(1)) { + for (PqDev dev : devList) { + DevType devType = devTypeService.getById(dev.getDevType()); + String dirPath = reportPath.concat(File.separator).concat(devType.getName()); + File reportFile = new File(dirPath.concat(File.separator).concat(dev.getCreateId()).concat(ReportConstant.DOCX)); + // 如果reportFile存在,则将reportFile中的文件添加到已有的zip文件中 + if (FileUtil.exist(reportFile)) { + // 复制reportFile到临时目录 + FileUtil.copy(reportFile, tempZipDir, true); + } + + } + } + + // 重新创建zip文件,包含所有文件 + ZipUtil.zip(tempZipDir.getAbsolutePath(), zipFile.getAbsolutePath()); + + // 删除临时目录 + FileUtil.del(tempZipDir); + FileUtil.del(tempDataDir); + + // 设置响应头 + response.reset(); + response.setContentType("application/octet-stream;charset=UTF-8"); + response.setHeader("Content-Disposition", "attachment; filename=\"" + zipFileName + "\""); + + // 将 ZIP 文件写入响应 + ServletOutputStream os = response.getOutputStream(); + FileUtil.writeToStream(zipFile, os); + os.flush(); + os.close(); + + // 删除临时文件 + FileUtil.del(tempDir); + } catch (IOException e) { + log.error("导出计划检测数据.zip文件失败: ", e); + throw new BusinessException(CommonResponseEnum.FAIL); + } + } + + /** + * 比对模式下计划的检测项获取 + * @param planId 计划ID + * @return 检测项 + */ + @Override + public List getScriptListContrast(String planId) { + List scriptList = new ArrayList<>(); + AdPlan adPlan = this.baseMapper.selectById(planId); + String pattern = adPlan.getPattern(); + DictData dictData = dictDataService.getDictDataById(pattern); + if (ObjectUtil.isNotNull(dictData)) { + if(dictData.getCode().equalsIgnoreCase("Contrast")){ + String[] items = adPlan.getTestItem().split(","); + scriptList = new ArrayList<>(Arrays.asList(items)); + } + } + return scriptList; + } + // 构建分页查询SQL private String buildPaginatedQuery(String tableName, List devMonitorIds, int limit, int offset) { StringBuilder sql = new StringBuilder("SELECT * FROM " + tableName); diff --git a/detection/src/main/java/com/njcn/gather/report/pojo/DevReportParam.java b/detection/src/main/java/com/njcn/gather/report/pojo/DevReportParam.java index 81adf7e3..78517bc0 100644 --- a/detection/src/main/java/com/njcn/gather/report/pojo/DevReportParam.java +++ b/detection/src/main/java/com/njcn/gather/report/pojo/DevReportParam.java @@ -35,5 +35,8 @@ public class DevReportParam implements Serializable { */ private String devId; + /** + * 批量下载时传递的被检设备id列表 + */ private List devIdList; } diff --git a/detection/src/main/java/com/njcn/gather/report/pojo/constant/PowerConstant.java b/detection/src/main/java/com/njcn/gather/report/pojo/constant/PowerConstant.java index 58e3b471..ce06c843 100644 --- a/detection/src/main/java/com/njcn/gather/report/pojo/constant/PowerConstant.java +++ b/detection/src/main/java/com/njcn/gather/report/pojo/constant/PowerConstant.java @@ -23,6 +23,11 @@ public interface PowerConstant { */ List T_PHASE = Arrays.asList("VOLTAGE", "IMBV", "IMBA", "FREQ"); + /** + * T相指标存B相字段 + */ + List TB_PHASE = Arrays.asList("IMBV", "IMBA"); + /** * 有次数的指标 @@ -39,6 +44,12 @@ public interface PowerConstant { */ List DATA_RANGE = Arrays.asList(1, 2); + /** + * abc相别 + */ + List PHASE_ABC = Arrays.asList("a", "b", "c"); + + /** * 暂态符号 */ diff --git a/detection/src/main/java/com/njcn/gather/report/pojo/enums/BaseReportKeyEnum.java b/detection/src/main/java/com/njcn/gather/report/pojo/enums/BaseReportKeyEnum.java index 53d2a24e..91290ce0 100644 --- a/detection/src/main/java/com/njcn/gather/report/pojo/enums/BaseReportKeyEnum.java +++ b/detection/src/main/java/com/njcn/gather/report/pojo/enums/BaseReportKeyEnum.java @@ -30,6 +30,12 @@ public enum BaseReportKeyEnum { MONTH("month","月份"), DAY("day","日"), YEAR_MONTH_DAY("year-month-day","年-月-日"), + REPORT_DATE("reportDate","年-月-日"), + SUB_NAME("subName","变电站"), + CHECK_BY("checkBy","检测人"), + AUDIT_BY("auditBy","负责人、审核人"), + ERROR_SYS_NAME("errorSysName","误差体系"), + CREATE_DATE("createDate","生产日期"), TEMPERATURE("temp","温度"), HUMIDITY("hum","相对湿度"), DELEGATE("delegate","委托方"); diff --git a/detection/src/main/java/com/njcn/gather/report/pojo/enums/ItemReportKeyEnum.java b/detection/src/main/java/com/njcn/gather/report/pojo/enums/ItemReportKeyEnum.java index 33ad03bd..4c12055e 100644 --- a/detection/src/main/java/com/njcn/gather/report/pojo/enums/ItemReportKeyEnum.java +++ b/detection/src/main/java/com/njcn/gather/report/pojo/enums/ItemReportKeyEnum.java @@ -15,6 +15,9 @@ public enum ItemReportKeyEnum { NAME_DETAIL("nameDetail", "检测项详细,比如:频率测量准确度"), INFLUENCE("influence", "影响量的描述,比如额定工作条件、谐波对电压的影响等等"), ERROR_SCOPE("errorScope", "误差范围,注:在段落中时需加上(),表格中无需添加"), + A_ERROR_SCOPE("a_errorScope", "A级误差,比如:±0.1%Un\n(±0.05774),需要换行,电压需要转为幅值"), + NUM_OF_DATA("numOfData", "有效数据组数"), + PHASE("phase", "相别"), ERROR_SCOPE_MAG("errorScopeMag", "特征幅值:误差范围"), ERROR_SCOPE_DUR("errorScopeDur", "持续时间:误差范围"), SCRIPT_DETAIL("scriptDetail", "脚本输出明细。比如:基波电压UN=57.74V,f=50Hz,谐波含有率Uh=10%UN=5.774V"), diff --git a/detection/src/main/java/com/njcn/gather/report/pojo/enums/PowerIndexEnum.java b/detection/src/main/java/com/njcn/gather/report/pojo/enums/PowerIndexEnum.java index 53596e66..c26acfa3 100644 --- a/detection/src/main/java/com/njcn/gather/report/pojo/enums/PowerIndexEnum.java +++ b/detection/src/main/java/com/njcn/gather/report/pojo/enums/PowerIndexEnum.java @@ -14,6 +14,7 @@ public enum PowerIndexEnum { UNKNOWN("UNKNOWN", "未知指标"), FREQ("FREQ", "频率"), + LINE_TITLE("LINE_TITLE", "测量回路"), V("V", "电压"), I("I", "电流"), P("P", "功率"), diff --git a/detection/src/main/java/com/njcn/gather/report/pojo/result/ContrastTestResult.java b/detection/src/main/java/com/njcn/gather/report/pojo/result/ContrastTestResult.java new file mode 100644 index 00000000..d760b9d3 --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/report/pojo/result/ContrastTestResult.java @@ -0,0 +1,55 @@ +package com.njcn.gather.report.pojo.result; + +import lombok.Data; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +/** + * @author hongawen + * @version 1.0 + * @data 2025/9/18 13:22 + */ +@Data +public class ContrastTestResult implements Serializable { + + /** + * 指标CODE + */ + private String scriptCode; + + /** + * 指标名称 + */ + private String scriptName; + + /** + * 是否谐波类 + */ + private boolean isHarmonic; + + /** + * 是否合格 + */ + private String result; + + /** + * 特殊情况的一些描述 + * 比如:存在无法比较时,比如谐波类存在标准和被检均为0的情况 + */ + private String specialCase; + + /** + * 非谐波类检测结果 + * 相别 -- 占位符名称 -- 占位符值 + */ + Map> checkResultNonHarmonic; + + /** + * 谐波类检测结果 + * 次数 -- 相别 -- 占位符名称 -- 占位符值 + */ + List>>> checkResultHarmonic; + +} diff --git a/detection/src/main/java/com/njcn/gather/report/service/impl/PqReportServiceImpl.java b/detection/src/main/java/com/njcn/gather/report/service/impl/PqReportServiceImpl.java index 759c0e28..a2e29d03 100644 --- a/detection/src/main/java/com/njcn/gather/report/service/impl/PqReportServiceImpl.java +++ b/detection/src/main/java/com/njcn/gather/report/service/impl/PqReportServiceImpl.java @@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.date.DatePattern; import cn.hutool.core.date.DateUtil; +import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.text.StrPool; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ObjectUtil; @@ -36,9 +37,14 @@ import com.njcn.gather.device.pojo.po.PqDevSub; import com.njcn.gather.device.pojo.vo.PqDevVO; import com.njcn.gather.device.service.IPqDevService; import com.njcn.gather.device.service.IPqDevSubService; +import com.njcn.gather.err.pojo.po.PqErrSys; +import com.njcn.gather.err.service.IPqErrSysService; +import com.njcn.gather.plan.pojo.enums.DataSourceEnum; import com.njcn.gather.plan.pojo.enums.PlanReportStateEnum; import com.njcn.gather.plan.pojo.po.AdPlan; +import com.njcn.gather.plan.pojo.po.AdPlanTestConfig; import com.njcn.gather.plan.service.IAdPlanService; +import com.njcn.gather.plan.service.IAdPlanTestConfigService; import com.njcn.gather.pojo.enums.DetectionResponseEnum; import com.njcn.gather.report.mapper.PqReportMapper; import com.njcn.gather.report.pojo.DevReportParam; @@ -46,6 +52,7 @@ import com.njcn.gather.report.pojo.constant.PowerConstant; import com.njcn.gather.report.pojo.enums.*; import com.njcn.gather.report.pojo.param.ReportParam; import com.njcn.gather.report.pojo.po.PqReport; +import com.njcn.gather.report.pojo.result.ContrastTestResult; import com.njcn.gather.report.pojo.result.SingleTestResult; import com.njcn.gather.report.pojo.vo.PqReportVO; import com.njcn.gather.report.service.IPqReportService; @@ -68,6 +75,8 @@ import com.njcn.gather.tools.report.util.DocxMergeUtil; import com.njcn.gather.tools.report.util.WordDocumentUtil; import com.njcn.gather.type.pojo.po.DevType; import com.njcn.gather.type.service.IDevTypeService; +import com.njcn.gather.user.user.pojo.po.SysUser; +import com.njcn.gather.user.user.service.ISysUserService; import com.njcn.http.util.RestTemplateUtil; import com.njcn.web.factory.PageFactory; import com.njcn.web.utils.RequestUtil; @@ -80,6 +89,7 @@ import org.docx4j.jaxb.Context; import org.docx4j.openpackaging.packages.WordprocessingMLPackage; import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart; import org.docx4j.wml.*; +import org.docx4j.wml.Color; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.ClassPathResource; @@ -97,9 +107,11 @@ import java.io.*; import java.math.BigDecimal; import java.math.BigInteger; import java.math.RoundingMode; +import java.net.URLEncoder; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.time.LocalDateTime; import java.util.List; import java.util.*; import java.util.concurrent.CompletableFuture; @@ -163,6 +175,9 @@ public class PqReportServiceImpl extends ServiceImpl i private final ISysTestConfigService sysTestConfigService; private final SocketManager socketManager; private final IWordReportService wordReportService; + private final ISysUserService sysUserService; + private final IPqErrSysService pqErrSysService; + private final IAdPlanTestConfigService adPlanTestConfigService; @Resource private RestTemplateUtil restTemplateUtil; @@ -538,7 +553,17 @@ public class PqReportServiceImpl extends ServiceImpl i AdPlan plan = adPlanService.getById(devReportParam.getPlanId()); // 0 - 模板占位符更新, 1 - 根据配置模版动态组合生产的报告 if (plan.getAssociateReport() == 1) { - this.generateReportByPlan(plan, devReportParam); + DictData dictDataById = dictDataService.getDictDataById(plan.getPattern()); + if (Objects.isNull(dictDataById)) { + throw new BusinessException("计划没有配置合理的模式"); + } + // 比对模式 + if (dictDataById.getCode().equals("Contrast")) { + this.generateReportByPlanContrast(plan, devReportParam); + } else { + // 数模式 + this.generateReportByPlan(plan, devReportParam); + } } else if (plan.getAssociateReport() == 0) { this.generateReportByDevType(devReportParam); } @@ -546,6 +571,7 @@ public class PqReportServiceImpl extends ServiceImpl i /** + * * 根据设备类型生成报告 * 注:该方法目前仅支持楼下出厂检测场景,属于模板占位符替换方式,后期可能会有调整 * @@ -623,6 +649,12 @@ public class PqReportServiceImpl extends ServiceImpl i } + /** + * 发送二维码至装置 + * + * @param devIp 装置IP + * @param reportName 报告名称 + */ private void sendQrToDevice(String devIp, String reportName) { try { // 上传没问题后,拼接url生成二维码,并将二维码转为bin格式文件传递给设备通讯模块 @@ -732,6 +764,66 @@ public class PqReportServiceImpl extends ServiceImpl i } } + /** + * 需要支持批量生成,如果用户选择批量生成,则默认都采用测试数据的第一个合格,如果 + * 比对模式下生成检测报告,实际后期需要根据用户选择的检测数据或者某次波形数据生成报告 + * + * @param plan 计划信息 + * @param devReportParam 设备信息 + */ + private void generateReportByPlanContrast(AdPlan plan, DevReportParam devReportParam) { + // 支持批量生成报告 + devReportParam.getDevIdList().forEach(devId -> { + // 准备被检设备的基础信息 + PqDevVO pqDevVO = iPqDevService.getPqDevById(devId); + if (Objects.isNull(pqDevVO)) { + throw new BusinessException(ReportResponseEnum.DEVICE_NOT_EXIST); + } + devReportParam.setDevId(devId); + // 获取设备型号 + DevType devType = devTypeService.getById(pqDevVO.getDevType()); + if (Objects.isNull(devType)) { + throw new BusinessException(ReportResponseEnum.DEVICE_TYPE_NOT_EXIST); + } + PqReport report = this.lambdaQuery().eq(PqReport::getId, plan.getReportTemplateId()).eq(PqReport::getState, DataStateEnum.ENABLE.getCode()).one(); + if (Objects.isNull(report)) { + throw new BusinessException(ReportResponseEnum.REPORT_TEMPLATE_NOT_EXIST); + } + Path basePath = Paths.get(report.getBasePath()); + Path detailPath = Paths.get(report.getDetailPath()); + try (InputStream baseInputStream = Files.newInputStream(basePath); + InputStream detailInputStream = Files.newInputStream(detailPath)) { + WordprocessingMLPackage detailModelDocument = WordprocessingMLPackage.load(detailInputStream); + // 获取文档基础部分,并替换占位符 + Map baseModelDataMap = dealBaseModelContrastData(plan, pqDevVO, devType); + InputStream wordFinishInputStream = wordReportService.replacePlaceholders(baseInputStream, baseModelDataMap); + WordprocessingMLPackage baseModelDocument = WordprocessingMLPackage.load(wordFinishInputStream); + MainDocumentPart baseDocumentPart = baseModelDocument.getMainDocumentPart(); + + // 获取数据模版页内容,根据脚本动态组装数据页内容 + MainDocumentPart detailDocumentPart = detailModelDocument.getMainDocumentPart(); + dealDataModelScatteredByBookmarkByPlanContrast(baseDocumentPart, detailDocumentPart, devReportParam, pqDevVO); + // 保存新的文档 + String dirPath = reportPath.concat(File.separator).concat(plan.getName()); + // 确保目录存在 + ensureDirectoryExists(dirPath); + // 构建文件名:cityName_gdName_subName_name.docx + String fileName = String.format("%s_%s_%s_%s.docx", + pqDevVO.getCityName() != null ? pqDevVO.getCityName() : "未知地市", + pqDevVO.getGdName() != null ? pqDevVO.getGdName() : "未知供电公司", + pqDevVO.getSubName() != null ? pqDevVO.getSubName() : "未知电站", + pqDevVO.getName() != null ? pqDevVO.getName() : "未知设备"); + Docx4jUtil.cleanBlankPagesAndRedundantPageBreaks(baseModelDocument); + baseModelDocument.save(new File(dirPath.concat(File.separator).concat(fileName))); + this.updateDevAndPlanState(devId, devReportParam.getPlanId()); + } catch (Exception e) { + log.error(ReportResponseEnum.GENERATE_REPORT_ERROR.getMessage(), e); + throw new BusinessException(ReportResponseEnum.GENERATE_REPORT_ERROR); + } + }); + + } + /** * 根据计划绑定的报告模板生成报告 @@ -786,6 +878,288 @@ public class PqReportServiceImpl extends ServiceImpl i }); } + /** + * 比对式报告 + * 通过提前在模板文档里埋下书签 + * 1、目录信息 + * 2、准确度测试详情 + * 3、测试结果页 + * 上述3个锚点位置不固定,可能结果页在通用信息中间,也有可能在文档最末端。 + * 注:当存在目录信息时,目录最后生成。 + * + * @param baseDocumentPart 通用信息文档 + * @param detailDocumentPart 数据项模板文档 + * @param devReportParam 测试报告参数 + * @param pqDevVO 被检设备 + */ + private void dealDataModelScatteredByBookmarkByPlanContrast(MainDocumentPart baseDocumentPart, MainDocumentPart detailDocumentPart, DevReportParam devReportParam, PqDevVO pqDevVO) { + // 查找 base 文档中所有书签 + List bookmarks = BookmarkUtil.findAllBookmarks(baseDocumentPart); + if (CollUtil.isNotEmpty(bookmarks)) { + // 转换为枚举,便于排序,防止结论性的书签在文档的前面 + List bookmarkEnums = new ArrayList<>(); + for (BookmarkUtil.BookmarkInfo info : bookmarks) { + String name = info.bookmark.getName(); + BookmarkEnum bookmarkEnum = BookmarkEnum.getByKey(name); + if (Objects.nonNull(bookmarkEnum)) { + bookmarkEnums.add(bookmarkEnum); + } + } + /* + * 从结构上分析,处理的顺序: + * 1、数据项 + * 2、结果信息 + * 3、目录信息 + * 所以要先先获取的书签进行操作排序 + * */ + Collections.sort(bookmarkEnums); + List todoInsertList; + BookmarkUtil.BookmarkInfo bookmarkInfo; + Map> resultMap = null; + // 书签在文档的位置 + for (int i = 0; i < bookmarkEnums.size(); i++) { + BookmarkEnum bookmarkEnum = bookmarkEnums.get(i); + switch (bookmarkEnum) { + case DATA_LINE: + // 获取标签信息 + bookmarkInfo = BookmarkUtil.getBookmarkInfo(BookmarkEnum.DATA_LINE.getKey(), bookmarks); + // 第一次调用时获取数据并保存到resultMap + if (resultMap == null) { + resultMap = resultService.getContrastResultForReport(devReportParam, pqDevVO); + List contrastTestResults = resultMap.get(2); + resultMap.put(3, contrastTestResults); + } + todoInsertList = dealDataLineContrast(detailDocumentPart, devReportParam, pqDevVO, resultMap); + if (Objects.nonNull(bookmarkInfo) && CollectionUtil.isNotEmpty(todoInsertList)) { + BookmarkUtil.insertElement(bookmarkInfo, todoInsertList); + BookmarkUtil.removeBookmark(bookmarkInfo); + } + break; + case TEST_RESULT_DETAIL: + // 判断是否已经处理过数据了,有了结论性的描述 + if (CollUtil.isEmpty(resultMap)) { + // 如果resultMap还是空的,先获取数据 + resultMap = resultService.getContrastResultForReport(devReportParam, pqDevVO); + dealDataLineContrast(baseDocumentPart, devReportParam, pqDevVO, resultMap); + } + bookmarkInfo = BookmarkUtil.getBookmarkInfo(BookmarkEnum.TEST_RESULT_DETAIL.getKey(), bookmarks); + todoInsertList = dealTestResultLineContrast(devReportParam, resultMap, DocAnchorEnum.TEST_RESULT_DETAIL); + if (Objects.nonNull(bookmarkInfo) && CollectionUtil.isNotEmpty(todoInsertList)) { + BookmarkUtil.insertElement(bookmarkInfo, todoInsertList); + BookmarkUtil.removeBookmark(bookmarkInfo); + } + break; + default: + break; + } + } + } + + } + + /** + * 比对式的结论数据创建 + * + * @param devReportParam 被检设备的参数 + * @param resultMap 测试结果 + * @param docAnchorEnum 文档电接点 + */ + private List dealTestResultLineContrast(DevReportParam devReportParam, Map> resultMap, DocAnchorEnum docAnchorEnum) { + List todoInsertList = new ArrayList<>(); + ObjectFactory factory = new ObjectFactory(); + + // 1. 准备数据:提取所有检测项目和回路信息 + Set allTestItemsSet = new LinkedHashSet<>(); + List circuitNames = new ArrayList<>(); + Map> testItemResults = new LinkedHashMap<>(); + + // 遍历所有回路,收集检测项目和结果 + for (Map.Entry> entry : resultMap.entrySet()) { + Integer monitorNum = entry.getKey(); + circuitNames.add("测量回路 " + monitorNum); + for (ContrastTestResult testResult : entry.getValue()) { + String itemKey = testResult.getScriptName(); + allTestItemsSet.add(itemKey); + // 存储每个检测项目在各回路的结果 + testItemResults.putIfAbsent(itemKey, new LinkedHashMap<>()); + testItemResults.get(itemKey).put(monitorNum, testResult.getResult()); + } + } + + // 2. 转换数据格式以适配表格生成方法 + List testItems = new ArrayList<>(allTestItemsSet); + String[][] testResultsArray = new String[testItems.size()][circuitNames.size()]; + for (int i = 0; i < testItems.size(); i++) { + String testItem = testItems.get(i); + Map itemResults = testItemResults.get(testItem); + int j = 0; + for (Map.Entry> entry : resultMap.entrySet()) { + Integer monitorNum = entry.getKey(); + String result = itemResults.getOrDefault(monitorNum, "未检测"); + testResultsArray[i][j] = result; + j++; + } + } + + String planId = devReportParam.getPlanId(); + AdPlan adPlan = adPlanService.getById(planId); + // 4. 根据结果确定样品判定和数据处理参数 + String sampleResult = calculateOverallResult(testResultsArray); + String dataType = getDataTypeFromParam(adPlan); + String numOfSamples = getSampleCount(adPlan); + String dataRule = getDataRuleFromParam(adPlan); + // 5. 生成动态表格 + JAXBElement table = Docx4jUtil.createDynamicTestResultTable( + factory, + testItems, + circuitNames, + testResultsArray, + sampleResult, + dataType, + numOfSamples, + dataRule + ); + todoInsertList.add(table); + + return todoInsertList; + } + + /** + * 计算整体判定结果 + * 逻辑: + * 1. 存在一个检测项为"不符合"则样品结论就为"不符合" + * 2. 所有的均为"无法比较",样品结论才是"无法比较" + * 3. 其余均为"符合" + */ + private String calculateOverallResult(String[][] results) { + int totalCount = 0; + int uncomparableCount = 0; + + for (String[] row : results) { + for (String result : row) { + if (result != null && !result.trim().isEmpty()) { + totalCount++; + + // 只要有一个"不符合",立即返回"不符合" + if ("不符合".equals(result) || "不合格".equals(result)) { + return "不符合"; + } + + // 统计"无法比较"的数量 + if ("无法比较".equals(result)) { + uncomparableCount++; + } + } + } + } + + // 如果所有检测项都是"无法比较",返回"无法比较" + if (totalCount > 0 && uncomparableCount == totalCount) { + return "无法比较"; + } + + // 其余情况均为"符合" + return "符合"; + } + + /** + * 获取数据类型配置 + * 实时数据:3秒数据(150周波数据) + * 录波:录波数据 + * 分钟统计数据:分钟统计数据-最大值、分钟统计数据-最小值、分钟统计数据-平均值、分钟统计数据-CP95值 + * + */ + private String getDataTypeFromParam(AdPlan adPlan) { + String dataSource = adPlan.getDatasourceId(); + String[] dataSourceArray = dataSource.split(","); + StringBuilder dataType = new StringBuilder(); + for (String item : dataSourceArray) { + dataType.append(Objects.requireNonNull(DataSourceEnum.ofByValue(item)).getMsg()).append("、"); + } + dataType.deleteCharAt(dataType.length() - 1); + return dataType.toString(); + } + + /** + * 获取样本数量 + * 不同的dataSource对应不同的样本数量 + */ + private String getSampleCount(AdPlan adPlan) { + String dataSource = adPlan.getDatasourceId(); + String[] dataSourceArray = dataSource.split(","); + AdPlanTestConfig adPlanTestConfig = adPlanTestConfigService.getByPlanId(adPlan.getId()); + StringBuilder sampleCount = new StringBuilder(); + for (String item : dataSourceArray) { + if(item.equalsIgnoreCase(DataSourceEnum.REAL_DATA.getValue())){ + sampleCount.append("实时数据采集").append(adPlanTestConfig.getRealTime()).append("组,"); + }else if(item.equalsIgnoreCase(DataSourceEnum.WAVE_DATA.getValue())){ + sampleCount.append("录波数据采集").append(adPlanTestConfig.getWaveRecord()).append("组,"); + }else{ + sampleCount.append("统计数据采集").append(adPlanTestConfig.getStatistics()).append("组,"); + } + } + sampleCount.deleteCharAt(sampleCount.length() - 1); + return sampleCount.toString(); + } + + /** + * 获取数据处理规则 + */ + private String getDataRuleFromParam(AdPlan adPlan) { + String dataRule = adPlan.getDataRule(); + // 去字典获取 + DictData dictData = dictDataService.getDictDataById(dataRule); + return dictData.getName(); + } + + + /** + * 比对模式下获取报告需要的数据项 + * + * @param detailDocumentPart 数据项模板文档 + * @param devReportParam 测试报告参数 + * @param pqDevVO 被检设备 + * @param dataMap 各指标对应的各回路结论 + */ + private List dealDataLineContrast(MainDocumentPart detailDocumentPart, DevReportParam devReportParam, PqDevVO pqDevVO, Map> dataMap) { + List todoInsertList = new ArrayList<>(); + ObjectFactory factory = new ObjectFactory(); + // 处理表格数据 + List allContent = detailDocumentPart.getContent(); + List headingContents = Docx4jUtil.extractHeading5Contents(allContent, detailDocumentPart); + Map> contentMap = headingContents.stream().collect(Collectors.groupingBy(Docx4jUtil.HeadingContent::getHeadingText, Collectors.toList())); + Iterator>> iterator = dataMap.entrySet().iterator(); + int stepIndex = 1; + while (iterator.hasNext()) { + Map.Entry> next = iterator.next(); + // 线路号 + Integer monitorNum = next.getKey(); + // 线路下的指标数据 + List contrastTestResults = next.getValue(); + // 插入回路号前,先换个页 + todoInsertList.add(Docx4jUtil.createPageBreakParagraph()); + // 回路标题 + todoInsertList.add(getContrastLineTitle(contentMap, monitorNum, stepIndex, factory)); + int scriptIndex = 1; + for (ContrastTestResult contrastTestResult : contrastTestResults) { + // 比如电压 V + String scriptCode = contrastTestResult.getScriptCode(); + String scriptName = contrastTestResult.getScriptName(); + // 创建测试项标题 + todoInsertList.add(getContrastScriptTitle(stepIndex + "." + scriptIndex + " " + scriptName + "检测分析", factory)); + // 根据code获取对应需要填充的内容 + List tempContent = contentMap.get(scriptCode); + // 需要区分下谐波类和非谐波类 + List tempList = fillContentInTemplateContrast(tempContent, factory, contrastTestResult); + todoInsertList.addAll(tempList); + scriptIndex++; + } + stepIndex++; + } + + return todoInsertList; + } + /** * 通过提前在模板文档里埋下书签 @@ -999,34 +1373,24 @@ public class PqReportServiceImpl extends ServiceImpl i List pqScriptDtlsList = pqScriptDtlsService.getScriptDtlsDataList(devReportParam.getScriptId()); Map> scriptMap = pqScriptDtlsList.stream().collect(Collectors.groupingBy(PqScriptDtlDataVO::getScriptCode, LinkedHashMap::new, Collectors.toList())); List allContent = detailModelDocument.getContent(); - List headingContents = Docx4jUtil.extractHeading5Contents(allContent); + List headingContents = Docx4jUtil.extractHeading5Contents(allContent, detailModelDocument); Map> contentMap = headingContents.stream().collect(Collectors.groupingBy(Docx4jUtil.HeadingContent::getHeadingText, Collectors.toList())); for (int i = 0; i < devChns; i++) { // 回路标题 - P titleParagraph = factory.createP(); - // 如果回路只有一个,则不需要加编号 - int lineNo = i + 1; - if (devChns > 1) { - Docx4jUtil.createTitle(factory, titleParagraph, "测量回路" + lineNo, 28, true); - } else { - Docx4jUtil.createTitle(factory, titleParagraph, "测量回路", 28, true); - } - todoInsertList.add(titleParagraph); + todoInsertList.add(getLineTitle(contentMap, i, devChns, factory)); // 依次处理大项文档内容 Iterator>> iterator = scriptMap.entrySet().iterator(); String prefixCode = ""; - boolean first = true; + boolean first; while (iterator.hasNext()) { Map.Entry> next = iterator.next(); + // 比如电压 V String scriptCode = next.getKey(); List dtlScriptItemList = next.getValue(); List tempContent = contentMap.get(scriptCode); - // 获取需要填充keys,索引0对应的段落key,索引1对应的表格key - List> keys = Docx4jUtil.getFillKeys(tempContent); + // 获取表格需要填充的key,此处主要是用于可能暂态时间的单位需要特殊判断用途,没有其他特殊用途 + List tableKeys = Docx4jUtil.getTableFillKeys(tempContent); // 段落keys值赋值 - List pKeys = keys.get(0); - Map pKeyValueMap = resultService.getParagraphKeysValue(scriptCode, pKeys); - List tableKeys = keys.get(1); /* tableKeys值赋值,注:由于谐波类检测数据与非谐波检测类数据的区别,此处要做区分 * 1、谐波类每个scriptIndex对应一个Excel表格 * 2、非谐波类则是一个误差范围对应一个Excel表格 @@ -1038,16 +1402,11 @@ public class PqReportServiceImpl extends ServiceImpl i if (CollUtil.isEmpty(scriptResult)) { scriptResult = new ArrayList<>(); needFill = true; - } else if (scriptResult.size() < lineNo) { + } else if (scriptResult.size() < (i + 1)) { needFill = true; } - // 控制指标名是否第一次,如果第一次就显示,不是则隐藏 - if (prefixCode.equalsIgnoreCase(scriptCode)) { - first = false; - } else { - first = true; - } + first = !prefixCode.equalsIgnoreCase(scriptCode); prefixCode = scriptCode; if (PowerConstant.TIME.contains(scriptCode)) { // 谐波类,以scriptIndex区分 @@ -1059,20 +1418,20 @@ public class PqReportServiceImpl extends ServiceImpl i )); // 谐波类针对是否第一次还要额外做个处理,因为每个测点需要单独表示 for (List scriptDtlDataItem : scriptIndexMap.values()) { - singleTestResult = resultService.getFinalContent(scriptDtlDataItem, devReportParam.getPlanCode(), pqDevVO.getId(), lineNo, tableKeys); + singleTestResult = resultService.getFinalContent(scriptDtlDataItem, devReportParam.getPlanCode(), pqDevVO.getId(), (i + 1), tableKeys); List tempList; if (first) { - tempList = fillContentInTemplate(singleTestResult.getDetail(), tempContent, factory, pKeyValueMap, tableKeys, first); + tempList = fillContentInTemplate(singleTestResult.getDetail(), tempContent, factory, tableKeys, true, scriptCode); first = false; } else { - tempList = fillContentInTemplate(singleTestResult.getDetail(), tempContent, factory, pKeyValueMap, tableKeys, first); + tempList = fillContentInTemplate(singleTestResult.getDetail(), tempContent, factory, tableKeys, false, scriptCode); } todoInsertList.addAll(tempList); } } else { // 非谐波类 - singleTestResult = resultService.getFinalContent(dtlScriptItemList, devReportParam.getPlanCode(), pqDevVO.getId(), lineNo, tableKeys); - List tempList = fillContentInTemplate(singleTestResult.getDetail(), tempContent, factory, pKeyValueMap, tableKeys, first); + singleTestResult = resultService.getFinalContent(dtlScriptItemList, devReportParam.getPlanCode(), pqDevVO.getId(), (i + 1), tableKeys); + List tempList = fillContentInTemplate(singleTestResult.getDetail(), tempContent, factory, tableKeys, first, scriptCode); todoInsertList.addAll(tempList); } if (Objects.nonNull(singleTestResult) && needFill) { @@ -1094,9 +1453,8 @@ public class PqReportServiceImpl extends ServiceImpl i * 将查询的所有有效数据填充到集合中,待后续填充文档 */ private List fillContentInTemplate(Map>>>> finalContent, List tempContent, - ObjectFactory factory, Map pKeyValueMap, List tableKeys, boolean first) { + ObjectFactory factory, List tableKeys, boolean first, String scriptCode) { String influenceContent = ""; - List todoInsertList = new ArrayList<>(); if (CollUtil.isNotEmpty(finalContent)) { Iterator>>>>> allContentIterator = finalContent.entrySet().iterator(); @@ -1145,19 +1503,19 @@ public class PqReportServiceImpl extends ServiceImpl i if (textFromP.contains(StrPool.DASHED)) { String[] pArray = textFromP.split(StrPool.DASHED); for (String item : pArray) { - text = text.concat(getValueFromDataMap(item, pKeyValueMap, errorContentKey, influence)); + text = text.concat(getValueFromDataMap(item, errorContentKey, influence, scriptCode)); } } else { // 调整标志位 if (textFromP.contains(ItemReportKeyEnum.INFLUENCE.getKey()) && !influenceFlag) { if (!influenceContent.equalsIgnoreCase(influence)) { - text = getValueFromDataMap(textFromP, pKeyValueMap, errorContentKey, influence); + text = getValueFromDataMap(textFromP, errorContentKey, influence, scriptCode); influenceFlag = true; influenceContent = influence; } } else { if (!nameFlag && first) { - text = getValueFromDataMap(textFromP, pKeyValueMap, errorContentKey, influence); + text = getValueFromDataMap(textFromP, errorContentKey, influence, scriptCode); nameFlag = true; } } @@ -1173,7 +1531,7 @@ public class PqReportServiceImpl extends ServiceImpl i if (StrUtil.isBlank(text)) { text = ""; } - text = text.concat(getValueFromDataMap(item, pKeyValueMap, errorContentKey, influence)); + text = text.concat(getValueFromDataMap(item, errorContentKey, influence, scriptCode)); } } } else { @@ -1202,7 +1560,7 @@ public class PqReportServiceImpl extends ServiceImpl i List rows = tbl.getContent(); boolean isRow = Docx4jUtil.judgeTableCross(rows.get(0)); if (isRow) { - // 获取现有行的样式 + // 获取现有表头的样式 Tr headerRow = (Tr) tbl.getContent().get(0); // 设置表头行属性 TrPr headerTrPr = factory.createTrPr(); @@ -1243,21 +1601,156 @@ public class PqReportServiceImpl extends ServiceImpl i return todoInsertList; } + /** + * 比对模式下对所有数据的填充 + * + * @return + */ + private List fillContentInTemplateContrast(List tempContent, ObjectFactory factory, ContrastTestResult contrastTestResult) { + List todoInsertList = new ArrayList<>(); + Docx4jUtil.HeadingContent headingContent = tempContent.get(0); + List tableKeys = Docx4jUtil.getTableFillKeys(tempContent); + for (Object object : headingContent.getSubContent()) { + // 段落 + if (object instanceof P) { + P innerP = factory.createP(); + // 如果是段落,渲染段落内容 + P paragraph = (P) object; + // 获取该段落的样式 + RPr rPr = Docx4jUtil.getTcPrFromParagraph(paragraph); + PPr pPr = paragraph.getPPr(); + String textFromP = Docx4jUtil.getTextFromP(paragraph); + String text = ""; + if (StrUtil.isNotBlank(textFromP)) { + // todo ... 根据配置文本填充 + } + // text为空,则不创建段落 + if (StrUtil.isNotBlank(text)) { + Docx4jUtil.addPContent(factory, innerP, text, rPr, pPr); + //插入段落 + todoInsertList.add(innerP); + } + } else if (object instanceof JAXBElement) { + // 表格需要注意深拷贝,避免修改了原对象 + JAXBElement temp = (JAXBElement) object; + JAXBElement copiedTableElement; + try { + copiedTableElement = Docx4jUtil.deepCopyTbl(temp); + } catch (Exception e) { + throw new RuntimeException(e); + } + // 解析表格并插入对应数据,最关键的是得知道表格是横向还是纵向以及表头占了几行 + Tbl tbl = copiedTableElement.getValue(); + // 获取表格的行 + List rows = tbl.getContent(); + boolean isRow = Docx4jUtil.judgeTableCross(rows.get(0)); + if (isRow) { + // 获取现有表头的样式 + Tr headerRow = (Tr) tbl.getContent().get(0); + // 设置表头行属性 + TrPr headerTrPr = factory.createTrPr(); + headerRow.setTrPr(headerTrPr); + + // 获取现有行的样式 + Tr existingRow = (Tr) tbl.getContent().get(rows.size() - 1); + // 获取现有样式 + TrPr trPr = existingRow.getTrPr(); + JAXBElement element = (JAXBElement) existingRow.getContent().get(0); + TcPr tcPr = element.getValue().getTcPr(); + TblWidth cellWidth = factory.createTblWidth(); + cellWidth.setType("dxa"); + cellWidth.setW(BigInteger.valueOf(5000 / tableKeys.size())); + tcPr.setTcW(cellWidth); + tbl.getContent().remove(existingRow); + // 需要区分谐波和非谐波 + boolean hasValidData = false; + if (contrastTestResult.isHarmonic()) { + List>>> checkResultHarmonic = contrastTestResult.getCheckResultHarmonic(); + // 检查谐波数据是否为空或无有效数据 + if (checkResultHarmonic != null && !checkResultHarmonic.isEmpty()) { + // 三层循环获取最内层的Map + for (Map>> level1Map : checkResultHarmonic) { + if (level1Map != null && !level1Map.isEmpty()) { + // 对于每个谐波次数 + for (Map.Entry>> harmonicEntry : level1Map.entrySet()) { + // 谐波次数 + String harmonicOrder = harmonicEntry.getKey(); + // 相别数据 + Map> phaseDataMap = harmonicEntry.getValue(); + if (phaseDataMap != null && !phaseDataMap.isEmpty()) { + hasValidData = true; + // 用于跟踪是否是该次谐波的第一个相 + int phaseIndex = 0; + // 该次谐波的相数(通常是3) + int phaseCount = phaseDataMap.size(); + for (Map.Entry> phaseEntry : phaseDataMap.entrySet()) { + // 相别(A相、B相、C相) + String phase = phaseEntry.getKey(); + // 具体数据 + Map dataMap = phaseEntry.getValue(); + // 创建新行,传入额外的参数用于处理合并单元格 + Tr newRow = Docx4jUtil.createHarmonicTableRow(factory, dataMap, tableKeys, trPr, tcPr, + phaseIndex == 0, phaseIndex == phaseCount - 1, 21); + tbl.getContent().add(newRow); + phaseIndex++; + } + } + } + } + } + } + } else { + // 迭代增加行,需要填充的表格keys在tableKeys集合中 + Map> dataList = contrastTestResult.getCheckResultNonHarmonic(); + if (dataList != null && !dataList.isEmpty()) { + hasValidData = true; + for (Map stringStringMap : dataList.values()) { + Tr newRow = Docx4jUtil.createCustomRow(factory, stringStringMap, tableKeys, trPr, tcPr, true, 21); + tbl.getContent().add(newRow); + } + } + } + + // 只有当有有效数据时才插入表格 + if (hasValidData) { + // 插入段落,处理下样式 + todoInsertList.add(copiedTableElement); + } + } else { + // 纵向表格暂不考虑 + } + + // 如果存在特殊说明,在表格后添加一个段落 + if (StrUtil.isNotBlank(contrastTestResult.getSpecialCase())) { + P specialCaseP = Docx4jUtil.createSpecialCaseParagraph(factory, contrastTestResult.getSpecialCase()); + todoInsertList.add(specialCaseP); + } + } + } + return todoInsertList; + } + + /** * 从数据集合中获取对应key的值,误差范围需要做个特殊处理 * - * @param item key - * @param pKeyValueMap 数据集合 + * @param item key */ - private String getValueFromDataMap(String item, Map pKeyValueMap, String errorScope, String influence) { + private String getValueFromDataMap(String item, String errorScope, String influence, String scriptCode) { String value = ""; if (item.equalsIgnoreCase(ItemReportKeyEnum.ERROR_SCOPE.getKey())) { value = "(最大允许误差:" + errorScope + ")"; } else if (item.equalsIgnoreCase(ItemReportKeyEnum.INFLUENCE.getKey())) { value = influence; - } else { - if (StrUtil.isNotBlank(pKeyValueMap.get(item))) { - value = pKeyValueMap.get(item); + } else if (item.equalsIgnoreCase(ItemReportKeyEnum.NAME.getKey())) { + PowerIndexEnum powerIndexEnum = PowerIndexEnum.getByKey(scriptCode); + if (powerIndexEnum != null) { + value = powerIndexEnum.getDesc(); + } + } else if (item.equalsIgnoreCase(ItemReportKeyEnum.NAME_DETAIL.getKey())) { + PowerIndexEnum powerIndexEnum = PowerIndexEnum.getByKey(scriptCode); + if (powerIndexEnum != null) { + value = powerIndexEnum.getDesc() + "测量准确度"; } } return value; @@ -1285,21 +1778,53 @@ public class PqReportServiceImpl extends ServiceImpl i if (Objects.isNull(pqDevVO)) { throw new BusinessException("请检查装置是否存在!"); } + + // 获取计划信息以确定模式 + AdPlan plan = null; + if (devReportParam.getPlanId() != null) { + plan = adPlanService.getById(devReportParam.getPlanId()); + } + // 获取设备型号 DevType devType = devTypeService.getById(pqDevVO.getDevType()); if (Objects.isNull(devType)) { throw new BusinessException("设备类型丢失,请联系管理员!"); } - // 找到该文件的路径 + + // 根据不同模式构建文件路径和文件名 + String filePath; + String downloadFileName; + String currrentScene = sysTestConfigService.getCurrrentScene(); - StringBuilder filePath = new StringBuilder(reportPath.concat(File.separator)); if (SceneEnum.LEAVE_FACTORY_TEST.getValue().equals(currrentScene)) { - filePath.append(pqDevVO.getCreateId()).append(ReportConstant.DOCX); + // 出厂测试场景 + filePath = reportPath.concat(File.separator).concat(pqDevVO.getCreateId()).concat(ReportConstant.DOCX); + downloadFileName = pqDevVO.getCreateId() + ReportConstant.DOCX; + } else if (plan != null) { + // 根据计划模式确定路径结构 + DictData dictDataById = dictDataService.getById(plan.getPattern()); + if (dictDataById != null && "Contrast".equals(dictDataById.getCode())) { + // 比对模式:使用新的路径结构 + String fileName = String.format("%s_%s_%s_%s.docx", + pqDevVO.getCityName() != null ? pqDevVO.getCityName() : "未知地市", + pqDevVO.getGdName() != null ? pqDevVO.getGdName() : "未知供电公司", + pqDevVO.getSubName() != null ? pqDevVO.getSubName() : "未知电站", + pqDevVO.getName() != null ? pqDevVO.getName() : "未知设备"); + filePath = reportPath.concat(File.separator).concat(plan.getName()).concat(File.separator).concat(fileName); + downloadFileName = fileName; + } else { + // 数字/模拟模式:使用原来的路径结构 + filePath = reportPath.concat(File.separator).concat(devType.getName()).concat(File.separator).concat(pqDevVO.getCreateId()).concat(ReportConstant.DOCX); + downloadFileName = pqDevVO.getCreateId() + ReportConstant.DOCX; + } } else { - filePath.append(devType.getName()).append(File.separator).append(pqDevVO.getCreateId()).append(ReportConstant.DOCX); + // 兜底:使用旧的路径结构 + filePath = reportPath.concat(File.separator).concat(devType.getName()).concat(File.separator).concat(pqDevVO.getCreateId()).concat(ReportConstant.DOCX); + downloadFileName = pqDevVO.getCreateId() + ReportConstant.DOCX; } - File reportFile = new File(filePath.toString()); + + File reportFile = new File(filePath); if (!reportFile.exists()) { // 如果文件不存在,则将改设备的报告生成状态调整为未生成 iPqDevService.updatePqDevReportState(devReportParam.getDevId(), 0); @@ -1310,8 +1835,24 @@ public class PqReportServiceImpl extends ServiceImpl i // 设置响应头 response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document"); - String fileName = pqDevVO.getCreateId() + ReportConstant.DOCX; - response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\""); +// response.setHeader("Content-Disposition", "attachment; filename=\"" + downloadFileName + "\""); + try { + // 对中文文件名进行URL编码// 将+号替换为%20(空格的正确编码) + String encodedFileName = URLEncoder.encode(downloadFileName, "UTF-8") + .replaceAll("\\+", "%20"); + + // 使用RFC 5987标准格式 + response.setHeader("Content-Disposition", + "attachment; filename*=UTF-8''" + encodedFileName); + + // 或者同时提供两种格式以获得最好的兼容性 + // response.setHeader("Content-Disposition", + // "attachment; filename=\"" + encodedFileName + "\"; filename*=UTF-8''" + encodedFileName); + } catch (UnsupportedEncodingException e) { + // 处理编码异常 + response.setHeader("Content-Disposition", + "attachment; filename=\"download.docx\""); + } // 将文件内容写入响应输出流 byte[] buffer = new byte[1024]; @@ -1380,7 +1921,7 @@ public class PqReportServiceImpl extends ServiceImpl i /** * 处理基础模版中的信息,非数据页报告 - * 此处为什么要抽出拼接的前缀&后缀,是因为Docx4j工具包替换时会默认增加${},故在使用docx4j时前后缀必须为空 + * 因为Docx4j工具包替换时会默认增加${} */ private Map dealBaseModelData(PqDevVO pqDevVO, DevType devType) { // 首先获取非数据页中需要的信息 @@ -1417,7 +1958,7 @@ public class PqReportServiceImpl extends ServiceImpl i if (StrUtil.isNotBlank(delegate)) { DictData delegateDictData = dictDataService.getDictDataById(pqDevVO.getManufacturer()); if (ObjectUtil.isNotNull(delegateDictData)) { - baseModelMap.put(BaseReportKeyEnum.DELEGATE.getKey(), dictData.getName()); + baseModelMap.put(BaseReportKeyEnum.DELEGATE.getKey(), delegateDictData.getName()); } else { baseModelMap.put(BaseReportKeyEnum.DELEGATE.getKey(), StrPool.TAB); } @@ -1442,6 +1983,80 @@ public class PqReportServiceImpl extends ServiceImpl i baseModelMap.put(BaseReportKeyEnum.MONTH.getKey(), DateUtil.format(new Date(), DatePattern.SIMPLE_MONTH_PATTERN).substring(4)); baseModelMap.put(BaseReportKeyEnum.DAY.getKey(), DateUtil.format(new Date(), DatePattern.PURE_DATE_PATTERN).substring(6)); baseModelMap.put(BaseReportKeyEnum.YEAR_MONTH_DAY.getKey(), DateUtil.format(new Date(), DatePattern.NORM_DATE_PATTERN)); + baseModelMap.put(BaseReportKeyEnum.REPORT_DATE.getKey(), DateUtil.format(new Date(), DatePattern.CHINESE_DATE_PATTERN)); + return baseModelMap; + } + + /** + * + * 比对模式下需要获取的数据 + * 处理基础模版中的信息,非数据页报告 + * 因为Docx4j工具包替换时会默认增加${} + */ + private Map dealBaseModelContrastData(AdPlan plan, PqDevVO pqDevVO, DevType devType) { + // 首先获取非数据页中需要的信息 + Map baseModelMap = new HashMap<>(32); + // 委托方 + String delegate = pqDevVO.getDelegate(); + if (StrUtil.isNotBlank(delegate)) { + DictData delegateDictData = dictDataService.getDictDataById(pqDevVO.getManufacturer()); + if (ObjectUtil.isNotNull(delegateDictData)) { + baseModelMap.put(BaseReportKeyEnum.DELEGATE.getKey(), delegateDictData.getName()); + } else { + baseModelMap.put(BaseReportKeyEnum.DELEGATE.getKey(), StrPool.TAB); + } + } else { + baseModelMap.put(BaseReportKeyEnum.DELEGATE.getKey(), StrPool.TAB); + } + // 样品编号 + baseModelMap.put(BaseReportKeyEnum.SAMPLE_ID.getKey(), StrUtil.isEmpty(pqDevVO.getName()) ? StrPool.TAB : pqDevVO.getName()); + // 报告日期 + baseModelMap.put(BaseReportKeyEnum.REPORT_DATE.getKey(), DateUtil.format(new Date(), DatePattern.CHINESE_DATE_PATTERN)); + // 变电站名称 + baseModelMap.put(BaseReportKeyEnum.SUB_NAME.getKey(), pqDevVO.getSubName()); + // 检测人 + String[] members = plan.getMembers().split(","); + String member = members[0]; + SysUser memberUser = sysUserService.getById(member); + if (Objects.isNull(memberUser)) { + baseModelMap.put(BaseReportKeyEnum.CHECK_BY.getKey(), member); + } else { + baseModelMap.put(BaseReportKeyEnum.CHECK_BY.getKey(), memberUser.getName()); + } + // 负责人 + SysUser leader = sysUserService.getById(plan.getLeader()); + if (Objects.isNull(leader)) { + baseModelMap.put(BaseReportKeyEnum.AUDIT_BY.getKey(), plan.getLeader()); + } else { + baseModelMap.put(BaseReportKeyEnum.AUDIT_BY.getKey(), leader.getName()); + } + // 误差体系的名称 + String errorId = plan.getErrorSysId(); + PqErrSys errSys = pqErrSysService.getPqErrSysById(errorId); + baseModelMap.put(BaseReportKeyEnum.ERROR_SYS_NAME.getKey(), errSys.getStandardName()); + // 获取设备型号 + baseModelMap.put(BaseReportKeyEnum.DEV_TYPE.getKey(), devType.getName()); + // 制造厂家 + DictData dictData = dictDataService.getDictDataById(pqDevVO.getManufacturer()); + if (ObjectUtil.isNotNull(dictData)) { + baseModelMap.put(BaseReportKeyEnum.MANUFACTURER.getKey(), dictData.getName()); + } else { + baseModelMap.put(BaseReportKeyEnum.MANUFACTURER.getKey(), StrPool.TAB); + } + // 生产日期 + baseModelMap.put(BaseReportKeyEnum.CREATE_DATE.getKey(), LocalDateTimeUtil.format(pqDevVO.getCreateDate(), DatePattern.NORM_DATE_PATTERN)); + // 检测日期 + PqDevSub devSub = iPqDevSubService.lambdaQuery().eq(PqDevSub::getDevId, pqDevVO.getId()).one(); + if (Objects.nonNull(devSub)) { + LocalDateTime checkTime = devSub.getCheckTime(); + if (Objects.nonNull(checkTime)) { + baseModelMap.put(BaseReportKeyEnum.TEST_DATE.getKey(), DateUtil.format(checkTime, DatePattern.NORM_DATE_PATTERN)); + } else { + baseModelMap.put(BaseReportKeyEnum.TEST_DATE.getKey(), StrPool.TAB); + } + } else { + baseModelMap.put(BaseReportKeyEnum.TEST_DATE.getKey(), StrPool.TAB); + } return baseModelMap; } @@ -1736,4 +2351,256 @@ public class PqReportServiceImpl extends ServiceImpl i } + /** + * 创建回路标题到报告中 + */ + private P getLineTitle(Map> contentMap, int currentLine, int totalLine, ObjectFactory factory) { + List headingContents = contentMap.get(PowerIndexEnum.LINE_TITLE.getKey()); + int lineNo = currentLine + 1; + // 如果contentMap中有指定内容,创建大纲级别为2的标题 + if (CollUtil.isNotEmpty(headingContents)) { + return Docx4jUtil.createTitle(factory, 2, totalLine > 1 ? lineNo + ".测量回路" + lineNo : lineNo + ".测量回路", + "SimSun", 30, true); + } + // 没有模板配置时,创建默认样式段落 + P titleParagraph = factory.createP(); + Docx4jUtil.createTitle(factory, titleParagraph, totalLine > 1 ? "测量回路" + lineNo : "测量回路", + 28, true); + return titleParagraph; + } + + + /** + * 创建回路标题到报告中 + */ + private P getContrastLineTitle(Map> contentMap, int monitorNum, int index, ObjectFactory factory) { + List headingContents = contentMap.get(PowerIndexEnum.LINE_TITLE.getKey()); + // 如果contentMap中有指定内容,创建大纲级别为2的标题 + if (CollUtil.isNotEmpty(headingContents)) { + return Docx4jUtil.createTitle(factory, 2, index + ".测量回路" + monitorNum, + "SimSun", 30, true); + } + // 没有模板配置时,创建默认样式段落 + P titleParagraph = factory.createP(); + Docx4jUtil.createTitle(factory, titleParagraph, "测量回路" + monitorNum, + 28, true); + return titleParagraph; + } + + /** + * 创建检测项标题 + * + * @param title 标题名称 + * @return 标题段落 + */ + private P getContrastScriptTitle(String title, ObjectFactory factory) { + return Docx4jUtil.createTitle(factory, 3, title, + "SimSun", 28, true); + } + + /** + * 设置表格单元格纵向合并开始 + * + * @param row 表格行 + * @param cellIndex 单元格索引 + * @param rowSpan 合并行数 + * @param factory 对象工厂 + */ + private void setVerticalMergeStart(Tr row, int cellIndex, int rowSpan, ObjectFactory factory) { + if (row.getContent() != null && cellIndex < row.getContent().size()) { + Object cellObj = row.getContent().get(cellIndex); + if (cellObj instanceof JAXBElement) { + JAXBElement jaxbElement = (JAXBElement) cellObj; + if (jaxbElement.getValue() instanceof Tc) { + Tc tc = (Tc) jaxbElement.getValue(); + TcPr tcPr = tc.getTcPr(); + if (tcPr == null) { + tcPr = factory.createTcPr(); + tc.setTcPr(tcPr); + } + // 设置纵向合并开始 + TcPrInner.VMerge vMerge = new TcPrInner.VMerge(); + vMerge.setVal("restart"); + tcPr.setVMerge(vMerge); + + // 设置跨行数(如果需要的话) + if (rowSpan > 1) { + TcPrInner.GridSpan gridSpan = new TcPrInner.GridSpan(); + gridSpan.setVal(BigInteger.valueOf(1)); // 列跨度为1 + tcPr.setGridSpan(gridSpan); + } + } + } + } + } + + /** + * 设置表格单元格纵向合并延续 + * + * @param row 表格行 + * @param cellIndex 单元格索引 + * @param factory 对象工厂 + */ + private void setVerticalMergeContinue(Tr row, int cellIndex, ObjectFactory factory) { + if (row.getContent() != null && cellIndex < row.getContent().size()) { + Object cellObj = row.getContent().get(cellIndex); + if (cellObj instanceof JAXBElement) { + JAXBElement jaxbElement = (JAXBElement) cellObj; + if (jaxbElement.getValue() instanceof Tc) { + Tc tc = (Tc) jaxbElement.getValue(); + + // 清空单元格内容,因为它将被合并 + tc.getContent().clear(); + // 添加一个空段落(Word表格单元格需要至少有一个段落) + P emptyP = factory.createP(); + tc.getContent().add(emptyP); + + // 设置单元格属性 + TcPr tcPr = tc.getTcPr(); + if (tcPr == null) { + tcPr = factory.createTcPr(); + tc.setTcPr(tcPr); + } + // 设置纵向合并延续 + TcPrInner.VMerge vMerge = new TcPrInner.VMerge(); + // vMerge.setVal("continue"); // 不设置val或设置为空表示延续 + tcPr.setVMerge(vMerge); + } + } + } + } + + /** + * 设置谐波表格次数列的纵向合并 + * 参考Docx4jUtil的实现方式 + * + * @param row 表格行 + * @param cellIndex 单元格索引(通常是0,次数列) + * @param mergeType 合并类型:"restart"开始,"continue"延续 + * @param factory 对象工厂 + */ + private void setHarmonicCellVMerge(Tr row, int cellIndex, String mergeType, ObjectFactory factory) { + if (row.getContent() != null && cellIndex < row.getContent().size()) { + Object cellObj = row.getContent().get(cellIndex); + Tc tc = null; + + // 获取Tc对象 + if (cellObj instanceof JAXBElement) { + JAXBElement jaxbElement = (JAXBElement) cellObj; + if (jaxbElement.getValue() instanceof Tc) { + tc = (Tc) jaxbElement.getValue(); + } + } else if (cellObj instanceof Tc) { + tc = (Tc) cellObj; + } + + if (tc != null) { + // 设置单元格属性 + TcPr tcPr = tc.getTcPr(); + if (tcPr == null) { + tcPr = factory.createTcPr(); + tc.setTcPr(tcPr); + } + + // 设置VMerge + TcPrInner.VMerge vMerge = new TcPrInner.VMerge(); + vMerge.setVal(mergeType); + tcPr.setVMerge(vMerge); + } + } + } + + /** + * 只设置次数列的纵向合并,参考Docx4jUtil样品结果列的实现 + * + * @param row 表格行 + * @param cellIndex 次数列索引(通常是0) + * @param mergeType 合并类型:"restart"开始,"continue"延续 + * @param factory 对象工厂 + */ + private void setOnlyHarmonicColumnVMerge(Tr row, int cellIndex, String mergeType, ObjectFactory factory) { + if (row.getContent() != null && cellIndex < row.getContent().size()) { + Object cellObj = row.getContent().get(cellIndex); + Tc tc = null; + + // 获取Tc对象 + if (cellObj instanceof JAXBElement) { + JAXBElement jaxbElement = (JAXBElement) cellObj; + if (jaxbElement.getValue() instanceof Tc) { + tc = (Tc) jaxbElement.getValue(); + } + } else if (cellObj instanceof Tc) { + tc = (Tc) cellObj; + } + + if (tc != null) { + // 如果是continue,按照Docx4jUtil的方式清空内容 + if ("continue".equals(mergeType)) { + // 清空单元格内容 + tc.getContent().clear(); + // 添加空段落 + P emptyP = factory.createP(); + tc.getContent().add(emptyP); + } + + // 设置单元格属性 + TcPr tcPr = tc.getTcPr(); + if (tcPr == null) { + tcPr = factory.createTcPr(); + tc.setTcPr(tcPr); + } + + // 设置VMerge + TcPrInner.VMerge vMerge = new TcPrInner.VMerge(); + vMerge.setVal(mergeType); + tcPr.setVMerge(vMerge); + } + } + } + + /** + * 简单的VMerge设置,只处理次数列 + * + * @param row 表格行 + * @param cellIndex 单元格索引(0表示次数列) + * @param mergeType 合并类型:"restart"或"continue" + * @param factory 对象工厂 + */ + private void setSimpleVMerge(Tr row, int cellIndex, String mergeType, ObjectFactory factory) { + if (row.getContent() != null && cellIndex < row.getContent().size()) { + Object cellObj = row.getContent().get(cellIndex); + Tc tc = null; + + // 获取Tc对象 + if (cellObj instanceof JAXBElement) { + JAXBElement jaxbElement = (JAXBElement) cellObj; + if (jaxbElement.getValue() instanceof Tc) { + tc = (Tc) jaxbElement.getValue(); + } + } else if (cellObj instanceof Tc) { + tc = (Tc) cellObj; + } + + if (tc != null) { + // 如果是continue,清空次数列内容 + if ("continue".equals(mergeType)) { + tc.getContent().clear(); + P emptyP = factory.createP(); + tc.getContent().add(emptyP); + } + + // 设置VMerge + TcPr tcPr = tc.getTcPr(); + if (tcPr == null) { + tcPr = factory.createTcPr(); + tc.setTcPr(tcPr); + } + TcPrInner.VMerge vMerge = new TcPrInner.VMerge(); + vMerge.setVal(mergeType); + tcPr.setVMerge(vMerge); + } + } + } + + } diff --git a/detection/src/main/java/com/njcn/gather/result/pojo/vo/MonitorResultVO.java b/detection/src/main/java/com/njcn/gather/result/pojo/vo/MonitorResultVO.java index 52857acf..94c33564 100644 --- a/detection/src/main/java/com/njcn/gather/result/pojo/vo/MonitorResultVO.java +++ b/detection/src/main/java/com/njcn/gather/result/pojo/vo/MonitorResultVO.java @@ -12,7 +12,7 @@ import javax.validation.constraints.NotNull; * @data 2025-09-10 */ @Data -public class MonitorResultVO { +public class MonitorResultVO implements Comparable { /** * 监测点id @@ -69,4 +69,21 @@ public class MonitorResultVO { @ApiModelProperty(value = "数据源类型", required = true) @NotBlank(message = DetectionValidMessage.DEV_MONITOR_RESULT_TYPE_NOT_BLANK) private String resultType; + + /** + * 根据线路号排序 + */ + @Override + public int compareTo(MonitorResultVO other) { + if (this.monitorNum == null && other.monitorNum == null) { + return 0; + } + if (this.monitorNum == null) { + return 1; + } + if (other.monitorNum == null) { + return -1; + } + return this.monitorNum.compareTo(other.monitorNum); + } } diff --git a/detection/src/main/java/com/njcn/gather/result/service/IResultService.java b/detection/src/main/java/com/njcn/gather/result/service/IResultService.java index 0a5a2bb5..a34beae0 100644 --- a/detection/src/main/java/com/njcn/gather/result/service/IResultService.java +++ b/detection/src/main/java/com/njcn/gather/result/service/IResultService.java @@ -1,9 +1,13 @@ package com.njcn.gather.result.service; +import com.njcn.gather.device.pojo.vo.PqDevVO; +import com.njcn.gather.report.pojo.DevReportParam; +import com.njcn.gather.report.pojo.result.ContrastTestResult; import com.njcn.gather.report.pojo.result.SingleTestResult; import com.njcn.gather.result.pojo.param.ResultParam; import com.njcn.gather.result.pojo.vo.*; import com.njcn.gather.script.pojo.vo.PqScriptDtlDataVO; +import com.njcn.gather.system.dictionary.pojo.po.DictTree; import java.util.List; import java.util.Map; @@ -88,13 +92,6 @@ public interface IResultService { */ SingleTestResult getFinalContent(List checkDataVOList, String planCode, String devId, Integer lineNo, List tableKeys); - /** - * 获取段落中指定的key对应的值 - * - * @param itemCode 测试大项code - * @param pKeys 待填充的值 - */ - Map getParagraphKeysValue(String itemCode, List pKeys); /** * 获取比对式表单头 @@ -128,4 +125,14 @@ public interface IResultService { * @return */ List getCheckItem(String devId, String chnNum, Integer num); + + /** + * 获取设备比对式结果,用于出比对检测的报告 + * @param devReportParam 设备报告参数 + * @param pqDevVO 设备信息 省去一次sql查询 + * @return 该设备的比对式结果 + */ + Map> getContrastResultForReport(DevReportParam devReportParam, PqDevVO pqDevVO); + + ContrastTestResult getContrastResultHarm(MonitorResultVO monitorResultVO, List scriptId, Integer planCode, DictTree dictTree); } diff --git a/detection/src/main/java/com/njcn/gather/result/service/impl/ResultServiceImpl.java b/detection/src/main/java/com/njcn/gather/result/service/impl/ResultServiceImpl.java index 105f6caf..e3a5f2ec 100644 --- a/detection/src/main/java/com/njcn/gather/result/service/impl/ResultServiceImpl.java +++ b/detection/src/main/java/com/njcn/gather/result/service/impl/ResultServiceImpl.java @@ -3,6 +3,7 @@ package com.njcn.gather.result.service.impl; import cn.afterturn.easypoi.excel.entity.ExportParams; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.date.DatePattern; import cn.hutool.core.text.StrPool; import cn.hutool.core.util.ObjectUtil; @@ -34,6 +35,7 @@ import com.njcn.gather.device.pojo.enums.CommonEnum; import com.njcn.gather.device.pojo.enums.PatternEnum; import com.njcn.gather.device.pojo.po.PqDev; import com.njcn.gather.device.pojo.po.PqStandardDev; +import com.njcn.gather.device.pojo.vo.PqDevVO; import com.njcn.gather.device.service.IPqDevService; import com.njcn.gather.device.service.IPqStandardDevService; import com.njcn.gather.err.service.IPqErrSysService; @@ -45,10 +47,12 @@ import com.njcn.gather.plan.pojo.po.AdPlanTestConfig; import com.njcn.gather.plan.service.IAdPlanService; import com.njcn.gather.plan.service.IAdPlanTestConfigService; import com.njcn.gather.pojo.enums.DetectionResponseEnum; +import com.njcn.gather.report.pojo.DevReportParam; import com.njcn.gather.report.pojo.constant.PowerConstant; import com.njcn.gather.report.pojo.enums.AffectEnum; import com.njcn.gather.report.pojo.enums.ItemReportKeyEnum; import com.njcn.gather.report.pojo.enums.PowerIndexEnum; +import com.njcn.gather.report.pojo.result.ContrastTestResult; import com.njcn.gather.report.pojo.result.SingleTestResult; import com.njcn.gather.result.pojo.enums.ResultUnitEnum; import com.njcn.gather.result.pojo.param.ResultParam; @@ -1520,43 +1524,6 @@ public class ResultServiceImpl implements IResultService { return JSONUtil.toBean(filedValue, DetectionData.class); } - - /** - * 获取段落中指定的key对应的值,目前主要为测试大项名称服务,通过code匹配 - * - * @param itemCode 测试大项code - * @param pKeys 待填充的值 - */ - @Override - public Map getParagraphKeysValue(String itemCode, List pKeys) { - Map map = new HashMap<>(); - if (CollUtil.isNotEmpty(pKeys)) { - for (String pKey : pKeys) { - ItemReportKeyEnum reportKeyEnum = ItemReportKeyEnum.getByKey(pKey); - if (Objects.nonNull(reportKeyEnum)) { - if (reportKeyEnum.getKey().equals(ItemReportKeyEnum.NAME.getKey())) { - PowerIndexEnum indexEnum = PowerIndexEnum.getByKey(itemCode); - if (Objects.nonNull(indexEnum)) { - map.put(reportKeyEnum.getKey(), indexEnum.getDesc()); - } else { - log.error("电能指标枚举未找到测试项"); - } - } else if (reportKeyEnum.getKey().equals(ItemReportKeyEnum.NAME_DETAIL.getKey())) { - PowerIndexEnum indexEnum = PowerIndexEnum.getByKey(itemCode); - if (Objects.nonNull(indexEnum)) { - map.put(reportKeyEnum.getKey(), indexEnum.getDesc().concat("测量准确度")); - } else { - log.error("电能指标枚举未找到测试项"); - } - } - } else { - log.error("段落枚举未找到占用符"); - } - } - } - return map; - } - @Override public FormContentVO getContrastFormContent(ResultParam.QueryParam queryParam) { FormContentVO formContentVO = new FormContentVO(); @@ -1843,6 +1810,798 @@ public class ResultServiceImpl implements IResultService { return result; } + /** + * 获取对比结果 + * + * @param devReportParam 设备报告参数 + * @param pqDevVO 设备信息 省去一次sql查询 + */ + @Override + public Map> getContrastResultForReport(DevReportParam devReportParam, PqDevVO pqDevVO) { + Map> finalContent = new LinkedHashMap<>(); + List contrastTestResults = new ArrayList<>(); + // 获取该设备下参与测试的回路(已有检测结论的) + List monitorResultVOS = this.getMonitorResult(devReportParam.getDevId()); + if (CollectionUtil.isNotEmpty(monitorResultVOS)) { + Collections.sort(monitorResultVOS); + // 获取该计划下所有的检测指标,注:如果用户应用的是录波数据,频率这个指标不用出结果,因为当前的算法无法得出合理的结果 + List scriptList = adPlanService.getScriptListContrast(devReportParam.getPlanId()); + // 对测试项按字典表排个序 + scriptList = dictTreeService.sort(scriptList); + + AdPlan adPlan = adPlanService.getById(devReportParam.getPlanId()); + if (CollectionUtil.isNotEmpty(scriptList)) { + for (MonitorResultVO monitorResultVO : monitorResultVOS) { + int monitorNum = monitorResultVO.getMonitorNum(); + // 看看当前这个回路结论的来源,可能是某次的实时数据、某次的测试下的某次录波数据、某次统计数据 + for (String scriptId : scriptList) { + // 获取该指标的code,需要判断是否为谐波还是非谐波的指标 + DictTree dictTree = dictTreeService.getById(scriptId); + // 本处的scriptId为父级,我需要获取自己 + List subScriptIds = dictTreeService.getChildIds(scriptId); + String scriptCode = dictTree.getCode(); + if (PowerConstant.TIME.contains(scriptCode)) { + // 谐波类,谐波类稍微特殊一点,存在2~50次的数据,所以需要特殊处理 + ContrastTestResult contrastTestResult = getContrastResultHarm(monitorResultVO, subScriptIds, adPlan.getCode(), dictTree); + if (Objects.nonNull(contrastTestResult)) { + contrastTestResults.add(contrastTestResult); + } + } else { + // 非谐波类,非谐波需要注意T相和三相 + ContrastTestResult contrastTestResult = getContrastResultNonHarm(monitorResultVO, subScriptIds, adPlan.getCode(), dictTree); + if (Objects.nonNull(contrastTestResult)) { + contrastTestResults.add(contrastTestResult); + } + } + } + finalContent.put(monitorNum, contrastTestResults); + } + } + return finalContent; + } + return Collections.emptyMap(); + } + + + /** + * 获取谐波类数据的结果数据 + * + * @param monitorResultVO 数据来源信息 + * @param subScriptIds 实际检测指标id + * @param planCode 计划code,表名需要 + * @param dictTree 测试指标信息 + * @return 结果数据 + */ + @Override + public ContrastTestResult getContrastResultHarm(MonitorResultVO monitorResultVO, List subScriptIds, Integer planCode, DictTree dictTree) { + ContrastHarmonicResult result = contrastHarmonicService.getContrastResultHarm(planCode, monitorResultVO.getMonitorId(), subScriptIds, monitorResultVO.getResultType(), Integer.parseInt(monitorResultVO.getWhichTime())); + // 收集组数数据 + int numOfData = contrastHarmonicService.getNumOfData(planCode, monitorResultVO.getMonitorId(), subScriptIds, monitorResultVO.getResultType(), Integer.parseInt(monitorResultVO.getWhichTime())); + if (Objects.nonNull(result)) { + // 谐波类的获取2~50次的数据 + ContrastTestResult contrastTestResult = new ContrastTestResult(); + contrastTestResult.setScriptCode(dictTree.getCode()); + contrastTestResult.setScriptName(dictTree.getName()); + contrastTestResult.setHarmonic(true); + + // 判断是否为间谐波 + boolean isInterHarmonic = "HSV".equals(dictTree.getCode()) || "HSI".equals(dictTree.getCode()); + + // 根据指标代码确定小数位数 + Integer decimalPlaces = getDecimalPlacesByScriptCode(dictTree.getCode()); + + // 默认结果是符合的,后续解析过程中存在不符合的,则改为不符合 + // 构建谐波数据结果Map: 次数 -> 相别 -> List> + List>>> harmonicResult = new ArrayList<>(); + List allResult = new ArrayList<>(); + + // 收集特殊情况信息 + Map> zeroFilteredMap = new LinkedHashMap<>(); // 双零过滤的次数和相别 + Map>> unComparableMap = new LinkedHashMap<>(); // 无法比较的次数和相别(按组数分组) + Map>> unqualifiedMap = new LinkedHashMap<>(); // 不符合的次数和相别(按组数分组) + int totalDataPoints = 0; // 统计总的数据点数 + int zeroFilteredPoints = 0; // 统计双零过滤的数据点数 + + // 遍历 2~50 次谐波 + for (int harmNum = 2; harmNum <= 50; harmNum++) { + String harmKey = String.valueOf(harmNum); + Map>> checkResultHarmonic = new LinkedHashMap<>(); + List zeroFilteredPhases = new ArrayList<>(); // 当前次数被过滤的相别 + + try { + Map> phaseData = new LinkedHashMap<>(); + boolean hasNonZeroData = false; // 标记是否有非双零数据 + + for (String phase : PowerConstant.PHASE_ABC) { + Field field = result.getClass().getDeclaredField(phase + "Value" + harmKey); + field.setAccessible(true); + String valueJson = (String) field.get(result); + Map singlePhaseData = parseHarmonicPhaseData(valueJson, phase.toUpperCase() + "相", harmNum, numOfData, dictTree.getCode(), decimalPlaces); + + if (CollUtil.isNotEmpty(singlePhaseData)) { + totalDataPoints++; // 统计总数据点 + + // 检查是否为双零情况 + String standardVal = singlePhaseData.get(ItemReportKeyEnum.STANDARD.getKey()); + String testVal = singlePhaseData.get(ItemReportKeyEnum.TEST.getKey()); + + // 判断是否为双零(标准值和被检值都为0或"0.0"或"0") + boolean isZeroFiltered = isZeroValue(standardVal) && isZeroValue(testVal); + + if (isZeroFiltered) { + zeroFilteredPoints++; // 统计双零点 + // 双零情况,记录但不加入结果判定 + zeroFilteredPhases.add(phase.toUpperCase() + "相"); + // 将结果改为特殊标记,不参与整体结论判定 + singlePhaseData.put(ItemReportKeyEnum.RESULT.getKey(), "双零过滤"); + } else { + // 有非双零数据 + hasNonZeroData = true; + // 非双零情况,正常处理结果 + String resultTemp = singlePhaseData.get(ItemReportKeyEnum.RESULT.getKey()); + if (StrUtil.isNotBlank(resultTemp)) { + allResult.add(resultTemp); + + // 收集特殊情况 + if ("无法比较".equals(resultTemp)) { + String numOfDataStr = singlePhaseData.get(ItemReportKeyEnum.NUM_OF_DATA.getKey()); + unComparableMap.computeIfAbsent(harmNum, k -> new LinkedHashMap<>()) + .computeIfAbsent(numOfDataStr, k -> new ArrayList<>()) + .add(phase.toUpperCase() + "相"); + } else if ("不符合".equals(resultTemp)) { + String numOfDataStr = singlePhaseData.get(ItemReportKeyEnum.NUM_OF_DATA.getKey()); + unqualifiedMap.computeIfAbsent(harmNum, k -> new LinkedHashMap<>()) + .computeIfAbsent(numOfDataStr, k -> new ArrayList<>()) + .add(phase.toUpperCase() + "相"); + } + } + } + + phaseData.put(phase.toUpperCase() + "相", singlePhaseData); + } + } + + // 记录当前次数的双零过滤情况 + if (!zeroFilteredPhases.isEmpty()) { + zeroFilteredMap.put(harmNum, zeroFilteredPhases); + } + + // 只有当该次谐波有非双零数据时,才添加到结果集中 + if (hasNonZeroData) { + checkResultHarmonic.put(harmKey, phaseData); + // 如果该次谐波有数据,添加到结果Map中 + if (CollUtil.isNotEmpty(checkResultHarmonic)) { + harmonicResult.add(checkResultHarmonic); + } + } + } catch (NoSuchFieldException | IllegalAccessException e) { + log.warn("获取第{}次谐波数据失败: {}", harmNum, e.getMessage()); + } + } + + // 判断是否所有数据都是双零 + boolean allDataIsZero = (totalDataPoints > 0 && totalDataPoints == zeroFilteredPoints); + + // 生成specialCase描述 + String specialCase = generateHarmonicSpecialCase(zeroFilteredMap, unComparableMap, unqualifiedMap, + totalDataPoints, zeroFilteredPoints, isInterHarmonic); + + // 设置检测结果 + String overallResult; + if (allDataIsZero) { + // 如果所有数据都是双零,结果为符合 + overallResult = "符合"; + } else { + // 根据所有相数据的结果判断总体结论 + overallResult = determineOverallResult(allResult); + } + + contrastTestResult.setResult(overallResult); + contrastTestResult.setSpecialCase(specialCase); + contrastTestResult.setCheckResultHarmonic(harmonicResult); + + return contrastTestResult; + } + return null; + } + + /** + * 判断值是否为零 + * @param value 字符串值 + * @return 是否为零 + */ + private boolean isZeroValue(String value) { + if (StrUtil.isBlank(value) || "/".equals(value) || "null".equals(value)) { + return false; // 空值或无效值不算零 + } + try { + double d = Double.parseDouble(value); + return d == 0.0; + } catch (NumberFormatException e) { + return false; + } + } + + /** + * 生成谐波类特殊情况描述 + * @param zeroFilteredMap 双零过滤的次数和相别 + * @param unComparableMap 无法比较的次数和相别 + * @param unqualifiedMap 不符合的次数和相别 + * @param totalDataPoints 总数据点数 + * @param zeroFilteredPoints 双零过滤的数据点数 + * @param isInterHarmonic 是否为间谐波 + * @return 特殊情况描述 + */ + private String generateHarmonicSpecialCase(Map> zeroFilteredMap, + Map>> unComparableMap, + Map>> unqualifiedMap, + int totalDataPoints, int zeroFilteredPoints, + boolean isInterHarmonic) { + StringBuilder specialCaseDesc = new StringBuilder(); + boolean hasZeroFiltered = false; + boolean allZero = false; + + // 1. 检查双零过滤情况 + if (!zeroFilteredMap.isEmpty()) { + hasZeroFiltered = true; + // 判断是否所有数据都是双零 + allZero = (totalDataPoints > 0 && totalDataPoints == zeroFilteredPoints); + + if (allZero) { + // 所有次数的所有相别都是双零,直接返回 + specialCaseDesc.append("注:因所有谐波次数的标准值与被检值均为0,检测结论为符合。"); + return specialCaseDesc.toString(); + } + } + + // 2. 处理无法比较情况 + if (!unComparableMap.isEmpty()) { + if (specialCaseDesc.length() > 0) { + specialCaseDesc.append(" "); + } else { + specialCaseDesc.append("注:"); + } + + // 合并相同情况的次数和相别 + Map>> groupedUnComparable = new LinkedHashMap<>(); + for (Map.Entry>> harmEntry : unComparableMap.entrySet()) { + Integer harmNum = harmEntry.getKey(); + for (Map.Entry> dataEntry : harmEntry.getValue().entrySet()) { + String numOfData = dataEntry.getKey(); + List phases = dataEntry.getValue(); + String phaseKey = String.join("、", phases); + groupedUnComparable.computeIfAbsent(numOfData, k -> new LinkedHashMap<>()) + .computeIfAbsent(phaseKey, k -> new ArrayList<>()) + .add(harmNum); + } + } + + // 生成描述 + for (Map.Entry>> dataEntry : groupedUnComparable.entrySet()) { + for (Map.Entry> phaseEntry : dataEntry.getValue().entrySet()) { + String phases = phaseEntry.getKey(); + List harmNums = phaseEntry.getValue(); + specialCaseDesc.append("第").append(formatHarmNumbers(harmNums, isInterHarmonic)).append("次谐波"); + specialCaseDesc.append(phases).append("无样本数据满足误差比较的前置条件,无法执行有效性判定。"); + } + } + } + + // 3. 处理不符合情况 + if (!unqualifiedMap.isEmpty()) { + if (specialCaseDesc.length() > 0) { + specialCaseDesc.append(" "); + } else { + specialCaseDesc.append("注:"); + } + + // 重新组织数据结构:收集所有相同组数的次数-相别组合 + Map> groupedByNumOfData = new LinkedHashMap<>(); + + for (Map.Entry>> harmEntry : unqualifiedMap.entrySet()) { + Integer harmNum = harmEntry.getKey(); + for (Map.Entry> dataEntry : harmEntry.getValue().entrySet()) { + String numOfData = dataEntry.getKey(); + List phases = dataEntry.getValue(); + + // 如果一个次数下有多个相别,合并成一个描述 + String harmPhaseDesc; + String displayHarmNum = getDisplayHarmNumber(harmNum, isInterHarmonic); + if (phases.size() == 1) { + harmPhaseDesc = "第" + displayHarmNum + "次谐波" + phases.get(0); + } else { + harmPhaseDesc = "第" + displayHarmNum + "次谐波" + String.join("、", phases); + } + groupedByNumOfData.computeIfAbsent(numOfData, k -> new ArrayList<>()) + .add(harmPhaseDesc); + } + } + + // 生成描述 + for (Map.Entry> entry : groupedByNumOfData.entrySet()) { + String numOfData = entry.getKey(); + List harmPhaseList = entry.getValue(); + + // 合并描述:第4次谐波A相、B相、C相、第32次谐波A相、第44次谐波C相 + specialCaseDesc.append(String.join("、", harmPhaseList)); + specialCaseDesc.append("收集有效组数为").append(numOfData) + .append("组,误差计算结果不符合误差标准要求。"); + } + } + + // 4. 最后添加双零过滤的笼统描述(如果有部分双零) + if (hasZeroFiltered && !allZero) { + if (specialCaseDesc.length() > 0) { + specialCaseDesc.append(" "); + } else { + specialCaseDesc.append("注:"); + } + specialCaseDesc.append("表格不展示标准值与被检值均为0的数据。"); + } + + return specialCaseDesc.length() > 0 ? specialCaseDesc.toString() : null; + } + + /** + * 获取显示用的谐波次数 + * @param harmNum 原始次数(2-50) + * @param isInterHarmonic 是否为间谐波 + * @return 显示用的次数字符串 + */ + private String getDisplayHarmNumber(int harmNum, boolean isInterHarmonic) { + if (isInterHarmonic) { + // 间谐波:2->1.5, 3->2.5, 4->3.5 ... 50->49.5 + double displayNum = harmNum - 0.5; + // 如果是整数则显示整数,否则显示小数 + if (displayNum == (int) displayNum) { + return String.valueOf((int) displayNum); + } + return String.valueOf(displayNum); + } + return String.valueOf(harmNum); + } + + /** + * 格式化谐波次数列表 + * @param harmNumbers 谐波次数列表 + * @param isInterHarmonic 是否为间谐波 + * @return 格式化的字符串,如"2、3、5"或"2-5、7、9-11"(间谐波为"1.5、2.5、3.5") + */ + private String formatHarmNumbers(List harmNumbers, boolean isInterHarmonic) { + if (harmNumbers.isEmpty()) { + return ""; + } + if (harmNumbers.size() == 1) { + return getDisplayHarmNumber(harmNumbers.get(0), isInterHarmonic); + } + + Collections.sort(harmNumbers); + + if (isInterHarmonic) { + // 间谐波不使用范围表示,直接用顿号分隔 + List displayNumbers = new ArrayList<>(); + for (Integer harmNum : harmNumbers) { + displayNumbers.add(getDisplayHarmNumber(harmNum, true)); + } + return String.join("、", displayNumbers); + } else { + // 普通谐波使用范围表示 + List parts = new ArrayList<>(); + int start = harmNumbers.get(0); + int end = start; + + for (int i = 1; i < harmNumbers.size(); i++) { + int current = harmNumbers.get(i); + if (current == end + 1) { + end = current; + } else { + if (start == end) { + parts.add(String.valueOf(start)); + } else if (end - start == 1) { + parts.add(start + "、" + end); + } else { + parts.add(start + "-" + end); + } + start = current; + end = current; + } + } + + // 处理最后一组 + if (start == end) { + parts.add(String.valueOf(start)); + } else if (end - start == 1) { + parts.add(start + "、" + end); + } else { + parts.add(start + "-" + end); + } + + return String.join("、", parts); + } + } + + /** + * 获取非谐波类数据的结果数据 + * + * @param monitorResultVO 数据来源信息 + * @param subScriptId 实际检测指标id + * @param planCode 计划code,表名需要 + * @param dictTree 测试指标信息 + * @return 结果数据 + */ + private ContrastTestResult getContrastResultNonHarm(MonitorResultVO monitorResultVO, List subScriptId, Integer planCode, DictTree dictTree) { + ContrastNonHarmonicResult result = contrastNonHarmonicService.getContrastResultHarm(planCode, monitorResultVO.getMonitorId(), subScriptId, monitorResultVO.getResultType(), Integer.parseInt(monitorResultVO.getWhichTime())); + // 收集组数数据 + int numOfData = contrastNonHarmonicService.getNumOfData(planCode, monitorResultVO.getMonitorId(), subScriptId, monitorResultVO.getResultType(), Integer.parseInt(monitorResultVO.getWhichTime())); + if (Objects.nonNull(result)) { + // 非谐波类 + ContrastTestResult contrastTestResult = new ContrastTestResult(); + contrastTestResult.setScriptCode(dictTree.getCode()); + contrastTestResult.setScriptName(dictTree.getName()); + contrastTestResult.setHarmonic(false); + List allResult = new ArrayList<>(); + Map> checkResultNonHarmonic = new LinkedHashMap<>(); + + // 根据指标代码确定小数位数 + Integer decimalPlaces = getDecimalPlacesByScriptCode(dictTree.getCode()); + + try { + // 非谐波的需要注意是否为T相还是ABC三相的 + if (PowerConstant.T_PHASE.contains(dictTree.getCode())) { + Field field; + if (PowerConstant.TB_PHASE.contains(dictTree.getCode())) { + field = result.getClass().getDeclaredField("bValue"); + } else { + field = result.getClass().getDeclaredField("tValue"); + } + field.setAccessible(true); + String valueJson = (String) field.get(result); + Map singlePhaseData = parseNonHarmonicPhaseData(valueJson, "T相", numOfData, dictTree.getCode(), decimalPlaces); + checkResultNonHarmonic.put("T相", singlePhaseData); + if (CollUtil.isNotEmpty(singlePhaseData)) { + String resultTemp = singlePhaseData.get(ItemReportKeyEnum.RESULT.getKey()); + if (StrUtil.isNotBlank(resultTemp)) { + allResult.add(resultTemp); + } + } + } else { + // ABC三相 + for (String phase : PowerConstant.PHASE_ABC) { + Field field = result.getClass().getDeclaredField(phase + "Value"); + field.setAccessible(true); + String valueJson = (String) field.get(result); + Map singlePhaseData = parseNonHarmonicPhaseData(valueJson, phase.toUpperCase() + "相", numOfData, dictTree.getCode(), decimalPlaces); + checkResultNonHarmonic.put(phase.toUpperCase() + "相", singlePhaseData); + if (CollUtil.isNotEmpty(singlePhaseData)) { + String resultTemp = singlePhaseData.get(ItemReportKeyEnum.RESULT.getKey()); + if (StrUtil.isNotBlank(resultTemp)) { + allResult.add(resultTemp); + } + } + } + } + + // 生成 specialCase 描述 + StringBuilder specialCaseDesc = new StringBuilder(); + List unComparablePhases = new ArrayList<>(); + Map> unqualifiedPhasesMap = new LinkedHashMap<>(); // 按组数分组 + + // 遍历每个相别的结果,收集特殊情况信息 + for (Map.Entry> entry : checkResultNonHarmonic.entrySet()) { + String phase = entry.getKey(); + Map phaseData = entry.getValue(); + String phaseResult = phaseData.get(ItemReportKeyEnum.RESULT.getKey()); + + if ("无法比较".equals(phaseResult)) { + unComparablePhases.add(phase); + } else if ("不符合".equals(phaseResult)) { + String numOfDataStr = phaseData.get(ItemReportKeyEnum.NUM_OF_DATA.getKey()); + // 按组数分组存储相别 + unqualifiedPhasesMap.computeIfAbsent(numOfDataStr, k -> new ArrayList<>()).add(phase); + } + } + + // 生成无法比较的描述 + if (!unComparablePhases.isEmpty()) { + specialCaseDesc.append("注:"); + if (unComparablePhases.size() == 1) { + specialCaseDesc.append(unComparablePhases.get(0)); + } else { + specialCaseDesc.append(String.join("、", unComparablePhases)); + } + specialCaseDesc.append("无样本数据满足误差比较的前置条件,无法执行有效性判定。"); + } + + // 生成不符合的描述(合并相同组数的相别) + if (!unqualifiedPhasesMap.isEmpty()) { + if (specialCaseDesc.length() > 0) { + specialCaseDesc.append(" "); + } else { + specialCaseDesc.append("注:"); + } + for (Map.Entry> entry : unqualifiedPhasesMap.entrySet()) { + String numOfDataStr = entry.getKey(); + List phases = entry.getValue(); + if (phases.size() == 1) { + specialCaseDesc.append(phases.get(0)); + } else { + specialCaseDesc.append(String.join("、", phases)); + } + specialCaseDesc.append("收集有效组数为").append(numOfDataStr).append("组,误差计算结果不符合误差标准要求。"); + if (unqualifiedPhasesMap.size() > 1) { + specialCaseDesc.append(" "); + } + } + } + + // 设置检测结果 - 根据所有相数据的结果判断总体结论 + String overallResult = determineOverallResult(allResult); + contrastTestResult.setResult(overallResult); + contrastTestResult.setSpecialCase(specialCaseDesc.length() > 0 ? specialCaseDesc.toString() : null); + contrastTestResult.setCheckResultNonHarmonic(checkResultNonHarmonic); + return contrastTestResult; + } catch (Exception e) { + log.warn("获取{}数据失败", dictTree.getName()); + } + } + return null; + } + + /** + * 解析非谐波数据 + * + * @param valueJson JSON格式的数据 + * @param phase 相别标识 + * @param numOfData 收集组数 + * @param scriptCode 脚本代码,用于判断是否需要特殊格式化 + * @param decimalPlaces 小数位数,null则不格式化 + * @return 解析后的数据列表 + */ + private Map parseNonHarmonicPhaseData(String valueJson, String phase, int numOfData, String scriptCode, Integer decimalPlaces) { + try { + // 解析JSON数据,假设是 DetectionData 对象列表 + List dataList = JSON.parseArray(valueJson, DetectionData.class); + Map dataMap = new LinkedHashMap<>(); + if (CollUtil.isNotEmpty(dataList)) { + DetectionData detectionData = dataList.get(0); + + // 相别 + dataMap.put(ItemReportKeyEnum.PHASE.getKey(), phase); + // 有效组数 todo... 目前是对齐组数 + dataMap.put(ItemReportKeyEnum.NUM_OF_DATA.getKey(), String.valueOf(numOfData)); + + // 标准值 - 根据参数决定是否格式化 + String standardValue = String.valueOf(detectionData.getResultData()); + if (decimalPlaces != null && detectionData.getResultData() != null) { + standardValue = formatSignificantDigits(detectionData.getResultData(), decimalPlaces); + } + dataMap.put(ItemReportKeyEnum.STANDARD.getKey(), standardValue); + + // 被检值 - 根据参数决定是否格式化 + String testValue = String.valueOf(detectionData.getData()); + if (decimalPlaces != null && detectionData.getData() != null) { + testValue = formatSignificantDigits(detectionData.getData(), decimalPlaces); + } + dataMap.put(ItemReportKeyEnum.TEST.getKey(), testValue); + + // 误差 - 根据参数决定是否格式化 + String errorValue = String.valueOf(detectionData.getErrorData()); + if (decimalPlaces != null && detectionData.getErrorData() != null) { + errorValue = formatSignificantDigits(detectionData.getErrorData().doubleValue(), decimalPlaces); + } + dataMap.put(ItemReportKeyEnum.ERROR.getKey(), errorValue); + + // 误差范围 - 根据参数决定是否格式化 + String errorScope = String.valueOf(detectionData.getRadius()); + if (decimalPlaces != null && detectionData.getRadius() != null) { + errorScope = formatErrorRange(detectionData.getRadius(), decimalPlaces); + } + dataMap.put(ItemReportKeyEnum.A_ERROR_SCOPE.getKey(), errorScope); + + // 结论 + dataMap.put(ItemReportKeyEnum.RESULT.getKey(), getTestResult(detectionData.getIsData())); + } else { + // 相别 + dataMap.put(ItemReportKeyEnum.PHASE.getKey(), phase); + // 有效组数 + dataMap.put(ItemReportKeyEnum.NUM_OF_DATA.getKey(), String.valueOf(0)); + // 标准值 + dataMap.put(ItemReportKeyEnum.STANDARD.getKey(), "/"); + // 被检值 + dataMap.put(ItemReportKeyEnum.TEST.getKey(), "/"); + // 误差 + dataMap.put(ItemReportKeyEnum.ERROR.getKey(), "/"); + // 误差范围 + dataMap.put(ItemReportKeyEnum.A_ERROR_SCOPE.getKey(), "/"); + // 结论 + dataMap.put(ItemReportKeyEnum.RESULT.getKey(), "无法比较"); + } + return dataMap; + } catch (Exception e) { + log.warn("获取数据失败"); + + } + return new HashMap<>(); + } + + + /** + * 根据所有相数据的检测结果判断总体指标结论 + * 判断原则:所有都符合就符合,有一个不符合就不符合,如果所有都是无法比较就无法比较 + * + * @param allResult 所有相数据的检测结果集合 + * @return 总体指标结论 + */ + private String determineOverallResult(List allResult) { + if (CollUtil.isEmpty(allResult)) { + return "无法比较"; + } + + // 统计各种结果的数量 + // 是否有不符合 + boolean hasUnqualified = false; + // 是否有符合 + boolean hasQualified = false; + // 是否有无法比较 + boolean hasUncomparable = false; + + for (String result : allResult) { + if ("不符合".equals(result)) { + hasUnqualified = true; + } else if ("符合".equals(result)) { + hasQualified = true; + } else if ("无法比较".equals(result)) { + hasUncomparable = true; + } + } + + // 按照优先级判断:有一个不符合就不符合 + if (hasUnqualified) { + return "不符合"; + } + + // 如果没有不符合,且有符合的,就是符合 + if (hasQualified) { + return "符合"; + } + + // 如果全部都是无法比较 + if (hasUncomparable) { + return "无法比较"; + } + + // 默认返回无法比较(理论上不会到这里) + return "无法比较"; + } + + /** + * 解析谐波相数据 + * + * @param jsonData JSON格式的数据 + * @param phase 相别标识 + * @param harmNum 谐波次数 + * @param numOfData 收集组数 + * @param scriptCode 脚本代码,用于判断是否需要特殊格式化 + * @param decimalPlaces 小数位数,null则不格式化 + * @return 解析后的数据列表 + */ + private Map parseHarmonicPhaseData(String jsonData, String phase, int harmNum, int numOfData, String scriptCode, Integer decimalPlaces) { + try { + // 解析JSON数据,假设是 DetectionData 对象列表 + List dataList = JSON.parseArray(jsonData, DetectionData.class); + Map dataMap = new LinkedHashMap<>(); + if (CollUtil.isNotEmpty(dataList)) { + DetectionData detectionData = dataList.get(0); + // 次数 + dataMap.put(ItemReportKeyEnum.TIME.getKey(), String.valueOf(harmNum)); + // 相别 + dataMap.put(ItemReportKeyEnum.PHASE.getKey(), phase); + // 有效组数 todo... 目前是对齐组数 + dataMap.put(ItemReportKeyEnum.NUM_OF_DATA.getKey(), String.valueOf(numOfData)); + + // 标准值 - 根据参数决定是否格式化 + String standardValue = String.valueOf(detectionData.getResultData()); + if (decimalPlaces != null && detectionData.getResultData() != null) { + standardValue = formatSignificantDigits(detectionData.getResultData(), decimalPlaces); + } + dataMap.put(ItemReportKeyEnum.STANDARD.getKey(), standardValue); + + // 被检值 - 根据参数决定是否格式化 + String testValue = String.valueOf(detectionData.getData()); + if (decimalPlaces != null && detectionData.getData() != null) { + testValue = formatSignificantDigits(detectionData.getData(), decimalPlaces); + } + dataMap.put(ItemReportKeyEnum.TEST.getKey(), testValue); + + // 误差 - 根据参数决定是否格式化 + String errorValue = String.valueOf(detectionData.getErrorData()); + if (decimalPlaces != null && detectionData.getErrorData() != null) { + errorValue = formatSignificantDigits(detectionData.getErrorData().doubleValue(), decimalPlaces); + } + dataMap.put(ItemReportKeyEnum.ERROR.getKey(), errorValue); + + // 误差范围 - 根据参数决定是否格式化 + String errorScope = String.valueOf(detectionData.getRadius()); + if (decimalPlaces != null && detectionData.getRadius() != null) { + errorScope = formatErrorRange(detectionData.getRadius(), decimalPlaces); + } + dataMap.put(ItemReportKeyEnum.A_ERROR_SCOPE.getKey(), errorScope); + + // 结论 + dataMap.put(ItemReportKeyEnum.RESULT.getKey(), getTestResult(detectionData.getIsData())); + } else { + // 次数 + dataMap.put(ItemReportKeyEnum.TIME.getKey(), String.valueOf(harmNum)); + // 相别 + dataMap.put(ItemReportKeyEnum.PHASE.getKey(), phase); + // 有效组数 + dataMap.put(ItemReportKeyEnum.NUM_OF_DATA.getKey(), "0"); + // 标准值 + dataMap.put(ItemReportKeyEnum.STANDARD.getKey(), "/"); + // 被检值 + dataMap.put(ItemReportKeyEnum.TEST.getKey(), "/"); + // 误差 + dataMap.put(ItemReportKeyEnum.ERROR.getKey(), "/"); + // 误差范围 + dataMap.put(ItemReportKeyEnum.A_ERROR_SCOPE.getKey(), "/"); + // 结论 + dataMap.put(ItemReportKeyEnum.RESULT.getKey(), "无法比较"); + } + return dataMap; + } catch (Exception e) { + log.error("解析{}第{}次谐波数据失败: {}", phase, harmNum, e.getMessage()); + } + + return new HashMap<>(); + } + + /** + * 检查是否有不合格的数据 + * + * @param dataList 数据列表 + * @return true表示有不合格数据 + */ + private boolean hasUnqualifiedData(List> dataList) { + if (CollUtil.isEmpty(dataList)) { + return false; + } + for (Map data : dataList) { + String resultFlag = data.get("resultFlag"); + // resultFlag: 1-合格,2-不合格,4-无法比较 + if ("2".equals(resultFlag)) { + return true; + } + } + return false; + } + + /** + * 获取检测结果 + * + * @param monitorResultVO 回路检测结论信息 + */ + private String getTestResult(MonitorResultVO monitorResultVO) { + Integer checkResult = monitorResultVO.getCheckResult(); + return getTestResult(checkResult); + } + + /** + * 获取检测结果 + * + * @param checkResult 回路检测结论信息 + */ + private String getTestResult(Integer checkResult) { + switch (checkResult) { + case 2: + return "不符合"; + case 4: + return "无法比较"; + default: + // 因为只有 1、2、4,所以这里直接返回符合 + return "符合"; + } + } + private Map> getResultMap(DictTree dictTree, List adTypeList, String monitorId, String unit, Integer num, Integer waveNum, Boolean isWave, String code) { Map> resultMap = new LinkedHashMap<>(); @@ -2511,4 +3270,129 @@ public class ResultServiceImpl implements IResultService { } return info; } + + /** + * 格式化数值,保留指定的小数位数 + * + * @param value 原始数值 + * @param decimalPlaces 小数位数 + * @return 格式化后的字符串 + */ + private String formatSignificantDigits(double value, int decimalPlaces) { + if (value == 0.0) { + return "0"; + } + + // 使用BigDecimal进行精确计算 + BigDecimal bd = new BigDecimal(String.valueOf(value)); + + // 保留指定的小数位数,四舍五入 + bd = bd.setScale(decimalPlaces, BigDecimal.ROUND_HALF_UP); + + // 移除末尾的零(可选,根据需要决定是否保留) + String result = bd.stripTrailingZeros().toPlainString(); + + return result; + } + + /** + * 格式化误差范围,将 "-0.057735~0.057735" 格式化为 "±0.05774" + * + * @param radiusValue 误差范围值(通常是正数,表示±范围) + * @return 格式化后的误差范围字符串 + */ + private String formatErrorRange(double radiusValue) { + return formatErrorRangeWithDecimalPlaces(radiusValue, 5); + } + + /** + * 格式化误差范围,支持自定义小数位数 + * + * @param radiusValue 误差范围值(通常是正数,表示±范围) + * @param decimalPlaces 小数位数 + * @return 格式化后的误差范围字符串 + */ + /** + * 格式化误差范围字符串,支持处理"-0.05~0.05"格式并转换为"±0.05"格式 + * + * @param errorRange 误差范围字符串 + * @param decimalPlaces 小数位数 + * @return 格式化后的误差范围 + */ + private String formatErrorRange(String errorRange, Integer decimalPlaces) { + if (errorRange == null || decimalPlaces == null) { + return errorRange; + } + + try { + // 尝试直接解析为数字 + double radiusValue = Double.parseDouble(errorRange); + return formatErrorRangeWithDecimalPlaces(radiusValue, decimalPlaces); + } catch (NumberFormatException e) { + // 处理"-0.05~0.05"这种格式 + if (errorRange.contains("~")) { + String[] parts = errorRange.split("~"); + if (parts.length == 2) { + try { + double minValue = Double.parseDouble(parts[0].trim()); + double maxValue = Double.parseDouble(parts[1].trim()); + // 取绝对值的最大值作为范围 + double absMax = Math.max(Math.abs(minValue), Math.abs(maxValue)); + return formatErrorRangeWithDecimalPlaces(absMax, decimalPlaces); + } catch (NumberFormatException ex) { + log.warn("无法解析误差范围格式: {}", errorRange); + } + } + } + // 如果无法解析,返回原值 + return errorRange; + } + } + + private String formatErrorRangeWithDecimalPlaces(double radiusValue, int decimalPlaces) { + if (radiusValue == 0.0) { + return "±0"; + } + + // 取绝对值并保留指定小数位数 + double absValue = Math.abs(radiusValue); + String formattedValue = formatSignificantDigits(absValue, decimalPlaces); + + return "±" + formattedValue; + } + + /** + * 根据指标代码获取对应的小数位数 + * + * @param scriptCode 指标代码 + * @return 小数位数,null表示不格式化 + */ + private Integer getDecimalPlacesByScriptCode(String scriptCode) { + if (scriptCode == null) { + return null; + } + switch (scriptCode) { + // 保留2位小数 + case "FREQ": // 频率 + return 2; + // 保留3位小数 + case "I": // 电流 + return 3; + // 保留4位小数 + case "IMBV": // 电压不平衡度 + case "IMBA": // 电流不平衡度 + return 4; + // 保留5位小数 + case "V": // 电压 + case "HV": // 电压谐波 + case "HI": // 电流谐波 + case "HP": // 功率谐波 + case "HSV": // 电压间谐波 + case "HSI": // 电流间谐波 + return 5; + // 其他指标不格式化 + default: + return null; + } + } } diff --git a/entrance/src/test/java/com/njcn/AnalysisServiceStreamTest.java b/entrance/src/test/java/com/njcn/AnalysisServiceStreamTest.java index 541de510..6a813182 100644 --- a/entrance/src/test/java/com/njcn/AnalysisServiceStreamTest.java +++ b/entrance/src/test/java/com/njcn/AnalysisServiceStreamTest.java @@ -25,10 +25,16 @@ public class AnalysisServiceStreamTest { private ICompareWaveService compareWaveServiceImpl; // 测试文件路径 - 请根据实际情况修改 - private static final String SOURCE_CFG_PATH = "C:\\Users\\hongawen\\Desktop\\Event\\192.168.1.239\\PQ_PQLD1_000251_20250904_145126_769.cfg"; - private static final String SOURCE_DAT_PATH = "C:\\Users\\hongawen\\Desktop\\Event\\192.168.1.239\\PQ_PQLD1_000251_20250904_145126_769.dat"; - private static final String TARGET_CFG_PATH = "C:\\Users\\hongawen\\Desktop\\Event\\192.168.1.238\\PQ_PQLD2_000099_20250904_145126_750.cfg"; - private static final String TARGET_DAT_PATH = "C:\\Users\\hongawen\\Desktop\\Event\\192.168.1.238\\PQ_PQLD2_000099_20250904_145126_750.dat"; + private static final String SOURCE_CFG_PATH = "C:\\Users\\hongawen\\Desktop\\Event\\192.168.1.239\\PQ_PQLD1_000574_20250910_135244_231.cfg"; + private static final String SOURCE_DAT_PATH = "C:\\Users\\hongawen\\Desktop\\Event\\192.168.1.239\\PQ_PQLD1_000574_20250910_135244_231.dat"; + private static final String TARGET_CFG_PATH = "C:\\Users\\hongawen\\Desktop\\Event\\192.168.1.238\\PQ_PQLD2_000508_20250910_135244_197.cfg"; + private static final String TARGET_DAT_PATH = "C:\\Users\\hongawen\\Desktop\\Event\\192.168.1.238\\PQ_PQLD2_000508_20250910_135244_197.dat"; + + +// private static final String SOURCE_CFG_PATH = "F:\\hatch\\wavecompare\\数据比对\\统计数据1\\B码\\217\\PQMonitor_PQM1_000006_20200430_115517_889.cfg"; +// private static final String SOURCE_DAT_PATH = "F:\\hatch\\wavecompare\\数据比对\\统计数据1\\B码\\217\\PQMonitor_PQM1_000006_20200430_115517_889.dat"; +// private static final String TARGET_CFG_PATH = "F:\\hatch\\wavecompare\\数据比对\\统计数据1\\B码\\216\\PQMonitor_PQM1_000006_20200430_115515_479.cfg"; +// private static final String TARGET_DAT_PATH = "F:\\hatch\\wavecompare\\数据比对\\统计数据1\\B码\\216\\PQMonitor_PQM1_000006_20200430_115515_479.dat"; // 输出路径 private static final String OUTPUT_PATH = "./test-output/"; diff --git a/storage/src/main/java/com/njcn/gather/storage/service/ContrastHarmonicService.java b/storage/src/main/java/com/njcn/gather/storage/service/ContrastHarmonicService.java index 6e3a502f..e3926e10 100644 --- a/storage/src/main/java/com/njcn/gather/storage/service/ContrastHarmonicService.java +++ b/storage/src/main/java/com/njcn/gather/storage/service/ContrastHarmonicService.java @@ -3,6 +3,7 @@ package com.njcn.gather.storage.service; import com.baomidou.mybatisplus.extension.service.IService; import com.njcn.gather.storage.pojo.po.ContrastHarmonicResult; +import javax.validation.constraints.NotBlank; import java.util.List; /** @@ -33,4 +34,27 @@ public interface ContrastHarmonicService extends IService listAllResultData(String code, Integer num, Integer waveNum, Boolean isWave, String devId, List adTypeList); + + + /** + * 获取谐波检测项的比对结果 + * @param planCode 计划code + * @param monitorId 监测点ID + * @param scriptId 指标id + * @param resultType 结果类型 + * @param time 第几次检测 + * @return 检测结果 + */ + ContrastHarmonicResult getContrastResultHarm(Integer planCode, String monitorId, List scriptId, String resultType, int time); + + /** + * 去原始表获取总次数 + * @param planCode 计划code + * @param monitorId 监测点ID + * @param scriptId 指标id + * @param resultType 结果类型 + * @param time 第几次检测 + * @return 数据组数 + */ + int getNumOfData(Integer planCode, String monitorId, List scriptId, String resultType, int time); } diff --git a/storage/src/main/java/com/njcn/gather/storage/service/ContrastNonHarmonicService.java b/storage/src/main/java/com/njcn/gather/storage/service/ContrastNonHarmonicService.java index 850b4521..c1cfe0ed 100644 --- a/storage/src/main/java/com/njcn/gather/storage/service/ContrastNonHarmonicService.java +++ b/storage/src/main/java/com/njcn/gather/storage/service/ContrastNonHarmonicService.java @@ -3,6 +3,7 @@ package com.njcn.gather.storage.service; import com.baomidou.mybatisplus.extension.service.IService; import com.njcn.gather.storage.pojo.po.ContrastNonHarmonicResult; +import javax.validation.constraints.NotBlank; import java.util.List; /** @@ -39,4 +40,27 @@ public interface ContrastNonHarmonicService extends IService listAllResultData(String code, Integer num, Integer waveNum, Boolean isWave, String devId, List adTypeList); + + /** + * 获取非谐波检测项的比对结果 + * @param planCode 计划code + * @param monitorId 监测点ID + * @param scriptId 指标id + * @param resultType 结果类型 + * @param time 第几次检测 + * @return 检测结果 + */ + ContrastNonHarmonicResult getContrastResultHarm(Integer planCode, String monitorId, List scriptId, String resultType, int time); + + /** + * 去原始表获取总次数 + * @param planCode 计划code + * @param monitorId 监测点ID + * @param scriptId 指标id + * @param resultType 结果类型 + * @param time 第几次检测 + * @return 数据组数 + */ + int getNumOfData(Integer planCode, String monitorId, List scriptId, String resultType, int time); + } diff --git a/storage/src/main/java/com/njcn/gather/storage/service/impl/ContrastHarmonicServiceImpl.java b/storage/src/main/java/com/njcn/gather/storage/service/impl/ContrastHarmonicServiceImpl.java index 695c7a00..4a1fcca6 100644 --- a/storage/src/main/java/com/njcn/gather/storage/service/impl/ContrastHarmonicServiceImpl.java +++ b/storage/src/main/java/com/njcn/gather/storage/service/impl/ContrastHarmonicServiceImpl.java @@ -1,9 +1,11 @@ package com.njcn.gather.storage.service.impl; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.ObjectUtil; import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.github.yulichang.wrapper.MPJLambdaWrapper; import com.njcn.db.mybatisplus.handler.DynamicTableNameHandler; import com.njcn.gather.storage.mapper.ContrastHarmonicMappper; import com.njcn.gather.storage.pojo.po.ContrastHarmonicResult; @@ -11,6 +13,7 @@ import com.njcn.gather.storage.service.ContrastHarmonicService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.util.Collections; import java.util.List; /** @@ -45,6 +48,9 @@ public class ContrastHarmonicServiceImpl extends ServiceImpl listAllResultData(String code, Integer num, Integer waveNum, Boolean isWave, String devId, List adTypeList) { String prefix = "ad_harmonic_result_" + code; @@ -65,4 +71,72 @@ public class ContrastHarmonicServiceImpl extends ServiceImpl scriptId, String resultType, int time) { + boolean isWave = resultType.contains("wave_data"); + int waveTime = 1; + if (isWave) { + // 说明是要进一步拆解type,查询录波的数据 + // 从 wave_data_1 格式中提取录波次数 + String[] parts = resultType.split("_"); + if (parts.length > 2) { + waveTime = Integer.parseInt(parts[parts.length - 1]); + } + } + List result = this.listAllResultData(String.valueOf(planCode), time, waveTime, isWave, monitorId, scriptId); + if (CollectionUtil.isNotEmpty(result)) { + return result.get(0); + } + return null; + } + + /** + * 去原始表获取总次数 + * @param planCode 计划code + * @param monitorId 监测点ID + * @param scriptId 指标id + * @param resultType 结果类型 + * @param time 第几次检测 + * @return 数据组数 + */ + @Override + public int getNumOfData(Integer planCode, String monitorId, List scriptId, String resultType, int time) { + String prefix = "ad_harmonic_" + planCode; + DynamicTableNameHandler.setTableName(prefix); + boolean isWave = resultType.contains("wave_data"); + int waveTime = 1; + if (isWave) { + // 说明是要进一步拆解type,查询录波的数据 + // 从 wave_data_1 格式中提取录波次数 + String[] parts = resultType.split("_"); + if (parts.length > 2) { + waveTime = Integer.parseInt(parts[parts.length - 1]); + } + } + LambdaQueryChainWrapper wrapper = this.lambdaQuery() + .eq(ContrastHarmonicResult::getDevMonitorId, monitorId) + .in(ContrastHarmonicResult::getAdType, scriptId) + .eq(ContrastHarmonicResult::getFlag, 0) + .eq(ContrastHarmonicResult::getNum, time); + if(isWave){ + wrapper.eq(ContrastHarmonicResult::getDataType, "wave_data") + .eq(ContrastHarmonicResult::getWaveNum, waveTime); + }else{ + wrapper.eq(ContrastHarmonicResult::getDataType, resultType); + } + // 执行查询并统计满足条件的记录数 + int count = wrapper.count(); + DynamicTableNameHandler.remove(); + return count; + } } diff --git a/storage/src/main/java/com/njcn/gather/storage/service/impl/ContrastNonHarmonicServiceImpl.java b/storage/src/main/java/com/njcn/gather/storage/service/impl/ContrastNonHarmonicServiceImpl.java index 203f86e9..40dfd7c3 100644 --- a/storage/src/main/java/com/njcn/gather/storage/service/impl/ContrastNonHarmonicServiceImpl.java +++ b/storage/src/main/java/com/njcn/gather/storage/service/impl/ContrastNonHarmonicServiceImpl.java @@ -1,6 +1,7 @@ package com.njcn.gather.storage.service.impl; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.ObjectUtil; import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; @@ -11,6 +12,7 @@ import com.njcn.gather.storage.service.ContrastNonHarmonicService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.util.Collections; import java.util.List; /** @@ -65,4 +67,54 @@ public class ContrastNonHarmonicServiceImpl extends ServiceImpl scriptId, String resultType, int time) { + boolean isWave = resultType.contains("wave_data"); + int waveTime = 1; + if (isWave) { + // 说明是要进一步拆解type,查询录波的数据 + // 从 wave_data_1 格式中提取录波次数 + String[] parts = resultType.split("_"); + if (parts.length > 2) { + waveTime = Integer.parseInt(parts[parts.length - 1]); + } + } + List result = this.listAllResultData(String.valueOf(planCode), time, waveTime, isWave, monitorId, scriptId); + if (CollectionUtil.isNotEmpty(result)) { + return result.get(0); + } + return null; + } + + @Override + public int getNumOfData(Integer planCode, String monitorId, List scriptId, String resultType, int time) { + String prefix = "ad_non_harmonic_" + planCode; + DynamicTableNameHandler.setTableName(prefix); + boolean isWave = resultType.contains("wave_data"); + int waveTime = 1; + if (isWave) { + // 说明是要进一步拆解type,查询录波的数据 + // 从 wave_data_1 格式中提取录波次数 + String[] parts = resultType.split("_"); + if (parts.length > 2) { + waveTime = Integer.parseInt(parts[parts.length - 1]); + } + } + LambdaQueryChainWrapper wrapper = this.lambdaQuery() + .eq(ContrastNonHarmonicResult::getDevMonitorId, monitorId) + .in(ContrastNonHarmonicResult::getAdType, scriptId) + .eq(ContrastNonHarmonicResult::getFlag, 0) + .eq(ContrastNonHarmonicResult::getNum, time); + if(isWave){ + wrapper.eq(ContrastNonHarmonicResult::getDataType, "wave_data") + .eq(ContrastNonHarmonicResult::getWaveNum, waveTime); + }else{ + wrapper.eq(ContrastNonHarmonicResult::getDataType, resultType); + } + // 执行查询并统计满足条件的记录数 + int count = wrapper.count(); + DynamicTableNameHandler.remove(); + return count; + } } diff --git a/system/src/main/java/com/njcn/gather/system/dictionary/service/IDictTreeService.java b/system/src/main/java/com/njcn/gather/system/dictionary/service/IDictTreeService.java index 190496f3..0d8e9d06 100644 --- a/system/src/main/java/com/njcn/gather/system/dictionary/service/IDictTreeService.java +++ b/system/src/main/java/com/njcn/gather/system/dictionary/service/IDictTreeService.java @@ -62,4 +62,25 @@ public interface IDictTreeService extends IService { DictTree getDictTreeByCode(String code); List listByFatherIds(List fatherIdList); + + /** + * 获取父级字典树 + * @param id 字典树ID + * @return 父级字典树 + */ + DictTree queryParentById(String id); + + /** + * 根据id获取所有子节点的ID + * @param scriptId 字典树ID + * @return 所有子节点ID + */ + List getChildIds(String scriptId); + + /** + * 测试项排个序 + * @param scriptList 测试项 + * @return 有序的测试项 + */ + List sort(List scriptList); } diff --git a/system/src/main/java/com/njcn/gather/system/dictionary/service/impl/DictTreeServiceImpl.java b/system/src/main/java/com/njcn/gather/system/dictionary/service/impl/DictTreeServiceImpl.java index 5aae695d..b821df45 100644 --- a/system/src/main/java/com/njcn/gather/system/dictionary/service/impl/DictTreeServiceImpl.java +++ b/system/src/main/java/com/njcn/gather/system/dictionary/service/impl/DictTreeServiceImpl.java @@ -1,6 +1,7 @@ package com.njcn.gather.system.dictionary.service.impl; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.text.StrPool; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; @@ -20,6 +21,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; +import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Objects; @@ -146,6 +148,38 @@ public class DictTreeServiceImpl extends ServiceImpl i return null; } + @Override + public DictTree queryParentById(String id) { + DictTree temp = this.lambdaQuery().eq(DictTree::getId, id).eq(DictTree::getState, DictConst.ENABLE).one(); + return this.lambdaQuery().eq(DictTree::getId, temp.getPid()).one(); + } + + @Override + public List getChildIds(String scriptId) { + List subTree = this.lambdaQuery().eq(DictTree::getPid, scriptId) + .eq(DictTree::getState, DictConst.ENABLE) + .list(); + if(CollectionUtil.isNotEmpty(subTree)){ + return subTree.stream().map(DictTree::getId).collect(Collectors.toList()); + } + return Collections.emptyList(); + } + + @Override + public List sort(List scriptList) { + if (CollectionUtil.isEmpty(scriptList)) { + return Collections.emptyList(); + } + + return this.lambdaQuery() + .in(DictTree::getId, scriptList) + .orderByAsc(DictTree::getSort) + .list() + .stream() + .map(DictTree::getId) + .collect(Collectors.toList()); + } + private void checkRepeat(DictTreeParam dictTreeParam, boolean isExcludeSelf) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(DictTree::getPid, dictTreeParam.getPid()) // 同一父节点下不能有相同的code diff --git a/tools/report-generator/src/main/java/com/njcn/gather/tools/report/util/Docx4jUtil.java b/tools/report-generator/src/main/java/com/njcn/gather/tools/report/util/Docx4jUtil.java index bd013761..5c3f48dc 100644 --- a/tools/report-generator/src/main/java/com/njcn/gather/tools/report/util/Docx4jUtil.java +++ b/tools/report-generator/src/main/java/com/njcn/gather/tools/report/util/Docx4jUtil.java @@ -3,13 +3,17 @@ package com.njcn.gather.tools.report.util; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.text.StrPool; import cn.hutool.core.util.StrUtil; -import com.njcn.gather.tools.report.model.constant.ReportConstant; import lombok.Getter; import lombok.Setter; import org.docx4j.XmlUtils; import org.docx4j.openpackaging.packages.WordprocessingMLPackage; import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart; import org.docx4j.wml.*; +import org.docx4j.wml.TcPrInner.GridSpan; +import org.docx4j.wml.TcPrInner.VMerge; +import org.docx4j.wml.CTVerticalJc; +import org.docx4j.wml.STVerticalJc; +import org.docx4j.wml.STHeightRule; import javax.xml.bind.JAXBElement; import java.math.BigInteger; @@ -46,14 +50,12 @@ public class Docx4jUtil { // 宋体 fonts.setEastAsia("SimSun"); rPr.setRFonts(fonts); - // 设置字号 HpsMeasure size = new HpsMeasure(); // 12号字=24 size.setVal(new BigInteger("" + fontSize)); rPr.setSz(size); rPr.setSzCs(size); - // 设置粗体 if (isBold) { BooleanDefaultTrue b = new BooleanDefaultTrue(); @@ -61,28 +63,87 @@ public class Docx4jUtil { } run.setRPr(rPr); run.getContent().add(text); - // 换行 -// Br br = factory.createBr(); -// run.getContent().add(br); paragraph.getContent().add(run); } + + /** + * 创建标题,与上面的方法区别是本方法指定了大纲级别 + * + * @param factory 工厂类 + * @param level 大纲级别 + * @param titleContent 标题内容 + * @param fontStyle 字体样式 + * @param size 字体大小 + * @return 段落 + */ + public static P createTitle(ObjectFactory factory, int level, String titleContent, String fontStyle, int size, boolean isBold) { + P titleParagraph = factory.createP(); + // 设置段落属性 - 大纲级别2(标题3) + PPr pPr = factory.createPPr(); + PPrBase.OutlineLvl outlineLvl = factory.createPPrBaseOutlineLvl(); + outlineLvl.setVal(BigInteger.valueOf(level)); + pPr.setOutlineLvl(outlineLvl); + // 设置段落间距:段前1磅,段后1磅,行距1.5倍 + PPrBase.Spacing spacing = factory.createPPrBaseSpacing(); + // 段前间距:1磅 = 20缇(1磅=20缇) + spacing.setBefore(BigInteger.valueOf(20)); + // 段后间距:1磅 = 20缇 + spacing.setAfter(BigInteger.valueOf(20)); + // 行间距:1.5倍 = 360(240为单倍行距,360为1.5倍) + spacing.setLine(BigInteger.valueOf(360)); + // 行距类型设置为倍数 + spacing.setLineRule(STLineSpacingRule.AUTO); + pPr.setSpacing(spacing); + titleParagraph.setPPr(pPr); + // 创建文本运行,格式为:lineNo.测量回路 + R run = factory.createR(); + // 设置字体样式:宋体、小三(16号) + RPr rPr = factory.createRPr(); + // 设置字体为宋体 + RFonts fonts = factory.createRFonts(); + fonts.setAscii("Arial"); + fonts.setEastAsia(fontStyle); + fonts.setHAnsi(fontStyle); + rPr.setRFonts(fonts); + // 设置字号为小三(15号 = 30半角) + HpsMeasure fontSize = factory.createHpsMeasure(); + fontSize.setVal(BigInteger.valueOf(size)); + rPr.setSz(fontSize); + rPr.setSzCs(fontSize); + // 设置粗体 + if (isBold) { + BooleanDefaultTrue bold = factory.createBooleanDefaultTrue(); + rPr.setB(bold); + // 复杂脚本也设置加粗 + rPr.setBCs(bold); + } + run.setRPr(rPr); + Text text = factory.createText(); + text.setValue(titleContent); + run.getContent().add(text); + titleParagraph.getContent().add(run); + return titleParagraph; + } + + /** * 提取文档中所有Heading 5标题及其子内容 - * + *

* 该方法按文档顺序遍历内容,识别Heading 5级别的标题,并收集每个标题下的所有子内容, * 直到遇到下一个Heading 5标题为止。 - * + *

* 处理逻辑: * 1. 遇到Heading 5时,保存前一个标题组并开始新的收集 * 2. 在标题组内时,收集所有段落和表格等子内容(包括1-4级标题) * 3. 只有遇到下一个Heading 5标题时才结束当前标题组的收集 * 4. 文档末尾时,保存最后一个标题组 - * - * @param allContent 文档内所有内容对象的列表(包含段落、表格等) + * + * @param allContent 文档内所有内容对象的列表(包含段落、表格等) + * @param mainDocumentPart 主文档部分,用于获取样式定义 * @return Heading 5标题及其子内容的列表,按文档中出现的顺序排列 */ - public static List extractHeading5Contents(List allContent) { + public static List extractHeading5Contents(List allContent, MainDocumentPart mainDocumentPart) { // 参数验证 if (allContent == null || allContent.isEmpty()) { return new ArrayList<>(); @@ -95,7 +156,7 @@ public class Docx4jUtil { for (Object obj : allContent) { if (obj instanceof P) { P paragraph = (P) obj; - if (isHeading5(paragraph)) { + if (isHeading5(paragraph, mainDocumentPart)) { // 发现新的Heading 5标题,保存前一个标题组并创建新的 if (currentHeading != null) { result.add(currentHeading); @@ -118,25 +179,50 @@ public class Docx4jUtil { if (currentHeading != null) { result.add(currentHeading); } - + return result; } + /** - * 判断段落是否为Heading 5 - * @param paragraph 段落 + * 判断段落是否为Heading 5 - 通过样式名称判断 + * + * @param paragraph 段落 + * @param mainDocumentPart 主文档部分 + * @return 是否为Heading 5标题 */ - private static boolean isHeading5(P paragraph) { + private static boolean isHeading5(P paragraph, MainDocumentPart mainDocumentPart) { PPr ppr = paragraph.getPPr(); if (ppr != null) { PPrBase.PStyle pStyle = ppr.getPStyle(); - return pStyle != null && "5".equals(pStyle.getVal()); + if (pStyle != null) { + String styleVal = pStyle.getVal(); + + try { + org.docx4j.openpackaging.parts.WordprocessingML.StyleDefinitionsPart stylesPart = + mainDocumentPart.getStyleDefinitionsPart(); + if (stylesPart != null) { + org.docx4j.wml.Style style = stylesPart.getStyleById(styleVal); + if (style != null && style.getName() != null) { + String styleName = style.getName().getVal(); + + return styleName != null && ( + styleName.toLowerCase().contains("heading 5") || + styleName.toLowerCase().contains("标题 5") || + styleName.toLowerCase().equals("heading5") || + (styleName.toLowerCase().contains("heading") && styleName.contains("5")) + ); + } + } + } catch (Exception e) { + return false; + } + } } return false; } - /** * 判断表格是否横向 * @@ -230,7 +316,7 @@ public class Docx4jUtil { /** * 段落中添加内容 */ - public static void addPContent(ObjectFactory factory, P paragraph, String content, RPr rPr,PPr ppr) { + public static void addPContent(ObjectFactory factory, P paragraph, String content, RPr rPr, PPr ppr) { R run = factory.createR(); Text text = factory.createText(); text.setValue(content); @@ -281,7 +367,7 @@ public class Docx4jUtil { List pKeys = new ArrayList<>(); List tableKeys = new ArrayList<>(); if (CollUtil.isNotEmpty(tempContent)) { - // 读取该表下模板里面的内容,这整个内容需要跟随误差范围循环的,确保内容的数据比较用的一个误差范围 + // 读取该标题下所有的内容,并准备读取出所有需替换的占位符 Docx4jUtil.HeadingContent headingContent = tempContent.get(0); for (Object object : headingContent.getSubContent()) { if (object instanceof P) { @@ -314,6 +400,39 @@ public class Docx4jUtil { } + /** + * 获取内容中表格需要填充的keys + * + * @param tempContent 标题下配置的内容 + */ + public static List getTableFillKeys(List tempContent) { + List tableKeys = new ArrayList<>(); + if (CollUtil.isNotEmpty(tempContent)) { + // 读取该标题下所有的内容,并准备读取出所有需替换的占位符 + Docx4jUtil.HeadingContent headingContent = tempContent.get(0); + for (Object object : headingContent.getSubContent()) { + if (object instanceof JAXBElement) { + // 复制表格元素 + JAXBElement copiedTableElement = (JAXBElement) object; + // 解析表格并插入对应数据,最关键的是得知道表格是横向还是纵向以及表头占了几行 + Tbl tbl = copiedTableElement.getValue(); + // 获取表格的行 + List rows = tbl.getContent(); + boolean isRow = Docx4jUtil.judgeTableCross(rows.get(0)); + if (isRow) { + // 获取需要填的值的key + List cellKeys = Docx4jUtil.getTableKey((Tr) rows.get(rows.size() - 1)); + tableKeys.addAll(cellKeys); + } else { + // 纵向表格暂不考虑 todo... + } + } + } + } + return tableKeys; + + } + /** * 根据已知信息创建新航 * @@ -356,13 +475,19 @@ public class Docx4jUtil { pPr.setJc(jc); paragraph.setPPr(pPr); } - if (value.equals("不合格")) { - Color color = factory.createColor(); - // 红色 - color.setVal("FF0000"); - rPr.setColor(color); - run.setRPr(rPr); + // 根据检测结果设置颜色 + if (value != null) { + if (value.equals("不合格") || value.equals("不符合")) { + Color color = factory.createColor(); + color.setVal("FF0000"); // 红色 + rPr.setColor(color); + } else if (value.equals("无法比较")) { + Color color = factory.createColor(); + color.setVal("E36C09"); // 橙色 + rPr.setColor(color); + } } + run.setRPr(rPr); HpsMeasure sz = factory.createHpsMeasure(); // 10号字体 = 20 half-points sz.setVal(new BigInteger("20")); @@ -370,15 +495,325 @@ public class Docx4jUtil { cell.getContent().add(paragraph); cell.setTcPr(tcPr); + // 添加垂直居中 + addVerticalCenter(factory, cell); row.getContent().add(cell); row.setTrPr(trPr); } return row; } + /** + * 根据已知信息创建新行(支持自定义字号) + * + * @param factory 工厂 + * @param valueMap 数据 + * @param tableKeys keys + * @param trPr 行样式 + * @param tcPr 单元格样式 + * @param centerFlag 是否居中 + * @param fontSize 字号大小(half-points,五号=21) + */ + public static Tr createCustomRow(ObjectFactory factory, Map valueMap, List tableKeys, TrPr trPr, TcPr tcPr, boolean centerFlag, int fontSize) { + Tr row = factory.createTr(); + for (String tableKey : tableKeys) { + Tc cell = factory.createTc(); + P paragraph = factory.createP(); + R run = factory.createR(); + String value = valueMap.get(tableKey); + Text text = factory.createText(); + text.setValue(value); + run.getContent().add(text); + paragraph.getContent().add(run); + // 字体 + // 设置字体 + RPr rPr = factory.createRPr(); + RFonts rFonts = factory.createRFonts(); + if (containsChinese(value)) { + rFonts.setEastAsia("宋体"); + rFonts.setAscii("宋体"); + rFonts.setHAnsi("宋体"); + } else { + rFonts.setEastAsia("Arial"); + rFonts.setAscii("Arial"); + rFonts.setHAnsi("Arial"); + } + rPr.setRFonts(rFonts); + // 设置段落居中和1.5倍行距 + if (centerFlag) { + PPr pPr = factory.createPPr(); + Jc jc = factory.createJc(); + jc.setVal(JcEnumeration.CENTER); + pPr.setJc(jc); + + // 设置1.5倍行距 + PPrBase.Spacing spacing = factory.createPPrBaseSpacing(); + spacing.setLine(BigInteger.valueOf(360)); // 360缇 = 1.5倍行距(240缇是单倍行距) + spacing.setLineRule(STLineSpacingRule.AUTO); + pPr.setSpacing(spacing); + + paragraph.setPPr(pPr); + } else { + // 即使不居中,也要设置1.5倍行距 + PPr pPr = factory.createPPr(); + + // 设置1.5倍行距 + PPrBase.Spacing spacing = factory.createPPrBaseSpacing(); + spacing.setLine(BigInteger.valueOf(360)); // 360缇 = 1.5倍行距(240缇是单倍行距) + spacing.setLineRule(STLineSpacingRule.AUTO); + pPr.setSpacing(spacing); + + paragraph.setPPr(pPr); + } + // 根据检测结果设置颜色 + if (value != null) { + if (value.equals("不合格") || value.equals("不符合")) { + Color color = factory.createColor(); + color.setVal("FF0000"); // 红色 + rPr.setColor(color); + } else if (value.equals("无法比较")) { + Color color = factory.createColor(); + color.setVal("E36C09"); // 橙色 + rPr.setColor(color); + } + } + run.setRPr(rPr); + HpsMeasure sz = factory.createHpsMeasure(); + // 使用传入的字号参数 + sz.setVal(new BigInteger(String.valueOf(fontSize))); + rPr.setSz(sz); + + cell.getContent().add(paragraph); + cell.setTcPr(tcPr); + // 添加垂直居中 + addVerticalCenter(factory, cell); + row.getContent().add(cell); + row.setTrPr(trPr); + } + return row; + } + + /** + * 创建谐波表格行,支持次数列的垂直合并 + * + * @param factory ObjectFactory实例 + * @param dataMap 数据Map + * @param tableKeys 表格键列表 + * @param trPr 行属性 + * @param tcPr 单元格属性 + * @param isFirstPhase 是否是该次谐波的第一个相 + * @param isLastPhase 是否是该次谐波的最后一个相 + * @return 创建的表格行 + */ + public static Tr createHarmonicTableRow(ObjectFactory factory, Map dataMap, + List tableKeys, TrPr trPr, TcPr tcPr, + boolean isFirstPhase, boolean isLastPhase) { + Tr row = factory.createTr(); + row.setTrPr(trPr); + + for (String tableKey : tableKeys) { + Tc cell = factory.createTc(); + P paragraph = factory.createP(); + R run = factory.createR(); + String value = dataMap.get(tableKey); + Text text = factory.createText(); + text.setValue(value != null ? value : ""); + run.getContent().add(text); + paragraph.getContent().add(run); + + // 设置单元格属性 + TcPr cellPr = factory.createTcPr(); + if (tcPr != null && tcPr.getTcW() != null) { + cellPr.setTcW(tcPr.getTcW()); + } + + // 如果是"次数"列(通常是第一列),处理垂直合并 + if (tableKey.equals("次数") || tableKey.equals("TIME") || tableKey.equals("time")) { + if (isFirstPhase) { + // 第一个相:设置为合并起始单元格 + VMerge vMerge = factory.createTcPrInnerVMerge(); + vMerge.setVal("restart"); + cellPr.setVMerge(vMerge); + + // 设置垂直居中对齐 + CTVerticalJc vAlign = factory.createCTVerticalJc(); + vAlign.setVal(STVerticalJc.CENTER); + cellPr.setVAlign(vAlign); + } else { + // 其他相:设置为继续合并,并清空单元格内容 + VMerge vMerge = factory.createTcPrInnerVMerge(); + // 不设置val属性,或设置为null,表示继续合并 + cellPr.setVMerge(vMerge); + // 清空单元格内容 + text.setValue(""); + } + } + + // 设置居中对齐 + PPr pPr = factory.createPPr(); + Jc jc = factory.createJc(); + jc.setVal(JcEnumeration.CENTER); + pPr.setJc(jc); + paragraph.setPPr(pPr); + + // 设置字体 + RPr rPr = factory.createRPr(); + RFonts rFonts = factory.createRFonts(); + if (value != null && containsChinese(value)) { + rFonts.setEastAsia("宋体"); + rFonts.setAscii("宋体"); + rFonts.setHAnsi("宋体"); + } else { + rFonts.setEastAsia("Arial"); + rFonts.setAscii("Arial"); + rFonts.setHAnsi("Arial"); + } + rPr.setRFonts(rFonts); + + // 设置字体大小 + HpsMeasure size = factory.createHpsMeasure(); + size.setVal(BigInteger.valueOf(20)); // 10pt = 20 half-points + rPr.setSz(size); + rPr.setSzCs(size); + + // 根据检测结果设置颜色 + if (value != null) { + Color color = factory.createColor(); + if (value.equals("不合格") || value.equals("不符合")) { + color.setVal("FF0000"); // 红色 + rPr.setColor(color); + } else if (value.equals("无法比较")) { + color.setVal("E36C09"); // 橙色 + rPr.setColor(color); + } + } + + run.setRPr(rPr); + + cell.setTcPr(cellPr); + cell.getContent().add(paragraph); + row.getContent().add(cell); + } + + return row; + } + + /** + * 创建谐波表格行,支持次数列的垂直合并(支持自定义字号) + * + * @param factory ObjectFactory实例 + * @param dataMap 数据Map + * @param tableKeys 表格键列表 + * @param trPr 行属性 + * @param tcPr 单元格属性 + * @param isFirstPhase 是否是该次谐波的第一个相 + * @param isLastPhase 是否是该次谐波的最后一个相 + * @param fontSize 字号大小(half-points,五号=21) + * @return 创建的表格行 + */ + public static Tr createHarmonicTableRow(ObjectFactory factory, Map dataMap, + List tableKeys, TrPr trPr, TcPr tcPr, + boolean isFirstPhase, boolean isLastPhase, int fontSize) { + Tr row = factory.createTr(); + row.setTrPr(trPr); + + for (String tableKey : tableKeys) { + Tc cell = factory.createTc(); + P paragraph = factory.createP(); + R run = factory.createR(); + String value = dataMap.get(tableKey); + Text text = factory.createText(); + text.setValue(value != null ? value : ""); + run.getContent().add(text); + paragraph.getContent().add(run); + + // 设置单元格属性 + TcPr cellPr = factory.createTcPr(); + if (tcPr != null && tcPr.getTcW() != null) { + cellPr.setTcW(tcPr.getTcW()); + } + + // 如果是"次数"列(通常是第一列),处理垂直合并 + if (tableKey.equals("次数") || tableKey.equals("TIME") || tableKey.equals("time")) { + if (isFirstPhase) { + // 第一个相:设置为合并起始单元格 + VMerge vMerge = factory.createTcPrInnerVMerge(); + vMerge.setVal("restart"); + cellPr.setVMerge(vMerge); + + // 设置垂直居中对齐 + CTVerticalJc vAlign = factory.createCTVerticalJc(); + vAlign.setVal(STVerticalJc.CENTER); + cellPr.setVAlign(vAlign); + } else { + // 其他相:设置为继续合并,并清空单元格内容 + VMerge vMerge = factory.createTcPrInnerVMerge(); + // 不设置val属性,或设置为null,表示继续合并 + cellPr.setVMerge(vMerge); + // 清空单元格内容 + text.setValue(""); + } + } + + // 设置居中对齐和1.5倍行距 + PPr pPr = factory.createPPr(); + Jc jc = factory.createJc(); + jc.setVal(JcEnumeration.CENTER); + pPr.setJc(jc); + + // 设置1.5倍行距 + PPrBase.Spacing spacing = factory.createPPrBaseSpacing(); + spacing.setLine(BigInteger.valueOf(360)); // 360缇 = 1.5倍行距(240缇是单倍行距) + spacing.setLineRule(STLineSpacingRule.AUTO); + pPr.setSpacing(spacing); + + paragraph.setPPr(pPr); + + // 设置字体 + RPr rPr = factory.createRPr(); + RFonts rFonts = factory.createRFonts(); + if (value != null && containsChinese(value)) { + rFonts.setEastAsia("宋体"); + rFonts.setAscii("宋体"); + rFonts.setHAnsi("宋体"); + } else { + rFonts.setEastAsia("Arial"); + rFonts.setAscii("Arial"); + rFonts.setHAnsi("Arial"); + } + rPr.setRFonts(rFonts); + + // 设置字体大小(使用传入的字号参数) + HpsMeasure size = factory.createHpsMeasure(); + size.setVal(BigInteger.valueOf(fontSize)); + rPr.setSz(size); + rPr.setSzCs(size); + + // 根据检测结果设置颜色 + if (value != null) { + Color color = factory.createColor(); + if (value.equals("不合格") || value.equals("不符合")) { + color.setVal("FF0000"); // 红色 + rPr.setColor(color); + } else if (value.equals("无法比较")) { + color.setVal("E36C09"); // 橙色 + rPr.setColor(color); + } + } + + run.setRPr(rPr); + + cell.setTcPr(cellPr); + cell.getContent().add(paragraph); + row.getContent().add(cell); + } + + return row; + } /** * 判断字符串是否包含中文 + * * @param str 需要判断的字符串 * @return 是否包含中文 */ @@ -426,11 +861,17 @@ public class Docx4jUtil { paragraph.setPPr(pPr); } RPr rPr = factory.createRPr(); - if (value.equals("不合格")) { - Color color = factory.createColor(); - // 红色 - color.setVal("FF0000"); - rPr.setColor(color); + // 根据检测结果设置颜色 + if (value != null) { + if (value.equals("不合格") || value.equals("不符合")) { + Color color = factory.createColor(); + color.setVal("FF0000"); // 红色 + rPr.setColor(color); + } else if (value.equals("无法比较")) { + Color color = factory.createColor(); + color.setVal("E36C09"); // 橙色 + rPr.setColor(color); + } } if (boldFlag) { BooleanDefaultTrue bold = factory.createBooleanDefaultTrue(); @@ -463,18 +904,23 @@ public class Docx4jUtil { bottom.setW(BigInteger.valueOf(100)); mar.setBottom(bottom); cellProperties.setTcMar(mar); + // 添加垂直居中 + addVerticalCenter(factory, cell); row.getContent().add(cell); } return row; } + /** + * 深拷贝表格 + * + * @param original 原表格 + */ public static JAXBElement deepCopyTbl(JAXBElement original) throws Exception { // 使用 docx4j 的 XmlUtils 进行深拷贝 - WordprocessingMLPackage wordMLPackage = WordprocessingMLPackage.createPackage(); - Tbl clonedTbl = (Tbl) XmlUtils.deepCopy(original.getValue()); - + Tbl clonedTbl = XmlUtils.deepCopy(original.getValue()); // 重新包装为 JAXBElement - return new JAXBElement( + return new JAXBElement<>( original.getName(), original.getDeclaredType(), original.getScope(), @@ -563,5 +1009,1135 @@ public class Docx4jUtil { return null; } + /** + * 创建分页符段落 - 用于在书签位置强制换页 + * + * @return 包含分页符的段落,可直接添加到文档内容中 + */ + public static P createPageBreakParagraph() { + ObjectFactory factory = new ObjectFactory(); + + // 创建段落 + P paragraph = factory.createP(); + + // 创建运行 + R run = factory.createR(); + + // 创建分页符 + Br pageBreak = factory.createBr(); + pageBreak.setType(STBrType.PAGE); // 设置为页面分页符 + + // 将分页符添加到运行中 + run.getContent().add(pageBreak); + + // 将运行添加到段落中 + paragraph.getContent().add(run); + + return paragraph; + } + + /** + * 检查并清除文档中仅由createPageBreakParagraph创建的独占页面 + * + *

使用示例:

+ *
+     * try {
+     *     // 1. 完成所有文档内容的构建
+     *     // ... 您的文档构建逻辑
+     *
+     *     // 2. 在保存前清理孤立的分页符页面
+     *     int cleanedCount = Docx4jUtil.cleanBlankPagesAndRedundantPageBreaks(wordPackage);
+     *     System.out.println("已清理 " + cleanedCount + " 个孤立分页符");
+     *
+     *     // 3. 保存文档
+     *     wordPackage.save(new java.io.File(outputPath));
+     * } catch (Exception e) {
+     *     e.printStackTrace();
+     * }
+     * 
+ * + *

清理功能:

+ *
    + *
  • 只清理由 createPageBreakParagraph() 创建且独占一页的分页符
  • + *
  • 不影响文档的其他内容和格式
  • + *
+ * + * @param wordPackage Word文档包 + * @return 清理的孤立分页符数量 + */ + public static int cleanBlankPagesAndRedundantPageBreaks(WordprocessingMLPackage wordPackage) { + try { + MainDocumentPart mainDocumentPart = wordPackage.getMainDocumentPart(); + List allContent = mainDocumentPart.getContent(); + + int cleanedCount = 0; + + // 只清理独占页面的createPageBreakParagraph分页符 + cleanedCount += removeStandalonePageBreaks(allContent); + + return cleanedCount; + } catch (Exception e) { + System.err.println("清理孤立分页符时发生错误: " + e.getMessage()); + e.printStackTrace(); + return 0; + } + } + + /** + * 移除独占页面的分页符(非常保守的策略,只删除明确的重复分页符) + */ + private static int removeStandalonePageBreaks(List allContent) { + int removedCount = 0; + + for (int i = allContent.size() - 1; i >= 0; i--) { + Object obj = allContent.get(i); + + // 检查当前元素是否为分页符段落 + if (isPageBreakParagraph(obj)) { + // 只删除连续的分页符(保留第一个) + if (i > 0 && isPageBreakParagraph(allContent.get(i - 1))) { + allContent.remove(i); + removedCount++; + } + } + } + + return removedCount; + } + + + /** + * 判断段落是否为分页符段落 + */ + private static boolean isPageBreakParagraph(Object obj) { + if (!(obj instanceof P)) { + return false; + } + + P paragraph = (P) obj; + + // 检查段落内容 + for (Object content : paragraph.getContent()) { + if (content instanceof R) { + R run = (R) content; + for (Object runContent : run.getContent()) { + if (runContent instanceof Br) { + Br br = (Br) runContent; + return STBrType.PAGE.equals(br.getType()); + } + } + } + } + + return false; + } + + /** + * 判断段落是否为空白段落 + */ + private static boolean isBlankParagraph(P paragraph) { + String text = getTextFromP(paragraph); + return text == null || text.trim().isEmpty(); + } + + + /** + * 复制段落的所有样式信息并更新文本内容(原有方法) + * + * @param factory 对象工厂 + * @param sourceParagraph 源段落(包含要复制的样式) + * @param newText 新的文本内容 + * @param lineNumber 行号,用于更新标题编号(从1开始) + * @return 复制样式后的新段落 + */ + public static P copyParagraphStyleAndUpdateText(ObjectFactory factory, P sourceParagraph, String newText, int lineNumber) { + if (sourceParagraph == null) { + return null; + } + + // 创建新段落 + P newParagraph = factory.createP(); + + // 复制段落属性(PPr) + if (sourceParagraph.getPPr() != null) { + try { + PPr copiedPPr = (PPr) XmlUtils.deepCopy(sourceParagraph.getPPr()); + newParagraph.setPPr(copiedPPr); + } catch (Exception e) { + // 如果深拷贝失败,创建基本的段落属性 + PPr basicPPr = factory.createPPr(); + if (sourceParagraph.getPPr().getPStyle() != null) { + basicPPr.setPStyle(sourceParagraph.getPPr().getPStyle()); + } + newParagraph.setPPr(basicPPr); + } + } + + // 复制第一个Run的样式并设置新文本 + List sourceContent = sourceParagraph.getContent(); + if (!sourceContent.isEmpty()) { + for (Object obj : sourceContent) { + if (obj instanceof R) { + R sourceRun = (R) obj; + R newRun = factory.createR(); + + // 复制Run属性(RPr) + if (sourceRun.getRPr() != null) { + try { + RPr copiedRPr = (RPr) XmlUtils.deepCopy(sourceRun.getRPr()); + newRun.setRPr(copiedRPr); + } catch (Exception e) { + // 如果深拷贝失败,创建基本的运行属性 + RPr basicRPr = factory.createRPr(); + if (sourceRun.getRPr().getRFonts() != null) { + basicRPr.setRFonts(sourceRun.getRPr().getRFonts()); + } + if (sourceRun.getRPr().getSz() != null) { + basicRPr.setSz(sourceRun.getRPr().getSz()); + } + if (sourceRun.getRPr().getB() != null) { + basicRPr.setB(sourceRun.getRPr().getB()); + } + newRun.setRPr(basicRPr); + } + } + + // 设置新文本(处理编号逻辑) + Text text = factory.createText(); + String finalText = updateTitleWithLineNumber(newText, lineNumber); + text.setValue(finalText); + newRun.getContent().add(text); + + newParagraph.getContent().add(newRun); + // 只复制第一个Run的样式,后续Run忽略 + break; + } + } + } else { + // 如果源段落没有Run,创建一个基本的Run + R newRun = factory.createR(); + Text text = factory.createText(); + String finalText = updateTitleWithLineNumber(newText, lineNumber); + text.setValue(finalText); + newRun.getContent().add(text); + newParagraph.getContent().add(newRun); + } + + return newParagraph; + } + + /** + * 完整复制段落的所有样式信息和内容(新增方法 - 不修改文本) + * + * @param factory 对象工厂 + * @param sourceParagraph 源段落(包含要复制的样式) + * @return 完整复制的新段落 + */ + public static P copyParagraphCompletely(ObjectFactory factory, P sourceParagraph) { + if (sourceParagraph == null) { + return null; + } + + try { + // 使用XmlUtils进行完整的深拷贝,确保所有样式信息都被保留 + P copiedParagraph = (P) XmlUtils.deepCopy(sourceParagraph); + System.out.println("深拷贝成功"); + // 验证复制后的outlineLvl + if (copiedParagraph.getPPr() != null && copiedParagraph.getPPr().getOutlineLvl() != null) { + System.out.println("复制后 outlineLvl: " + copiedParagraph.getPPr().getOutlineLvl().getVal()); + } else { + System.out.println("复制后没有 outlineLvl"); + } + return copiedParagraph; + } catch (Exception e) { + // 深拷贝失败时的手动复制逻辑 + System.out.println("深拷贝失败,使用手动复制: " + e.getMessage()); + P manualCopied = manualCopyParagraph(factory, sourceParagraph); + // 验证手动复制后的outlineLvl + if (manualCopied.getPPr() != null && manualCopied.getPPr().getOutlineLvl() != null) { + System.out.println("手动复制后 outlineLvl: " + manualCopied.getPPr().getOutlineLvl().getVal()); + } else { + System.out.println("手动复制后没有 outlineLvl"); + } + return manualCopied; + } + } + + /** + * 基于样式名称创建具有正确标题等级的段落 + * + * @param factory 对象工厂 + * @param mainDocumentPart 主文档部分,用于获取样式定义 + * @param sourceParagraph 源段落(用于获取样式ID) + * @param text 段落文本内容 + * @return 具有正确标题样式的新段落 + */ + public static P createParagraphWithCorrectHeadingStyle(ObjectFactory factory, MainDocumentPart mainDocumentPart, P sourceParagraph, String text) { + if (sourceParagraph == null || mainDocumentPart == null) { + return null; + } + + // 创建新段落 + P newParagraph = factory.createP(); + + // 获取源段落的样式ID + String styleId = null; + if (sourceParagraph.getPPr() != null && sourceParagraph.getPPr().getPStyle() != null) { + styleId = sourceParagraph.getPPr().getPStyle().getVal(); + } + + if (styleId != null) { + try { + // 获取样式定义部分 + org.docx4j.openpackaging.parts.WordprocessingML.StyleDefinitionsPart stylesPart = + mainDocumentPart.getStyleDefinitionsPart(); + + if (stylesPart != null) { + // 获取样式定义对象 + org.docx4j.wml.Style style = stylesPart.getStyleById(styleId); + + if (style != null && style.getName() != null) { + String styleName = style.getName().getVal(); + System.out.println("样式名称: " + styleName); + + // 根据样式名称判断并设置正确的outlineLvl + int correctOutlineLvl = getOutlineLvlByStyleName(styleName); + System.out.println("根据样式名称确定的 outlineLvl: " + correctOutlineLvl); + + // 设置段落属性 + PPr pPr = factory.createPPr(); + PPrBase.PStyle pStyle = factory.createPPrBasePStyle(); + pStyle.setVal(styleId); + pPr.setPStyle(pStyle); + + // 设置正确的大纲等级 + PPrBase.OutlineLvl outlineLvl = factory.createPPrBaseOutlineLvl(); + outlineLvl.setVal(BigInteger.valueOf(correctOutlineLvl)); + pPr.setOutlineLvl(outlineLvl); + + // 复制其他段落属性 + if (sourceParagraph.getPPr() != null) { + if (sourceParagraph.getPPr().getJc() != null) { + pPr.setJc(sourceParagraph.getPPr().getJc()); + } + if (sourceParagraph.getPPr().getSpacing() != null) { + pPr.setSpacing(sourceParagraph.getPPr().getSpacing()); + } + if (sourceParagraph.getPPr().getInd() != null) { + pPr.setInd(sourceParagraph.getPPr().getInd()); + } + } + + newParagraph.setPPr(pPr); + + // 创建Run并设置文本 + R run = factory.createR(); + + // 复制Run属性 + copyRunPropertiesFromSource(factory, sourceParagraph, run); + + // 设置文本内容 + Text textElement = factory.createText(); + textElement.setValue(text); + run.getContent().add(textElement); + + newParagraph.getContent().add(run); + + return newParagraph; + } + } + } catch (Exception e) { + System.err.println("获取样式定义失败: " + e.getMessage()); + } + } + + // 如果以上方式都失败,使用深拷贝作为最后手段 + return copyParagraphCompletely(factory, sourceParagraph); + } + + /** + * 根据样式名称确定正确的outlineLvl值 + */ + private static int getOutlineLvlByStyleName(String styleName) { + if (styleName == null) { + return 2; // 默认返回3级标题 + } + + String lowerName = styleName.toLowerCase(); + + if (lowerName.contains("heading 1") || lowerName.contains("标题 1")) { + return 0; // Heading 1 + } else if (lowerName.contains("heading 2") || lowerName.contains("标题 2")) { + return 1; // Heading 2 + } else if (lowerName.contains("heading 3") || lowerName.contains("标题 3")) { + return 2; // Heading 3 + } else if (lowerName.contains("heading 4") || lowerName.contains("标题 4")) { + return 3; // Heading 4 + } else if (lowerName.contains("heading 5") || lowerName.contains("标题 5")) { + return 4; // Heading 5 + } else if (lowerName.contains("heading 6") || lowerName.contains("标题 6")) { + return 5; // Heading 6 + } + + // 如果没有匹配到标准标题样式,默认返回3级标题 + return 2; + } + + /** + * 从源段落复制Run属性到目标Run + */ + private static void copyRunPropertiesFromSource(ObjectFactory factory, P sourceParagraph, R targetRun) { + // 从源段落的第一个Run获取属性 + for (Object obj : sourceParagraph.getContent()) { + if (obj instanceof R) { + R sourceRun = (R) obj; + if (sourceRun.getRPr() != null) { + try { + RPr copiedRPr = (RPr) XmlUtils.deepCopy(sourceRun.getRPr()); + targetRun.setRPr(copiedRPr); + } catch (Exception e) { + // 手动复制基本属性 + RPr rPr = factory.createRPr(); + if (sourceRun.getRPr().getRFonts() != null) { + rPr.setRFonts(sourceRun.getRPr().getRFonts()); + } + if (sourceRun.getRPr().getSz() != null) { + rPr.setSz(sourceRun.getRPr().getSz()); + } + if (sourceRun.getRPr().getB() != null) { + rPr.setB(sourceRun.getRPr().getB()); + } + targetRun.setRPr(rPr); + } + } + break; + } + } + } + + /** + * 手动复制段落(当深拷贝失败时的备用方案) + */ + private static P manualCopyParagraph(ObjectFactory factory, P sourceParagraph) { + P newParagraph = factory.createP(); + + // 复制段落属性(PPr)- 包括样式ID、对齐方式、间距等 + if (sourceParagraph.getPPr() != null) { + PPr sourcePPr = sourceParagraph.getPPr(); + PPr newPPr = factory.createPPr(); + + // 复制样式引用(最重要 - 保持标题等级) + if (sourcePPr.getPStyle() != null) { + PPrBase.PStyle newPStyle = factory.createPPrBasePStyle(); + newPStyle.setVal(sourcePPr.getPStyle().getVal()); + newPPr.setPStyle(newPStyle); + } + + // 复制对齐方式 + if (sourcePPr.getJc() != null) { + newPPr.setJc(sourcePPr.getJc()); + } + + // 复制段落间距 + if (sourcePPr.getSpacing() != null) { + newPPr.setSpacing(sourcePPr.getSpacing()); + } + + // 复制大纲等级(重要 - 影响标题等级) + if (sourcePPr.getOutlineLvl() != null) { + newPPr.setOutlineLvl(sourcePPr.getOutlineLvl()); + } + + newParagraph.setPPr(newPPr); + } + + // 复制所有Run内容 + for (Object obj : sourceParagraph.getContent()) { + if (obj instanceof R) { + R sourceRun = (R) obj; + R newRun = factory.createR(); + + // 复制Run属性(RPr) + if (sourceRun.getRPr() != null) { + RPr sourceRPr = sourceRun.getRPr(); + RPr newRPr = factory.createRPr(); + + // 复制字体 + if (sourceRPr.getRFonts() != null) { + newRPr.setRFonts(sourceRPr.getRFonts()); + } + + // 复制字号 + if (sourceRPr.getSz() != null) { + newRPr.setSz(sourceRPr.getSz()); + } + if (sourceRPr.getSzCs() != null) { + newRPr.setSzCs(sourceRPr.getSzCs()); + } + + // 复制粗体 + if (sourceRPr.getB() != null) { + newRPr.setB(sourceRPr.getB()); + } + if (sourceRPr.getBCs() != null) { + newRPr.setBCs(sourceRPr.getBCs()); + } + + // 复制颜色 + if (sourceRPr.getColor() != null) { + newRPr.setColor(sourceRPr.getColor()); + } + + newRun.setRPr(newRPr); + } + + // 复制Run的所有内容 + for (Object runObj : sourceRun.getContent()) { + newRun.getContent().add(runObj); + } + + newParagraph.getContent().add(newRun); + } else { + // 复制非Run对象(如书签等) + newParagraph.getContent().add(obj); + } + } + + return newParagraph; + } + + /** + * 创建动态检测结果表格(正确的合并单元格版本) + * + * @param factory 对象工厂 + * @param testItems 检测项目列表 ["电压", "频率", "电压不平衡度"...] + * @param circuitNames 回路名称列表 ["测量回路1", "测量回路2"...] 或 ["#1母线", "#2母线"...] + * @param testResults 检测结果数据 testResults[项目索引][回路索引] = "合格/不合格/无法比较" + * @param sampleResult 样品结果 "合格/不合格" + * @param dataType 数据类型(如:"任意值"、"部分值"、"平均值"等) + * @param numOfSamples 样本数量 + * @param dataRule 处理原则(如:"取第一个满足条件的数据"、"去除最大最小值"等) + * @return 动态表格 + */ + public static JAXBElement createDynamicTestResultTable(ObjectFactory factory, + List testItems, List circuitNames, String[][] testResults, String sampleResult, + String dataType, String numOfSamples, String dataRule) { + + // 创建表格 + Tbl table = factory.createTbl(); + table.setTblPr(getTblPr(factory)); + + int circuitCount = circuitNames.size(); + int totalColumns = circuitCount + 2; // 检测项目列 + 回路列 + 样品结果列 + + // 创建第一行表头:检测项目(纵向合并)| 检测结果(横向合并) + Tr titleHeaderRow = createTestResultTitleHeader(factory, circuitCount, testItems.size() + 2); + table.getContent().add(titleHeaderRow); + + // 创建第二行表头:(空)| 回路名称列表 | 样品结果 + Tr columnHeaderRow = createTestResultColumnHeader(factory, circuitNames, sampleResult); + table.getContent().add(columnHeaderRow); + + // 创建检测项目行 + for (int i = 0; i < testItems.size(); i++) { + String testItem = testItems.get(i); + String[] itemResults = testResults[i]; + Tr dataRow = createTestResultDataRow(factory, testItem, itemResults, sampleResult, i, testItems.size()); + table.getContent().add(dataRow); + } + + // 添加说明内容行(在表格内部) + Tr descriptionRow = createDescriptionRow(factory, totalColumns, dataType, numOfSamples, dataRule); + table.getContent().add(descriptionRow); + + // 包装为JAXBElement + return new JAXBElement<>( + new javax.xml.namespace.QName("http://schemas.openxmlformats.org/wordprocessingml/2006/main", "tbl"), + Tbl.class, + table + ); + } + + /** + * 创建第一行表头:检测项目(纵向合并)| 检测结果(横向合并所有列) + */ + private static Tr createTestResultTitleHeader(ObjectFactory factory, int circuitCount, int totalRows) { + Tr titleRow = factory.createTr(); + + // 设置行高 + TrPr trPr = factory.createTrPr(); + CTHeight height = new CTHeight(); + height.setVal(BigInteger.valueOf(600)); // 设置行高(单位:缇,600缇约30磅) + height.setHRule(STHeightRule.AT_LEAST); // 至少这个高度 + // 使用正确的方法名 + JAXBElement trHeight = factory.createCTTrPrBaseTrHeight(height); + trPr.getCnfStyleOrDivIdOrGridBefore().add(trHeight); + titleRow.setTrPr(trPr); + + // 1. 创建"检测项目"单元格(纵向合并2行) + Tc itemTitleCell = createCenteredTableCell(factory, "检测项目", "宋体", 21, true); + TcPr itemCellPr = itemTitleCell.getTcPr(); + if (itemCellPr == null) { + itemCellPr = factory.createTcPr(); + itemTitleCell.setTcPr(itemCellPr); + // 重新添加垂直居中 + addVerticalCenter(factory, itemTitleCell); + } + VMerge itemVMerge = new VMerge(); + itemVMerge.setVal("restart"); + itemCellPr.setVMerge(itemVMerge); + titleRow.getContent().add(itemTitleCell); + + // 2. 创建"检测结果"合并单元格(横向合并所有回路列+样品结果列) + Tc resultTitleCell = createCenteredTableCell(factory, "检测结果", "宋体", 21, true); + TcPr resultCellPr = resultTitleCell.getTcPr(); + if (resultCellPr == null) { + resultCellPr = factory.createTcPr(); + resultTitleCell.setTcPr(resultCellPr); + } + GridSpan gridSpan = new GridSpan(); + gridSpan.setVal(BigInteger.valueOf(circuitCount + 1)); // +1 包括样品结果列 + resultCellPr.setGridSpan(gridSpan); + titleRow.getContent().add(resultTitleCell); + + return titleRow; + } + + /** + * 创建第二行表头:(空)| 回路名称列表 | 样品结果 + */ + private static Tr createTestResultColumnHeader(ObjectFactory factory, List circuitNames, String sampleResult) { + Tr headerRow = factory.createTr(); + + // 设置行高 + TrPr trPr = factory.createTrPr(); + CTHeight height = new CTHeight(); + height.setVal(BigInteger.valueOf(600)); // 设置行高 + height.setHRule(STHeightRule.AT_LEAST); // 至少这个高度 + // 使用正确的方法名 + JAXBElement trHeight = factory.createCTTrPrBaseTrHeight(height); + trPr.getCnfStyleOrDivIdOrGridBefore().add(trHeight); + headerRow.setTrPr(trPr); + + // 1. 检测项目列(继续纵向合并) + Tc itemEmptyCell = createCenteredTableCell(factory, "", "宋体", 21, true); + TcPr itemCellPr = itemEmptyCell.getTcPr(); + if (itemCellPr == null) { + itemCellPr = factory.createTcPr(); + itemEmptyCell.setTcPr(itemCellPr); + } + VMerge itemVMerge = new VMerge(); + itemVMerge.setVal("continue"); + itemCellPr.setVMerge(itemVMerge); + headerRow.getContent().add(itemEmptyCell); + + // 2. 添加回路列(使用传入的回路名称) + for (String circuitName : circuitNames) { + Tc circuitCell = createCenteredTableCell(factory, circuitName, "宋体", 21, true); + headerRow.getContent().add(circuitCell); + } + + // 3. 样品结果列(显示"样品结果"标题) + Tc sampleResultCell = createCenteredTableCell(factory, "样品结果", "宋体", 21, true); + headerRow.getContent().add(sampleResultCell); + + return headerRow; + } + + /** + * 创建检测结果数据行(样品结果纵向合并) + */ + private static Tr createTestResultDataRow(ObjectFactory factory, String testItem, + String[] results, String sampleResult, int rowIndex, int totalRows) { + + Tr dataRow = factory.createTr(); + + // 设置行高 + TrPr trPr = factory.createTrPr(); + CTHeight height = new CTHeight(); + height.setVal(BigInteger.valueOf(500)); // 数据行稍微矮一点(500缇约25磅) + height.setHRule(STHeightRule.AT_LEAST); + JAXBElement trHeight = factory.createCTTrPrBaseTrHeight(height); + trPr.getCnfStyleOrDivIdOrGridBefore().add(trHeight); + dataRow.setTrPr(trPr); + + // 1. 检测项目列 + Tc itemCell = createTableCell(factory, testItem, "宋体", 21, false); + dataRow.getContent().add(itemCell); + + // 2. 各回路结果列 + for (String result : results) { + Tc resultCell = createTableCell(factory, result, "宋体", 21, false); + // 根据结果设置颜色 + if ("不合格".equals(result) || "不符合".equals(result)) { + setRedColor(factory, resultCell); + } else if ("无法比较".equals(result)) { + setOrangeColor(factory, resultCell); + } + dataRow.getContent().add(resultCell); + } + + // 3. 样品结果列(根据行索引决定是否显示内容或合并) + if (rowIndex == 0) { + // 第一行数据行,显示样品结果并开始纵向合并 + Tc sampleDataCell = createTableCell(factory, sampleResult, "宋体", 21, false); + TcPr sampleCellPr = sampleDataCell.getTcPr(); + if (sampleCellPr == null) { + sampleCellPr = factory.createTcPr(); + sampleDataCell.setTcPr(sampleCellPr); + } + + // 开始纵向合并到其他数据行 + VMerge sampleVMerge = new VMerge(); + sampleVMerge.setVal("restart"); + sampleCellPr.setVMerge(sampleVMerge); + + // 根据样品结果设置颜色 + if ("不合格".equals(sampleResult) || "不符合".equals(sampleResult)) { + setRedColor(factory, sampleDataCell); + } else if ("无法比较".equals(sampleResult)) { + setOrangeColor(factory, sampleDataCell); + } + + dataRow.getContent().add(sampleDataCell); + } else { + // 其他数据行,继续纵向合并 + Tc sampleDataCell = createTableCell(factory, "", "宋体", 21, false); + TcPr sampleCellPr = sampleDataCell.getTcPr(); + if (sampleCellPr == null) { + sampleCellPr = factory.createTcPr(); + sampleDataCell.setTcPr(sampleCellPr); + } + VMerge sampleVMerge = new VMerge(); + sampleVMerge.setVal("continue"); + sampleCellPr.setVMerge(sampleVMerge); + dataRow.getContent().add(sampleDataCell); + } + + return dataRow; + } + + /** + * 创建表格单元格(水平和垂直居中) + */ + private static Tc createTableCell(ObjectFactory factory, String text, String fontFamily, int fontSize, boolean isBold) { + Tc cell = factory.createTc(); + P paragraph = factory.createP(); + R run = factory.createR(); + Text textElement = factory.createText(); + textElement.setValue(text); + + // 设置字体样式 + RPr rPr = factory.createRPr(); + RFonts fonts = factory.createRFonts(); + fonts.setEastAsia(fontFamily); + fonts.setAscii("Arial"); + rPr.setRFonts(fonts); + + HpsMeasure size = factory.createHpsMeasure(); + size.setVal(BigInteger.valueOf(fontSize)); + rPr.setSz(size); + + if (isBold) { + BooleanDefaultTrue bold = factory.createBooleanDefaultTrue(); + rPr.setB(bold); + } + + run.setRPr(rPr); + run.getContent().add(textElement); + paragraph.getContent().add(run); + + // 设置段落水平居中 + PPr pPr = factory.createPPr(); + Jc jc = factory.createJc(); + jc.setVal(JcEnumeration.CENTER); + pPr.setJc(jc); + + // 设置段落间距为0,以便更好地垂直居中 + PPrBase.Spacing spacing = factory.createPPrBaseSpacing(); + spacing.setBefore(BigInteger.ZERO); + spacing.setAfter(BigInteger.ZERO); + spacing.setLine(BigInteger.valueOf(240)); // 单倍行距 + pPr.setSpacing(spacing); + + paragraph.setPPr(pPr); + + // 设置单元格属性并垂直居中 + TcPr cellPr = factory.createTcPr(); + + // 设置垂直居中 + CTVerticalJc vAlign = new CTVerticalJc(); + vAlign.setVal(STVerticalJc.CENTER); + cellPr.setVAlign(vAlign); + + cell.setTcPr(cellPr); + + cell.getContent().add(paragraph); + return cell; + } + + /** + * 创建居中的表格单元格(专门用于表头) + */ + private static Tc createCenteredTableCell(ObjectFactory factory, String text, String fontFamily, int fontSize, boolean isBold) { + return createTableCell(factory, text, fontFamily, fontSize, isBold); + } + + /** + * 为单元格添加垂直居中属性 + */ + private static void addVerticalCenter(ObjectFactory factory, Tc cell) { + TcPr cellPr = cell.getTcPr(); + if (cellPr == null) { + cellPr = factory.createTcPr(); + cell.setTcPr(cellPr); + } + + // 设置垂直居中 - 使用正确的方式创建CTVerticalJc + CTVerticalJc vAlign = new CTVerticalJc(); + vAlign.setVal(STVerticalJc.CENTER); + cellPr.setVAlign(vAlign); + + // 同时确保段落也设置了居中对齐(这有助于内容更好地显示) + for (Object content : cell.getContent()) { + if (content instanceof P) { + P paragraph = (P) content; + PPr pPr = paragraph.getPPr(); + if (pPr == null) { + pPr = factory.createPPr(); + paragraph.setPPr(pPr); + } + // 设置段落水平居中(保持原有的水平居中) + Jc jc = pPr.getJc(); + if (jc == null) { + jc = factory.createJc(); + jc.setVal(JcEnumeration.CENTER); + pPr.setJc(jc); + } + // 设置段落间距为0,并使用1.5倍行距 + PPrBase.Spacing spacing = factory.createPPrBaseSpacing(); + spacing.setBefore(BigInteger.ZERO); + spacing.setAfter(BigInteger.ZERO); + spacing.setLine(BigInteger.valueOf(360)); // 1.5倍行距 + spacing.setLineRule(STLineSpacingRule.AUTO); + pPr.setSpacing(spacing); + } + } + } + + /** + * 为单元格设置红色文字 + */ + private static void setRedColor(ObjectFactory factory, Tc cell) { + if (!cell.getContent().isEmpty()) { + Object content = cell.getContent().get(0); + if (content instanceof P) { + P paragraph = (P) content; + if (!paragraph.getContent().isEmpty()) { + Object runContent = paragraph.getContent().get(0); + if (runContent instanceof R) { + R run = (R) runContent; + RPr rPr = run.getRPr(); + if (rPr == null) { + rPr = factory.createRPr(); + run.setRPr(rPr); + } + Color color = factory.createColor(); + color.setVal("FF0000"); // 红色 + rPr.setColor(color); + } + } + } + } + } + + /** + * 为单元格设置橙色文字 + */ + private static void setOrangeColor(ObjectFactory factory, Tc cell) { + if (!cell.getContent().isEmpty()) { + Object content = cell.getContent().get(0); + if (content instanceof P) { + P paragraph = (P) content; + if (!paragraph.getContent().isEmpty()) { + Object runContent = paragraph.getContent().get(0); + if (runContent instanceof R) { + R run = (R) runContent; + RPr rPr = run.getRPr(); + if (rPr == null) { + rPr = factory.createRPr(); + run.setRPr(rPr); + } + Color color = factory.createColor(); + color.setVal("E36C09"); // 橙色 + rPr.setColor(color); + } + } + } + } + } + + + /** + * 创建表格内的说明行(跨所有列合并) + */ + private static Tr createDescriptionRow(ObjectFactory factory, int totalColumns, + String dataType, String numOfSamples, String dataRule) { + + Tr descRow = factory.createTr(); + + // 创建跨列合并的单元格 + Tc descCell = factory.createTc(); + + // 设置单元格跨列合并 + TcPr tcPr = factory.createTcPr(); + GridSpan gridSpan = new GridSpan(); + gridSpan.setVal(BigInteger.valueOf(totalColumns)); + tcPr.setGridSpan(gridSpan); + descCell.setTcPr(tcPr); + + // 创建说明内容的字体样式(宋体、五号、非粗体) + RPr descRPr = factory.createRPr(); + RFonts fonts = factory.createRFonts(); + fonts.setEastAsia("宋体"); + fonts.setAscii("Arial"); + descRPr.setRFonts(fonts); + HpsMeasure size = factory.createHpsMeasure(); + size.setVal(BigInteger.valueOf(21)); // 五号字体 + descRPr.setSz(size); + // 不设置粗体(非粗体) + + // 创建段落格式(1.5倍行距,段前段后为0) + PPr descPPr = factory.createPPr(); + // 设置行距为1.5倍 + PPrBase.Spacing spacing = factory.createPPrBaseSpacing(); + spacing.setLine(BigInteger.valueOf(360)); // 360缇 = 1.5倍行距(240缇是单倍行距) + spacing.setLineRule(STLineSpacingRule.AUTO); // 自动行距 + spacing.setBefore(BigInteger.ZERO); // 段前间距为0 + spacing.setAfter(BigInteger.ZERO); // 段后间距为0 + descPPr.setSpacing(spacing); + + // 创建子项段落格式(包含首行缩进2个字符) + PPr subItemPPr = factory.createPPr(); + // 设置行距为1.5倍 + PPrBase.Spacing subSpacing = factory.createPPrBaseSpacing(); + subSpacing.setLine(BigInteger.valueOf(360)); + subSpacing.setLineRule(STLineSpacingRule.AUTO); + subSpacing.setBefore(BigInteger.ZERO); + subSpacing.setAfter(BigInteger.ZERO); + subItemPPr.setSpacing(subSpacing); + // 设置首行缩进2个字符(2个字符 = 420缇,1个字符约210缇) + PPrBase.Ind indentation = factory.createPPrBaseInd(); + indentation.setFirstLine(BigInteger.valueOf(420)); // 首行缩进2个字符 + subItemPPr.setInd(indentation); + + // 创建说明内容的段落 + // 1. 数据源标题 + P dataSourceTitle = factory.createP(); + addPContent(factory, dataSourceTitle, "1. 数据源:", descRPr, descPPr); + descCell.getContent().add(dataSourceTitle); + + // 数据类型(去掉手动缩进的空格,使用首行缩进) + P dataTypeP = factory.createP(); + addPContent(factory, dataTypeP, "数据类型:" + dataType + ";", descRPr, subItemPPr); + descCell.getContent().add(dataTypeP); + + // 采集规模 + P sampleSizeP = factory.createP(); + addPContent(factory, sampleSizeP, "采集规模:" + numOfSamples + "数据样本;", descRPr, subItemPPr); + descCell.getContent().add(sampleSizeP); + + // 处理原则 + P dataRuleP = factory.createP(); + addPContent(factory, dataRuleP, "处理原则:" + dataRule + "。", descRPr, subItemPPr); + descCell.getContent().add(dataRuleP); + + // 2. 检测结论判定标准 + P criteriaTitle = factory.createP(); + addPContent(factory, criteriaTitle, "2. 检测结论判定标准:", descRPr, descPPr); + descCell.getContent().add(criteriaTitle); + + // 合格(去掉手动缩进的空格,使用首行缩进) + P qualifiedP = factory.createP(); + addPContent(factory, qualifiedP, + "合格:收条数据中存在符合误差计算条件的样本,经" + dataRule + "处理后,所有样本数据误差计算结果均符合误差标准要求;", + descRPr, subItemPPr); + descCell.getContent().add(qualifiedP); + + // 不合格 + P unqualifiedP = factory.createP(); + addPContent(factory, unqualifiedP, + "不合格:收条数据中存在符合误差计算条件的样本,经" + dataRule + "处理后,存在样本数据误差计算结果不符合误差标准要求;", + descRPr, subItemPPr); + descCell.getContent().add(unqualifiedP); + + // 无法比较(去掉手动缩进的空格,使用首行缩进) + P incomparableP = factory.createP(); + addPContent(factory, incomparableP, + "无法比较:收集数据中,无样本数据满足误差比较的前置条件,无法执行有效性判定。", + descRPr, subItemPPr); + descCell.getContent().add(incomparableP); + + descRow.getContent().add(descCell); + return descRow; + } + + /** + * 创建检测结果说明段落 + * + * @param factory 对象工厂 + * @param dataType 数据类型 + * @param numOfSamples 样本数量 + * @param dataRule 处理原则 + * @return 说明段落列表 + */ + public static List

createTestResultDescription(ObjectFactory factory, + String dataType, int numOfSamples, String dataRule) { + + List

descriptions = new ArrayList<>(); + + // 1. 数据源 + P dataSourceP = factory.createP(); + addPContent(factory, dataSourceP, "1. 数据源:", null, null); + descriptions.add(dataSourceP); + + P dataTypeP = factory.createP(); + addPContent(factory, dataTypeP, " 数据类型:" + dataType + ";", null, null); + descriptions.add(dataTypeP); + + P sampleSizeP = factory.createP(); + addPContent(factory, sampleSizeP, " 采集规模:" + numOfSamples + " 组数据样本;", null, null); + descriptions.add(sampleSizeP); + + P dataRuleP = factory.createP(); + addPContent(factory, dataRuleP, " 处理原则:" + dataRule + "。", null, null); + descriptions.add(dataRuleP); + + // 2. 检测结论判定标准 + P criteriaP = factory.createP(); + addPContent(factory, criteriaP, "2. 检测结论判定标准:", null, null); + descriptions.add(criteriaP); + + P qualifiedP = factory.createP(); + addPContent(factory, qualifiedP, + " 合格:收条数据中存在符合误差计算条件的样本,经部分值处理后,所有样本数据误差计算结果均符合误差标准要求;", + null, null); + descriptions.add(qualifiedP); + + P unqualifiedP = factory.createP(); + R run = factory.createR(); + Text text = factory.createText(); + text.setValue(" 不合格:收条数据中存在符合误差计算条件的样本,经部分值处理后,存在样本数据误差计算结果不符合误差标准要求;"); + + // 设置红色 + RPr rPr = factory.createRPr(); + Color color = factory.createColor(); + color.setVal("FF0000"); + rPr.setColor(color); + run.setRPr(rPr); + run.getContent().add(text); + unqualifiedP.getContent().add(run); + descriptions.add(unqualifiedP); + + P incomparableP = factory.createP(); + addPContent(factory, incomparableP, + " 无法比较:收集的200组数据中,无样本数据满足误差比较的前置条件,无法执行有效性判定。", + null, null); + descriptions.add(incomparableP); + + return descriptions; + } + + /** + * 根据行号更新标题文本中的编号 + * + * @param originalText 原始文本 + * @param lineNumber 行号(从1开始) + * @return 更新编号后的文本 + */ + private static String updateTitleWithLineNumber(String originalText, int lineNumber) { + if (originalText == null || originalText.trim().isEmpty()) { + return "测量回路" + lineNumber; + } + + // 常见的标题编号模式替换 + String text = originalText; + + // 替换"回路1"、"回路2"等模式 + text = text.replaceAll("回路\\d+", "回路" + lineNumber); + + // 替换"第1回路"、"第2回路"等模式 + text = text.replaceAll("第\\d+回路", "第" + lineNumber + "回路"); + + // 替换"1#"、"2#"等模式 + text = text.replaceAll("\\d+#", lineNumber + "#"); + + // 替换单独的数字(在开头或被空格包围) + text = text.replaceAll("^\\d+\\s*", lineNumber + " "); + text = text.replaceAll("\\s+\\d+\\s+", " " + lineNumber + " "); + + // 如果没有找到任何编号模式,在文本前添加编号 + if (text.equals(originalText)) { + text = lineNumber + " " + originalText; + } + + return text; + } + + /** + * 创建特殊情况说明段落 + * 统一格式:宋体5号字、首行缩进2字符 + * + * @param factory Word文档对象工厂 + * @param specialCase 特殊情况说明文本 + * @return 格式化的段落对象 + */ + public static P createSpecialCaseParagraph(ObjectFactory factory, String specialCase) { + P specialCaseP = factory.createP(); + + // 设置段落属性 + PPr pPr = factory.createPPr(); + + // 设置首行缩进2字符(中文字符) + PPrBase.Ind ind = factory.createPPrBaseInd(); + // 2字符缩进,1个中文字符约等于240twips,2字符=480twips + ind.setFirstLine(BigInteger.valueOf(480)); + pPr.setInd(ind); + + specialCaseP.setPPr(pPr); + + // 创建运行元素 + R run = factory.createR(); + + // 设置字体样式 + RPr rPr = factory.createRPr(); + + // 设置字体为宋体 + RFonts rFonts = factory.createRFonts(); + rFonts.setAscii("宋体"); + rFonts.setEastAsia("宋体"); + rFonts.setHAnsi("宋体"); + rPr.setRFonts(rFonts); + + // 设置字号为5号(10.5磅,21半磅) + HpsMeasure size = factory.createHpsMeasure(); + size.setVal(BigInteger.valueOf(21)); + rPr.setSz(size); + rPr.setSzCs(size); // 复杂脚本字体大小 + + run.setRPr(rPr); + + // 添加文本内容 + Text text = factory.createText(); + text.setValue(specialCase); + text.setSpace("preserve"); + run.getContent().add(text); + + // 将运行元素添加到段落 + specialCaseP.getContent().add(run); + + return specialCaseP; + } + }