From 7a8071c9d753adf59298363a956e0d8367416e6b Mon Sep 17 00:00:00 2001 From: hongawen <83944980@qq.com> Date: Tue, 7 Jul 2026 22:49:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(report):=20=E4=BC=98=E5=8C=96=E6=8A=A5?= =?UTF-8?q?=E5=91=8A=E7=94=9F=E6=88=90=E5=B7=A5=E5=85=B7=E4=B8=AD=E7=9A=84?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E5=A4=84=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在Docx4jUtil中添加enableUpdateFieldsOnOpen方法,确保Word打开时自动刷新页码等域 - 移除ItemAnchorAssembler中不必要的导入包,精简代码结构 - 重构谐波数据处理逻辑,添加谐波衍生值计算功能 - 实现固定占位符替换机制,支持谐波电压和功率表的动态填充 - 优化表格结构分离逻辑,使用空段落而非可见标题分隔多表 - 增强错误范围处理,移除单位后缀并优化谐波电流计算逻辑 - 添加全面的单元测试验证各种谐波检测场景的功能正确性 --- .../report/pojo/enums/ItemReportKeyEnum.java | 16 +- .../report/pojo/enums/PowerIndexEnum.java | 1 + .../service/impl/PqReportServiceImpl.java | 23 +- .../report/util/ItemAnchorAssembler.java | 326 +++++++++++++++--- .../service/impl/ResultServiceImpl.java | 110 ++++++ .../result/util/AngleResultAssembler.java | 191 ++++++++++ .../result/util/UnbalanceResultAssembler.java | 1 - .../service/impl/PqReportServiceImplTest.java | 49 +++ .../util/Docx4jUtilUpdateFieldsTest.java | 26 ++ .../report/util/ItemAnchorAssemblerTest.java | 321 +++++++++++++++-- .../service/impl/ResultServiceImplTest.java | 232 +++++++++++++ .../result/util/AngleResultAssemblerTest.java | 64 ++++ .../util/UnbalanceResultAssemblerTest.java | 12 + .../gather/tools/report/util/Docx4jUtil.java | 22 ++ 14 files changed, 1322 insertions(+), 72 deletions(-) create mode 100644 detection/src/main/java/com/njcn/gather/result/util/AngleResultAssembler.java create mode 100644 detection/src/test/java/com/njcn/gather/report/util/Docx4jUtilUpdateFieldsTest.java create mode 100644 detection/src/test/java/com/njcn/gather/result/service/impl/ResultServiceImplTest.java create mode 100644 detection/src/test/java/com/njcn/gather/result/util/AngleResultAssemblerTest.java 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 616e187f..b6f961ef 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 @@ -58,7 +58,21 @@ public enum ItemReportKeyEnum { POWER_FACTOR("powerFactor", "P表:功率因数实测值(数据侧暂无,留空)"), POS_SEQ("posSeq", "不平衡表:基波正序(数据侧暂无,留空)"), NEG_SEQ("negSeq", "不平衡表:基波负序(数据侧暂无,留空)"), - ZERO_SEQ("zeroSeq", "不平衡表:基波零序(数据侧暂无,留空)"); + ZERO_SEQ("zeroSeq", "不平衡表:基波零序(数据侧暂无,留空)"), + BASE_CURRENT_A("baseCurrentA", "HI表:A相基波电流幅值,用于计算谐波电流含有率"), + BASE_CURRENT_B("baseCurrentB", "HI表:B相基波电流幅值,用于计算谐波电流含有率"), + BASE_CURRENT_C("baseCurrentC", "HI表:C相基波电流幅值,用于计算谐波电流含有率"), + STANDARD_RATE("standardRate", "HI表:标准源输出含有率"), + TEST_RATE_A("testRateA", "HI表:A相谐波电流含有率"), + TEST_RATE_B("testRateB", "HI表:B相谐波电流含有率"), + TEST_RATE_C("testRateC", "HI表:C相谐波电流含有率"), + ERROR_RATE_A("errorRateA", "HI表:A相谐波电流含有率误差"), + ERROR_RATE_B("errorRateB", "HI表:B相谐波电流含有率误差"), + ERROR_RATE_C("errorRateC", "HI表:C相谐波电流含有率误差"), + STANDARD_THD("standardThd", "HV/HI表:标准源总谐波畸变率"), + THD_A("thdA", "HV/HI表:A相总谐波畸变率"), + THD_B("thdB", "HV/HI表:B相总谐波畸变率"), + THD_C("thdC", "HV/HI表:C相总谐波畸变率"); private String key; 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 1510da18..7bc8540c 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 @@ -18,6 +18,7 @@ public enum PowerIndexEnum { V("V", "电压"), I("I", "电流"), P("P", "功率"), + ANGLE("Angle", "基波相位"), IMBV("IMBV", "负序电压不平衡度"), IMBA("IMBA", "负序电流不平衡度"), F("F", "短时电压闪变"), 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 74b8baca..a086557a 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 @@ -865,6 +865,7 @@ public class PqReportServiceImpl extends ServiceImpl i Docx4jUtil.addWatermarkToDocument(baseModelDocument, "非正式"); } Docx4jUtil.cleanBlankPagesAndRedundantPageBreaks(baseModelDocument); + Docx4jUtil.enableUpdateFieldsOnOpen(baseModelDocument); baseModelDocument.save(new File(dirPath.concat(File.separator).concat(fileName))); this.updateDevAndPlanState(devId, devReportParam.getPlanId()); } catch (NoSuchFileException e) { @@ -945,6 +946,7 @@ public class PqReportServiceImpl extends ServiceImpl i String dirPath = pathConfig.getReportPath().concat(File.separator).concat(FilePathSanitizer.toSafeFileName(devType.getName())); // 确保目录存在 ensureDirectoryExists(dirPath); + Docx4jUtil.enableUpdateFieldsOnOpen(baseModelDocument); baseModelDocument.save(new File(dirPath.concat(File.separator).concat(pqDevVO.getCreateId()).concat(ReportConstant.DOCX))); this.updateDevAndPlanState(devId, devReportParam.getPlanId()); } catch (NoSuchFileException e) { @@ -1271,11 +1273,11 @@ public class PqReportServiceImpl extends ServiceImpl i // 检测脚本明细按 scriptCode 分组 List pqScriptDtlsList = pqScriptDtlsService.getScriptDtlsDataList(devReportParam.getScriptId()); Map> scriptMap = pqScriptDtlsList.stream() - .collect(Collectors.groupingBy(PqScriptDtlDataVO::getScriptCode, LinkedHashMap::new, Collectors.toList())); + .collect(Collectors.groupingBy(item -> itemAnchorGroupKey(item.getScriptCode()), LinkedHashMap::new, Collectors.toList())); // detail 模板池按 H5 分组 List headingContents = Docx4jUtil.extractHeading5Contents(detailDocumentPart.getContent(), detailDocumentPart); Map> contentMap = headingContents.stream() - .collect(Collectors.groupingBy(Docx4jUtil.HeadingContent::getHeadingText, Collectors.toList())); + .collect(Collectors.groupingBy(item -> itemAnchorGroupKey(item.getHeadingText()), LinkedHashMap::new, Collectors.toList())); int devChns = pqDevVO.getDevChns() == null ? 1 : pqDevVO.getDevChns(); Set hiddenCodes = new LinkedHashSet<>(); for (BookmarkUtil.BookmarkInfo info : bookmarks) { @@ -1344,17 +1346,26 @@ public class PqReportServiceImpl extends ServiceImpl i ItemAnchorSectionUtil.stripItemAnchorBookmarks(baseDocumentPart); } + private static String itemAnchorGroupKey(String value) { + return value == null ? "" : value.trim().toUpperCase(Locale.ROOT); + } + /** * 新型模板的回路标题(仅多回路时被调用):"测量回路{n}",n 从 1 起。 * 与老路径 getLineTitle 的差异(有意):不带 "n." 前缀、单回路由调用方直接省略。 */ private P buildItemLineTitle(Map> contentMap, int lineIndex, ObjectFactory factory) { int lineNo = lineIndex + 1; - if (CollUtil.isNotEmpty(contentMap.get(PowerIndexEnum.LINE_TITLE.getKey()))) { - return Docx4jUtil.createTitle(factory, 2, "测量回路" + lineNo, "SimSun", 30, true); - } P titleParagraph = factory.createP(); - Docx4jUtil.createTitle(factory, titleParagraph, "测量回路" + lineNo, 28, true); + PPr pPr = factory.createPPr(); + PPrBase.Spacing spacing = factory.createPPrBaseSpacing(); + spacing.setBefore(BigInteger.valueOf(60)); + spacing.setAfter(BigInteger.valueOf(20)); + spacing.setLine(BigInteger.valueOf(240)); + spacing.setLineRule(STLineSpacingRule.AUTO); + pPr.setSpacing(spacing); + titleParagraph.setPPr(pPr); + Docx4jUtil.createTitle(factory, titleParagraph, "测量回路" + lineNo, 24, true); return titleParagraph; } diff --git a/detection/src/main/java/com/njcn/gather/report/util/ItemAnchorAssembler.java b/detection/src/main/java/com/njcn/gather/report/util/ItemAnchorAssembler.java index 2b652e05..6c2d7adb 100644 --- a/detection/src/main/java/com/njcn/gather/report/util/ItemAnchorAssembler.java +++ b/detection/src/main/java/com/njcn/gather/report/util/ItemAnchorAssembler.java @@ -6,15 +6,9 @@ import com.njcn.common.pojo.exception.BusinessException; import com.njcn.gather.report.pojo.enums.ItemReportKeyEnum; import com.njcn.gather.report.pojo.result.SingleTestResult; import com.njcn.gather.tools.report.util.Docx4jUtil; -import org.docx4j.wml.BooleanDefaultTrue; -import org.docx4j.wml.HpsMeasure; -import org.docx4j.wml.Jc; -import org.docx4j.wml.JcEnumeration; import org.docx4j.wml.ObjectFactory; import org.docx4j.wml.P; -import org.docx4j.wml.PPr; import org.docx4j.wml.R; -import org.docx4j.wml.RFonts; import org.docx4j.wml.RPr; import org.docx4j.wml.Tbl; import org.docx4j.wml.Tc; @@ -25,11 +19,11 @@ import org.docx4j.wml.TrPr; import javax.xml.bind.JAXBElement; import java.math.BigDecimal; -import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; /** @@ -63,11 +57,15 @@ public final class ItemAnchorAssembler { if (templateTbl == null || CollUtil.isEmpty(results) || CollUtil.isEmpty(tableKeys)) { return out; } + int tableNo = 0; for (SingleTestResult result : results) { List> rows = flattenRows(result, scriptCode); if (rows.isEmpty()) { continue; } + tableNo++; + applyHarmonicDerivedValues(rows, scriptCode); + Map fixedPlaceholders = buildHarmonicFixedPlaceholders(rows, scriptCode); JAXBElement copied; try { copied = Docx4jUtil.deepCopyTbl(templateTbl); @@ -75,6 +73,8 @@ public final class ItemAnchorAssembler { throw new RuntimeException("指标锚点模板表深拷贝失败: " + scriptCode, e); } Tbl tbl = copied.getValue(); + replaceFixedPlaceholders(tbl, fixedPlaceholders); + applyHarmonicPowerHeader(tbl, scriptCode, tableNo, rows); List trs = tbl.getContent(); // 模板规范约定:最后一行是 key 行(不走 judgeTableCross,新路径自带定位依据); // 从尾部找最后一个真正的 Tr——Word 重存可能把 bookmarkStart/End 漏成 w:tbl 直接子节点(本仓 _GoBack 先例) @@ -121,14 +121,16 @@ public final class ItemAnchorAssembler { out.add(copied); } // 多表(谐波类多 scriptIndex)背靠背插入时,OOXML 相邻 w:tbl 会被 Word 合并渲染成一张大表; - // 给每张表补"测点{n}"标题段分隔(客户源文档中多测点谐波表本就是带标题的独立表)。单表不加,输出不变 + // 只插入空段落做结构分隔。测点号属于 HP 表内标题单元格,不能作为表外可见段落。 if (out.size() > 1) { - List withCaptions = new ArrayList<>(out.size() * 2); + List withSeparators = new ArrayList<>(out.size() * 2 - 1); for (int i = 0; i < out.size(); i++) { - withCaptions.add(createPointCaption(factory, i + 1)); - withCaptions.add(out.get(i)); + withSeparators.add(out.get(i)); + if (i < out.size() - 1) { + withSeparators.add(factory.createP()); + } } - return withCaptions; + return withSeparators; } return out; } @@ -182,37 +184,6 @@ public final class ItemAnchorAssembler { return null; } - /** "测点{n}"表标题段:仿宋 9 号加粗居中,与客户源文档"谐波功率检验结果(测点n)"标题段同款式 */ - private static P createPointCaption(ObjectFactory factory, int pointNo) { - P p = factory.createP(); - PPr pPr = factory.createPPr(); - Jc jc = factory.createJc(); - jc.setVal(JcEnumeration.CENTER); - pPr.setJc(jc); - p.setPPr(pPr); - R run = factory.createR(); - RPr rPr = factory.createRPr(); - RFonts fonts = factory.createRFonts(); - fonts.setAscii("仿宋"); - fonts.setHAnsi("仿宋"); - fonts.setEastAsia("仿宋"); - fonts.setCs("仿宋"); - rPr.setRFonts(fonts); - HpsMeasure sz = factory.createHpsMeasure(); - sz.setVal(BigInteger.valueOf(18)); - rPr.setSz(sz); - rPr.setSzCs(sz); - BooleanDefaultTrue bold = factory.createBooleanDefaultTrue(); - rPr.setB(bold); - rPr.setBCs(bold); - run.setRPr(rPr); - Text text = factory.createText(); - text.setValue("测点" + pointNo); - run.getContent().add(text); - p.getContent().add(run); - return p; - } - /** * 把 SingleTestResult 的三层嵌套 detail 摊平成行列表,并做新模板专属的行级补充: * errorScope 注入(HV/HI/HP 的标准限值列)、time 缺失时"测点{n}"兜底(F/IMBV/IMBA)、P 有功特例。 @@ -239,7 +210,7 @@ public final class ItemAnchorAssembler { for (Map raw : errorEntry.getValue()) { seq++; Map row = new LinkedHashMap<>(raw); - row.putIfAbsent(ItemReportKeyEnum.ERROR_SCOPE.getKey(), errorScope); + applyErrorScope(row, scriptCode, errorScope); if (StrUtil.isBlank(row.get(ItemReportKeyEnum.TIME.getKey()))) { row.put(ItemReportKeyEnum.TIME.getKey(), "测点" + seq); } @@ -254,6 +225,23 @@ public final class ItemAnchorAssembler { return out; } + private static void applyErrorScope(Map row, String scriptCode, String errorScope) { + String key = ItemReportKeyEnum.ERROR_SCOPE.getKey(); + if ("HV".equalsIgnoreCase(scriptCode) || "HP".equalsIgnoreCase(scriptCode)) { + String value = StrUtil.isBlank(row.get(key)) ? errorScope : row.get(key); + row.put(key, stripTrailingUnit(value)); + return; + } + row.putIfAbsent(key, errorScope); + } + + private static String stripTrailingUnit(String value) { + if (StrUtil.isBlank(value)) { + return value; + } + return value.trim().replaceFirst("\\s*(?:(?:[%%‰°℃]|[a-zA-Z]+|[\\u4e00-\\u9fa5]+)\\s*)+$", ""); + } + /** * P(有功功率)特例:activePower = 三相实测之和;设置类与无功/视在/功率因数数据侧暂无,留空。 * spec §6 #2 处置。 @@ -279,6 +267,256 @@ public final class ItemAnchorAssembler { } } + private static void applyHarmonicDerivedValues(List> rows, String scriptCode) { + if (!"HI".equalsIgnoreCase(scriptCode)) { + return; + } + for (Map row : rows) { + String scriptStandardRate = row.get(ItemReportKeyEnum.STANDARD_RATE.getKey()); + String standardRate = parseNumber(scriptStandardRate) == null ? "" : scriptStandardRate; + String testRateA = rate(row.get(ItemReportKeyEnum.TEST_A.getKey()), row.get(ItemReportKeyEnum.BASE_CURRENT_A.getKey())); + String testRateB = rate(row.get(ItemReportKeyEnum.TEST_B.getKey()), row.get(ItemReportKeyEnum.BASE_CURRENT_B.getKey())); + String testRateC = rate(row.get(ItemReportKeyEnum.TEST_C.getKey()), row.get(ItemReportKeyEnum.BASE_CURRENT_C.getKey())); + String errorRateA = difference(testRateA, standardRate); + String errorRateB = difference(testRateB, standardRate); + String errorRateC = difference(testRateC, standardRate); + row.put(ItemReportKeyEnum.STANDARD_RATE.getKey(), standardRate); + row.put(ItemReportKeyEnum.TEST_RATE_A.getKey(), testRateA); + row.put(ItemReportKeyEnum.TEST_RATE_B.getKey(), testRateB); + row.put(ItemReportKeyEnum.TEST_RATE_C.getKey(), testRateC); + row.put(ItemReportKeyEnum.ERROR_RATE_A.getKey(), errorRateA); + row.put(ItemReportKeyEnum.ERROR_RATE_B.getKey(), errorRateB); + row.put(ItemReportKeyEnum.ERROR_RATE_C.getKey(), errorRateC); + row.put(ItemReportKeyEnum.ERROR_SCOPE.getKey(), harmonicCurrentErrorScope(standardRate)); + } + } + + private static String harmonicCurrentErrorScope(String standardRateText) { + Double standardRate = parseNumber(standardRateText); + if (standardRate == null) { + return ""; + } + double limit = standardRate >= 3D ? standardRate * 0.05D : 0.15D; + return "±" + format4(limit); + } + + private static Map buildHarmonicFixedPlaceholders(List> rows, String scriptCode) { + Map placeholders = new LinkedHashMap<>(); + if ("HV".equalsIgnoreCase(scriptCode)) { + placeholders.put(ItemReportKeyEnum.STANDARD_THD.getKey(), thd(rows, ItemReportKeyEnum.STANDARD.getKey())); + placeholders.put(ItemReportKeyEnum.THD_A.getKey(), thd(rows, ItemReportKeyEnum.TEST_A.getKey())); + placeholders.put(ItemReportKeyEnum.THD_B.getKey(), thd(rows, ItemReportKeyEnum.TEST_B.getKey())); + placeholders.put(ItemReportKeyEnum.THD_C.getKey(), thd(rows, ItemReportKeyEnum.TEST_C.getKey())); + } else if ("HI".equalsIgnoreCase(scriptCode)) { + placeholders.put(ItemReportKeyEnum.STANDARD_THD.getKey(), thd(rows, ItemReportKeyEnum.STANDARD_RATE.getKey())); + placeholders.put(ItemReportKeyEnum.THD_A.getKey(), thd(rows, ItemReportKeyEnum.TEST_RATE_A.getKey())); + placeholders.put(ItemReportKeyEnum.THD_B.getKey(), thd(rows, ItemReportKeyEnum.TEST_RATE_B.getKey())); + placeholders.put(ItemReportKeyEnum.THD_C.getKey(), thd(rows, ItemReportKeyEnum.TEST_RATE_C.getKey())); + } + return placeholders; + } + + private static void applyHarmonicPowerHeader(Tbl tbl, String scriptCode, int pointNo, List> rows) { + if (!"HP".equalsIgnoreCase(scriptCode)) { + return; + } + Tr firstRow = findFirstRow(tbl.getContent()); + if (firstRow == null) { + return; + } + Tc titleCell = cellAt(firstRow, 0); + if (titleCell == null) { + return; + } + String title = Docx4jUtil.getTextFromCell(titleCell); + String baseTitle = StrUtil.blankToDefault(title, StrUtil.EMPTY) + .replaceFirst("(测点\\s*\\d+)$", ""); + setCellText(titleCell, baseTitle + "(测点" + pointNo + ")"); + fillNextCellByLabel(tbl, "基波电压", firstDataValue(rows, ItemReportKeyEnum.STANDARD_V.getKey())); + fillNextCellByLabel(tbl, "基波电流", firstDataValue(rows, ItemReportKeyEnum.STANDARD_I.getKey())); + } + + private static String firstDataValue(List> rows, String key) { + if (CollUtil.isEmpty(rows)) { + return ""; + } + for (Map row : rows) { + String value = row.get(key); + if (StrUtil.isNotBlank(value) && !"/".equals(value.trim())) { + return value; + } + } + return ""; + } + + private static void fillNextCellByLabel(Tbl tbl, String label, String value) { + if (StrUtil.isBlank(value) || tbl == null || tbl.getContent() == null) { + return; + } + for (Object o : tbl.getContent()) { + Object v = o instanceof JAXBElement ? ((JAXBElement) o).getValue() : o; + if (v instanceof Tr && fillNextCellByLabel((Tr) v, label, value)) { + return; + } + } + } + + private static boolean fillNextCellByLabel(Tr row, String label, String value) { + if (StrUtil.isBlank(value) || row == null || row.getContent() == null) { + return false; + } + List cells = row.getContent(); + for (int i = 0; i < cells.size() - 1; i++) { + Tc labelCell = cellAt(row, i); + if (labelCell != null && Docx4jUtil.getTextFromCell(labelCell).contains(label)) { + Tc valueCell = cellAt(row, i + 1); + if (valueCell != null) { + setCellText(valueCell, value); + } + return true; + } + } + return false; + } + + private static Tr findFirstRow(List content) { + for (Object o : content) { + Object v = o instanceof JAXBElement ? ((JAXBElement) o).getValue() : o; + if (v instanceof Tr) { + return (Tr) v; + } + } + return null; + } + + private static Tc cellAt(Tr row, int index) { + if (row == null || row.getContent() == null || row.getContent().size() <= index) { + return null; + } + Object cellObj = row.getContent().get(index); + Object value = cellObj instanceof JAXBElement ? ((JAXBElement) cellObj).getValue() : cellObj; + return value instanceof Tc ? (Tc) value : null; + } + + private static String rate(String numeratorText, String denominatorText) { + Double numerator = parseNumber(numeratorText); + Double denominator = parseNumber(denominatorText); + if (numerator == null || denominator == null || denominator == 0D) { + return ""; + } + return format4(numerator / denominator * 100D); + } + + private static String difference(String minuendText, String subtrahendText) { + Double minuend = parseNumber(minuendText); + Double subtrahend = parseNumber(subtrahendText); + if (minuend == null || subtrahend == null) { + return ""; + } + return format4(minuend - subtrahend); + } + + private static String thd(List> rows, String key) { + double sum = 0D; + boolean hasValue = false; + for (Map row : rows) { + Double value = parseNumber(row.get(key)); + if (value != null) { + sum += value * value; + hasValue = true; + } + } + return hasValue ? format4(Math.sqrt(sum)) : ""; + } + + private static Double parseNumber(String text) { + if (StrUtil.isBlank(text) || "/".equals(text.trim()) || "--".equals(text.trim())) { + return null; + } + try { + return Double.parseDouble(text.trim()); + } catch (NumberFormatException ignored) { + return null; + } + } + + private static String format4(double value) { + return String.format(Locale.ROOT, "%.4f", value); + } + + private static void replaceFixedPlaceholders(Tbl tbl, Map values) { + if (values.isEmpty()) { + return; + } + for (Object rowObj : tbl.getContent()) { + Object rowValue = rowObj instanceof JAXBElement ? ((JAXBElement) rowObj).getValue() : rowObj; + if (!(rowValue instanceof Tr)) { + continue; + } + for (Object cellObj : ((Tr) rowValue).getContent()) { + Object cellValue = cellObj instanceof JAXBElement ? ((JAXBElement) cellObj).getValue() : cellObj; + if (cellValue instanceof Tc) { + Tc cell = (Tc) cellValue; + String replacement = values.get(Docx4jUtil.getTextFromCell(cell)); + if (replacement != null) { + setCellText(cell, replacement); + } + } + } + } + } + + private static void setCellText(Tc cell, String value) { + P firstParagraph = null; + for (Object content : cell.getContent()) { + if (content instanceof P) { + firstParagraph = (P) content; + break; + } + } + if (firstParagraph == null) { + firstParagraph = new P(); + cell.getContent().add(firstParagraph); + } + boolean written = false; + for (Object paragraphObj : cell.getContent()) { + if (!(paragraphObj instanceof P)) { + continue; + } + P paragraph = (P) paragraphObj; + for (Object runObj : paragraph.getContent()) { + if (!(runObj instanceof R)) { + continue; + } + R run = (R) runObj; + for (Object textObj : run.getContent()) { + Text text = unwrapText(textObj); + if (text != null) { + text.setValue(written ? "" : value); + written = true; + } + } + } + } + if (!written) { + R run = new R(); + Text text = new Text(); + text.setValue(value); + run.getContent().add(text); + firstParagraph.getContent().add(run); + } + } + + private static Text unwrapText(Object textObj) { + if (textObj instanceof Text) { + return (Text) textObj; + } + if (textObj instanceof JAXBElement && ((JAXBElement) textObj).getValue() instanceof Text) { + return (Text) ((JAXBElement) textObj).getValue(); + } + return null; + } + @SuppressWarnings("unchecked") private static JAXBElement findFirstTable(Docx4jUtil.HeadingContent template) { if (template == null || CollUtil.isEmpty(template.getSubContent())) { 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 924e5f92..2a2364aa 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 @@ -64,6 +64,7 @@ import com.njcn.gather.result.pojo.param.ResultParam; import com.njcn.gather.result.pojo.vo.*; import com.njcn.gather.result.service.DataCheckAsyncNotifier; import com.njcn.gather.result.service.IResultService; +import com.njcn.gather.result.util.AngleResultAssembler; import com.njcn.gather.result.util.PowerResultAssembler; import com.njcn.gather.result.util.UnbalanceResultAssembler; import com.njcn.gather.script.mapper.PqScriptMapper; @@ -1469,6 +1470,22 @@ public class ResultServiceImpl implements IResultService { finalContent.put(affectName, errorList); return; } + if (PowerIndexEnum.ANGLE.getKey().equalsIgnoreCase(scriptCode)) { + SingleNonHarmParam param = new SingleNonHarmParam(planCode, devId, lineNo, valueTypeList, indexList); + List angleResults = simAndDigNonHarmonicService.queryBySortListWithAdTypeCode(param); + List> keyFillMapList = AngleResultAssembler.assembleRows(angleResults); + if (CollUtil.isEmpty(keyFillMapList)) { + log.warn("生成基波相位类表格数据跳过,结果表数据为空,planCode={}, devId={}, lineNo={}, scriptCode={}, scriptIndexList={}", + planCode, devId, lineNo, scriptCode, indexList); + return; + } + Map>> errorScoperMap = keyFillMapList.stream() + .collect(Collectors.groupingBy(map -> map.get(ItemReportKeyEnum.ERROR_SCOPE.getKey()), LinkedHashMap::new, Collectors.toList())); + List>>> errorList = new ArrayList<>(); + errorList.add(errorScoperMap); + finalContent.put(affectName, errorList); + return; + } if (PowerIndexEnum.IMBV.getKey().equalsIgnoreCase(scriptCode) || PowerIndexEnum.IMBA.getKey().equalsIgnoreCase(scriptCode)) { SingleNonHarmParam param = new SingleNonHarmParam(planCode, devId, lineNo, valueTypeList, indexList); @@ -1552,6 +1569,8 @@ public class ResultServiceImpl implements IResultService { int timeInt = (int) timeDouble; // 填充结果数据 if (fillThreePhaseData(singleResult, timeInt, keyFillMap, scriptCode)) { + fillHarmonicScriptStandard(keyFillMap, scriptCode, dtlsList); + fillHarmonicPowerSourceSettings(keyFillMap, scriptCode, scriptDtlDataVOList); keyFillMapList.add(keyFillMap); } }); @@ -1754,9 +1773,100 @@ public class ResultServiceImpl implements IResultService { keyFillMap.put(ItemReportKeyEnum.RESULT.getKey(), result); errorScope = dealErrorScope(errorScope).concat(unit); keyFillMap.put(ItemReportKeyEnum.ERROR_SCOPE.getKey(), errorScope); + if (PowerIndexEnum.HI.getKey().equalsIgnoreCase(scriptCode)) { + fillBaseCurrentData(singleResult, keyFillMap); + } return true; } + /** + * HI 新模板明细显示谐波电流含有率,装配期需要同一条谐波结果中的三相基波电流作为分母。 + */ + private void fillBaseCurrentData(Object singleResult, Map keyFillMap) { + putBaseCurrent(singleResult, PowerConstant.PHASE_A, ItemReportKeyEnum.BASE_CURRENT_A.getKey(), keyFillMap); + putBaseCurrent(singleResult, PowerConstant.PHASE_B, ItemReportKeyEnum.BASE_CURRENT_B.getKey(), keyFillMap); + putBaseCurrent(singleResult, PowerConstant.PHASE_C, ItemReportKeyEnum.BASE_CURRENT_C.getKey(), keyFillMap); + } + + private void putBaseCurrent(Object singleResult, String phase, String testKey, Map keyFillMap) { + DetectionData base = getResultData(singleResult, 1, phase); + keyFillMap.put(testKey, formatBaseCurrent(base == null ? null : base.getData())); + } + + private String formatBaseCurrent(Double value) { + String formatted = value == null ? "" : PubUtils.doubleRoundStr(4, value); + return formatted == null ? "" : formatted; + } + + /** + * HI 的标准源输出值在云南模板中展示含有率,优先沿用脚本下发的百分比值。 + */ + private void fillHarmonicScriptStandard(Map keyFillMap, String scriptCode, List checkDataList) { + if (!PowerIndexEnum.HI.getKey().equalsIgnoreCase(scriptCode) || CollUtil.isEmpty(checkDataList)) { + return; + } + if (StrUtil.isNotBlank(keyFillMap.get(ItemReportKeyEnum.STANDARD_RATE.getKey()))) { + return; + } + PqScriptCheckData standard = firstCheckValue(checkDataList); + if (standard == null || standard.getValue() == null) { + return; + } + String value = PubUtils.doubleRoundStr(4, standard.getValue()); + if (value != null) { + keyFillMap.put(ItemReportKeyEnum.STANDARD_RATE.getKey(), value); + } + } + + private PqScriptCheckData firstCheckValue(List checkDataList) { + for (String phase : Arrays.asList(PowerConstant.PHASE_A, PowerConstant.PHASE_B, PowerConstant.PHASE_C)) { + for (PqScriptCheckData data : checkDataList) { + if (phase.equalsIgnoreCase(data.getPhase()) && data.getValue() != null) { + return data; + } + } + } + return checkDataList.stream().filter(data -> data.getValue() != null).findFirst().orElse(null); + } + + /** + * HP 新模板表头需要基波电压/基波电流设置值,来源是同一 scriptIndex 的 VOL/CUR 脚本明细。 + */ + private void fillHarmonicPowerSourceSettings(Map keyFillMap, String scriptCode, + List scriptDetails) { + if (!PowerIndexEnum.HP.getKey().equalsIgnoreCase(scriptCode) || CollUtil.isEmpty(scriptDetails)) { + return; + } + putScriptSetting(keyFillMap, ItemReportKeyEnum.STANDARD_V.getKey(), firstScriptSetting(scriptDetails, "VOL")); + putScriptSetting(keyFillMap, ItemReportKeyEnum.STANDARD_I.getKey(), firstScriptSetting(scriptDetails, "CUR")); + } + + private void putScriptSetting(Map keyFillMap, String key, PqScriptDtls detail) { + if (detail == null || detail.getValue() == null || StrUtil.isNotBlank(keyFillMap.get(key))) { + return; + } + String value = PubUtils.doubleRoundStr(4, detail.getValue()); + if (StrUtil.isNotBlank(value)) { + keyFillMap.put(key, value); + } + } + + private PqScriptDtls firstScriptSetting(List scriptDetails, String valueType) { + for (String phase : Arrays.asList(PowerConstant.PHASE_A, PowerConstant.PHASE_B, PowerConstant.PHASE_C)) { + for (PqScriptDtls detail : scriptDetails) { + if (valueType.equalsIgnoreCase(detail.getValueType()) + && phase.equalsIgnoreCase(detail.getPhase()) + && detail.getValue() != null) { + return detail; + } + } + } + return scriptDetails.stream() + .filter(detail -> valueType.equalsIgnoreCase(detail.getValueType()) && detail.getValue() != null) + .findFirst() + .orElse(null); + } + /** * 针对浙江杭州处理脚本输出细节,todo... 需要更改更合理的方式,现在算是写死的了部分 * 目前浙江这边就是(间)谐波电压/电流 diff --git a/detection/src/main/java/com/njcn/gather/result/util/AngleResultAssembler.java b/detection/src/main/java/com/njcn/gather/result/util/AngleResultAssembler.java new file mode 100644 index 00000000..8abc91d7 --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/result/util/AngleResultAssembler.java @@ -0,0 +1,191 @@ +package com.njcn.gather.result.util; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.njcn.gather.detection.pojo.enums.DetectionCodeEnum; +import com.njcn.gather.detection.pojo.vo.DetectionData; +import com.njcn.gather.report.pojo.enums.ItemReportKeyEnum; +import com.njcn.gather.storage.pojo.po.SimAndDigNonHarmonicResult; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.stream.Collectors; + +/** + * 基波相位报告结果行装配:云南模板以电压相角为 0 度基准,取 I1A 的 A/B/C 三相电流相角填表。 + */ +public final class AngleResultAssembler { + + private AngleResultAssembler() { + } + + public static List> assembleRows(List results) { + List> rows = new ArrayList<>(); + if (CollUtil.isEmpty(results)) { + return rows; + } + Map> bySort = results.stream() + .filter(result -> result.getSort() != null) + .collect(Collectors.groupingBy(SimAndDigNonHarmonicResult::getSort, TreeMap::new, Collectors.toList())); + for (List sameSortResults : bySort.values()) { + SimAndDigNonHarmonicResult currentAngle = firstCurrentAngle(sameSortResults); + if (currentAngle == null) { + continue; + } + DetectionData[] phases = phases(currentAngle); + DetectionData primary = firstPresent(phases); + if (primary == null) { + continue; + } + Map row = defaultAngleRow(); + row.put(ItemReportKeyEnum.STANDARD.getKey(), valueOrSlash(primary.getResultData())); + putPhaseDetails(row, phases); + row.put(ItemReportKeyEnum.ERROR_SCOPE.getKey(), errorScope(primary)); + row.put(ItemReportKeyEnum.RESULT.getKey(), overallResult(phases)); + rows.add(row); + } + return rows; + } + + private static SimAndDigNonHarmonicResult firstCurrentAngle(List results) { + if (CollUtil.isEmpty(results)) { + return null; + } + for (SimAndDigNonHarmonicResult result : results) { + if (DetectionCodeEnum.I1A.getCode().equalsIgnoreCase(result.getAdType())) { + return result; + } + } + return null; + } + + private static Map defaultAngleRow() { + Map row = new LinkedHashMap<>(); + for (ItemReportKeyEnum key : Arrays.asList( + ItemReportKeyEnum.STANDARD, + ItemReportKeyEnum.TEST_A, + ItemReportKeyEnum.TEST_B, + ItemReportKeyEnum.TEST_C, + ItemReportKeyEnum.ERROR_A, + ItemReportKeyEnum.ERROR_B, + ItemReportKeyEnum.ERROR_C, + ItemReportKeyEnum.ERROR_SCOPE, + ItemReportKeyEnum.RESULT, + ItemReportKeyEnum.RESULT_A, + ItemReportKeyEnum.RESULT_B, + ItemReportKeyEnum.RESULT_C)) { + row.put(key.getKey(), "/"); + } + return row; + } + + private static DetectionData[] phases(SimAndDigNonHarmonicResult result) { + return new DetectionData[]{ + parse(result.getAValue()), + parse(result.getBValue()), + parse(result.getCValue()) + }; + } + + private static void putPhaseDetails(Map row, DetectionData[] phases) { + List tests = Arrays.asList(ItemReportKeyEnum.TEST_A.getKey(), + ItemReportKeyEnum.TEST_B.getKey(), ItemReportKeyEnum.TEST_C.getKey()); + List errors = Arrays.asList(ItemReportKeyEnum.ERROR_A.getKey(), + ItemReportKeyEnum.ERROR_B.getKey(), ItemReportKeyEnum.ERROR_C.getKey()); + List results = Arrays.asList(ItemReportKeyEnum.RESULT_A.getKey(), + ItemReportKeyEnum.RESULT_B.getKey(), ItemReportKeyEnum.RESULT_C.getKey()); + for (int i = 0; i < phases.length; i++) { + DetectionData phase = phases[i]; + row.put(tests.get(i), phase == null ? "/" : valueOrSlash(phase.getData())); + row.put(errors.get(i), phase == null || phase.getErrorData() == null ? "/" : format(phase.getErrorData())); + row.put(results.get(i), phase == null ? "/" : resultText(phase.getIsData())); + } + } + + private static DetectionData firstPresent(DetectionData[] values) { + for (DetectionData value : values) { + if (value != null) { + return value; + } + } + return null; + } + + private static DetectionData parse(String value) { + if (StrUtil.isBlank(value)) { + return null; + } + return JSONUtil.toBean(value, DetectionData.class); + } + + private static String overallResult(DetectionData[] phases) { + boolean hasResult = false; + for (DetectionData phase : phases) { + if (phase == null || phase.getIsData() == null) { + continue; + } + hasResult = true; + if (phase.getIsData() == 2) { + return "不合格"; + } + } + return hasResult ? "合格" : "/"; + } + + private static String resultText(Integer isData) { + if (isData == null) { + return "/"; + } + if (isData == 1) { + return "合格"; + } + if (isData == 2) { + return "不合格"; + } + return "/"; + } + + private static String errorScope(DetectionData primary) { + String scope = dealErrorScope(primary.getRadius()); + if (StrUtil.isNotBlank(primary.getUnit()) && !"/".equals(scope)) { + return scope + primary.getUnit(); + } + return scope; + } + + private static String dealErrorScope(String errorScope) { + if (StrUtil.isBlank(errorScope)) { + return "/"; + } + if (errorScope.contains("~")) { + String[] split = errorScope.split("~"); + String begin = split[0]; + if (begin.startsWith("-")) { + begin = begin.substring(1); + } + if (split.length > 1 && split[1].equalsIgnoreCase(begin)) { + return "±" + begin; + } + } + return errorScope; + } + + private static String valueOrSlash(Double value) { + return value == null ? "/" : format(value); + } + + private static String format(Double value) { + return value == null ? "/" : BigDecimal.valueOf(value).setScale(4, RoundingMode.HALF_UP).toPlainString(); + } + + private static String format(BigDecimal value) { + return value == null ? "/" : value.setScale(4, RoundingMode.HALF_UP).toPlainString(); + } +} diff --git a/detection/src/main/java/com/njcn/gather/result/util/UnbalanceResultAssembler.java b/detection/src/main/java/com/njcn/gather/result/util/UnbalanceResultAssembler.java index c2a8a180..08ecb4ee 100644 --- a/detection/src/main/java/com/njcn/gather/result/util/UnbalanceResultAssembler.java +++ b/detection/src/main/java/com/njcn/gather/result/util/UnbalanceResultAssembler.java @@ -58,7 +58,6 @@ public final class UnbalanceResultAssembler { private static Map defaultRow() { Map row = new LinkedHashMap<>(); for (ItemReportKeyEnum key : Arrays.asList( - ItemReportKeyEnum.TIME, ItemReportKeyEnum.STANDARD, ItemReportKeyEnum.TEST, ItemReportKeyEnum.ERROR, diff --git a/detection/src/test/java/com/njcn/gather/report/service/impl/PqReportServiceImplTest.java b/detection/src/test/java/com/njcn/gather/report/service/impl/PqReportServiceImplTest.java index a55b9d81..78138264 100644 --- a/detection/src/test/java/com/njcn/gather/report/service/impl/PqReportServiceImplTest.java +++ b/detection/src/test/java/com/njcn/gather/report/service/impl/PqReportServiceImplTest.java @@ -1,10 +1,24 @@ package com.njcn.gather.report.service.impl; +import com.njcn.gather.report.pojo.enums.PowerIndexEnum; +import com.njcn.gather.tools.report.util.Docx4jUtil; +import org.docx4j.wml.ObjectFactory; +import org.docx4j.wml.P; +import org.docx4j.wml.R; import org.junit.Test; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.math.BigInteger; import java.time.LocalDate; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; public class PqReportServiceImplTest { @@ -22,4 +36,39 @@ public class PqReportServiceImplTest { public void formatNumberChineseDateUsesChineseDateSeparators() { assertEquals("2021年1月1日", PqReportServiceImpl.formatNumberChineseDate(LocalDate.of(2021, 1, 1))); } + + @Test + public void buildItemLineTitleCreatesNormalSmallParagraph() throws Exception { + PqReportServiceImpl service = newReportService(); + ObjectFactory factory = new ObjectFactory(); + Docx4jUtil.HeadingContent lineTitleTemplate = new Docx4jUtil.HeadingContent(); + lineTitleTemplate.setHeadingText(PowerIndexEnum.LINE_TITLE.getKey()); + Map> contentMap = new HashMap<>(); + contentMap.put(PowerIndexEnum.LINE_TITLE.getKey(), Collections.singletonList(lineTitleTemplate)); + + Method method = PqReportServiceImpl.class.getDeclaredMethod("buildItemLineTitle", Map.class, int.class, ObjectFactory.class); + method.setAccessible(true); + P paragraph = (P) method.invoke(service, contentMap, 0, factory); + + assertEquals("测量回路1", Docx4jUtil.getTextFromP(paragraph)); + assertNull(paragraph.getPPr().getOutlineLvl()); + R run = (R) paragraph.getContent().get(0); + assertNotNull(run.getRPr()); + assertEquals(BigInteger.valueOf(24), run.getRPr().getSz().getVal()); + } + + @Test + public void itemAnchorGroupKeyNormalizesAngleScriptCode() throws Exception { + Method method = PqReportServiceImpl.class.getDeclaredMethod("itemAnchorGroupKey", String.class); + method.setAccessible(true); + + assertEquals("ANGLE", method.invoke(null, "Angle")); + assertEquals("ANGLE", method.invoke(null, "ANGLE")); + } + + private PqReportServiceImpl newReportService() throws Exception { + Constructor constructor = PqReportServiceImpl.class.getDeclaredConstructors()[0]; + constructor.setAccessible(true); + return (PqReportServiceImpl) constructor.newInstance(new Object[constructor.getParameterCount()]); + } } diff --git a/detection/src/test/java/com/njcn/gather/report/util/Docx4jUtilUpdateFieldsTest.java b/detection/src/test/java/com/njcn/gather/report/util/Docx4jUtilUpdateFieldsTest.java new file mode 100644 index 00000000..4b363d9a --- /dev/null +++ b/detection/src/test/java/com/njcn/gather/report/util/Docx4jUtilUpdateFieldsTest.java @@ -0,0 +1,26 @@ +package com.njcn.gather.report.util; + +import com.njcn.gather.tools.report.util.Docx4jUtil; +import org.docx4j.openpackaging.packages.WordprocessingMLPackage; +import org.docx4j.openpackaging.parts.WordprocessingML.DocumentSettingsPart; +import org.docx4j.wml.CTSettings; +import org.junit.Test; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +public class Docx4jUtilUpdateFieldsTest { + + @Test + public void enableUpdateFieldsOnOpenSetsWordSettingsFlag() throws Exception { + WordprocessingMLPackage wordPackage = WordprocessingMLPackage.createPackage(); + + Docx4jUtil.enableUpdateFieldsOnOpen(wordPackage); + + DocumentSettingsPart settingsPart = wordPackage.getMainDocumentPart().getDocumentSettingsPart(); + assertNotNull(settingsPart); + CTSettings settings = settingsPart.getContents(); + assertNotNull(settings.getUpdateFields()); + assertTrue(settings.getUpdateFields().isVal()); + } +} diff --git a/detection/src/test/java/com/njcn/gather/report/util/ItemAnchorAssemblerTest.java b/detection/src/test/java/com/njcn/gather/report/util/ItemAnchorAssemblerTest.java index 1b81813c..c7701471 100644 --- a/detection/src/test/java/com/njcn/gather/report/util/ItemAnchorAssemblerTest.java +++ b/detection/src/test/java/com/njcn/gather/report/util/ItemAnchorAssemblerTest.java @@ -75,10 +75,10 @@ public class ItemAnchorAssemblerTest { private Docx4jUtil.HeadingContent hvTemplate(String... keys) { Tbl tbl = f.createTbl(); tbl.getContent().add(row("谐波电压检验结果", "基波电压(V)", "57.7000")); - tbl.getContent().add(row("总谐波畸变率(%)", "", "", "", "")); + tbl.getContent().add(row("总谐波畸变率(%)", "", "standardThd", "", "")); tbl.getContent().add(row("测点", "标准源输出值", "被检仪器测量值", "误差", "标准限值", "检验结论")); tbl.getContent().add(row("", "", "A", "B", "C", "A", "B", "C", "", "A", "B", "C")); - tbl.getContent().add(row("THD", "", "", "", "", "", "", "", "", "", "", "")); + tbl.getContent().add(row("THD", "standardThd", "thdA", "thdB", "thdC", "", "", "", "", "", "", "")); tbl.getContent().add(row(keys)); JAXBElement el = new JAXBElement<>(new QName(W_NS, "tbl"), Tbl.class, tbl); Docx4jUtil.HeadingContent hc = new Docx4jUtil.HeadingContent(); @@ -87,6 +87,33 @@ public class ItemAnchorAssemblerTest { return hc; } + private Docx4jUtil.HeadingContent hpTemplate(String... keys) { + Tbl tbl = f.createTbl(); + tbl.getContent().add(row("谐波功率检验结果", "基波电压(V)", "", "基波电流(A)", "")); + tbl.getContent().add(row("谐波次数", "标准源输出值", "被检仪器测量值", "误差", "允许误差", "检定结论")); + tbl.getContent().add(row("", "", "L1", "L2", "L3", "L1", "L2", "L3", "", "")); + tbl.getContent().add(row(keys)); + JAXBElement el = new JAXBElement<>(new QName(W_NS, "tbl"), Tbl.class, tbl); + Docx4jUtil.HeadingContent hc = new Docx4jUtil.HeadingContent(); + hc.setHeadingText("HP"); + hc.addSubContent(el); + return hc; + } + + private Docx4jUtil.HeadingContent hpTemplateWithSplitHeader(String... keys) { + Tbl tbl = f.createTbl(); + tbl.getContent().add(row("谐波功率检验结果", "基波电压(V)", "")); + tbl.getContent().add(row("", "基波电流(A)", "")); + tbl.getContent().add(row("谐波次数", "标准源输出值", "被检仪器测量值", "误差", "允许误差", "检定结论")); + tbl.getContent().add(row("", "", "L1", "L2", "L3", "L1", "L2", "L3", "", "")); + tbl.getContent().add(row(keys)); + JAXBElement el = new JAXBElement<>(new QName(W_NS, "tbl"), Tbl.class, tbl); + Docx4jUtil.HeadingContent hc = new Docx4jUtil.HeadingContent(); + hc.setHeadingText("HP"); + hc.addSubContent(el); + return hc; + } + private SingleTestResult resultWithRows(String errorScope, List> rows) { Map>> byError = new LinkedHashMap<>(); byError.put(errorScope, rows); @@ -146,6 +173,27 @@ public class ItemAnchorAssemblerTest { assertEquals("", cellText(tbl, 1, 3)); // 数据侧没有的列留空 } + @Test + public void buildTables_harmonicVoltageAndPowerStripErrorScopeUnit() { + Docx4jUtil.HeadingContent tpl = template("time", "errorScope"); + Map row = new HashMap<>(); + row.put("time", "2"); + SingleTestResult hv = resultWithRows("0.15%", Collections.singletonList(row)); + SingleTestResult hp = resultWithRows("-0.1~0.1W", Collections.singletonList(row)); + + List hvOut = ItemAnchorAssembler.buildTables("HV", Collections.singletonList(hv), tpl, + Arrays.asList("time", "errorScope"), f); + List hpOut = ItemAnchorAssembler.buildTables("HP", Collections.singletonList(hp), tpl, + Arrays.asList("time", "errorScope"), f); + + @SuppressWarnings("unchecked") + Tbl hvTbl = ((JAXBElement) hvOut.get(0)).getValue(); + @SuppressWarnings("unchecked") + Tbl hpTbl = ((JAXBElement) hpOut.get(0)).getValue(); + assertEquals("0.15", cellText(hvTbl, 1, 1)); + assertEquals("-0.1~0.1", cellText(hpTbl, 1, 1)); + } + @Test public void buildTables_powerMapping_sumsThreePhases() { Docx4jUtil.HeadingContent tpl = template("standardV", "activePower", "reactivePower"); @@ -192,28 +240,66 @@ public class ItemAnchorAssemblerTest { SingleTestResult empty = new SingleTestResult(); // detail 为 null → 跳过 List out = ItemAnchorAssembler.buildTables("HV", Arrays.asList(r1, empty, r2), tpl, Arrays.asList("time", "standard"), f); - // 空结果跳过 → 2 张表;多表时每张表前有"测点{n}"标题段 → 共 4 个元素 - assertEquals(4, out.size()); + // 空结果跳过 → 2 张表;多表之间允许有空段落防止 Word 合并,但不得出现可见的表外"测点"标题 assertEquals(2, countTables(out)); + assertTrue(nonBlankParagraphTexts(out).isEmpty()); } - /** F1:多表(谐波多 scriptIndex)背靠背插入会被 Word 合并渲染成一张表,相邻表之间必须有段落分隔 */ + /** HP 多测点:测点号必须写在表格首行标题单元格内,不能生成表外可见标题段 */ @Test - public void buildTables_multipleTables_separatedByPointCaption() { - Docx4jUtil.HeadingContent tpl = template("time", "standard"); + public void buildTables_harmonicPowerMultipleTables_putsPointCaptionInTitleCell() { + List keys = Arrays.asList("time", "standard"); + Docx4jUtil.HeadingContent tpl = hpTemplate(keys.toArray(new String[0])); Map row = new HashMap<>(); - row.put("time", "2"); + row.put("time", "3"); row.put("standard", "0.5000"); SingleTestResult r1 = resultWithRows("0.05", Collections.singletonList(row)); SingleTestResult r2 = resultWithRows("0.05", Collections.singletonList(row)); - List out = ItemAnchorAssembler.buildTables("HV", Arrays.asList(r1, r2), tpl, - Arrays.asList("time", "standard"), f); - // 期望序列:测点1标题段、表1、测点2标题段、表2 - assertEquals(4, out.size()); - assertEquals("测点1", captionText(out.get(0))); - assertTrue(unwrap(out.get(1)) instanceof Tbl); - assertEquals("测点2", captionText(out.get(2))); - assertTrue(unwrap(out.get(3)) instanceof Tbl); + List out = ItemAnchorAssembler.buildTables("HP", Arrays.asList(r1, r2), tpl, keys, f); + + assertEquals(2, countTables(out)); + assertTrue(nonBlankParagraphTexts(out).isEmpty()); + List tables = collectTables(out); + assertEquals("谐波功率检验结果(测点1)", cellText(tables.get(0), 0, 0)); + assertEquals("谐波功率检验结果(测点2)", cellText(tables.get(1), 0, 0)); + } + + @Test + public void buildTables_harmonicPowerFillsBaseVoltageAndCurrentHeaderCells() { + List keys = Arrays.asList("time", "standard"); + Docx4jUtil.HeadingContent tpl = hpTemplate(keys.toArray(new String[0])); + Map row = new HashMap<>(); + row.put("time", "3"); + row.put("standard", "23.0800"); + row.put("standardV", "57.7000"); + row.put("standardI", "5.0000"); + SingleTestResult result = resultWithRows("1.5W", Collections.singletonList(row)); + + List out = ItemAnchorAssembler.buildTables("HP", Collections.singletonList(result), tpl, keys, f); + + @SuppressWarnings("unchecked") + Tbl tbl = ((JAXBElement) out.get(0)).getValue(); + assertEquals("57.7000", cellText(tbl, 0, 2)); + assertEquals("5.0000", cellText(tbl, 0, 4)); + } + + @Test + public void buildTables_harmonicPowerFillsCurrentWhenHeaderIsOnSecondRow() { + List keys = Arrays.asList("time", "standard"); + Docx4jUtil.HeadingContent tpl = hpTemplateWithSplitHeader(keys.toArray(new String[0])); + Map row = new HashMap<>(); + row.put("time", "3"); + row.put("standard", "23.0800"); + row.put("standardV", "57.7000"); + row.put("standardI", "5.0000"); + SingleTestResult result = resultWithRows("1.5W", Collections.singletonList(row)); + + List out = ItemAnchorAssembler.buildTables("HP", Collections.singletonList(result), tpl, keys, f); + + @SuppressWarnings("unchecked") + Tbl tbl = ((JAXBElement) out.get(0)).getValue(); + assertEquals("57.7000", cellText(tbl, 0, 2)); + assertEquals("5.0000", cellText(tbl, 1, 2)); } /** F1 边界:单表不加标题段,既有单表输出不变 */ @@ -281,6 +367,182 @@ public class ItemAnchorAssemblerTest { assertEquals(expected, ItemAnchorAssembler.extractTableKeys(Collections.singletonList(tpl))); } + @Test + public void buildTables_harmonicVoltageCalculatesThdFixedRowFromPhaseRates() { + List keys = Arrays.asList("time", "standard", "testA", "testB", "testC", + "errorA", "errorB", "errorC", "errorScope", "resultA", "resultB", "resultC"); + Docx4jUtil.HeadingContent tpl = hvTemplate(keys.toArray(new String[0])); + Map row1 = harmonicRow("2", "3.0000", "6.0000", "8.0000"); + row1.put("standard", "3.0000"); + Map row2 = harmonicRow("3", "4.0000", "8.0000", "15.0000"); + row2.put("standard", "4.0000"); + SingleTestResult r = resultWithRows("0.05", Arrays.asList(row1, row2)); + + List out = ItemAnchorAssembler.buildTables("HV", Collections.singletonList(r), tpl, keys, f); + + @SuppressWarnings("unchecked") + Tbl tbl = ((JAXBElement) out.get(0)).getValue(); + assertEquals("5.0000", cellText(tbl, 1, 2)); + assertEquals("5.0000", cellText(tbl, 4, 1)); + assertEquals("5.0000", cellText(tbl, 4, 2)); + assertEquals("10.0000", cellText(tbl, 4, 3)); + assertEquals("17.0000", cellText(tbl, 4, 4)); + } + + @Test + public void buildTables_harmonicCurrentCalculatesRateRowsAndThdFromBaseCurrent() { + List keys = Arrays.asList("time", "standardRate", "testRateA", "testRateB", "testRateC", + "errorRateA", "errorRateB", "errorRateC", "errorScope", "resultA", "resultB", "resultC"); + Docx4jUtil.HeadingContent tpl = hvTemplate(keys.toArray(new String[0])); + Map row1 = harmonicRow("2", "1.2000", "1.6000", "2.0000"); + row1.put("standard", "1.0000"); + row1.put("standardRate", "25.0000"); + row1.put("errorA", "0.2000"); + row1.put("errorB", "0.6000"); + row1.put("errorC", "1.0000"); + row1.put("baseCurrentA", "4.0000"); + row1.put("baseCurrentB", "4.0000"); + row1.put("baseCurrentC", "4.0000"); + Map row2 = harmonicRow("3", "0.9000", "1.2000", "1.5000"); + row2.put("standard", "1.0000"); + row2.put("standardRate", "25.0000"); + row2.put("errorA", "-0.1000"); + row2.put("errorB", "0.2000"); + row2.put("errorC", "0.5000"); + row2.put("baseCurrentA", "4.0000"); + row2.put("baseCurrentB", "4.0000"); + row2.put("baseCurrentC", "4.0000"); + SingleTestResult r = resultWithRows("0.05", Arrays.asList(row1, row2)); + + List out = ItemAnchorAssembler.buildTables("HI", Collections.singletonList(r), tpl, keys, f); + + @SuppressWarnings("unchecked") + Tbl tbl = ((JAXBElement) out.get(0)).getValue(); + assertEquals("35.3553", cellText(tbl, 1, 2)); + assertEquals("35.3553", cellText(tbl, 4, 1)); + assertEquals("37.5000", cellText(tbl, 4, 2)); + assertEquals("50.0000", cellText(tbl, 4, 3)); + assertEquals("62.5000", cellText(tbl, 4, 4)); + assertEquals("25.0000", cellText(tbl, 5, 1)); + assertEquals("30.0000", cellText(tbl, 5, 2)); + assertEquals("40.0000", cellText(tbl, 5, 3)); + assertEquals("50.0000", cellText(tbl, 5, 4)); + assertEquals("5.0000", cellText(tbl, 5, 5)); + assertEquals("15.0000", cellText(tbl, 5, 6)); + assertEquals("25.0000", cellText(tbl, 5, 7)); + assertEquals("±1.2500", cellText(tbl, 5, 8)); + assertEquals("±1.2500", cellText(tbl, 6, 8)); + } + + @Test + public void buildTables_harmonicCurrentLeavesStandardRateBlankWhenScriptRateMissing() { + List keys = Arrays.asList("time", "standardRate", "testRateA", "testRateB", "testRateC", + "errorRateA", "errorRateB", "errorRateC", "errorScope", "resultA", "resultB", "resultC"); + Docx4jUtil.HeadingContent tpl = hvTemplate(keys.toArray(new String[0])); + Map row = harmonicRow("2", "1.0000", "1.0000", "1.0000"); + row.put("standard", "1.0000"); + row.put("baseCurrentA", "5.0000"); + row.put("baseCurrentB", "5.0000"); + row.put("baseCurrentC", "5.0000"); + SingleTestResult r = resultWithRows("0.05", Collections.singletonList(row)); + + List out = ItemAnchorAssembler.buildTables("HI", Collections.singletonList(r), tpl, keys, f); + + @SuppressWarnings("unchecked") + Tbl tbl = ((JAXBElement) out.get(0)).getValue(); + assertEquals("", cellText(tbl, 1, 2)); + assertEquals("", cellText(tbl, 4, 1)); + assertEquals("", cellText(tbl, 5, 1)); + assertEquals("20.0000", cellText(tbl, 5, 2)); + assertEquals("", cellText(tbl, 5, 5)); + assertEquals("", cellText(tbl, 5, 8)); + } + + @Test + public void buildTables_harmonicCurrentKeepsScriptStandardRateWhenAmplitudeAvailable() { + List keys = Arrays.asList("time", "standardRate", "testRateA", "testRateB", "testRateC", + "errorRateA", "errorRateB", "errorRateC", "errorScope", "resultA", "resultB", "resultC"); + Docx4jUtil.HeadingContent tpl = hvTemplate(keys.toArray(new String[0])); + Map row = harmonicRow("2", "1.0000", "1.0000", "1.0000"); + row.put("standard", "2.0000"); + row.put("standardRate", "1.0000"); + row.put("baseCurrentA", "10.0000"); + row.put("baseCurrentB", "10.0000"); + row.put("baseCurrentC", "10.0000"); + SingleTestResult r = resultWithRows("0.05", Collections.singletonList(row)); + + List out = ItemAnchorAssembler.buildTables("HI", Collections.singletonList(r), tpl, keys, f); + + @SuppressWarnings("unchecked") + Tbl tbl = ((JAXBElement) out.get(0)).getValue(); + assertEquals("1.0000", cellText(tbl, 5, 1)); + assertEquals("9.0000", cellText(tbl, 5, 5)); + assertEquals("±0.1500", cellText(tbl, 5, 8)); + } + + @Test + public void buildTables_harmonicCurrentErrorRateUsesRateDifference() { + List keys = Arrays.asList("time", "standardRate", "testRateA", "testRateB", "testRateC", + "errorRateA", "errorRateB", "errorRateC", "errorScope", "resultA", "resultB", "resultC"); + Docx4jUtil.HeadingContent tpl = hvTemplate(keys.toArray(new String[0])); + Map row = harmonicRow("2", "2.0000", "1.0000", "1.0000"); + row.put("standard", "1.0000"); + row.put("standardRate", "10.0000"); + row.put("errorA", "0.0100"); + row.put("baseCurrentA", "10.0000"); + row.put("baseCurrentB", "10.0000"); + row.put("baseCurrentC", "10.0000"); + SingleTestResult r = resultWithRows("0.05", Collections.singletonList(row)); + + List out = ItemAnchorAssembler.buildTables("HI", Collections.singletonList(r), tpl, keys, f); + + @SuppressWarnings("unchecked") + Tbl tbl = ((JAXBElement) out.get(0)).getValue(); + assertEquals("10.0000", cellText(tbl, 5, 1)); + assertEquals("20.0000", cellText(tbl, 5, 2)); + assertEquals("10.0000", cellText(tbl, 5, 5)); + } + + @Test + public void buildTables_harmonicCurrentKeepsScriptStandardRateWhenAmplitudeMissing() { + List keys = Arrays.asList("time", "standardRate", "testRateA", "testRateB", "testRateC", + "errorRateA", "errorRateB", "errorRateC", "errorScope", "resultA", "resultB", "resultC"); + Docx4jUtil.HeadingContent tpl = hvTemplate(keys.toArray(new String[0])); + Map row = harmonicRow("2", "0.040144", "0.040248", "0.040116"); + row.put("standard", "/"); + row.put("standardRate", "1.0000"); + row.put("baseCurrentA", "4.0000"); + row.put("baseCurrentB", "4.0000"); + row.put("baseCurrentC", "4.0000"); + SingleTestResult r = resultWithRows("0.0075A", Collections.singletonList(row)); + + List out = ItemAnchorAssembler.buildTables("HI", Collections.singletonList(r), tpl, keys, f); + + @SuppressWarnings("unchecked") + Tbl tbl = ((JAXBElement) out.get(0)).getValue(); + assertEquals("1.0000", cellText(tbl, 1, 2)); + assertEquals("1.0000", cellText(tbl, 4, 1)); + assertEquals("1.0000", cellText(tbl, 5, 1)); + assertEquals("1.0036", cellText(tbl, 5, 2)); + assertEquals("0.0036", cellText(tbl, 5, 5)); + } + + private Map harmonicRow(String time, String testA, String testB, String testC) { + Map row = new HashMap<>(); + row.put("time", time); + row.put("standard", "0.0000"); + row.put("testA", testA); + row.put("testB", testB); + row.put("testC", testC); + row.put("errorA", "0.0000"); + row.put("errorB", "0.0000"); + row.put("errorC", "0.0000"); + row.put("resultA", "合格"); + row.put("resultB", "合格"); + row.put("resultC", "合格"); + return row; + } + private Object unwrap(Object o) { return (o instanceof JAXBElement) ? ((JAXBElement) o).getValue() : o; } @@ -295,10 +557,29 @@ public class ItemAnchorAssemblerTest { return n; } - private String captionText(Object o) { - Object v = unwrap(o); - assertTrue("相邻表之间应为段落,实际是 " + v.getClass().getSimpleName(), v instanceof P); - return Docx4jUtil.getTextFromP((P) v); + private List collectTables(List out) { + List tables = new ArrayList<>(); + for (Object o : out) { + Object v = unwrap(o); + if (v instanceof Tbl) { + tables.add((Tbl) v); + } + } + return tables; + } + + private List nonBlankParagraphTexts(List out) { + List texts = new ArrayList<>(); + for (Object o : out) { + Object v = unwrap(o); + if (v instanceof P) { + String text = Docx4jUtil.getTextFromP((P) v); + if (text != null && !text.trim().isEmpty()) { + texts.add(text); + } + } + } + return texts; } private List collectTrs(Tbl tbl) { diff --git a/detection/src/test/java/com/njcn/gather/result/service/impl/ResultServiceImplTest.java b/detection/src/test/java/com/njcn/gather/result/service/impl/ResultServiceImplTest.java new file mode 100644 index 00000000..99553af6 --- /dev/null +++ b/detection/src/test/java/com/njcn/gather/result/service/impl/ResultServiceImplTest.java @@ -0,0 +1,232 @@ +package com.njcn.gather.result.service.impl; + +import com.njcn.gather.report.pojo.enums.ItemReportKeyEnum; +import com.njcn.gather.report.pojo.result.SingleTestResult; +import com.njcn.gather.script.pojo.po.PqScriptCheckData; +import com.njcn.gather.script.pojo.vo.PqScriptDtlDataVO; +import com.njcn.gather.script.service.IPqScriptCheckDataService; +import com.njcn.gather.storage.pojo.po.SimAndDigHarmonicResult; +import com.njcn.gather.storage.pojo.po.SimAndDigNonHarmonicResult; +import com.njcn.gather.storage.pojo.param.SingleNonHarmParam; +import com.njcn.gather.storage.service.SimAndDigHarmonicService; +import com.njcn.gather.storage.service.SimAndDigNonHarmonicService; +import org.junit.Test; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ResultServiceImplTest { + + @Test + public void fillThreePhaseData_harmonicCurrentCarriesBaseCurrentForRateCalculation() throws Exception { + SimAndDigHarmonicResult result = new SimAndDigHarmonicResult(); + result.setAValue1(detectionData("4.0000", "10.0000")); + result.setBValue1(detectionData("5.0000", "11.0000")); + result.setCValue1(detectionData("6.0000", "12.0000")); + result.setAValue2(detectionData("1.0000")); + result.setBValue2(detectionData("1.5000")); + result.setCValue2(detectionData("2.0000")); + Map row = new HashMap<>(); + + Method method = ResultServiceImpl.class.getDeclaredMethod( + "fillThreePhaseData", Object.class, Integer.class, Map.class, String.class); + method.setAccessible(true); + + boolean filled = (Boolean) method.invoke(newResultService(), result, 2, row, "HI"); + + assertTrue(filled); + assertEquals("4.0000", row.get(ItemReportKeyEnum.BASE_CURRENT_A.getKey())); + assertEquals("5.0000", row.get(ItemReportKeyEnum.BASE_CURRENT_B.getKey())); + assertEquals("6.0000", row.get(ItemReportKeyEnum.BASE_CURRENT_C.getKey())); + } + + @Test + public void fillThreePhaseData_harmonicCurrentUsesBaseCurrentDataWhenResultDataMissing() throws Exception { + SimAndDigHarmonicResult result = new SimAndDigHarmonicResult(); + result.setAValue1(detectionDataOnlyData("1.0000")); + result.setBValue1(detectionDataOnlyData("2.0000")); + result.setCValue1(detectionDataOnlyData("3.0000")); + result.setAValue2(detectionData("0.1000")); + result.setBValue2(detectionData("0.2000")); + result.setCValue2(detectionData("0.3000")); + Map row = new HashMap<>(); + + Method method = ResultServiceImpl.class.getDeclaredMethod( + "fillThreePhaseData", Object.class, Integer.class, Map.class, String.class); + method.setAccessible(true); + + boolean filled = (Boolean) method.invoke(newResultService(), result, 2, row, "HI"); + + assertTrue(filled); + assertEquals("1.0000", row.get(ItemReportKeyEnum.BASE_CURRENT_A.getKey())); + assertEquals("2.0000", row.get(ItemReportKeyEnum.BASE_CURRENT_B.getKey())); + assertEquals("3.0000", row.get(ItemReportKeyEnum.BASE_CURRENT_C.getKey())); + } + + @Test + public void fillHarmonicScriptStandard_harmonicCurrentCopiesScriptPercentToStandardRate() throws Exception { + Map row = new HashMap<>(); + List checkData = Arrays.asList( + checkData("A", 2D, 1D), + checkData("B", 2D, 1D), + checkData("C", 2D, 1D) + ); + + Method method = ResultServiceImpl.class.getDeclaredMethod( + "fillHarmonicScriptStandard", Map.class, String.class, List.class); + method.setAccessible(true); + + method.invoke(newResultService(), row, "HI", checkData); + + assertEquals("1.0000", row.get(ItemReportKeyEnum.STANDARD_RATE.getKey())); + } + + @Test + public void getFinalContent_harmonicPowerCarriesBaseVoltageAndCurrentFromScriptDetails() throws Exception { + IPqScriptCheckDataService checkDataService = mock(IPqScriptCheckDataService.class); + SimAndDigHarmonicService harmonicService = mock(SimAndDigHarmonicService.class); + when(checkDataService.listCheckData(eq("script"), anyList())) + .thenReturn(Collections.singletonList(checkData("A", 3D, 23.08D))); + SimAndDigHarmonicResult harmonicResult = new SimAndDigHarmonicResult(); + harmonicResult.setAValue3(detectionData("23.0745", "23.0800")); + harmonicResult.setBValue3(detectionData("23.0809", "23.0800")); + harmonicResult.setCValue3(detectionData("23.0814", "23.0800")); + when(harmonicService.getSingleResult(any())).thenReturn(harmonicResult); + List scriptDetails = Arrays.asList( + scriptDetail("VOL", "A", 57.7D), + scriptDetail("CUR", "A", 5.0D) + ); + + SingleTestResult result = newResultService(harmonicService, checkDataService) + .getFinalContent(scriptDetails, "plan", "dev", 1, + Arrays.asList("time", "standardV", "standardI")); + + Map row = firstRow(result); + assertEquals("57.7000", row.get(ItemReportKeyEnum.STANDARD_V.getKey())); + assertEquals("5.0000", row.get(ItemReportKeyEnum.STANDARD_I.getKey())); + } + + @Test + public void getFinalContent_angleUsesCurrentAngleResultRows() throws Exception { + IPqScriptCheckDataService checkDataService = mock(IPqScriptCheckDataService.class); + SimAndDigNonHarmonicService nonHarmonicService = mock(SimAndDigNonHarmonicService.class); + when(checkDataService.listCheckData(eq("script"), anyList())).thenReturn(Collections.emptyList()); + SimAndDigNonHarmonicResult result = new SimAndDigNonHarmonicResult(); + result.setSort(21); + result.setAdType("I1A"); + result.setAValue(detectionAngleData("61.4", "60", "1.4", 2)); + result.setBValue(detectionAngleData("61", "60", "1", 1)); + result.setCValue(detectionAngleData("61", "60", "1", 1)); + when(nonHarmonicService.queryBySortListWithAdTypeCode(any(SingleNonHarmParam.class))) + .thenReturn(Collections.singletonList(result)); + + SingleTestResult reportResult = newResultService(null, checkDataService, nonHarmonicService) + .getFinalContent(Collections.singletonList(scriptDetail("Angle", "Base", 21)), + "plan", "dev", 1, Arrays.asList("standard", "testA", "testB", "testC")); + + Map row = firstRow(reportResult); + assertEquals("60.0000", row.get(ItemReportKeyEnum.STANDARD.getKey())); + assertEquals("61.4000", row.get(ItemReportKeyEnum.TEST_A.getKey())); + assertEquals("61.0000", row.get(ItemReportKeyEnum.TEST_B.getKey())); + assertEquals("61.0000", row.get(ItemReportKeyEnum.TEST_C.getKey())); + assertEquals("不合格", row.get(ItemReportKeyEnum.RESULT_A.getKey())); + assertEquals("合格", row.get(ItemReportKeyEnum.RESULT_B.getKey())); + assertEquals("合格", row.get(ItemReportKeyEnum.RESULT_C.getKey())); + } + + private ResultServiceImpl newResultService() throws Exception { + return newResultService(null, null); + } + + private ResultServiceImpl newResultService(SimAndDigHarmonicService harmonicService, + IPqScriptCheckDataService checkDataService) throws Exception { + return newResultService(harmonicService, checkDataService, null); + } + + private ResultServiceImpl newResultService(SimAndDigHarmonicService harmonicService, + IPqScriptCheckDataService checkDataService, + SimAndDigNonHarmonicService nonHarmonicService) throws Exception { + Constructor constructor = ResultServiceImpl.class.getDeclaredConstructors()[0]; + constructor.setAccessible(true); + Class[] parameterTypes = constructor.getParameterTypes(); + Object[] args = new Object[parameterTypes.length]; + for (int i = 0; i < parameterTypes.length; i++) { + if (nonHarmonicService != null && parameterTypes[i].equals(SimAndDigNonHarmonicService.class)) { + args[i] = nonHarmonicService; + } else if (harmonicService != null && parameterTypes[i].equals(SimAndDigHarmonicService.class)) { + args[i] = harmonicService; + } else if (checkDataService != null && parameterTypes[i].equals(IPqScriptCheckDataService.class)) { + args[i] = checkDataService; + } + } + return (ResultServiceImpl) constructor.newInstance(args); + } + + private Map firstRow(SingleTestResult result) { + return result.getDetail().values().iterator().next() + .get(0).values().iterator().next() + .get(0); + } + + private String detectionData(String value) { + return detectionData(value, value); + } + + private String detectionData(String data, String resultData) { + return "{\"isData\":1,\"data\":" + data + ",\"resultData\":" + resultData + + ",\"radius\":\"0.05\",\"unit\":\"A\"}"; + } + + private String detectionDataOnlyData(String data) { + return "{\"num\":1,\"isData\":4,\"data\":" + data + ",\"radius\":\"0.05\",\"unit\":\"A\"}"; + } + + private String detectionAngleData(String data, String resultData, String errorData, int isData) { + return "{\"isData\":" + isData + ",\"data\":" + data + ",\"resultData\":" + resultData + + ",\"errorData\":" + errorData + ",\"radius\":\"-1.0~1.0\",\"unit\":\"\\u00b0\"}"; + } + + private PqScriptCheckData checkData(String phase, Double harmNum, Double value) { + PqScriptCheckData data = new PqScriptCheckData(); + data.setScriptIndex(3); + data.setValueType("HP"); + data.setPhase(phase); + data.setHarmNum(harmNum); + data.setValue(value); + return data; + } + + private PqScriptDtlDataVO scriptDetail(String valueType, String phase, Double value) { + PqScriptDtlDataVO detail = new PqScriptDtlDataVO(); + detail.setScriptId("script"); + detail.setScriptCode("HP"); + detail.setScriptSubType("Base"); + detail.setScriptIndex(3); + detail.setValueType(valueType); + detail.setPhase(phase); + detail.setValue(value); + return detail; + } + + private PqScriptDtlDataVO scriptDetail(String scriptCode, String scriptSubType, Integer scriptIndex) { + PqScriptDtlDataVO detail = new PqScriptDtlDataVO(); + detail.setScriptId("script"); + detail.setScriptCode(scriptCode); + detail.setScriptSubType(scriptSubType); + detail.setScriptIndex(scriptIndex); + return detail; + } +} diff --git a/detection/src/test/java/com/njcn/gather/result/util/AngleResultAssemblerTest.java b/detection/src/test/java/com/njcn/gather/result/util/AngleResultAssemblerTest.java new file mode 100644 index 00000000..a1345b2d --- /dev/null +++ b/detection/src/test/java/com/njcn/gather/result/util/AngleResultAssemblerTest.java @@ -0,0 +1,64 @@ +package com.njcn.gather.result.util; + +import com.njcn.gather.report.pojo.enums.ItemReportKeyEnum; +import com.njcn.gather.storage.pojo.po.SimAndDigNonHarmonicResult; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class AngleResultAssemblerTest { + + @Test + public void assembleRows_usesCurrentAngleI1AAndIgnoresVoltageAngleU1A() { + SimAndDigNonHarmonicResult voltageAngle = result("U1A", + data(0D, 0D, 0D, 1), data(0D, 0D, 0D, 1), data(0D, 0D, 0D, 1)); + SimAndDigNonHarmonicResult currentAngle = result("I1A", + data(61.4D, 60D, 1.4D, 2), data(61D, 60D, 1D, 1), data(61D, 60D, 1D, 1)); + + List> rows = AngleResultAssembler.assembleRows(Arrays.asList(voltageAngle, currentAngle)); + + assertEquals(1, rows.size()); + Map row = rows.get(0); + assertEquals("60.0000", row.get(ItemReportKeyEnum.STANDARD.getKey())); + assertEquals("61.4000", row.get(ItemReportKeyEnum.TEST_A.getKey())); + assertEquals("61.0000", row.get(ItemReportKeyEnum.TEST_B.getKey())); + assertEquals("61.0000", row.get(ItemReportKeyEnum.TEST_C.getKey())); + assertEquals("1.4000", row.get(ItemReportKeyEnum.ERROR_A.getKey())); + assertEquals("1.0000", row.get(ItemReportKeyEnum.ERROR_B.getKey())); + assertEquals("1.0000", row.get(ItemReportKeyEnum.ERROR_C.getKey())); + assertEquals("不合格", row.get(ItemReportKeyEnum.RESULT_A.getKey())); + assertEquals("合格", row.get(ItemReportKeyEnum.RESULT_B.getKey())); + assertEquals("合格", row.get(ItemReportKeyEnum.RESULT_C.getKey())); + assertEquals("\u00b11.0\u00b0", row.get(ItemReportKeyEnum.ERROR_SCOPE.getKey())); + } + + @Test + public void assembleRows_voltageAngleOnlyReturnsNoRows() { + SimAndDigNonHarmonicResult voltageAngle = result("U1A", + data(0D, 0D, 0D, 1), data(0D, 0D, 0D, 1), data(0D, 0D, 0D, 1)); + + assertTrue(AngleResultAssembler.assembleRows(Collections.singletonList(voltageAngle)).isEmpty()); + } + + private SimAndDigNonHarmonicResult result(String adType, String a, String b, String c) { + SimAndDigNonHarmonicResult result = new SimAndDigNonHarmonicResult(); + result.setSort(21); + result.setAdType(adType); + result.setAValue(a); + result.setBValue(b); + result.setCValue(c); + return result; + } + + private String data(Double data, Double resultData, Double errorData, Integer isData) { + return "{\"data\":" + data + ",\"unit\":\"\\u00b0\",\"isData\":" + isData + + ",\"radius\":\"-1.0~1.0\",\"errorData\":" + errorData + + ",\"resultData\":" + resultData + "}"; + } +} diff --git a/detection/src/test/java/com/njcn/gather/result/util/UnbalanceResultAssemblerTest.java b/detection/src/test/java/com/njcn/gather/result/util/UnbalanceResultAssemblerTest.java index de56f6c3..32fe525f 100644 --- a/detection/src/test/java/com/njcn/gather/result/util/UnbalanceResultAssemblerTest.java +++ b/detection/src/test/java/com/njcn/gather/result/util/UnbalanceResultAssemblerTest.java @@ -9,6 +9,7 @@ import java.util.List; import java.util.Map; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; public class UnbalanceResultAssemblerTest { @@ -72,6 +73,17 @@ public class UnbalanceResultAssemblerTest { assertEquals("合格", row.get(ItemReportKeyEnum.RESULT.getKey())); } + @Test + public void assembleRows_leavesTimeUnsetForItemAnchorPointFallback() { + SimAndDigNonHarmonicResult unbalance = total("V_UNBAN", 7, + "{\"data\": 0.05, \"unit\": \"%\", \"isData\": 1, \"radius\": \"-0.2~0.2\", \"errorData\": 0.01, \"resultData\": 0.04}"); + + List> rows = UnbalanceResultAssembler.assembleRows(Arrays.asList(unbalance)); + + assertEquals(1, rows.size()); + assertNull(rows.get(0).get(ItemReportKeyEnum.TIME.getKey())); + } + private SimAndDigNonHarmonicResult total(String code, Integer sort, String tValue) { SimAndDigNonHarmonicResult result = new SimAndDigNonHarmonicResult(); result.setAdType(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 cd86a9cf..c4dc1acd 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 @@ -8,6 +8,7 @@ import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.docx4j.XmlUtils; import org.docx4j.openpackaging.packages.WordprocessingMLPackage; +import org.docx4j.openpackaging.parts.WordprocessingML.DocumentSettingsPart; import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart; import org.docx4j.wml.*; import org.docx4j.wml.TcPrInner.GridSpan; @@ -31,6 +32,27 @@ import java.util.Map; @Slf4j public class Docx4jUtil { + /** + * 让 Word 打开文档时自动刷新 PAGE / NUMPAGES 等域,避免动态插表后页码显示旧值。 + */ + public static void enableUpdateFieldsOnOpen(WordprocessingMLPackage wordPackage) throws Exception { + if (wordPackage == null || wordPackage.getMainDocumentPart() == null) { + return; + } + DocumentSettingsPart settingsPart = wordPackage.getMainDocumentPart().getDocumentSettingsPart(true); + CTSettings settings = settingsPart.getContents(); + if (settings == null) { + settings = new CTSettings(); + settingsPart.setJaxbElement(settings); + } + BooleanDefaultTrue updateFields = settings.getUpdateFields(); + if (updateFields == null) { + updateFields = new BooleanDefaultTrue(); + settings.setUpdateFields(updateFields); + } + updateFields.setVal(Boolean.TRUE); + } + /** * 创建标题 *