From 6f890daad6d148eb8760f21b15b23c2b920a2ce2 Mon Sep 17 00:00:00 2001 From: hongawen <83944980@qq.com> Date: Fri, 11 Apr 2025 11:03:16 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B5=99=E6=B1=9F=E6=8A=A5=E5=91=8A=E4=B8=8E?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- detection/pom.xml | 23 + .../detection/pojo/vo/DetectionData.java | 2 +- .../device/service/impl/PqDevServiceImpl.java | 6 + .../report/pojo/constant/PowerConstant.java | 77 +++ .../report/pojo/constant/ReportConstant.java | 22 + .../gather/report/pojo/enums/AffectEnum.java | 43 ++ .../report/pojo/enums/BaseReportKeyEnum.java | 42 ++ .../report/pojo/enums/DocAnchorEnum.java | 50 ++ .../report/pojo/enums/ItemReportKeyEnum.java | 70 ++ .../report/pojo/enums/PowerIndexEnum.java | 52 ++ .../report/pojo/enums/ReportResponseEnum.java | 4 +- .../report/pojo/result/SingleTestResult.java | 40 ++ .../njcn/gather/report/pojo/vo/Bookmark.java | 39 ++ .../service/impl/PqReportServiceImpl.java | 600 ++++++++++++++---- .../report/utils/Docx4jInsertParagraph.java | 44 ++ .../njcn/gather/report/utils/Docx4jUtil.java | 537 ++++++++++++++++ .../njcn/gather/report/utils/WordUtil.java | 90 ++- .../gather/result/service/IResultService.java | 21 + .../service/impl/ResultServiceImpl.java | 534 ++++++++++++++++ .../mapper/PqScriptCheckDataMapper.java | 4 + .../script/mapper/PqScriptDtlsMapper.java | 6 + .../mapping/PqScriptCheckDataMapper.xml | 1 - .../mapper/mapping/PqScriptDtlsMapper.xml | 16 + .../script/pojo/vo/PqScriptCheckDataVO.java | 24 + .../script/pojo/vo/PqScriptDtlDataVO.java | 16 +- .../service/IPqScriptCheckDataService.java | 8 + .../script/service/IPqScriptDtlsService.java | 7 + .../impl/PqScriptCheckDataServiceImpl.java | 13 + .../service/impl/PqScriptDtlsServiceImpl.java | 7 + .../com/njcn/gather/advice/LogAdvice.java | 105 +++ .../GlobalBusinessExceptionHandler.java | 95 +-- .../src/main/resources/model/njcn_882.docx | Bin 27466 -> 27649 bytes .../src/main/resources/model/njcn_882A.docx | Bin 27437 -> 27676 bytes .../src/main/resources/model/njcn_882B1.docx | Bin 27409 -> 27570 bytes .../src/main/resources/model/njcn_882B2.docx | Bin 27468 -> 27647 bytes .../src/main/resources/model/njcn_882B3.docx | Bin 27565 -> 27636 bytes .../src/main/resources/model/njcn_882B4.docx | Bin 27467 -> 27629 bytes .../src/main/resources/model/njcn_882B5.docx | Bin 27391 -> 27650 bytes .../src/main/resources/model/njcn_882B6.docx | Bin 27415 -> 27511 bytes .../src/test/java/com/njcn/BaseJunitTest.java | 37 ++ .../pojo/param/SingleNonHarmParam.java | 40 ++ .../storage/service/AdNonHarmonicService.java | 8 + .../service/impl/AdHarmonicServiceImpl.java | 1 + .../impl/AdNonHarmonicServiceImpl.java | 15 + .../njcn/gather/system/log/aop/LogAdvice.java | 153 ----- .../system/log/pojo/po/SysLogAudit.java | 49 +- .../log/service/ISysLogAuditService.java | 29 +- .../service/impl/SysLogAuditServiceImpl.java | 300 +++++---- user/pom.xml | 1 + .../user/user/controller/AuthController.java | 16 +- 50 files changed, 2703 insertions(+), 544 deletions(-) create mode 100644 detection/src/main/java/com/njcn/gather/report/pojo/constant/PowerConstant.java create mode 100644 detection/src/main/java/com/njcn/gather/report/pojo/constant/ReportConstant.java create mode 100644 detection/src/main/java/com/njcn/gather/report/pojo/enums/AffectEnum.java create mode 100644 detection/src/main/java/com/njcn/gather/report/pojo/enums/BaseReportKeyEnum.java create mode 100644 detection/src/main/java/com/njcn/gather/report/pojo/enums/DocAnchorEnum.java create mode 100644 detection/src/main/java/com/njcn/gather/report/pojo/enums/ItemReportKeyEnum.java create mode 100644 detection/src/main/java/com/njcn/gather/report/pojo/enums/PowerIndexEnum.java create mode 100644 detection/src/main/java/com/njcn/gather/report/pojo/result/SingleTestResult.java create mode 100644 detection/src/main/java/com/njcn/gather/report/pojo/vo/Bookmark.java create mode 100644 detection/src/main/java/com/njcn/gather/report/utils/Docx4jInsertParagraph.java create mode 100644 detection/src/main/java/com/njcn/gather/report/utils/Docx4jUtil.java create mode 100644 detection/src/main/java/com/njcn/gather/script/pojo/vo/PqScriptCheckDataVO.java create mode 100644 entrance/src/main/java/com/njcn/gather/advice/LogAdvice.java create mode 100644 entrance/src/test/java/com/njcn/BaseJunitTest.java delete mode 100644 system/src/main/java/com/njcn/gather/system/log/aop/LogAdvice.java diff --git a/detection/pom.xml b/detection/pom.xml index e6be0098..b615de17 100644 --- a/detection/pom.xml +++ b/detection/pom.xml @@ -74,6 +74,29 @@ poi-ooxml 4.1.2 + + + + org.docx4j + docx4j + 3.3.4 + + + com.fasterxml.jackson.core + jackson-databind + 2.12.0 + + + com.fasterxml.jackson.core + jackson-core + 2.12.0 + + + com.fasterxml.jackson.core + jackson-annotations + 2.12.0 + + diff --git a/detection/src/main/java/com/njcn/gather/detection/pojo/vo/DetectionData.java b/detection/src/main/java/com/njcn/gather/detection/pojo/vo/DetectionData.java index bbfcb2f6..e65b73be 100644 --- a/detection/src/main/java/com/njcn/gather/detection/pojo/vo/DetectionData.java +++ b/detection/src/main/java/com/njcn/gather/detection/pojo/vo/DetectionData.java @@ -20,7 +20,7 @@ public class DetectionData { private Double num; /** - * 是否是符合数据(1.合格 2.不合格 3.网络超时 4.无法处理 5.不参与误差比较) + * 1.合格 2.不合格 3.网络超时 4.无法处理 5.不参与误差比较 */ private Integer isData; 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 d42c7290..822133bd 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 @@ -43,6 +43,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.service.ISysUserService; import com.njcn.web.factory.PageFactory; import com.njcn.web.utils.ExcelUtil; import com.njcn.web.utils.PoiUtil; @@ -75,6 +76,7 @@ public class PqDevServiceImpl extends ServiceImpl implements private final IDevTypeService devTypeService; private final ISysTestConfigService sysTestConfigService; private final IDictTypeService dictTypeService; + private final ISysUserService userService; @Override public Page listPqDevs(PqDevParam.QueryParam queryParam) { @@ -390,6 +392,7 @@ public class PqDevServiceImpl extends ServiceImpl implements return CheckStateEnum.UNCHECKED.getValue(); } +// // @Override // public List getPieData(String planId) { // List pqDevList = this.lambdaQuery().eq(PqDev::getPlanId, planId).eq(PqDev::getState, DataStateEnum.ENABLE.getCode()).list(); @@ -411,6 +414,9 @@ public class PqDevServiceImpl extends ServiceImpl implements @Override public PqDevVO getPqDevById(String id) { PqDev pqDev = this.getById(id); + if(StrUtil.isNotBlank(pqDev.getCheckBy())){ + pqDev.setCheckBy(userService.getById(pqDev.getCheckBy()).getName()); + } PqDevVO pqDevVO = new PqDevVO(); BeanUtil.copyProperties(pqDev, pqDevVO); 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 new file mode 100644 index 00000000..9e85a35e --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/report/pojo/constant/PowerConstant.java @@ -0,0 +1,77 @@ +package com.njcn.gather.report.pojo.constant; + + +import java.util.Arrays; +import java.util.List; + +/** + * 电能质量指标常用的一些常量池 + * + * @author hongawen + * @version 1.0 + * @data 2025/3/27 11:11 + */ +public interface PowerConstant { + + /** + * 有三相的指标 + */ + List THREE_PHASE = Arrays.asList("V", "HV", "HI", "HP", "HSV", "HSI", "I", "P", "F"); + + /** + * T相指标 + */ + List T_PHASE = Arrays.asList("VOLTAGE", "IMBV", "IMBA"); + + + /** + * 有次数的指标 + */ + List TIME = Arrays.asList("HV", "HI", "HP", "HSV", "HSI"); + + /** + * 没有次数的指标 + */ + List NO_TIME = Arrays.asList("V", "I", "P", "VOLTAGE", "IMBV", "IMBA", "F"); + + /** + * 有数据范围 + */ + List DATA_RANGE = Arrays.asList(1, 2); + + /** + * 暂态符号 + */ + String VOLTAGE = "VOLTAGE"; + + /** + * A相 + */ + String PHASE_A = "A"; + + /** + * B相 + */ + String PHASE_B = "B"; + + /** + * C相 + */ + String PHASE_C = "C"; + + /** + * T相 + */ + String PHASE_T = "T"; + + /** + * 电流单位 + */ + String CURRENT_UNIT = "A"; + + /** + * 电压单位 + */ + String VOLTAGE_UNIT = "V"; + +} diff --git a/detection/src/main/java/com/njcn/gather/report/pojo/constant/ReportConstant.java b/detection/src/main/java/com/njcn/gather/report/pojo/constant/ReportConstant.java new file mode 100644 index 00000000..3b4cfc8b --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/report/pojo/constant/ReportConstant.java @@ -0,0 +1,22 @@ +package com.njcn.gather.report.pojo.constant; + +/** + * + * 报告相关的一些常量 + * + * @author hongawen + * @version 1.0 + * @data 2025/4/3 13:44 + */ +public interface ReportConstant { + + /** + * docx文档后缀 + */ + String DOCX = ".docx"; + + /** + * 报告模板中书签的起始标识 + */ + String BOOKMARK_START = "#{"; +} diff --git a/detection/src/main/java/com/njcn/gather/report/pojo/enums/AffectEnum.java b/detection/src/main/java/com/njcn/gather/report/pojo/enums/AffectEnum.java new file mode 100644 index 00000000..0eaf122d --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/report/pojo/enums/AffectEnum.java @@ -0,0 +1,43 @@ +package com.njcn.gather.report.pojo.enums; + +import lombok.Getter; + +/** + * 影响量枚举 + * @author hongawen + * @version 1.0 + * @data 2025/3/27 21:07 + */ +@Getter +public enum AffectEnum { + + + BASE("base", "额定工作条件下的检测"), + VOL("vol", "电压对XX测量的影响"), + FREQ("freq", "频率对XX测量的影响"), + HARM("single", "谐波对XX测量的影响"); + + private String key; + + private String desc; + + AffectEnum(String key, String desc) { + this.key = key; + this.desc = desc; + } + + /** + * 根据key找到适配的枚举 + * + * @param key 枚举的key + * @return 匹配的枚举实例,如果没有找到则返回null + */ + public static AffectEnum getByKey(String key) { + for (AffectEnum affectEnum : AffectEnum.values()) { + if (affectEnum.getKey().equalsIgnoreCase(key)) { + return affectEnum; + } + } + return null; + } +} 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 new file mode 100644 index 00000000..25a0b829 --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/report/pojo/enums/BaseReportKeyEnum.java @@ -0,0 +1,42 @@ +package com.njcn.gather.report.pojo.enums; + +import lombok.Getter; + +/** + * + * 本枚举用于记录模板中关键字的含义 + * 比如 ${manufacturer} 对应的设备厂家 + * + * @author hongawen + * @version 1.0 + * @data 2025/3/24 14:34 + * + * */ +@Getter +public enum BaseReportKeyEnum { + + DEV_TYPE("devType","设备型号、规格"), + DEV_CODE("createId","装置编号"), + POWER("power","工作电源"), + DEV_CURR("devCurr","额定电流"), + DEV_VOLT("devVolt","额定电压"), + COUNT("count","通道数"), + MANUFACTURER("manufacturer","设备厂家、制造厂商"), + SAMPLE_ID("sampleId","样品编号"), + ARRIVED_DATE("arrivedDate","收样日期"), + TEST_DATE("testDate","检测日期"), + INSPECTOR("inspector","检测员"), + YEAR("year","年份"), + MONTH("month","月份"), + DAY("day","日"), + YEAR_MONTH_DAY("year-month-day","年-月-日"); + + private String key; + + private String desc; + + BaseReportKeyEnum(String key, String desc) { + this.key = key; + this.desc = desc; + } +} diff --git a/detection/src/main/java/com/njcn/gather/report/pojo/enums/DocAnchorEnum.java b/detection/src/main/java/com/njcn/gather/report/pojo/enums/DocAnchorEnum.java new file mode 100644 index 00000000..9e3143dd --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/report/pojo/enums/DocAnchorEnum.java @@ -0,0 +1,50 @@ +package com.njcn.gather.report.pojo.enums; + +import lombok.Getter; + +/** + * 统计文档锚点类型 + * + * @author hongawen + * @version 1.0 + * @data 2025/3/24 18:42 + */ +@Getter +public enum DocAnchorEnum { + + + DATA_LINE("#{data:line}", "准确度数据展示区域,以测试回路维度展示", 1), + DATA_SCRIPT("#{data:script}", "准确度数据展示区域,以检测项维度展示", 1), + TEST_RESULT_DEV("#{testResult:dev}", "检测结论,仅有设备结论", 2), + TEST_RESULT_LINE("#{testResult:line}", "检测结论,仅有回路结论", 2), + TEST_RESULT_DETAIL("#{testResult:detail}", "检测结论,包含回路、设备结论", 2), + CATALOG("#{catalog}", "目录信息", 3); + + private String key; + + private String desc; + + private Integer sort; + + DocAnchorEnum(String key, String desc, Integer sort) { + this.key = key; + this.desc = desc; + this.sort = sort; + } + + /** + * 根据key找到适配的枚举 + * + * @param key 枚举的key + * @return 匹配的枚举实例,如果没有找到则返回null + */ + public static DocAnchorEnum getByKey(String key) { + for (DocAnchorEnum docAnchorEnum : DocAnchorEnum.values()) { + if (docAnchorEnum.getKey().equalsIgnoreCase(key)) { + return docAnchorEnum; + } + } + return null; + } + +} 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 new file mode 100644 index 00000000..b7e1f068 --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/report/pojo/enums/ItemReportKeyEnum.java @@ -0,0 +1,70 @@ +package com.njcn.gather.report.pojo.enums; + +import lombok.Getter; + +/** + * 检测项模版枚举 + * @author hongawen + * @version 1.0 + * @data 2025/3/27 10:24 + */ +@Getter +public enum ItemReportKeyEnum { + + NAME("name", "检测项,比如:频率"), + NAME_DETAIL("nameDetail", "检测项详细,比如:频率测量准确度"), + ERROR_SCOPE("errorScope", "误差范围,注:在段落中时需加上(),表格中无需添加"), + ERROR_SCOPE_MAG("errorScopeMag", "特征幅值:误差范围"), + ERROR_SCOPE_DUR("errorScopeDur", "持续时间:误差范围"), + SCRIPT_DETAIL("scriptDetail", "脚本输出明细。比如:基波电压UN=57.74V,f=50Hz,谐波含有率Uh=10%UN=5.774V"), + TIME("time", "次数"), + STANDARD("standard", "标准值"), + STANDARD_A("standardA", "A相标准值"), + STANDARD_B("standardB", "B相标准值"), + STANDARD_C("standardC", "C相标准值"), + STANDARD_MAG("standardMag", "特征幅值的标准值"), + STANDARD_DUR("standardDur_ms", "持续时间的标准值"), + TEST("test", "测试值"), + TEST_MAG("testMag", "特征幅值测试值"), + TEST_DUR("testDur_ms", "持续时间测试值"), + TEST_A("testA", "A相测试值"), + TEST_B("testB", "B相测试值"), + TEST_C("testC", "C相测试值"), + ERROR("error", "误差"), + ERROR_MAG("errorMag", "特征幅值误差"), + ERROR_DUR("errorDur_ms", "持续时间误差"), + ERROR_A("errorA", "A相误差"), + ERROR_B("errorB", "B相误差"), + ERROR_C("errorC", "C相误差"), + RESULT("result", "结论 比如:合格/不合格"), + RESULT_A("resultA", "结论 比如:合格/不合格"), + RESULT_B("resultB", "结论 比如:合格/不合格"), + RESULT_C("resultC", "结论 比如:合格/不合格"), + RESULT_MAG("resultMag", "特征幅值结论 比如:合格/不合格"), + RESULT_DUR("resultDur", "持续时间结论 比如:合格/不合格"); + + private String key; + + private String desc; + + ItemReportKeyEnum(String key, String desc) { + this.key = key; + this.desc = desc; + } + + /** + * 根据key找到适配的枚举 + * + * @param key 枚举的key + * @return 匹配的枚举实例,如果没有找到则返回null + */ + public static ItemReportKeyEnum getByKey(String key) { + for (ItemReportKeyEnum itemReportKetEnum : ItemReportKeyEnum.values()) { + if (itemReportKetEnum.getKey().equalsIgnoreCase(key)) { + return itemReportKetEnum; + } + } + return null; + } + +} 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 new file mode 100644 index 00000000..5fbc0ee8 --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/report/pojo/enums/PowerIndexEnum.java @@ -0,0 +1,52 @@ +package com.njcn.gather.report.pojo.enums; + +import lombok.Getter; + +/** + * 电能质量测试大项枚举 + * @author hongawen + * @version 1.0 + * @data 2025/3/27 18:29 + */ +@Getter +public enum PowerIndexEnum { + + + UNKNOWN("UNKNOWN", "未知指标"), + FREQ("FREQ", "频率"), + V("V", "电压"), + I("I", "电流"), + IMBV("IMBV", "三相电压不平衡度"), + IMBA("IMBA", "三相电流不平衡度"), + F("F", "闪变"), + HP("HP", "谐波有功功率"), + HV("HV", "谐波电压"), + HI("HI", "谐波电流"), + HSV("HSV", "间谐波电压"), + HSI("HSI", "间谐波电流"), + VOLTAGE("VOLTAGE", "电压暂降、暂升及短时中断"); + + private String key; + + private String desc; + + PowerIndexEnum(String key, String desc) { + this.key = key; + this.desc = desc; + } + + /** + * 根据key找到适配的枚举 + * + * @param key 枚举的key + * @return 匹配的枚举实例,如果没有找到则返回null + */ + public static PowerIndexEnum getByKey(String key) { + for (PowerIndexEnum powerIndexEnum : PowerIndexEnum.values()) { + if (powerIndexEnum.getKey().equalsIgnoreCase(key)) { + return powerIndexEnum; + } + } + return null; + } +} diff --git a/detection/src/main/java/com/njcn/gather/report/pojo/enums/ReportResponseEnum.java b/detection/src/main/java/com/njcn/gather/report/pojo/enums/ReportResponseEnum.java index ffbbf832..2892943e 100644 --- a/detection/src/main/java/com/njcn/gather/report/pojo/enums/ReportResponseEnum.java +++ b/detection/src/main/java/com/njcn/gather/report/pojo/enums/ReportResponseEnum.java @@ -22,7 +22,9 @@ public enum ReportResponseEnum { FILE_RENAME_FAILED("A012011", "文件重命名失败"), REPORT_NAME_PATTERN_ERROR("A012012","报告名称格式错误,可包含中文、字母、数字、中划线、点号、空格,长度不能超过32个字符"), REPORT_VERSION_PATTERN_ERROR("A012013","报告版本号格式错误,可包含中文、字母、数字、中划线、点号、空格,长度不能超过32个字符"), - FILE_SIZE_ERROR("A012014","文件大小不能超过5MB" ); + FILE_SIZE_ERROR("A012014","文件大小不能超过5MB" ), + GENERATE_REPORT_ERROR("A012015","生成报告失败"), + ; private String code; private String message; diff --git a/detection/src/main/java/com/njcn/gather/report/pojo/result/SingleTestResult.java b/detection/src/main/java/com/njcn/gather/report/pojo/result/SingleTestResult.java new file mode 100644 index 00000000..ef9bf48b --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/report/pojo/result/SingleTestResult.java @@ -0,0 +1,40 @@ +package com.njcn.gather.report.pojo.result; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +/** + * + * 测试大项的检测结果 + * + * @author hongawen + * @version 1.0 + * @data 2025/4/7 20:17 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class SingleTestResult implements Serializable { + + /** + * 大项名称 + */ + private String scriptCode; + + /** + * 是否合格 + */ + private boolean qualified; + + + /** + * 细节集合 + */ + private Map>>>> detail; + +} diff --git a/detection/src/main/java/com/njcn/gather/report/pojo/vo/Bookmark.java b/detection/src/main/java/com/njcn/gather/report/pojo/vo/Bookmark.java new file mode 100644 index 00000000..1a4fc8c6 --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/report/pojo/vo/Bookmark.java @@ -0,0 +1,39 @@ +package com.njcn.gather.report.pojo.vo; + +import com.njcn.gather.report.pojo.enums.DocAnchorEnum; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * @author hongawen + * @version 1.0 + * @data 2025/4/7 15:36 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Bookmark implements Serializable,Comparable{ + + /** + * 在文档中段落的索引 + */ + private Integer index; + + /** + * 对应枚举 + */ + private DocAnchorEnum docAnchorEnum; + + + /** + * 根据书签的排序字段进行排序 + * @param bookmark the object to be compared. + */ + @Override + public int compareTo(Bookmark bookmark) { + return Integer.compare(this.docAnchorEnum.getSort(), bookmark.docAnchorEnum.getSort()); + } +} 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 04db917a..5f79ab45 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 @@ -1,5 +1,6 @@ package com.njcn.gather.report.service.impl; +import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.date.DatePattern; import cn.hutool.core.date.DateUtil; @@ -28,18 +29,24 @@ import com.njcn.gather.plan.service.IAdPlanService; import com.njcn.gather.pojo.enums.DetectionResponseEnum; import com.njcn.gather.report.mapper.PqReportMapper; import com.njcn.gather.report.pojo.DevReportParam; -import com.njcn.gather.report.pojo.enums.ReportResponseEnum; +import com.njcn.gather.report.pojo.constant.PowerConstant; +import com.njcn.gather.report.pojo.constant.ReportConstant; +import com.njcn.gather.report.pojo.enums.*; import com.njcn.gather.report.pojo.param.ReportParam; import com.njcn.gather.report.pojo.po.CellEntity; import com.njcn.gather.report.pojo.po.PqReport; +import com.njcn.gather.report.pojo.result.SingleTestResult; +import com.njcn.gather.report.pojo.vo.Bookmark; import com.njcn.gather.report.pojo.vo.PqReportVO; import com.njcn.gather.report.service.IPqReportService; +import com.njcn.gather.report.utils.Docx4jUtil; import com.njcn.gather.report.utils.WordUtil; import com.njcn.gather.result.pojo.param.ResultParam; import com.njcn.gather.result.pojo.vo.ResultVO; import com.njcn.gather.result.service.IResultService; import com.njcn.gather.script.pojo.po.PqScriptCheckData; import com.njcn.gather.script.pojo.po.PqScriptDtls; +import com.njcn.gather.script.pojo.vo.PqScriptDtlDataVO; import com.njcn.gather.script.service.IPqScriptCheckDataService; import com.njcn.gather.script.service.IPqScriptDtlsService; import com.njcn.gather.storage.pojo.param.SingleNonHarmParam; @@ -56,22 +63,25 @@ import com.njcn.gather.system.dictionary.service.IDictTreeService; import com.njcn.gather.system.pojo.enums.DicDataEnum; 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 lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.poi.xwpf.usermodel.*; +import org.docx4j.jaxb.Context; +import org.docx4j.openpackaging.packages.WordprocessingMLPackage; +import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart; +import org.docx4j.wml.*; import org.openxmlformats.schemas.wordprocessingml.x2006.main.*; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.ClassPathResource; -import org.springframework.core.io.FileSystemResource; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import javax.servlet.http.HttpServletResponse; +import javax.xml.bind.JAXBElement; import java.io.*; import java.lang.reflect.Field; import java.math.BigDecimal; @@ -229,10 +239,9 @@ public class PqReportServiceImpl extends ServiceImpl i // 删除对应的文件 this.deleteFile(ids); - boolean result = this.lambdaUpdate().in(CollectionUtil.isNotEmpty(ids), PqReport::getId, ids) + return this.lambdaUpdate().in(CollectionUtil.isNotEmpty(ids), PqReport::getId, ids) .set(PqReport::getState, DataStateEnum.DELETED.getCode()) .update(); - return result; } @Override @@ -497,62 +506,76 @@ public class PqReportServiceImpl extends ServiceImpl i @Override public void generateReport(DevReportParam devReportParam) { AdPlan plan = adPlanService.getById(devReportParam.getPlanId()); - if (StrUtil.isNotBlank(plan.getReportTemplateId())) { + if (plan.getAssociateReport() == 1) { this.generateReportByPlan(plan, devReportParam); - } else { - // 根据设备类型找到报告模板 - PqDevVO pqDevVO = iPqDevService.getPqDevById(devReportParam.getDevId()); - if (Objects.isNull(pqDevVO)) { - throw new BusinessException("请检查装置是否存在!"); - } - // 获取设备型号 - DevType devType = devTypeService.getById(pqDevVO.getDevType()); - if (Objects.isNull(devType)) { - throw new BusinessException("设备类型缺失,请联系管理员!"); - } - DictData reportName = devTypeService.getReportName(pqDevVO.getDevType()); - if (Objects.isNull(reportName)) { - throw new BusinessException("报告模板缺失,请联系管理员!"); - } - // 读取模板文件 - ClassPathResource resource = new ClassPathResource("/model/" + reportName.getCode() + ".docx"); - try (InputStream inputStream = resource.getInputStream()) { - // 加载Word文档 - XWPFDocument baseModelDocument = new XWPFDocument(inputStream); - // 处理基础模版中的信息 - dealBaseModel(baseModelDocument, pqDevVO, devType); - // 处理数据页中的信息 - dealDataModel(baseModelDocument, devReportParam, pqDevVO); - // 处理需要输出的目录地址 基础路径+设备类型+装置编号.docx - // 最终文件输出的路径 - String dirPath = reportPath.concat(File.separator).concat(devType.getName()); - ensureDirectoryExists(dirPath); // 确保目录存在 - FileOutputStream out = new FileOutputStream(dirPath.concat(File.separator).concat(pqDevVO.getCreateId()).concat(".docx")); - // 4. 保存新的Word文档 - try { - baseModelDocument.write(out); - } catch (IOException e) { - throw new BusinessException("生成报告文件失败"); - } - out.close(); + } else if (plan.getAssociateReport() == 0) { + this.generateReportByDevType(plan, devReportParam); + } + } - System.out.println("报告生成成功!"); - this.updateDevAndPlanState(devReportParam.getDevId(), devReportParam.getPlanId()); + /** + * 根据设备类型生成报告 + * 注:该方法目前仅支持楼下出厂检测场景,属于模板占位符替换方式,后期可能会有调整 + * + * @param plan 计划信息 + * @param devReportParam 被检设备信息 + */ + private void generateReportByDevType(AdPlan plan, DevReportParam devReportParam) { + // 根据设备类型找到报告模板 + PqDevVO pqDevVO = iPqDevService.getPqDevById(devReportParam.getDevId()); + if (Objects.isNull(pqDevVO)) { + throw new BusinessException(ReportResponseEnum.DEVICE_NOT_EXIST); + } + // 获取设备型号 + DevType devType = devTypeService.getById(pqDevVO.getDevType()); + if (Objects.isNull(devType)) { + throw new BusinessException(ReportResponseEnum.DEVICE_TYPE_NOT_EXIST); + } + DictData reportName = devTypeService.getReportName(pqDevVO.getDevType()); + if (Objects.isNull(reportName)) { + throw new BusinessException(ReportResponseEnum.REPORT_TEMPLATE_NOT_EXIST); + } + // 读取模板文件 + ClassPathResource resource = new ClassPathResource("/model/" + reportName.getCode() + ReportConstant.DOCX); + try (InputStream inputStream = resource.getInputStream()) { + // 加载Word文档 + XWPFDocument baseModelDocument = new XWPFDocument(inputStream); + // 处理基础模版中的信息 + Map baseModelDataMap = dealBaseModelData(pqDevVO, devType, "${", "}"); + // 替换模板中的信息,避免信息丢失,段落和表格均参与替换 + WordUtil.replacePlaceholders(baseModelDocument, baseModelDataMap); + // 处理数据页中的信息 + dealDataModel(baseModelDocument, devReportParam, pqDevVO); + // 处理需要输出的目录地址 基础路径+设备类型+装置编号.docx + // 最终文件输出的路径 + String dirPath = reportPath.concat(File.separator).concat(devType.getName()); + // 确保目录存在 + ensureDirectoryExists(dirPath); + FileOutputStream out = new FileOutputStream(dirPath.concat(File.separator).concat(pqDevVO.getCreateId()).concat(".docx")); + // 4. 保存新的Word文档 + try { + baseModelDocument.write(out); } catch (IOException e) { - log.error("生成报告文件失败", e); - throw new RuntimeException(e); + throw new BusinessException(ReportResponseEnum.GENERATE_REPORT_ERROR); } + out.close(); + + this.updateDevAndPlanState(devReportParam.getDevId(), devReportParam.getPlanId()); + } catch (IOException e) { + log.error(ReportResponseEnum.GENERATE_REPORT_ERROR.getMessage(), e); + throw new BusinessException(ReportResponseEnum.GENERATE_REPORT_ERROR); } } /** * 根据计划绑定的报告模板生成报告 + * 注:该方法目前属于同用信息占位符替换,数据页为面向对象动态填充拼凑方式 * - * @param plan - * @param devReportParam + * @param plan 计划信息 + * @param devReportParam 设备信息 */ private void generateReportByPlan(AdPlan plan, DevReportParam devReportParam) { - // 根据设备类型找到报告模板 + // 准备被检设备的基础信息 PqDevVO pqDevVO = iPqDevService.getPqDevById(devReportParam.getDevId()); if (Objects.isNull(pqDevVO)) { throw new BusinessException(ReportResponseEnum.DEVICE_NOT_EXIST); @@ -566,41 +589,397 @@ public class PqReportServiceImpl extends ServiceImpl i if (Objects.isNull(report)) { throw new BusinessException(ReportResponseEnum.REPORT_TEMPLATE_NOT_EXIST); } - - FileSystemResource resource = new FileSystemResource(report.getBasePath()); - try (InputStream inputStream = resource.getInputStream()) { - // 加载Word文档 - XWPFDocument baseModelDocument = new XWPFDocument(inputStream); - // 处理基础模版中的信息 - dealBaseModel(baseModelDocument, pqDevVO, devType); - // 处理数据页中的信息 - dealDataModelZJ(baseModelDocument, devReportParam, pqDevVO); - // 处理需要输出的目录地址 基础路径+设备类型+装置编号.docx - // 最终文件输出的路径 + try { + WordprocessingMLPackage baseModelDocument = WordprocessingMLPackage.load(new File(report.getBasePath())); + WordprocessingMLPackage detailModelDocument = WordprocessingMLPackage.load(new File(report.getDetailPath())); + // 获取文档基础部分,并替换占位符 + MainDocumentPart baseDocumentPart = baseModelDocument.getMainDocumentPart(); + Map baseModelDataMap = dealBaseModelData(pqDevVO, devType, "", ""); + baseDocumentPart.variableReplace(baseModelDataMap); + // 获取数据模版页内容,根据脚本动态组装数据页内容 + MainDocumentPart detailDocumentPart = detailModelDocument.getMainDocumentPart(); + dealDataModelScattered(baseDocumentPart, detailDocumentPart, devReportParam, pqDevVO); + // 保存新的文档 String dirPath = reportPath.concat(File.separator).concat(devType.getName()); - ensureDirectoryExists(dirPath); // 确保目录存在 - FileOutputStream out = new FileOutputStream(dirPath.concat(File.separator).concat(pqDevVO.getCreateId()).concat(".docx")); - // 4. 保存新的Word文档 - try { - baseModelDocument.write(out); - } catch (IOException e) { - throw new BusinessException("生成报告文件失败"); - } - out.close(); - - System.out.println("报告生成成功!"); - - this.updateDevAndPlanState(devReportParam.getDevId(), devReportParam.getPlanId()); - } catch (IOException e) { - log.error("生成报告文件失败", e); - throw new RuntimeException(e); + // 确保目录存在 + ensureDirectoryExists(dirPath); + baseModelDocument.save(new File(dirPath.concat(File.separator).concat(pqDevVO.getCreateId()).concat(ReportConstant.DOCX))); + } catch (Exception e) { + log.error(ReportResponseEnum.GENERATE_REPORT_ERROR.getMessage(), e); + throw new BusinessException(ReportResponseEnum.GENERATE_REPORT_ERROR); } } + /** + * 通用基础信息文档里可能会存在的书签锚点 + * 1、目录信息 + * 2、准确度测试详情 + * 3、测试结果页 + * 上述3个锚点位置不固定,可能结果页在通用信息中间,也有可能在文档最末端。 + * 注:当存在目录信息时,目录最后生成。 + * + * @param baseDocumentPart 通用信息文档 + * @param detailDocumentPart 数据项模板文档 + * @param devReportParam 测试报告参数 + * @param pqDevVO 被检设备 + */ + private void dealDataModelScattered(MainDocumentPart baseDocumentPart, MainDocumentPart detailDocumentPart, DevReportParam devReportParam, PqDevVO pqDevVO) { + // 查找 base 文档中所有以#{开始的书签所在的段落,并收集整理 + List baseContent = baseDocumentPart.getContent(); + List bookmarks = new ArrayList<>(); + for (int i = 0; i < baseContent.size(); i++) { + Object obj = baseContent.get(i); + if (obj instanceof P) { + P p = (P) obj; + String text = Docx4jUtil.getTextFromP(p).trim(); + if (text.startsWith(ReportConstant.BOOKMARK_START)) { + Bookmark bookmark = new Bookmark(i, DocAnchorEnum.getByKey(text)); + bookmarks.add(bookmark); + } + } + } + if (CollUtil.isNotEmpty(bookmarks)) { + /* + * 从结构上分析,处理的顺序: + * 1、数据项 + * 2、结果信息 + * 3、目录信息 + * 所以要先先获取的书签进行操作排序 + * */ + Collections.sort(bookmarks); + // 定义个结果,以便存在结果信息的书签 + Map> resultMap = new HashMap<>(); + // 书签在文档的位置 + int position; + for (int i = 0; i < bookmarks.size(); i++) { + Bookmark bookmark = bookmarks.get(i); + if (i == 0) { + position = bookmark.getIndex(); + } else { + // 经过处理后,原本的书签位置,已经被处理,所以需要重新获取 + position = Docx4jUtil.getParagraphPosition(baseDocumentPart, bookmark.getDocAnchorEnum()); + } + switch (bookmark.getDocAnchorEnum()) { + case DATA_LINE: + dealDataLine(baseDocumentPart, detailDocumentPart, devReportParam, pqDevVO, position, resultMap); + break; + case DATA_SCRIPT: + break; + case TEST_RESULT_DEV: + // 判断是否已经处理过数据了,有了结论性的描述 + if (CollUtil.isEmpty(resultMap)) { + dealDataLine(baseDocumentPart, detailDocumentPart, devReportParam, pqDevVO, position, resultMap); + } + dealTestResultLine(baseDocumentPart, detailDocumentPart, position, resultMap, DocAnchorEnum.TEST_RESULT_DEV); + break; + case TEST_RESULT_LINE: + // 判断是否已经处理过数据了,有了结论性的描述 + if (CollUtil.isEmpty(resultMap)) { + dealDataLine(baseDocumentPart, detailDocumentPart, devReportParam, pqDevVO, position, resultMap); + } + dealTestResultLine(baseDocumentPart, detailDocumentPart, position, resultMap, DocAnchorEnum.TEST_RESULT_LINE); + break; + case TEST_RESULT_DETAIL: + // 判断是否已经处理过数据了,有了结论性的描述 + if (CollUtil.isEmpty(resultMap)) { + dealDataLine(baseDocumentPart, detailDocumentPart, devReportParam, pqDevVO, position, resultMap); + } + dealTestResultLine(baseDocumentPart, detailDocumentPart, position, resultMap, DocAnchorEnum.TEST_RESULT_DETAIL); + break; + case CATALOG: + break; + default: + break; + } + } + } + + } + + + /** + * 如何处理结果性数据进文档,各省级平台的结果表格不一致,如何做到一致 + * + * @param baseDocumentPart 基础模板文档 + * @param detailDocumentPart 数据模板文档 + * @param position 书签在基础文档的位置 + * @param resultMap 各测试项的结果 + */ + private void dealTestResultLine(MainDocumentPart baseDocumentPart, MainDocumentPart detailDocumentPart, int position, Map> resultMap, DocAnchorEnum docAnchorEnum) { + // 先判断数据有没有,如果没有,则不处理 + if (CollUtil.isEmpty(resultMap.get(PowerIndexEnum.UNKNOWN.getKey()))) { + List paragraphs = baseDocumentPart.getContent(); + ObjectFactory factory = Context.getWmlObjectFactory(); + // 结论 + P newParagraph = factory.createP(); + Docx4jUtil.createTitle(factory, newParagraph, "检测结论", 28, true); + //插入段落 + paragraphs.add(position++, newParagraph); + // 源文档的内容 + // 创建表格(示例为3列,列数可任意调整) + Tbl table = factory.createTbl(); + List> tempResultList = new ArrayList<>(resultMap.values()); + int lineNum = tempResultList.get(0).size(); + // 处理表头 + List title = new ArrayList<>(); + title.add("检测项目"); + switch (docAnchorEnum) { + case TEST_RESULT_DEV: + title.add("结论"); + break; + case TEST_RESULT_LINE: + for (int i = 1; i < lineNum + 1; i++) { + title.add("测量回路" + i); + } + break; + case TEST_RESULT_DETAIL: + for (int i = 1; i < lineNum + 1; i++) { + title.add("测量回路" + i); + } + title.add("结论"); + break; + default: + break; + } + + Tr titleRow = Docx4jUtil.createCustomRow(factory, title, "SimSun", "宋体", 21, true, false); + table.getContent().add(titleRow); + // 处理业务数据 + resultMap.forEach((key, value) -> { + List cellValues = new ArrayList<>(); + PowerIndexEnum indexEnum = PowerIndexEnum.getByKey(key); + if (indexEnum != null) { + cellValues.add(indexEnum.getDesc().concat("测量准确度")); + } else { + cellValues.add(PowerIndexEnum.UNKNOWN.getDesc().concat("测量准确度")); + } + // 判断是否有不合格的 + List totalValue = value.stream().filter(item -> !item).collect(Collectors.toList()); + String total = !totalValue.isEmpty() ? "不合格" : "合格"; + switch (docAnchorEnum) { + case TEST_RESULT_DEV: + cellValues.add(total); + break; + case TEST_RESULT_LINE: + for (int i = 0; i < value.size(); i++) { + cellValues.add(value.get(i) ? "合格" : "不合格"); + } + break; + case TEST_RESULT_DETAIL: + for (int i = 0; i < value.size(); i++) { + cellValues.add(value.get(i) ? "合格" : "不合格"); + } + cellValues.add(total); + break; + default: + break; + } + Tr tempRow = Docx4jUtil.createCustomRow(factory, cellValues, "SimSun", "宋体", 21, false, false); + table.getContent().add(tempRow); + }); + TblPr tblPr = Docx4jUtil.getTblPr(factory); + table.setTblPr(tblPr); + paragraphs.add(position++, table); + // 标签的位置,以便清空原标签 + if (position >= 0 && position < paragraphs.size()) { + paragraphs.remove(position); + } else { + System.out.println("指定的索引超出范围"); + } + } + + + } + + + /** + * 处理以回路为维度的数据项 + * + * @param baseModelDocument 基础模板 + * @param detailModelDocument 数据项模板 + * @param devReportParam 测试报告参数 + * @param pqDevVO 被检设备 + * @param position 待处理书签的位置 + */ + private void dealDataLine(MainDocumentPart baseModelDocument, MainDocumentPart detailModelDocument, DevReportParam devReportParam, PqDevVO pqDevVO, int position, Map> resultMap) { + // 源文档的内容 + List paragraphs = baseModelDocument.getContent(); + // 以回路维度处理数据项 + Integer devChns = pqDevVO.getDevChns(); + ObjectFactory factory = new ObjectFactory(); + // 读取该计划的检测大项组装数据内容 + List pqScriptDtlsList = pqScriptDtlsService.getScriptDtlsDataList(devReportParam.getScriptId()); + Map> scriptMap = pqScriptDtlsList.stream().collect(Collectors.groupingBy(PqScriptDtlDataVO::getScriptCode)); + List allContent = detailModelDocument.getContent(); + List headingContents = Docx4jUtil.extractHeading5Contents(allContent); + Map> contentMap = headingContents.stream().collect(Collectors.groupingBy(Docx4jUtil.HeadingContent::getHeadingText, Collectors.toList())); + for (int i = 0; i < devChns; i++) { + // 回路标题 + P newParagraph = factory.createP(); + Integer lineNo = i + 1; + Docx4jUtil.createTitle(factory, newParagraph, "测量回路" + lineNo, 28, true); + //插入段落 + paragraphs.add(position++, newParagraph); + // 依次处理大项文档内容 + Iterator>> iterator = scriptMap.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry> next = iterator.next(); + String scriptCode = next.getKey(); + List checkckItemList = next.getValue(); + List tempContent = contentMap.get(scriptCode); + // 获取需要填充keys,索引0对应的段落key,索引1对应的表格key + List> keys = Docx4jUtil.getFillKeys(tempContent); + // 段落keys值赋值 + List pKeys = keys.get(0); + Map pKeyValueMap = resultService.getParagraphKeysValue(scriptCode, pKeys); + List tableKeys = keys.get(1); + /* tableKeys值赋值,注:由于谐波类检测数据与非谐波检测类数据的区别,此处要做区分 + * 1、谐波类每个scriptIndex对应一个Excel表格 + * 2、非谐波类则是一个误差范围对应一个Excel表格 + */ + SingleTestResult singleTestResult = null; + // 根据code找到名称 + List scriptResult = resultMap.get(scriptCode); + boolean needFill = false; + if (CollUtil.isEmpty(scriptResult)) { + scriptResult = new ArrayList<>(); + needFill = true; + } else if (scriptResult.size() < lineNo) { + needFill = true; + } + if (PowerConstant.TIME.contains(scriptCode)) { + // 谐波类,以scriptIndex区分 + Map> scriptIndexMap = checkckItemList.stream().collect(Collectors.groupingBy(PqScriptDtlDataVO::getScriptIndex)); + for (List scriptDtlDataItem : scriptIndexMap.values()) { + singleTestResult = resultService.getFinalContent(scriptDtlDataItem, devReportParam.getPlanCode(), pqDevVO.getId(), lineNo, tableKeys); + position = fillContentInTemplate(singleTestResult.getDetail(), position, tempContent, factory, pKeyValueMap, tableKeys, paragraphs); + } + } else { + // 非谐波类 + singleTestResult = resultService.getFinalContent(checkckItemList, devReportParam.getPlanCode(), pqDevVO.getId(), lineNo, tableKeys); + position = fillContentInTemplate(singleTestResult.getDetail(), position, tempContent, factory, pKeyValueMap, tableKeys, paragraphs); + } + if (Objects.nonNull(singleTestResult) && needFill) { + singleTestResult.setScriptCode(scriptCode); + scriptResult.add(singleTestResult.isQualified()); + resultMap.put(scriptCode, scriptResult); + } + } + } + // 如果经过一顿处理后,结果性数据集合还是空,塞个特殊数据进去,避免嵌套循环 + if (CollUtil.isEmpty(resultMap)) { + resultMap.put(PowerIndexEnum.UNKNOWN.getKey(), Collections.singletonList(false)); + } + // 标签的位置,以便清空原标签 + if (position >= 0 && position < paragraphs.size()) { + paragraphs.remove(position); + } else { + System.out.println("指定的索引超出范围"); + } + + } + + /** + * 将查询的所有有效数据填充到模板中 + */ + private int fillContentInTemplate(Map>>>> finalContent, int position, List tempContent, + ObjectFactory factory, Map pKeyValueMap, List tableKeys, List paragraphs) { + if (CollUtil.isNotEmpty(finalContent)) { + Iterator>>>>> iterator1 = finalContent.entrySet().iterator(); + while (iterator1.hasNext()) { + Map.Entry>>>> next1 = iterator1.next(); + // 此处的key是影响量的文字描述 + String key = next1.getKey(); + List>>> value = next1.getValue(); + for (Map>> stringListMap : value) { + Iterator>>> iterator2 = stringListMap.entrySet().iterator(); + while (iterator2.hasNext()) { + Map.Entry>> next2 = iterator2.next(); + // 此处的key是误差范围 + String key1 = next2.getKey(); + List> value1 = next2.getValue(); + // 填充模板内容 + if (CollUtil.isNotEmpty(tempContent)) { + // 读取该表下模板里面的内容,并动态赋值渲染文档 + Docx4jUtil.HeadingContent headingContent = tempContent.get(0); + for (Object object : headingContent.getSubContent()) { + // 段落 + if (object instanceof P) { + P innerP = factory.createP(); + // 如果是段落,渲染段落内容 + P paragraph = (P) object; + // 获取该段落的样式 + RPr rPr = Docx4jUtil.getTcPrFromParagraph(paragraph); + String textFromP = Docx4jUtil.getTextFromP(paragraph); + if (StrUtil.isNotBlank(textFromP)) { + // 如果是段落内容,渲染段落内容 + String[] splitP = textFromP.split(StrPool.DASHED); + String text = ""; + for (String item : splitP) { + if (StrUtil.isNotBlank(pKeyValueMap.get(item))) { + text = text.concat(pKeyValueMap.get(item)); + } else if (item.equalsIgnoreCase(ItemReportKeyEnum.ERROR_SCOPE.getKey())) { + text = text.concat("(").concat("最大允许误差:").concat(key1).concat(")"); + } + // 目前段落只有名称+误差范围,如果有补充后续在这里加。todo... + } + Docx4jUtil.addPContent(factory, innerP, text, rPr); + } + //插入段落 + paragraphs.add(position++, 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) { + // 删除最后一行,这行用来填模板key的,无需展示 + // 获取现有行的样式 + 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(); + tbl.getContent().remove(existingRow); + // 迭代增加行,需要填充的表格keys在tableKeys集合中 + for (Map stringStringMap : value1) { + Tr newRow = Docx4jUtil.createCustomRow(factory, stringStringMap, tableKeys, trPr, tcPr, true); + tbl.getContent().add(newRow); + } + } else { + // 纵向表格暂不考虑 + } + // 插入段落 + paragraphs.add(position++, copiedTableElement); + } + } + + // 全部渲染完毕后,添加几个换行 + P p = factory.createP(); + Docx4jUtil.addBr(factory, p, 2); + paragraphs.add(position++, p); + + } + } + } + } + } + return position; + } + + private void updateDevAndPlanState(String devId, String planId) { // 将改设备的报告生成状态调整为已生成 iPqDevService.updatePqDevReportState(devId, DevReportStateEnum.GENERATED.getValue()); - // 判断该计划下是否所有设备报告已生成,如果已生成则将计划的报告状态给为已生成 int count = iPqDevService.countUnReportDev(planId); LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper<>(); @@ -656,62 +1035,55 @@ public class PqReportServiceImpl extends ServiceImpl i } + /** * 处理基础模版中的信息,非数据页报告 - * - * @param baseModelDocument 模板文件 + * 此处为什么要抽出拼接的前缀&后缀,是因为Docx4j工具包替换时会默认增加${},故在使用docx4j时前后缀必须为空 */ - private void dealBaseModel(XWPFDocument baseModelDocument, PqDevVO pqDevVO, DevType devType) { + private Map dealBaseModelData(PqDevVO pqDevVO, DevType devType, String prefix, String suffix) { // 首先获取非数据页中需要的信息 - Map baseModelMap = new HashMap<>(16); + Map baseModelMap = new HashMap<>(32); // 获取设备型号 - baseModelMap.put("${devType}", devType.getName()); - baseModelMap.put("${device_type}", devType.getName()); - // 调试人员,todo... 待咨询曹泽辉如何获取当前用户信息,目前先写死 - //String userName = RequestUtil.getUserName(); - SysUser user = sysUserService.getById(pqDevVO.getCheckBy()); - baseModelMap.put("${userName}", user.getName()); + baseModelMap.put(prefix + BaseReportKeyEnum.DEV_TYPE.getKey() + suffix, devType.getName()); + // 检测员 + baseModelMap.put(prefix + BaseReportKeyEnum.INSPECTOR.getKey() + suffix, pqDevVO.getCheckBy()); // 调试日期 if (pqDevVO.getCheckTime() != null) { - baseModelMap.put("${testDate}", DateUtil.format(pqDevVO.getCheckTime(), DatePattern.CHINESE_DATE_PATTERN)); + baseModelMap.put(prefix + BaseReportKeyEnum.TEST_DATE.getKey() + suffix, DateUtil.format(pqDevVO.getCheckTime(), DatePattern.CHINESE_DATE_PATTERN)); } else { - baseModelMap.put("${testDate}", DateUtil.format(new Date(), DatePattern.CHINESE_DATE_PATTERN)); + baseModelMap.put(prefix + BaseReportKeyEnum.TEST_DATE.getKey() + suffix, DateUtil.format(new Date(), DatePattern.CHINESE_DATE_PATTERN)); } // 装置编码 - baseModelMap.put("${CreateId}", pqDevVO.getCreateId()); + baseModelMap.put(prefix + BaseReportKeyEnum.DEV_CODE.getKey() + suffix, pqDevVO.getCreateId()); // 工作电源 - baseModelMap.put("${power}", devType.getPower()); + baseModelMap.put(prefix + BaseReportKeyEnum.POWER.getKey() + suffix, devType.getPower()); // 额定电流 - baseModelMap.put("${devCurr}", pqDevVO.getDevCurr().toString().concat("A")); + baseModelMap.put(prefix + BaseReportKeyEnum.DEV_CURR.getKey() + suffix, pqDevVO.getDevCurr().toString().concat(PowerConstant.CURRENT_UNIT)); // 额定电压 - baseModelMap.put("${devVolt}", pqDevVO.getDevVolt().toString().concat("V")); - - // 共有多少通道参与测试 -// if (CollectionUtil.isEmpty(pqDevById.getMonitorList())) { -// baseModelMap.put("${count}", "0"); -// } else { -// baseModelMap.put("${count}", pqDevById.getMonitorList().size() + ""); -// } - - baseModelMap.put("${count}", pqDevVO.getDevChns().toString()); - + baseModelMap.put(prefix + BaseReportKeyEnum.DEV_VOLT.getKey() + suffix, pqDevVO.getDevVolt().toString().concat(PowerConstant.VOLTAGE_UNIT)); + // 通道数 + baseModelMap.put(prefix + BaseReportKeyEnum.COUNT.getKey() + suffix, pqDevVO.getDevChns().toString()); + // 制造厂家 DictData dictData = dictDataService.getDictDataById(pqDevVO.getManufacturer()); if (ObjectUtil.isNotNull(dictData)) { - baseModelMap.put("${manufacturer}", dictData.getName()); + baseModelMap.put(prefix + BaseReportKeyEnum.MANUFACTURER.getKey() + suffix, dictData.getName()); } else { - baseModelMap.put("${manufacturer}", "未知"); + baseModelMap.put(prefix + BaseReportKeyEnum.MANUFACTURER.getKey() + suffix, StrPool.TAB); } - baseModelMap.put("${sample_id}", String.valueOf(pqDevVO.getSampleId())); - baseModelMap.put("${arrived_date}", String.valueOf(pqDevVO.getArrivedDate())); - baseModelMap.put("${check_date}", String.valueOf(pqDevVO.getCheckTime()).substring(0, 10)); - baseModelMap.put("${tested_by}", user.getName()); - - // 替换模板中的信息,避免信息丢失,段落和表格均参与替换 - WordUtil.replacePlaceholdersInParagraphs(baseModelDocument, baseModelMap); - WordUtil.replacePlaceholdersInTables(baseModelDocument, baseModelMap); - + // 样品编号 + baseModelMap.put(prefix + BaseReportKeyEnum.SAMPLE_ID.getKey() + suffix, StrUtil.isEmpty(pqDevVO.getSampleId()) ? StrPool.TAB : pqDevVO.getSampleId()); + // 收样日期 + baseModelMap.put(prefix + BaseReportKeyEnum.ARRIVED_DATE.getKey() + suffix, Objects.isNull(pqDevVO.getArrivedDate()) ? StrPool.TAB : String.valueOf(pqDevVO.getArrivedDate())); + // 检测日期 + baseModelMap.put(prefix + BaseReportKeyEnum.TEST_DATE.getKey() + suffix, Objects.isNull(pqDevVO.getCheckTime()) ? StrPool.TAB : String.valueOf(pqDevVO.getCheckTime()).substring(0, 10)); + baseModelMap.put(prefix + BaseReportKeyEnum.YEAR.getKey() + suffix, DateUtil.format(new Date(), DatePattern.NORM_YEAR_PATTERN)); + baseModelMap.put(prefix + BaseReportKeyEnum.MONTH.getKey() + suffix, DateUtil.format(new Date(), DatePattern.SIMPLE_MONTH_PATTERN).substring(4)); + baseModelMap.put(prefix + BaseReportKeyEnum.DAY.getKey() + suffix, DateUtil.format(new Date(), DatePattern.PURE_DATE_PATTERN).substring(6)); + baseModelMap.put(prefix + BaseReportKeyEnum.YEAR_MONTH_DAY.getKey() + suffix, DateUtil.format(new Date(), DatePattern.NORM_DATE_PATTERN)); + return baseModelMap; } + /** * 获取数据页的信息 * diff --git a/detection/src/main/java/com/njcn/gather/report/utils/Docx4jInsertParagraph.java b/detection/src/main/java/com/njcn/gather/report/utils/Docx4jInsertParagraph.java new file mode 100644 index 00000000..fdc8cc5c --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/report/utils/Docx4jInsertParagraph.java @@ -0,0 +1,44 @@ +package com.njcn.gather.report.utils; + +/** + * @author hongawen + * @version 1.0 + * @data 2025/3/25 19:37 + */ +import org.docx4j.openpackaging.packages.WordprocessingMLPackage; +import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart; +import org.docx4j.wml.*; + +import java.io.File; +import java.math.BigInteger; +import java.util.List; + +public class Docx4jInsertParagraph { + public static void main(String[] args) throws Exception { + // 加载现有的 Word 文档 + WordprocessingMLPackage wordPackage = WordprocessingMLPackage.load(new File("C:\\Users\\hongawen\\Desktop\\base_template.docx")); + MainDocumentPart documentPart = wordPackage.getMainDocumentPart(); + + // 获取文档中的所有段落 + List paragraphs = documentPart.getContent(); + + // 在中间插入一个新段落 + int insertIndex = paragraphs.size() / 2; + P newParagraph = createParagraph("This is a new paragraph inserted in the middle."); + paragraphs.add(insertIndex, newParagraph); + + // 保存修改后的文档 + wordPackage.save(new File("example_modified.docx")); + } + + private static P createParagraph(String text) { + ObjectFactory factory = new ObjectFactory(); + P paragraph = factory.createP(); + R run = factory.createR(); + Text t = factory.createText(); + t.setValue(text); + run.getContent().add(t); + paragraph.getContent().add(run); + return paragraph; + } +} \ No newline at end of file diff --git a/detection/src/main/java/com/njcn/gather/report/utils/Docx4jUtil.java b/detection/src/main/java/com/njcn/gather/report/utils/Docx4jUtil.java new file mode 100644 index 00000000..a20a40fc --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/report/utils/Docx4jUtil.java @@ -0,0 +1,537 @@ +package com.njcn.gather.report.utils; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.text.StrPool; +import cn.hutool.core.util.StrUtil; +import com.njcn.gather.report.pojo.constant.ReportConstant; +import com.njcn.gather.report.pojo.enums.DocAnchorEnum; +import com.njcn.gather.report.pojo.vo.Bookmark; +import org.docx4j.XmlUtils; +import org.docx4j.jaxb.Context; +import org.docx4j.openpackaging.packages.WordprocessingMLPackage; +import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart; +import org.docx4j.wml.*; + +import javax.xml.bind.JAXBElement; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.Unmarshaller; +import java.io.StringReader; +import java.io.StringWriter; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * @author hongawen + * @version 1.0 + * @data 2025/3/26 13:47 + */ +public class Docx4jUtil { + + /** + * 创建标题 + * + * @param factory 工厂 + * @param paragraph 段落容器 + * @param content 标题内容 + * @param fontSize 字体大小 + * @param isBold 是否加粗 + */ + public static void createTitle(ObjectFactory factory, P paragraph, String content, int fontSize, boolean isBold) { + R run = factory.createR(); + Text text = factory.createText(); + text.setValue(content); + // 创建运行属性 + RPr rPr = factory.createRPr(); + // 设置字体 + RFonts fonts = factory.createRFonts(); + fonts.setAscii("Arial"); + // 宋体 + 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(); + rPr.setB(b); + } + run.setRPr(rPr); + run.getContent().add(text); + // 换行 +// Br br = factory.createBr(); +// run.getContent().add(br); + paragraph.getContent().add(run); + } + + /** + * 提取Heading 5及其子内容 + * + * @param allContent 文档内所有内容 + */ + public static List extractHeading5Contents(List allContent) { + List result = new ArrayList<>(); + boolean inHeading5Section = false; + HeadingContent currentHeading = null; + + for (Object obj : allContent) { + + if (obj instanceof P) { + P paragraph = (P) obj; + if (isHeading5(paragraph)) { + // 发现新的Heading 5,保存前一个并创建新的 + if (currentHeading != null) { + result.add(currentHeading); + } + currentHeading = new HeadingContent(); + currentHeading.setHeadingText(getTextFromP(paragraph)); + inHeading5Section = true; + } else if (inHeading5Section) { + // 当前内容属于Heading 5的子内容 + currentHeading.addSubContent(paragraph); + } + } else if (obj instanceof JAXBElement && inHeading5Section) { + // 表格属于当前Heading 5的子内容 + JAXBElement jaxbElement = (JAXBElement) obj; + if (jaxbElement.getValue() instanceof Tbl) { + currentHeading.addSubContent(obj); + } + } else if (isHigherLevelHeading(obj)) { + // 遇到更高级别的标题,结束当前Heading 5的收集 + if (currentHeading != null) { + result.add(currentHeading); + currentHeading = null; + } + inHeading5Section = false; + } + + + } + + // 添加最后一个Heading 5 + if (currentHeading != null) { + result.add(currentHeading); + } + return result; + } + + // 判断段落是否为Heading 5 + private static boolean isHeading5(P paragraph) { + PPr ppr = paragraph.getPPr(); + if (ppr != null) { + PPrBase.PStyle pStyle = ppr.getPStyle(); + if (pStyle != null && "5".equals(pStyle.getVal())) { + return true; + } + } + return false; + } + + // 判断是否为更高级别的标题(1-4) + private static boolean isHigherLevelHeading(Object obj) { + if (obj instanceof P) { + PPr ppr = ((P) obj).getPPr(); + if (ppr != null) { + PPrBase.PStyle pStyle = ppr.getPStyle(); + if (pStyle != null) { + String style = pStyle.getVal(); + return style != null && style.matches("[1-4]"); + } + } + } + return false; + } + + /** + * 判断表格是否横向 + * + * @param obj row + */ + public static boolean judgeTableCross(Object obj) { + Tr row = (Tr) obj; + List content = row.getContent(); + // 取最后一个单元格,判断是否包含汉字,有汉字就是横向的 + Object cellObject = content.get(content.size() - 1); + JAXBElement cellElement = (JAXBElement) cellObject; + Tc cell = cellElement.getValue(); + String text = getTextFromCell(cell); + if (StrUtil.isBlank(text)) { + return true; + } + // 遍历字符串中的每个字符,存在中文就认为是横向的,理论上设置的key全是英文 + for (char c : text.toCharArray()) { + // 检查字符是否在中文Unicode范围内 + if (c >= '\u4E00' && c <= '\u9FFF') { + // 发现中文字符,返回 false + return true; + } + } + return false; + } + + /** + * 读取cell内的文本内容 + */ + public static String getTextFromCell(Tc cell) { + List cellContent = cell.getContent(); + StringBuilder cellText = new StringBuilder(); + for (Object content : cellContent) { + if (content instanceof P) { + P paragraph = (P) content; + cellText.append(getTextFromP(paragraph)); + } + } + return cellText.toString(); + } + + + /** + * 从段落中提取纯文本 + * + * @param paragraph 段落 + * @return 段落内容 + */ + public static String getTextFromP(P paragraph) { + StringBuilder textContent = new StringBuilder(); + for (Object runObj : paragraph.getContent()) { + if (runObj instanceof R) { + R run = (R) runObj; + for (Object textObj : run.getContent()) { + if (textObj instanceof Text) { + textContent.append(((Text) textObj).getValue()); + } else if (textObj instanceof JAXBElement) { + JAXBElement jaxbElement = (JAXBElement) textObj; + if (jaxbElement.getValue() instanceof Text) { + Text temp = (Text) jaxbElement.getValue(); + textContent.append(temp.getValue()); + } + } + } + } + } + return textContent.toString().trim(); + } + + /** + * 获取段落的样式 + * + * @param paragraph 段落 + */ + public static RPr getTcPrFromParagraph(P paragraph) { + // 1. 获取段落中的所有内容 + List content = paragraph.getContent(); + // 2. 清空原有内容但保留第一个Run的样式 + RPr preservedRPr = null; + if (!content.isEmpty()) { + Object firstObj = content.get(0); + if (firstObj instanceof R) { + // 保存第一个Run的样式 + preservedRPr = ((R) firstObj).getRPr(); + } + } + return preservedRPr; + } + + /** + * 段落中添加内容 + */ + public static void addPContent(ObjectFactory factory, P paragraph, String content, RPr rPr) { + R run = factory.createR(); + Text text = factory.createText(); + text.setValue(content); + run.setRPr(rPr); + run.getContent().add(text); + paragraph.getContent().add(run); + } + + /** + * 创建N个换行符 + */ + public static void addBr(ObjectFactory factory, P paragraph, int n) { + R run = factory.createR(); + for (int i = 0; i < n; i++) { + // 换行 + Br br = factory.createBr(); + run.getContent().add(br); + } + paragraph.getContent().add(run); + } + + /** + * 根据表格行获取需要填的值的key + */ + public static List getTableKey(Tr row) { + List keys = new ArrayList<>(); + // 横向的,最后一行为需要填充的数据后,前面的均是表头 + // 遍历获取出该row的所有key + List content = row.getContent(); + for (Object cellObject : content) { + if (cellObject instanceof JAXBElement) { + JAXBElement cellElement = (JAXBElement) cellObject; + Tc cell = cellElement.getValue(); + keys.add(Docx4jUtil.getTextFromCell(cell)); + } + } + return keys; + } + + /** + * 获取内容中需要填充的keys + * + * @param tempContent 标题下配置的内容 + */ + public static List> getFillKeys(List tempContent) { + List> keys = new ArrayList<>(); + 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) { + // 如果是段落,渲染段落内容 + P paragraph = (P) object; + String textFromP = getTextFromP(paragraph); + String[] splitKeys = textFromP.split(StrPool.DASHED); + pKeys.addAll(Arrays.asList(splitKeys)); + } else 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 { + // 纵向表格暂不考虑 + } + } + } + } + keys.add(pKeys); + keys.add(tableKeys); + return keys; + + } + + /** + * 根据已知信息创建新航 + * + * @param factory 工厂 + * @param valueMap 数据 + * @param tableKeys keys + * @param trPr 行样式 + * @param tcPr 单元格样式 + */ + public static Tr createCustomRow(ObjectFactory factory, Map valueMap, List tableKeys, TrPr trPr, TcPr tcPr, boolean centerFlag) { + 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); + // 设置段落居中 + if (centerFlag) { + PPr pPr = factory.createPPr(); + Jc jc = factory.createJc(); + jc.setVal(JcEnumeration.CENTER); + pPr.setJc(jc); + paragraph.setPPr(pPr); + } + if (value.equals("不合格")) { + RPr rPr = factory.createRPr(); + Color color = factory.createColor(); + // 红色 + color.setVal("FF0000"); + rPr.setColor(color); + run.setRPr(rPr); + } + cell.getContent().add(paragraph); + cell.setTcPr(tcPr); + row.getContent().add(cell); + row.setTrPr(trPr); + } + return row; + } + + + /** + * 根据已知信息创建新行 + * + * @param factory 工厂 + * @param cellValues 数据 + * @param ascFontStyle 西文字体 + * @param eastFontStyle 中文字体 + * @param size 字体大小 + * @param boldFlag 是否加粗 + * @param centerFlag 是否居中 + */ + public static Tr createCustomRow(ObjectFactory factory, List cellValues, String ascFontStyle,String eastFontStyle, Integer size, boolean boldFlag, boolean centerFlag) { + Tr row = factory.createTr(); + for (String value : cellValues) { + Tc cell = factory.createTc(); + P paragraph = factory.createP(); + R run = factory.createR(); + Text text = factory.createText(); + text.setValue(value); + run.getContent().add(text); + paragraph.getContent().add(run); + // 设置段落居中 + if (centerFlag) { + PPr pPr = factory.createPPr(); + Jc jc = factory.createJc(); + jc.setVal(JcEnumeration.CENTER); + pPr.setJc(jc); + paragraph.setPPr(pPr); + } + RPr rPr = factory.createRPr(); + if (value.equals("不合格")) { + Color color = factory.createColor(); + // 红色 + color.setVal("FF0000"); + rPr.setColor(color); + } + if(boldFlag){ + BooleanDefaultTrue bold = factory.createBooleanDefaultTrue(); + rPr.setB(bold); + } + RFonts fonts = factory.createRFonts(); + // 西文字体 + fonts.setAscii(ascFontStyle); + // 中文字体 + fonts.setEastAsia(eastFontStyle); + rPr.setRFonts(fonts); + + HpsMeasure fontSize = factory.createHpsMeasure(); + fontSize.setVal(BigInteger.valueOf(size)); + // 西文字号 + rPr.setSz(fontSize); + // 中文字号 + rPr.setSzCs(fontSize); + + run.setRPr(rPr); + cell.getContent().add(paragraph); + row.getContent().add(cell); + } + return row; + } + + public static JAXBElement deepCopyTbl(JAXBElement original) throws Exception { + // 使用 docx4j 的 XmlUtils 进行深拷贝 + WordprocessingMLPackage wordMLPackage = WordprocessingMLPackage.createPackage(); + Tbl clonedTbl = (Tbl) XmlUtils.deepCopy(original.getValue()); + + // 重新包装为 JAXBElement + return new JAXBElement( + original.getName(), + original.getDeclaredType(), + original.getScope(), + clonedTbl + ); + } + + + + // 存储Heading 5及其子内容的辅助类 + public static class HeadingContent { + private String headingText; + private List subContent = new ArrayList<>(); + + public void setHeadingText(String text) { + this.headingText = text; + } + + public String getHeadingText() { + return headingText; + } + + public void addSubContent(Object obj) { + subContent.add(obj); + } + + public List getSubContent() { + return subContent; + } + } + + /** + * 获取段落在文档中的位置 + */ + public static int getParagraphPosition(MainDocumentPart baseDocumentPart, DocAnchorEnum docAnchorEnum) { + List baseContent = baseDocumentPart.getContent(); + for (int i = 0; i < baseContent.size(); i++) { + Object obj = baseContent.get(i); + if (obj instanceof P) { + P p = (P) obj; + String text = getTextFromP(p).trim(); + if (text.startsWith(ReportConstant.BOOKMARK_START)) { + DocAnchorEnum anchorEnum = DocAnchorEnum.getByKey(text); + if (anchorEnum != null && anchorEnum.getKey().equalsIgnoreCase(docAnchorEnum.getKey())) { + return i; + } + } + } + } + return -1; + } + + /** + * 获取表格样式 + */ + public static TblPr getTblPr(ObjectFactory factory) { + // **关键步骤:设置黑色实线边框** + TblPr tblPr = factory.createTblPr(); + TblBorders borders = factory.createTblBorders(); + + // 定义边框样式(1磅黑色单实线) + CTBorder border = new CTBorder(); + // 实线类型 + border.setVal(STBorder.SINGLE); + // 1磅=4单位(1/8磅) + border.setSz(BigInteger.valueOf(4)); + // 黑色 + border.setColor("000000"); + + // 应用边框到所有边 + borders.setTop(border); + borders.setBottom(border); + borders.setLeft(border); + borders.setRight(border); + // 内部水平线 + borders.setInsideH(border); + // 内部垂直线 + borders.setInsideV(border); + + tblPr.setTblBorders(borders); + + + // 设置表格宽度为96% + TblWidth tblWidth = factory.createTblWidth(); + // 百分比类型 + tblWidth.setType("pct"); + // 96% = 4800/5000 (ISO标准) + tblWidth.setW(BigInteger.valueOf(5000)); + tblPr.setTblW(tblWidth); + + return tblPr; + } + + +} diff --git a/detection/src/main/java/com/njcn/gather/report/utils/WordUtil.java b/detection/src/main/java/com/njcn/gather/report/utils/WordUtil.java index 7fdae256..450add31 100644 --- a/detection/src/main/java/com/njcn/gather/report/utils/WordUtil.java +++ b/detection/src/main/java/com/njcn/gather/report/utils/WordUtil.java @@ -1,7 +1,13 @@ package com.njcn.gather.report.utils; import org.apache.poi.xwpf.usermodel.*; +import org.apache.xmlbeans.XmlCursor; +import org.docx4j.openpackaging.packages.WordprocessingMLPackage; +import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart; +import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTP; +import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTbl; +import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -45,54 +51,6 @@ public class WordUtil { }); } - private static void insertPageBreak(XWPFDocument target) { - if(!isDocumentEmpty(target)){ - // 获取最后一个页面的段落 - XWPFParagraph pageBreakParagraph = getLastPageParagraph(target); - // 设置分页符 - pageBreakParagraph.setPageBreak(true); - } - - } - - public static boolean isDocumentEmpty(XWPFDocument document) { - // 检查段落 - List paragraphs = document.getParagraphs(); - if (paragraphs != null && !paragraphs.isEmpty()) { - for (XWPFParagraph paragraph : paragraphs) { - if (paragraph.getText() != null && !paragraph.getText().trim().isEmpty()) { - return false; - } - } - } - - // 检查表格 - List tables = document.getTables(); - if (tables != null && !tables.isEmpty()) { - return false; - } - - return true; - } - - public static XWPFParagraph getLastPageParagraph(XWPFDocument document) { - XWPFParagraph lastPageParagraph = null; - for (XWPFParagraph paragraph : document.getParagraphs()) { - lastPageParagraph = paragraph; - } - - return lastPageParagraph; - } - - private static boolean containsPageBreak(XWPFParagraph paragraph) { - for (XWPFRun run : paragraph.getRuns()) { - if (run.getText(0) != null && run.getText(0).contains("\f")) { - return true; - } - } - return false; - } - /** * 替换表格中的占位符 @@ -145,6 +103,42 @@ public class WordUtil { } } + /** + * 替换文档中的占位符 + * @param document 文档 + * @param placeholders 待替换的占位符 + */ + public static void replacePlaceholders(XWPFDocument document, Map placeholders) { + replacePlaceholdersInParagraphs(document,placeholders); + replacePlaceholdersInTables(document,placeholders); + } + + + public static List findHeadingLevel5Paragraphs(XWPFDocument document) { + List headingLevel5Paragraphs = new ArrayList<>(); + for (XWPFParagraph paragraph : document.getParagraphs()) { + String style = paragraph.getStyle(); + if ("5".equals(style)) { + headingLevel5Paragraphs.add(paragraph); + } + } + return headingLevel5Paragraphs; + } + + + /** + * 获取段落的位置(通过遍历bodyElements) + */ + public static int getBodyElementPosition(XWPFDocument document, XWPFParagraph paragraph) { + List bodyElements = document.getBodyElements(); + for (int i = 0; i < bodyElements.size(); i++) { + if (bodyElements.get(i) instanceof XWPFParagraph && bodyElements.get(i).equals(paragraph)) { + return i; + } + } + return -1; + } + } 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 3ac6f4b1..7e891e67 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,11 +1,14 @@ package com.njcn.gather.result.service; +import com.njcn.gather.report.pojo.result.SingleTestResult; import com.njcn.gather.result.pojo.param.ResultParam; import com.njcn.gather.result.pojo.vo.FormContentVO; import com.njcn.gather.result.pojo.vo.ResultVO; import com.njcn.gather.result.pojo.vo.TreeDataVO; +import com.njcn.gather.script.pojo.vo.PqScriptDtlDataVO; import java.util.List; +import java.util.Map; /** * @author caozehui @@ -75,4 +78,22 @@ public interface IResultService { * @param param */ void reCalculate(ResultParam.ChangeErrorSystemParam param); + + /** + * 获取某测试大项的检测项内容 + * + * @param checkDataVOList 检测项脚本 + * @param planCode 计划编号 + * @param devId 被检设备id + * @param lineNo 回路号 + * @param tableKeys 表格key + */ + SingleTestResult getFinalContent(List checkDataVOList, String planCode, String devId, Integer lineNo, List tableKeys); + + /** + * 获取段落中指定的key对应的值 + * @param itemCode 测试大项code + * @param pKeys 待填充的值 + */ + Map getParagraphKeysValue(String itemCode, List pKeys); } 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 8c6fa627..bb5f1080 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 @@ -4,20 +4,30 @@ import cn.afterturn.easypoi.excel.entity.ExportParams; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.date.DatePattern; +import cn.hutool.core.text.StrPool; import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; import com.github.yulichang.wrapper.MPJLambdaWrapper; import com.njcn.common.pojo.enums.common.DataStateEnum; import com.njcn.common.pojo.exception.BusinessException; +import com.njcn.common.utils.PubUtils; import com.njcn.gather.detection.pojo.enums.DetectionCodeEnum; import com.njcn.gather.detection.pojo.enums.SourceOperateCodeEnum; import com.njcn.gather.detection.pojo.param.PreDetectionParam; import com.njcn.gather.detection.pojo.po.DevData; +import com.njcn.gather.detection.pojo.vo.DetectionData; import com.njcn.gather.detection.service.impl.DetectionServiceImpl; import com.njcn.gather.device.service.IPqDevService; import com.njcn.gather.plan.pojo.po.AdPlan; import com.njcn.gather.plan.service.IAdPlanService; import com.njcn.gather.pojo.enums.DetectionResponseEnum; +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.SingleTestResult; import com.njcn.gather.result.pojo.enums.ResultUnitEnum; import com.njcn.gather.result.pojo.param.ResultParam; import com.njcn.gather.result.pojo.vo.FormContentVO; @@ -30,10 +40,12 @@ import com.njcn.gather.script.pojo.param.PqScriptIssueParam; import com.njcn.gather.script.pojo.po.PqScriptCheckData; import com.njcn.gather.script.pojo.po.PqScriptDtls; import com.njcn.gather.script.pojo.po.SourceIssue; +import com.njcn.gather.script.pojo.vo.PqScriptDtlDataVO; import com.njcn.gather.script.service.IPqScriptCheckDataService; import com.njcn.gather.script.service.IPqScriptDtlsService; import com.njcn.gather.script.util.ScriptDtlsDesc; import com.njcn.gather.storage.mapper.TableGenMapper; +import com.njcn.gather.storage.pojo.param.SingleNonHarmParam; import com.njcn.gather.storage.pojo.param.StorageParam; import com.njcn.gather.storage.pojo.po.AdBaseResult; import com.njcn.gather.storage.pojo.po.AdHarmonicResult; @@ -910,6 +922,528 @@ public class ResultServiceImpl implements IResultService { this.calculateResult(param.getPlanId(), param.getScriptId(), param.getCode(), param.getErrorSysId(), param.getDeviceId()); } + /** + * 获取某测试大项的检测项内容,主要针对表格需要填充的值 + * 注:虽然测试项区是三相,他们的标准值可能不同,但是误差范围是一致的 + * 1、区分额定条件、单影响量条件、单影响量条件 + * 2、区分scriptIndex + * 3、区分谐波次数(有必要的情况下) + * + * @param dtlDataVOList 检测项脚本 + * @param planCode 计划编号 + * @param devId 被检设备id + * @param lineNo 回路号 + * @param tableKeys 表格key + */ + @Override + public SingleTestResult getFinalContent(List dtlDataVOList, String planCode, String devId, Integer lineNo, List tableKeys) { + SingleTestResult singleTestResult = new SingleTestResult(); + Map>>>> finalContent = new HashMap<>(); + if (CollUtil.isNotEmpty(dtlDataVOList)) { + // 首先区分测试条件 + Map> subTypeMap = dtlDataVOList.stream().collect(Collectors.groupingBy(PqScriptDtlDataVO::getScriptSubType)); + subTypeMap.forEach((subType, scriptDtlDataVOList) -> { + AffectEnum affectEnum = AffectEnum.getByKey(subType); + if (Objects.nonNull(affectEnum)) { + String scriptCode = scriptDtlDataVOList.get(0).getScriptCode(); + String scriptId = scriptDtlDataVOList.get(0).getScriptId(); + PowerIndexEnum indexEnum = PowerIndexEnum.getByKey(scriptCode); + if (Objects.nonNull(indexEnum)) { + String affectName = affectEnum.getDesc().replaceAll("XX", indexEnum.getDesc()); + // 同一个影响量下,获取出子脚本信息 + List indexList = scriptDtlDataVOList.stream().map(PqScriptDtlDataVO::getScriptIndex).distinct().collect(Collectors.toList()); + List scriptCheckDataList = pqScriptCheckDataService.listCheckData(scriptId, indexList); + List valueTypeList = scriptCheckDataList.stream().map(PqScriptCheckData::getValueType).distinct().collect(Collectors.toList()); + // 查询检测结果,区分下表。 + if (PowerConstant.TIME.contains(scriptCode)) { + // 谐波的测试项,肯定是三相的,可能会存在多次的,本处要填充的key全集为:time、standard、standardA、standardB、standardC、testA、testB、testC、errorA、errorB、errorC、result + // 查询结果数据,经过上层处理谐波类此处的scriptIndex确保只有一个 + if (indexList.size() == 1 && valueTypeList.size() == 1) { + // 获取谐波数据 + SingleNonHarmParam param = new SingleNonHarmParam(Integer.parseInt(planCode), devId, lineNo, valueTypeList.get(0), indexList.get(0)); + AdHarmonicResult singleResult = adHarmonicService.getSingleResult(param); + // 注:如果ABC的标准值一致,则同步到standard中 + Map> checkDataHarmNumMap = scriptCheckDataList.stream().collect(Collectors.groupingBy(PqScriptCheckData::getHarmNum)); + List> keyFillMapList = new ArrayList<>(); + checkDataHarmNumMap.forEach((harmNum, dtlsList) -> { + Map keyFillMap = new HashMap<>(); + // 次数需要区分谐波&间谐波 + String time; + if (harmNum % 1 == 0) { + // 谐波,需要转为正数字符串 + time = String.valueOf(harmNum.intValue()); + } else { + // 间谐波,保留小数位转为字符串 + time = String.format("%.1f", harmNum); + } + keyFillMap.put(ItemReportKeyEnum.TIME.getKey(), time); + // 将间谐波次数取整1.5取2,2.5取3 + double timeDouble = Math.round(harmNum); + int timeInt = (int) timeDouble; + DetectionData tempA = getResultData(singleResult, timeInt, PowerConstant.PHASE_A); + DetectionData tempB = getResultData(singleResult, timeInt, PowerConstant.PHASE_B); + DetectionData tempC = getResultData(singleResult, timeInt, PowerConstant.PHASE_C); + // 待填充Key + String standard = "/", standardA = "/", standardB = "/", standardC = "/", + testA = "/", testB = "/", testC = "/", + errorA = "/", errorB = "/", errorC = "/", + resultA = "/", resultB = "/", resultC = "/", result = "/", + errorScope = "/", unit = ""; + + if (Objects.nonNull(tempA) && (PowerConstant.DATA_RANGE.contains(tempA.getIsData()))) { + standardA = PubUtils.doubleRoundStr(4, tempA.getResultData()); + testA = PubUtils.doubleRoundStr(4, tempA.getData()); + errorA = PubUtils.doubleRoundStr(4, tempA.getErrorData().doubleValue()); + resultA = tempA.getIsData() == 1 ? "合格" : "不合格"; + errorScope = tempA.getRadius(); + unit = tempA.getUnit(); + standard = PubUtils.doubleRoundStr(4, tempA.getResultData()); + } + if (Objects.nonNull(tempB) && (PowerConstant.DATA_RANGE.contains(tempB.getIsData()))) { + standardB = PubUtils.doubleRoundStr(4, tempB.getResultData()); + testB = PubUtils.doubleRoundStr(4, tempB.getData()); + errorB = PubUtils.doubleRoundStr(4, tempB.getErrorData().doubleValue()); + resultB = tempB.getIsData() == 1 ? "合格" : "不合格"; + if (errorScope.equals("/")) { + errorScope = tempB.getRadius(); + } + if (StrUtil.isBlank(unit)) { + unit = tempB.getUnit(); + } + if (standard.equals("/")) { + standard = PubUtils.doubleRoundStr(4, tempB.getResultData()); + } + } + if (Objects.nonNull(tempC) && (PowerConstant.DATA_RANGE.contains(tempC.getIsData()))) { + standardC = PubUtils.doubleRoundStr(4, tempC.getResultData()); + testC = PubUtils.doubleRoundStr(4, tempC.getData()); + errorC = PubUtils.doubleRoundStr(4, tempC.getErrorData().doubleValue()); + resultC = tempC.getIsData() == 1 ? "合格" : "不合格"; + if (errorScope.equals("/")) { + errorScope = tempC.getRadius(); + } + if (StrUtil.isBlank(unit)) { + unit = tempC.getUnit(); + } + if (standard.equals("/")) { + standard = PubUtils.doubleRoundStr(4, tempC.getResultData()); + } + } + if (standardA.equals(standardB) && standardA.equals(standardC)) { + standard = standardA; + } + // 标准值 + keyFillMap.put(ItemReportKeyEnum.STANDARD.getKey(), standard); + keyFillMap.put(ItemReportKeyEnum.STANDARD_A.getKey(), standardA); + keyFillMap.put(ItemReportKeyEnum.STANDARD_B.getKey(), standardB); + keyFillMap.put(ItemReportKeyEnum.STANDARD_C.getKey(), standardC); + // 测试值 + keyFillMap.put(ItemReportKeyEnum.TEST_A.getKey(), testA); + keyFillMap.put(ItemReportKeyEnum.TEST_B.getKey(), testB); + keyFillMap.put(ItemReportKeyEnum.TEST_C.getKey(), testC); + // 误差值 + keyFillMap.put(ItemReportKeyEnum.ERROR_A.getKey(), errorA); + keyFillMap.put(ItemReportKeyEnum.ERROR_B.getKey(), errorB); + keyFillMap.put(ItemReportKeyEnum.ERROR_C.getKey(), errorC); + // 结果 + keyFillMap.put(ItemReportKeyEnum.RESULT_A.getKey(), resultA); + keyFillMap.put(ItemReportKeyEnum.RESULT_B.getKey(), resultB); + keyFillMap.put(ItemReportKeyEnum.RESULT_C.getKey(), resultC); + if (resultA.equals("不合格") || resultB.equals("不合格") || resultC.equals("不合格")) { + result = "不合格"; + } else if (!resultA.equals("/") || !resultB.equals("/") || !resultC.equals("/")) { + result = "合格"; + } + keyFillMap.put(ItemReportKeyEnum.RESULT.getKey(), result); + errorScope = dealErrorScope(errorScope).concat(unit); + keyFillMap.put(ItemReportKeyEnum.ERROR_SCOPE.getKey(), errorScope); + keyFillMapList.add(keyFillMap); + }); + // 按次数排序 + PubUtils.sortByDoubleValue(keyFillMapList, ItemReportKeyEnum.TIME.getKey()); + // 取出任意一次谐波数据的误差范围作为key + String titleScope = keyFillMapList.get(0).get(ItemReportKeyEnum.ERROR_SCOPE.getKey()); + Map>> errorScoperMap = new HashMap<>(); + errorScoperMap.put(titleScope, keyFillMapList); + List>>> errorScoperMapList = new ArrayList<>(); + errorScoperMapList.add(errorScoperMap); + finalContent.put(affectName, errorScoperMapList); + } else { + log.error("生成谐波类表格数据失败,脚本配置不支持,请核实。"); + throw new BusinessException("生成谐波类表格数据失败,脚本配置不支持,请核实。"); + } + } else { + // 非谐波的需要区分是否为ABC相还是T相 + if (PowerConstant.THREE_PHASE.contains(scriptCode)) { + if (valueTypeList.size() == 1) { + // 获取该三相的数据 + SingleNonHarmParam param = new SingleNonHarmParam(Integer.parseInt(planCode), devId, lineNo, valueTypeList, indexList); + List nonHarmList = adNonHarmonicService.queryByCondition(param); + // 三相的数据通常包含:standard、standardA、standardB、standardC、testA、testB、testC、errorA、errorB、errorC、resultA、resultB、resultC、result、errorScope + if (CollUtil.isNotEmpty(nonHarmList)) { + List> keyFillMapList = new ArrayList<>(); + for (AdNonHarmonicResult adNonHarmonicResult : nonHarmList) { + Map keyFillMap = new HashMap<>(16); + DetectionData tempA = getResultData(adNonHarmonicResult, null, PowerConstant.PHASE_A); + DetectionData tempB = getResultData(adNonHarmonicResult, null, PowerConstant.PHASE_B); + DetectionData tempC = getResultData(adNonHarmonicResult, null, PowerConstant.PHASE_C); + // 待填充Key + String standard = "/", standardA = "/", standardB = "/", standardC = "/", + testA = "/", testB = "/", testC = "/", + errorA = "/", errorB = "/", errorC = "/", + resultA = "/", resultB = "/", resultC = "/", result = "/", + errorScope = "/", unit = ""; + if (Objects.nonNull(tempA) && (PowerConstant.DATA_RANGE.contains(tempA.getIsData()))) { + standardA = PubUtils.doubleRoundStr(4, tempA.getResultData()); + testA = PubUtils.doubleRoundStr(4, tempA.getData()); + errorA = PubUtils.doubleRoundStr(4, tempA.getErrorData().doubleValue()); + resultA = tempA.getIsData() == 1 ? "合格" : "不合格"; + errorScope = tempA.getRadius(); + unit = tempA.getUnit(); + standard = PubUtils.doubleRoundStr(4, tempA.getResultData()); + } + if (Objects.nonNull(tempB) && (PowerConstant.DATA_RANGE.contains(tempB.getIsData()))) { + standardB = PubUtils.doubleRoundStr(4, tempB.getResultData()); + testB = PubUtils.doubleRoundStr(4, tempB.getData()); + errorB = PubUtils.doubleRoundStr(4, tempB.getErrorData().doubleValue()); + resultB = tempB.getIsData() == 1 ? "合格" : "不合格"; + if (errorScope.equals("/")) { + errorScope = tempB.getRadius(); + } + if (StrUtil.isBlank(unit)) { + unit = tempB.getUnit(); + } + if (standard.equals("/")) { + standard = PubUtils.doubleRoundStr(4, tempB.getResultData()); + } + } + if (Objects.nonNull(tempC) && (PowerConstant.DATA_RANGE.contains(tempC.getIsData()))) { + standardC = PubUtils.doubleRoundStr(4, tempC.getResultData()); + testC = PubUtils.doubleRoundStr(4, tempC.getData()); + errorC = PubUtils.doubleRoundStr(4, tempC.getErrorData().doubleValue()); + resultC = tempC.getIsData() == 1 ? "合格" : "不合格"; + if (errorScope.equals("/")) { + errorScope = tempC.getRadius(); + } + if (StrUtil.isBlank(unit)) { + unit = tempC.getUnit(); + } + if (standard.equals("/")) { + standard = PubUtils.doubleRoundStr(4, tempC.getResultData()); + } + } + if (standardA.equals(standardB) && standardA.equals(standardC)) { + standard = standardA; + } + // 标准值 + keyFillMap.put(ItemReportKeyEnum.STANDARD.getKey(), standard); + keyFillMap.put(ItemReportKeyEnum.STANDARD_A.getKey(), standardA); + keyFillMap.put(ItemReportKeyEnum.STANDARD_B.getKey(), standardB); + keyFillMap.put(ItemReportKeyEnum.STANDARD_C.getKey(), standardC); + // 测试值 + keyFillMap.put(ItemReportKeyEnum.TEST_A.getKey(), testA); + keyFillMap.put(ItemReportKeyEnum.TEST_B.getKey(), testB); + keyFillMap.put(ItemReportKeyEnum.TEST_C.getKey(), testC); + // 误差值 + keyFillMap.put(ItemReportKeyEnum.ERROR_A.getKey(), errorA); + keyFillMap.put(ItemReportKeyEnum.ERROR_B.getKey(), errorB); + keyFillMap.put(ItemReportKeyEnum.ERROR_C.getKey(), errorC); + // 结果 + keyFillMap.put(ItemReportKeyEnum.RESULT_A.getKey(), resultA); + keyFillMap.put(ItemReportKeyEnum.RESULT_B.getKey(), resultB); + keyFillMap.put(ItemReportKeyEnum.RESULT_C.getKey(), resultC); + if (resultA.equals("不合格") || resultB.equals("不合格") || resultC.equals("不合格")) { + result = "不合格"; + } else if (!resultA.equals("/") || !resultB.equals("/") || !resultC.equals("/")) { + result = "合格"; + } + keyFillMap.put(ItemReportKeyEnum.RESULT.getKey(), result); + errorScope = dealErrorScope(errorScope).concat(unit); + keyFillMap.put(ItemReportKeyEnum.ERROR_SCOPE.getKey(), errorScope); + keyFillMapList.add(keyFillMap); + } + // 需要对所有的填充进行按误差范围分组 + Map>> errorScoperMap = keyFillMapList.stream() + .collect(Collectors.groupingBy(map -> map.get(ItemReportKeyEnum.ERROR_SCOPE.getKey()))); + // 分组后,还需要针对标准值进行一个升序 + errorScoperMap.forEach((errorScope, maps) -> { + PubUtils.sortByDoubleValue(maps, ItemReportKeyEnum.STANDARD.getKey()); + }); + List>>> errorList = new ArrayList<>(); + errorList.add(errorScoperMap); + // 最后赋值返回 + finalContent.put(affectName, errorList); + } else { + log.error("生成三相类表格数据失败,结果表数据丢失,请核实。"); + } + } else { + log.error("生成三相类表格数据失败,脚本配置不支持,请核实。"); + throw new BusinessException("生成三相类表格数据失败,脚本配置不支持,请核实。"); + } + } else { + // 非三相的还需要特殊处理下暂态的 + if (PowerConstant.VOLTAGE.equalsIgnoreCase(scriptCode)) { + // 暂态的valueType通常只有2个,一个特征幅值,一个持续时间 + List> keyFillMapList = new ArrayList<>(); + for (Integer sort : indexList) { + SingleNonHarmParam param = new SingleNonHarmParam(Integer.parseInt(planCode), devId, lineNo, valueTypeList, Collections.singletonList(sort)); + List nonHarmList = adNonHarmonicService.queryByCondition(param); + // 暂态的数据通常包含:standardMag、standardDur、testMag、testDur、errorMag、errorDur、resultMag、resultDur、result、errorScope、errorScopeMag、errorScopeDur + if (CollUtil.isNotEmpty(nonHarmList)) { + String standardMag = "/", standardDur = "/", + testMag = "/", testDur = "/", + errorMag = "/", errorDur = "/", + resultMag = "/", resultDur = "/", result, + errorScope, errorScopeMag = "/", errorScopeDur = "/", + unitMag = "", unitDur = ""; + Map keyFillMap = new HashMap<>(16); + for (AdNonHarmonicResult adNonHarmonicResult : nonHarmList) { + DetectionData tempT = getResultData(adNonHarmonicResult, null, PowerConstant.PHASE_T); + // 需要判断adNonHarmonicResult是特征幅值还是持续时间 + String adType = adNonHarmonicResult.getAdType(); + DictTree temp = dictTreeService.getById(adType); + if (temp.getCode().equalsIgnoreCase("MAG")) { + // 特征幅值 + if (Objects.nonNull(tempT) && PowerConstant.DATA_RANGE.contains(tempT.getIsData())) { + standardMag = PubUtils.doubleRoundStr(4, tempT.getResultData()); + testMag = PubUtils.doubleRoundStr(4, tempT.getData()); + errorMag = PubUtils.doubleRoundStr(4, tempT.getErrorData().doubleValue()); + resultMag = tempT.getIsData() == 1 ? "合格" : "不合格"; + unitMag = tempT.getUnit(); + errorScopeMag = tempT.getRadius(); + } + } else if (temp.getCode().equalsIgnoreCase("DUR")) { + // 持续时间,需要注意时间单位处理,默认是秒 + String timeUnit = "s"; + for (String tableKey : tableKeys) { + if (tableKey.contains(ItemReportKeyEnum.STANDARD_DUR.getKey())) { + //截取单位 + String[] tempStr = tableKey.split(StrPool.UNDERLINE); + if (tempStr.length > 1) { + if (tempStr[1].equalsIgnoreCase("ms")) { + timeUnit = "ms"; + } + } + } + } + standardDur = PubUtils.doubleRoundStr(4, tempT.getResultData()); + testDur = PubUtils.doubleRoundStr(4, tempT.getData()); + errorDur = PubUtils.doubleRoundStr(4, tempT.getErrorData().doubleValue()); + resultDur = tempT.getIsData() == 1 ? "合格" : "不合格"; + unitDur = tempT.getUnit(); + errorScopeDur = tempT.getRadius(); + if (timeUnit.equalsIgnoreCase("ms")) { + // 如果是ms,上述的一些数据需要重新处理 + if (!standardDur.equalsIgnoreCase("/")) { + standardDur = PubUtils.doubleRoundStr(4, Double.parseDouble(standardDur) * 1000); + } + if (!testDur.equalsIgnoreCase("/")) { + testDur = PubUtils.doubleRoundStr(4, Double.parseDouble(testDur) * 1000); + } + if (!errorDur.equalsIgnoreCase("/")) { + errorDur = PubUtils.doubleRoundStr(4, Double.parseDouble(errorDur) * 1000); + } + if(!errorScopeDur.equalsIgnoreCase("/")){ + if(errorScopeDur.contains("~")){ + String[] tempStr = errorScopeDur.split("~"); + errorScopeDur = PubUtils.doubleRoundStr(0, Double.parseDouble(tempStr[0]) * 1000).concat("~").concat(PubUtils.doubleRoundStr(0, Double.parseDouble(tempStr[1]) * 1000)); + } + } + unitDur = "ms"; + } + } + } + errorScopeMag = dealErrorScope(errorScopeMag).concat(unitMag); + errorScopeDur = dealErrorScope(errorScopeDur).concat(unitDur); + + errorScope = "特征幅值:".concat(errorScopeMag).concat(StrPool.COMMA).concat("持续时间:").concat(errorScopeDur); + keyFillMap.put(ItemReportKeyEnum.ERROR_SCOPE.getKey(), errorScope); + // 标准值 + keyFillMap.put(ItemReportKeyEnum.STANDARD_MAG.getKey(), standardMag); + keyFillMap.put(ItemReportKeyEnum.STANDARD_DUR.getKey(), standardDur); + // 测试值 + keyFillMap.put(ItemReportKeyEnum.TEST_MAG.getKey(), testMag); + keyFillMap.put(ItemReportKeyEnum.TEST_DUR.getKey(), testDur); + // 误差 + keyFillMap.put(ItemReportKeyEnum.ERROR_MAG.getKey(), errorMag); + keyFillMap.put(ItemReportKeyEnum.ERROR_DUR.getKey(), errorDur); + // 结果 + keyFillMap.put(ItemReportKeyEnum.RESULT_MAG.getKey(), resultMag); + keyFillMap.put(ItemReportKeyEnum.RESULT_DUR.getKey(), resultDur); + // 综合结果 + result = resultMag; + if (!resultDur.equals("/") && result.equals("合格")) { + if (resultDur.equals("不合格")) { + result = "不合格"; + } + } + keyFillMap.put(ItemReportKeyEnum.RESULT.getKey(), result); + keyFillMapList.add(keyFillMap); + } + } + // 需要对所有填充进行按误差范围分组 + Map>> errorScoperMap = keyFillMapList.stream() + .collect(Collectors.groupingBy(map -> map.get(ItemReportKeyEnum.ERROR_SCOPE.getKey()))); + // 分组后,还需要针对特征幅值标准值进行一个升序 + errorScoperMap.forEach((errorScope, maps) -> { + PubUtils.sortByDoubleValue(maps, ItemReportKeyEnum.STANDARD_MAG.getKey()); + }); + List>>> errorList = new ArrayList<>(); + errorList.add(errorScoperMap); + // 最后赋值返回 + finalContent.put(affectName, errorList); + } else { + // 非三相且非暂态,通常只有一个数据,所以直接赋值即可 + List> keyFillMapList = new ArrayList<>(); + SingleNonHarmParam param = new SingleNonHarmParam(Integer.parseInt(planCode), devId, lineNo, valueTypeList, indexList); + List nonHarmList = adNonHarmonicService.queryByCondition(param); + if (CollUtil.isNotEmpty(nonHarmList)) { + for (AdNonHarmonicResult adNonHarmonicResult : nonHarmList) { + String standard = "/", test = "/", error = "/", result = "/", errorScope = "/",unit = ""; + Map keyFillMap = new HashMap<>(8); + DetectionData tempT = getResultData(adNonHarmonicResult, null, PowerConstant.PHASE_T); + if (Objects.nonNull(tempT) && PowerConstant.DATA_RANGE.contains(tempT.getIsData())) { + standard = PubUtils.doubleRoundStr(4, tempT.getResultData()); + test = PubUtils.doubleRoundStr(4, tempT.getData()); + error = PubUtils.doubleRoundStr(4, tempT.getErrorData().doubleValue()); + result = tempT.getIsData() == 1 ? "合格" : "不合格"; + unit = tempT.getUnit(); + errorScope = tempT.getRadius(); + } + keyFillMap.put(ItemReportKeyEnum.STANDARD.getKey(), standard); + keyFillMap.put(ItemReportKeyEnum.TEST.getKey(), test); + keyFillMap.put(ItemReportKeyEnum.ERROR.getKey(), error); + keyFillMap.put(ItemReportKeyEnum.RESULT.getKey(), result); + errorScope = dealErrorScope(errorScope).concat(unit); + keyFillMap.put(ItemReportKeyEnum.ERROR_SCOPE.getKey(), errorScope); + keyFillMapList.add(keyFillMap); + } + } + // 需要对所有填充进行按误差范围分组 + Map>> errorScoperMap = keyFillMapList.stream() + .collect(Collectors.groupingBy(map -> map.get(ItemReportKeyEnum.ERROR_SCOPE.getKey()))); + // 分组后,还需要针对特征幅值标准值进行一个升序 + errorScoperMap.forEach((errorScope, maps) -> { + PubUtils.sortByDoubleValue(maps, ItemReportKeyEnum.STANDARD.getKey()); + }); + List>>> errorList = new ArrayList<>(); + errorList.add(errorScoperMap); + // 最后赋值返回 + finalContent.put(affectName, errorList); + } + } + } + } else { + log.error("未找到合适的脚本信息"); + } + } else { + log.error("未找到合适的测量条件"); + } + }); + } + + // 返回之前做下总结性判断 + singleTestResult.setQualified(judgeQualified(finalContent)); + singleTestResult.setDetail(finalContent); + return singleTestResult; + } + + /** + * 处理下误差范围,如果正负数一致时调整为±的形式 + * 数据库中一般形式为:-0.1155~0.1155 + * + * @param errorScope 误差范围 + */ + private String dealErrorScope(String errorScope) { + if (errorScope.contains("~")) { + String[] split = errorScope.split("~"); + String begin = split[0]; + if (begin.contains(StrPool.DASHED)) { + begin = begin.substring(1); + } + String end = split[1]; + if (end.equalsIgnoreCase(begin)) { + return "±" + begin; + } + } + return errorScope; + } + + /** + * 遍历所有的结果,是否存在不合格,但凡有一个不合格就返回false + * + * @param finalContent 最终结果 + */ + private boolean judgeQualified(Map>>>> finalContent) { + List results = finalContent.values().parallelStream() + .flatMap(List::stream) + .flatMap(m -> m.values().stream()) + .flatMap(List::stream) + .filter(map -> map.containsKey(ItemReportKeyEnum.RESULT.getKey())) + .map(map -> map.get(ItemReportKeyEnum.RESULT.getKey())) + .collect(Collectors.toList()); + List qualifiedList = results.stream().filter("不合格"::equals).collect(Collectors.toList()); + return CollUtil.isEmpty(qualifiedList); + } + + /** + * 根据谐波结果数据获取局的测试数据 + * + * @param singleResult 谐波数据 + * @param timeInt 次数 + * @param phaseA 相别 + */ + private DetectionData getResultData(Object singleResult, Integer timeInt, String phaseA) { + String fieldName = phaseA.toLowerCase().concat("Value"); + if (Objects.nonNull(timeInt)) { + fieldName = fieldName.concat(String.valueOf(timeInt)); + } + String filedValue; + try { + filedValue = (String) ReflectUtil.getFieldValue(singleResult, fieldName); + } catch (Exception exception) { + throw new BusinessException("获取对象字段属性失败"); + } + 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; + } + private Integer conform(Set numbers) { if (CollUtil.isNotEmpty(numbers)) { if (numbers.size() > 1) { diff --git a/detection/src/main/java/com/njcn/gather/script/mapper/PqScriptCheckDataMapper.java b/detection/src/main/java/com/njcn/gather/script/mapper/PqScriptCheckDataMapper.java index ca25a3fb..f578bf73 100644 --- a/detection/src/main/java/com/njcn/gather/script/mapper/PqScriptCheckDataMapper.java +++ b/detection/src/main/java/com/njcn/gather/script/mapper/PqScriptCheckDataMapper.java @@ -2,6 +2,10 @@ package com.njcn.gather.script.mapper; import com.github.yulichang.base.MPJBaseMapper; import com.njcn.gather.script.pojo.po.PqScriptCheckData; +import com.njcn.gather.script.pojo.vo.PqScriptCheckDataVO; +import org.apache.ibatis.annotations.Param; + +import java.util.List; /** * @Description: diff --git a/detection/src/main/java/com/njcn/gather/script/mapper/PqScriptDtlsMapper.java b/detection/src/main/java/com/njcn/gather/script/mapper/PqScriptDtlsMapper.java index 13fca7f8..1b87ebfa 100644 --- a/detection/src/main/java/com/njcn/gather/script/mapper/PqScriptDtlsMapper.java +++ b/detection/src/main/java/com/njcn/gather/script/mapper/PqScriptDtlsMapper.java @@ -2,8 +2,12 @@ package com.njcn.gather.script.mapper; import com.github.yulichang.base.MPJBaseMapper; import com.njcn.gather.script.pojo.po.PqScriptDtls; +import com.njcn.gather.script.pojo.vo.PqScriptCheckDataVO; +import com.njcn.gather.script.pojo.vo.PqScriptDtlDataVO; import org.apache.ibatis.annotations.Param; +import java.util.List; + /** * @author caozehui * @date 2024-11-18 @@ -17,5 +21,7 @@ public interface PqScriptDtlsMapper extends MPJBaseMapper { * @return */ Integer selectMaxIndex(@Param("scriptId") String scriptId); + + List getScriptDtlsDataList(@Param("scriptId")String scriptId); } diff --git a/detection/src/main/java/com/njcn/gather/script/mapper/mapping/PqScriptCheckDataMapper.xml b/detection/src/main/java/com/njcn/gather/script/mapper/mapping/PqScriptCheckDataMapper.xml index d06d03bf..5e1fe9de 100644 --- a/detection/src/main/java/com/njcn/gather/script/mapper/mapping/PqScriptCheckDataMapper.xml +++ b/detection/src/main/java/com/njcn/gather/script/mapper/mapping/PqScriptCheckDataMapper.xml @@ -2,6 +2,5 @@ - diff --git a/detection/src/main/java/com/njcn/gather/script/mapper/mapping/PqScriptDtlsMapper.xml b/detection/src/main/java/com/njcn/gather/script/mapper/mapping/PqScriptDtlsMapper.xml index 58e37fc8..e7464f82 100644 --- a/detection/src/main/java/com/njcn/gather/script/mapper/mapping/PqScriptDtlsMapper.xml +++ b/detection/src/main/java/com/njcn/gather/script/mapper/mapping/PqScriptDtlsMapper.xml @@ -10,5 +10,21 @@ WHERE Script_Id = #{scriptId} + + diff --git a/detection/src/main/java/com/njcn/gather/script/pojo/vo/PqScriptCheckDataVO.java b/detection/src/main/java/com/njcn/gather/script/pojo/vo/PqScriptCheckDataVO.java new file mode 100644 index 00000000..44602ee3 --- /dev/null +++ b/detection/src/main/java/com/njcn/gather/script/pojo/vo/PqScriptCheckDataVO.java @@ -0,0 +1,24 @@ +package com.njcn.gather.script.pojo.vo; + +import com.njcn.gather.script.pojo.po.PqScriptCheckData; +import lombok.Data; + +/** + * @author hongawen + * @version 1.0 + * @data 2025/3/26 14:45 + */ +@Data +public class PqScriptCheckDataVO extends PqScriptCheckData { + + /** + * 脚本项名称 + */ + private String scriptName; + + /** + * 脚本项Code + */ + private String scriptCode; + +} diff --git a/detection/src/main/java/com/njcn/gather/script/pojo/vo/PqScriptDtlDataVO.java b/detection/src/main/java/com/njcn/gather/script/pojo/vo/PqScriptDtlDataVO.java index a9d3f507..b2995687 100644 --- a/detection/src/main/java/com/njcn/gather/script/pojo/vo/PqScriptDtlDataVO.java +++ b/detection/src/main/java/com/njcn/gather/script/pojo/vo/PqScriptDtlDataVO.java @@ -1,23 +1,23 @@ package com.njcn.gather.script.pojo.vo; +import com.njcn.gather.script.pojo.po.PqScriptDtls; import lombok.Data; import java.io.Serializable; @Data -public class PqScriptDtlDataVO implements Serializable { +public class PqScriptDtlDataVO extends PqScriptDtls implements Serializable { private static final long serialVersionUID = 1L; /** - * 这条检测脚本的序号 + * 脚本项名称 */ - private String index; - - - - - + private String scriptName; + /** + * 脚本项Code + */ + private String scriptCode; } \ No newline at end of file diff --git a/detection/src/main/java/com/njcn/gather/script/service/IPqScriptCheckDataService.java b/detection/src/main/java/com/njcn/gather/script/service/IPqScriptCheckDataService.java index 73b6c591..fe737e16 100644 --- a/detection/src/main/java/com/njcn/gather/script/service/IPqScriptCheckDataService.java +++ b/detection/src/main/java/com/njcn/gather/script/service/IPqScriptCheckDataService.java @@ -3,6 +3,7 @@ package com.njcn.gather.script.service; import com.baomidou.mybatisplus.extension.service.IService; import com.njcn.gather.script.pojo.param.PqScriptCheckDataParam; import com.njcn.gather.script.pojo.po.PqScriptCheckData; +import com.njcn.gather.script.pojo.vo.PqScriptCheckDataVO; import java.util.List; import java.util.Map; @@ -36,4 +37,11 @@ public interface IPqScriptCheckDataService extends IService { List listCheckDataCode(PqScriptCheckDataParam param); Double getCheckDataValue(PqScriptCheckDataParam param); + + /** + * 查询条件范围内的参与比较的脚本详情 + * @param scriptId 脚本id + * @param indexList 脚本下标集合 + */ + List listCheckData(String scriptId, List indexList); } diff --git a/detection/src/main/java/com/njcn/gather/script/service/IPqScriptDtlsService.java b/detection/src/main/java/com/njcn/gather/script/service/IPqScriptDtlsService.java index d9e3e881..f784820e 100644 --- a/detection/src/main/java/com/njcn/gather/script/service/IPqScriptDtlsService.java +++ b/detection/src/main/java/com/njcn/gather/script/service/IPqScriptDtlsService.java @@ -9,6 +9,8 @@ import com.njcn.gather.script.pojo.param.ScriptParam; import com.njcn.gather.script.pojo.po.PqScriptCheckData; import com.njcn.gather.script.pojo.po.PqScriptDtls; import com.njcn.gather.script.pojo.po.SourceIssue; +import com.njcn.gather.script.pojo.vo.PqScriptCheckDataVO; +import com.njcn.gather.script.pojo.vo.PqScriptDtlDataVO; import java.util.List; import java.util.Map; @@ -139,4 +141,9 @@ public interface IPqScriptDtlsService extends IService { * @return */ Map getScriptToIcdCheckInfo(PreDetectionParam param); + /** + * 根据脚本id获取脚本详情数据 + * @param scriptId 脚本id + */ + List getScriptDtlsDataList(String scriptId); } diff --git a/detection/src/main/java/com/njcn/gather/script/service/impl/PqScriptCheckDataServiceImpl.java b/detection/src/main/java/com/njcn/gather/script/service/impl/PqScriptCheckDataServiceImpl.java index cf693e31..408596c8 100644 --- a/detection/src/main/java/com/njcn/gather/script/service/impl/PqScriptCheckDataServiceImpl.java +++ b/detection/src/main/java/com/njcn/gather/script/service/impl/PqScriptCheckDataServiceImpl.java @@ -13,6 +13,8 @@ import com.njcn.gather.detection.pojo.enums.DetectionResponseEnum; import com.njcn.gather.script.mapper.PqScriptCheckDataMapper; import com.njcn.gather.script.pojo.param.PqScriptCheckDataParam; import com.njcn.gather.script.pojo.po.PqScriptCheckData; +import com.njcn.gather.script.pojo.po.PqScriptDtls; +import com.njcn.gather.script.pojo.vo.PqScriptCheckDataVO; import com.njcn.gather.script.service.IPqScriptCheckDataService; import com.njcn.gather.system.dictionary.mapper.DictTreeMapper; import com.njcn.gather.system.dictionary.pojo.po.DictTree; @@ -121,5 +123,16 @@ public class PqScriptCheckDataServiceImpl extends ServiceImpl listCheckData(String scriptId, List indexList) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper() + .eq(PqScriptCheckData::getScriptId, scriptId) + .eq(PqScriptCheckData::getEnable, DataStateEnum.ENABLE.getCode()) + .eq(PqScriptCheckData::getErrorFlag, 1) + .in(PqScriptCheckData::getScriptIndex, indexList) + .orderByAsc(PqScriptCheckData::getHarmNum); + return this.list(queryWrapper); + } + } diff --git a/detection/src/main/java/com/njcn/gather/script/service/impl/PqScriptDtlsServiceImpl.java b/detection/src/main/java/com/njcn/gather/script/service/impl/PqScriptDtlsServiceImpl.java index 80ea98d6..cd172ab7 100644 --- a/detection/src/main/java/com/njcn/gather/script/service/impl/PqScriptDtlsServiceImpl.java +++ b/detection/src/main/java/com/njcn/gather/script/service/impl/PqScriptDtlsServiceImpl.java @@ -29,6 +29,8 @@ import com.njcn.gather.script.pojo.po.PqScript; import com.njcn.gather.script.pojo.po.PqScriptCheckData; import com.njcn.gather.script.pojo.po.PqScriptDtls; import com.njcn.gather.script.pojo.po.SourceIssue; +import com.njcn.gather.script.pojo.vo.PqScriptCheckDataVO; +import com.njcn.gather.script.pojo.vo.PqScriptDtlDataVO; import com.njcn.gather.script.service.IPqScriptCheckDataService; import com.njcn.gather.script.service.IPqScriptDtlsService; import com.njcn.gather.script.util.ScriptDtlsDesc; @@ -739,6 +741,11 @@ public class PqScriptDtlsServiceImpl extends ServiceImpl getScriptDtlsDataList(String scriptId) { + return this.baseMapper.getScriptDtlsDataList(scriptId); + } + private void unbanCheck(List info, PqScriptDtlsParam.CheckData channelListDTO, List list, diff --git a/entrance/src/main/java/com/njcn/gather/advice/LogAdvice.java b/entrance/src/main/java/com/njcn/gather/advice/LogAdvice.java new file mode 100644 index 00000000..91c6daf4 --- /dev/null +++ b/entrance/src/main/java/com/njcn/gather/advice/LogAdvice.java @@ -0,0 +1,105 @@ +package com.njcn.gather.advice; + + +import com.njcn.common.pojo.enums.response.CommonResponseEnum; +import com.njcn.common.pojo.response.HttpResult; +import com.njcn.gather.system.log.service.ISysLogAuditService; +import com.njcn.web.utils.HttpServletUtil; +import com.njcn.web.utils.ReflectCommonUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.MethodParameter; +import org.springframework.http.MediaType; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; + +import javax.annotation.Nonnull; +import javax.servlet.http.HttpServletRequest; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.*; + + +/** + * @author caozehui + * @data 2024-12-2 + */ +@Slf4j +@ControllerAdvice +public class LogAdvice implements ResponseBodyAdvice { + + @Autowired + private ISysLogAuditService logService; + + private final ThreadPoolExecutor executor = new ThreadPoolExecutor( + 4, 8, 30, TimeUnit.SECONDS, + new LinkedBlockingQueue<>(100), + // 队列满时由主线程执行 + new ThreadPoolExecutor.CallerRunsPolicy() + ); + + + /** + * 无需审计记录的操作,比如根据客户端获取客户端信息 + */ + + private final static List UN_LOG_INFO = Collections.singletonList("未知业务"); + + + /** + * controller返回的响应体包含的状态码,而非全局异常捕获器处抛出来的状态码 + */ + private final static List FILTER_CODE = Arrays.asList(CommonResponseEnum.SUCCESS.getCode(), CommonResponseEnum.FAIL.getCode(), CommonResponseEnum.NO_DATA.getCode()); + + + /** + * 判断下结果,是不是成功 + * + * @param returnType 返回类型,包含大量信息,controller、method、result等等 + * @param converterType 消息转换器类型,目前配置的是Jackson + */ + @Override + public boolean supports(MethodParameter returnType, Class converterType) { + return true; + } + + /** + * 拦截所有请求成功的操作进行日志入库处理 + * 需要 用户标识、事件描述、事件结果、操作IP、事件类型、事件严重度、操作时间、操作类型 + * + * @param body . + * @param returnType . + * @param selectedContentType . + * @param selectedConverterType . + * @param request . + * @param response . + * @return . + */ + @Override + public Object beforeBodyWrite(Object body, @Nonnull MethodParameter returnType, @Nonnull MediaType selectedContentType, @Nonnull Class selectedConverterType, @Nonnull ServerHttpRequest request, @Nonnull ServerHttpResponse response) { + if (body instanceof HttpResult) { + HttpResult httpResult = (HttpResult) body; + if (FILTER_CODE.contains(httpResult.getCode())) { + // 传递上下文 + HttpServletRequest httpServletRequest = HttpServletUtil.getRequest(); + String methodDescribe = ReflectCommonUtil.getMethodDescribeByMethod(returnType.getMethod()); + if (!UN_LOG_INFO.contains(methodDescribe)) { + Future future = executor.submit(() -> { + HttpServletUtil.setRequest(httpServletRequest); + logService.recodeAdviceLog(returnType, httpResult, methodDescribe); + }); + try { + // 抛出 ExecutionException + future.get(); + } catch (ExecutionException | InterruptedException e) { + log.error("保存审计日志异常,异常为:"+e.getMessage()); + } + } + } + } + return body; + } +} diff --git a/entrance/src/main/java/com/njcn/gather/handler/GlobalBusinessExceptionHandler.java b/entrance/src/main/java/com/njcn/gather/handler/GlobalBusinessExceptionHandler.java index 9e7e6d8f..d45170b8 100644 --- a/entrance/src/main/java/com/njcn/gather/handler/GlobalBusinessExceptionHandler.java +++ b/entrance/src/main/java/com/njcn/gather/handler/GlobalBusinessExceptionHandler.java @@ -6,11 +6,14 @@ import com.njcn.common.pojo.enums.response.CommonResponseEnum; import com.njcn.common.pojo.exception.BusinessException; import com.njcn.common.pojo.response.HttpResult; import com.njcn.common.utils.LogUtil; +import com.njcn.gather.system.log.service.ISysLogAuditService; import com.njcn.web.utils.HttpResultUtil; +import com.njcn.web.utils.HttpServletUtil; import com.njcn.web.utils.ReflectCommonUtil; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.json.JSONException; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.ObjectError; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; @@ -18,6 +21,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.util.NestedServletException; +import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.validation.ConstraintViolation; import javax.validation.ConstraintViolationException; @@ -25,6 +29,7 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.concurrent.*; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -40,7 +45,16 @@ import java.util.stream.Stream; @RestControllerAdvice public class GlobalBusinessExceptionHandler { -// private final ILogService logService; + @Resource + private final ISysLogAuditService sysLogAuditService; + + private final ThreadPoolExecutor executor = new ThreadPoolExecutor( + 4, 8, 30, TimeUnit.SECONDS, + new LinkedBlockingQueue<>(100), + // 队列满时由主线程执行 + new ThreadPoolExecutor.CallerRunsPolicy() + ); + /** * 捕获业务功能异常,通常为业务数据抛出的异常 @@ -48,16 +62,9 @@ public class GlobalBusinessExceptionHandler { * @param businessException 业务异常 */ @ExceptionHandler(BusinessException.class) - public HttpResult handleBusinessException(HttpServletRequest httpServletRequest, BusinessException businessException) { + public HttpResult handleBusinessException(BusinessException businessException) { String operate = ReflectCommonUtil.getMethodDescribeByException(businessException); -// logService.recodeBusinessExceptionLog(businessException, httpServletRequest, businessException.getMessage()); - //判断方法上是否有自定义注解,做特殊处理 - Method method = ReflectCommonUtil.getMethod(businessException); -// if (!Objects.isNull(method)){ -// if(method.isAnnotationPresent(ReturnMsg.class)){ -// return HttpResultUtil.assembleResult(businessException.getCode(), null, StrFormatter.format("{}",businessException.getMessage())); -// } -// } + recodeAdviceLog(businessException, businessException.getMessage()); return HttpResultUtil.assembleBusinessExceptionResult(businessException, null, operate); } @@ -68,9 +75,9 @@ public class GlobalBusinessExceptionHandler { * @param nullPointerException 空指针异常 */ @ExceptionHandler(NullPointerException.class) - public HttpResult handleNullPointerException(HttpServletRequest httpServletRequest, NullPointerException nullPointerException) { + public HttpResult handleNullPointerException(NullPointerException nullPointerException) { LogUtil.logExceptionStackInfo(CommonResponseEnum.NULL_POINTER_EXCEPTION.getMessage(), nullPointerException); -// logService.recodeBusinessExceptionLog(nullPointerException, httpServletRequest, CommonResponseEnum.NULL_POINTER_EXCEPTION.getMessage()); + recodeAdviceLog(nullPointerException, CommonResponseEnum.NULL_POINTER_EXCEPTION.getMessage()); return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.NULL_POINTER_EXCEPTION, null, ReflectCommonUtil.getMethodDescribeByException(nullPointerException)); } @@ -80,9 +87,9 @@ public class GlobalBusinessExceptionHandler { * @param arithmeticException 算数运算异常,由于除数为0引起的异常 */ @ExceptionHandler(ArithmeticException.class) - public HttpResult handleArithmeticException(HttpServletRequest httpServletRequest, ArithmeticException arithmeticException) { + public HttpResult handleArithmeticException(ArithmeticException arithmeticException) { LogUtil.logExceptionStackInfo(CommonResponseEnum.ARITHMETIC_EXCEPTION.getMessage(), arithmeticException); -// logService.recodeBusinessExceptionLog(arithmeticException, httpServletRequest, CommonResponseEnum.ARITHMETIC_EXCEPTION.getMessage()); + recodeAdviceLog(arithmeticException, CommonResponseEnum.ARITHMETIC_EXCEPTION.getMessage()); return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.ARITHMETIC_EXCEPTION, null, ReflectCommonUtil.getMethodDescribeByException(arithmeticException)); } @@ -92,9 +99,9 @@ public class GlobalBusinessExceptionHandler { * @param classCastException 类型转换异常 */ @ExceptionHandler(ClassCastException.class) - public HttpResult handleClassCastException(HttpServletRequest httpServletRequest, ClassCastException classCastException) { + public HttpResult handleClassCastException(ClassCastException classCastException) { LogUtil.logExceptionStackInfo(CommonResponseEnum.CLASS_CAST_EXCEPTION.getMessage(), classCastException); -// logService.recodeBusinessExceptionLog(classCastException, httpServletRequest, CommonResponseEnum.CLASS_CAST_EXCEPTION.getMessage()); + recodeAdviceLog(classCastException, CommonResponseEnum.CLASS_CAST_EXCEPTION.getMessage()); return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.CLASS_CAST_EXCEPTION, null, ReflectCommonUtil.getMethodDescribeByException(classCastException)); } @@ -105,9 +112,9 @@ public class GlobalBusinessExceptionHandler { * @param indexOutOfBoundsException 索引下标越界异常 */ @ExceptionHandler(IndexOutOfBoundsException.class) - public HttpResult handleIndexOutOfBoundsException(HttpServletRequest httpServletRequest, IndexOutOfBoundsException indexOutOfBoundsException) { + public HttpResult handleIndexOutOfBoundsException(IndexOutOfBoundsException indexOutOfBoundsException) { LogUtil.logExceptionStackInfo(CommonResponseEnum.INDEX_OUT_OF_BOUNDS_EXCEPTION.getMessage(), indexOutOfBoundsException); -// logService.recodeBusinessExceptionLog(indexOutOfBoundsException, httpServletRequest, CommonResponseEnum.INDEX_OUT_OF_BOUNDS_EXCEPTION.getMessage()); + recodeAdviceLog(indexOutOfBoundsException, CommonResponseEnum.INDEX_OUT_OF_BOUNDS_EXCEPTION.getMessage()); return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.INDEX_OUT_OF_BOUNDS_EXCEPTION, null, ReflectCommonUtil.getMethodDescribeByException(indexOutOfBoundsException)); } @@ -117,10 +124,10 @@ public class GlobalBusinessExceptionHandler { * @param httpMediaTypeNotSupportedException 请求中参数的媒体方式不支持异常 */ @ExceptionHandler(HttpMediaTypeNotSupportedException.class) - public HttpResult httpMediaTypeNotSupportedExceptionHandler(HttpServletRequest httpServletRequest, HttpMediaTypeNotSupportedException httpMediaTypeNotSupportedException) { + public HttpResult httpMediaTypeNotSupportedExceptionHandler(HttpMediaTypeNotSupportedException httpMediaTypeNotSupportedException) { LogUtil.logExceptionStackInfo(CommonResponseEnum.HTTP_MEDIA_TYPE_NOT_SUPPORTED_EXCEPTION.getMessage(), httpMediaTypeNotSupportedException); // 然后提取错误提示信息进行返回 -// logService.recodeBusinessExceptionLog(httpMediaTypeNotSupportedException, httpServletRequest, CommonResponseEnum.HTTP_MEDIA_TYPE_NOT_SUPPORTED_EXCEPTION.getMessage()); + recodeAdviceLog(httpMediaTypeNotSupportedException, CommonResponseEnum.HTTP_MEDIA_TYPE_NOT_SUPPORTED_EXCEPTION.getMessage()); return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.HTTP_MEDIA_TYPE_NOT_SUPPORTED_EXCEPTION, null, ReflectCommonUtil.getMethodDescribeByException(httpMediaTypeNotSupportedException)); } @@ -131,13 +138,13 @@ public class GlobalBusinessExceptionHandler { * @param methodArgumentNotValidException 参数校验异常 */ @ExceptionHandler(MethodArgumentNotValidException.class) - public HttpResult methodArgumentNotValidExceptionHandler(HttpServletRequest httpServletRequest, MethodArgumentNotValidException methodArgumentNotValidException) { + public HttpResult methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException methodArgumentNotValidException) { // 从异常对象中拿到allErrors数据 String messages = methodArgumentNotValidException.getBindingResult().getAllErrors() .stream().map(ObjectError::getDefaultMessage).collect(Collectors.joining(";")); // 然后提取错误提示信息进行返回 LogUtil.njcnDebug(log, "参数校验异常,异常为:{}", messages); -// logService.recodeBusinessExceptionLog(methodArgumentNotValidException, httpServletRequest, CommonResponseEnum.METHOD_ARGUMENT_NOT_VALID_EXCEPTION.getMessage()); + recodeAdviceLog(methodArgumentNotValidException, CommonResponseEnum.METHOD_ARGUMENT_NOT_VALID_EXCEPTION.getMessage()); return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.METHOD_ARGUMENT_NOT_VALID_EXCEPTION, messages, ControllerUtil.getMethodArgumentNotValidException(methodArgumentNotValidException)); } @@ -148,7 +155,7 @@ public class GlobalBusinessExceptionHandler { * @param constraintViolationException 参数校验异常 */ @ExceptionHandler(ConstraintViolationException.class) - public HttpResult constraintViolationExceptionExceptionHandler(HttpServletRequest httpServletRequest, ConstraintViolationException constraintViolationException) { + public HttpResult constraintViolationExceptionExceptionHandler(ConstraintViolationException constraintViolationException) { String exceptionMessage = constraintViolationException.getMessage(); StringBuilder messages = new StringBuilder(); if (exceptionMessage.indexOf(StrUtil.COMMA) > 0) { @@ -161,7 +168,7 @@ public class GlobalBusinessExceptionHandler { } // 然后提取错误提示信息进行返回 LogUtil.njcnDebug(log, "参数校验异常,异常为:{}", messages); -// logService.recodeBusinessExceptionLog(constraintViolationException, httpServletRequest, CommonResponseEnum.METHOD_ARGUMENT_NOT_VALID_EXCEPTION.getMessage()); + recodeAdviceLog(constraintViolationException, CommonResponseEnum.METHOD_ARGUMENT_NOT_VALID_EXCEPTION.getMessage()); List> constraintViolationList = new ArrayList<>(constraintViolationException.getConstraintViolations()); ConstraintViolation constraintViolation = constraintViolationList.get(0); Class rootBeanClass = constraintViolation.getRootBeanClass(); @@ -182,24 +189,12 @@ public class GlobalBusinessExceptionHandler { * @param illegalArgumentException 参数校验异常 */ @ExceptionHandler(IllegalArgumentException.class) - public HttpResult handleIndexOutOfBoundsException(HttpServletRequest httpServletRequest, IllegalArgumentException illegalArgumentException) { + public HttpResult handleIndexOutOfBoundsException(IllegalArgumentException illegalArgumentException) { LogUtil.logExceptionStackInfo(CommonResponseEnum.ILLEGAL_ARGUMENT_EXCEPTION.getMessage(), illegalArgumentException); -// logService.recodeBusinessExceptionLog(illegalArgumentException, httpServletRequest, CommonResponseEnum.ILLEGAL_ARGUMENT_EXCEPTION.getMessage()); + recodeAdviceLog(illegalArgumentException, CommonResponseEnum.ILLEGAL_ARGUMENT_EXCEPTION.getMessage()); return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.ILLEGAL_ARGUMENT_EXCEPTION, illegalArgumentException.getMessage(), ReflectCommonUtil.getMethodDescribeByException(illegalArgumentException)); } -// /** -// * 表格校验异常 -// * -// * @param excelAnalysisException 表格校验异常 -// */ -// @ExceptionHandler(ExcelAnalysisException.class) -// public HttpResult handleExcelAnalysisException(HttpServletRequest httpServletRequest, ExcelAnalysisException excelAnalysisException) { -// LogUtil.logExceptionStackInfo(CommonResponseEnum.ILLEGAL_ARGUMENT_EXCEPTION.getMessage(), excelAnalysisException); -// // logService.recodeBusinessExceptionLog(excelAnalysisException, httpServletRequest,CommonResponseEnum.ILLEGAL_ARGUMENT_EXCEPTION.getMessage()); -// return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.ILLEGAL_ARGUMENT_EXCEPTION, excelAnalysisException.getCause().getMessage(), ReflectCommonUtil.getMethodDescribeByException(excelAnalysisException)); -// } - /** * 未声明异常捕捉 @@ -207,7 +202,7 @@ public class GlobalBusinessExceptionHandler { * @param exception 未声明异常 */ @ExceptionHandler(Exception.class) - public HttpResult handleException(HttpServletRequest httpServletRequest, Exception exception) { + public HttpResult handleException(Exception exception) { //针对fallbackFactory降级异常特殊处理 Exception tempException = exception; String exceptionCause = CommonResponseEnum.UN_DECLARE.getMessage(); @@ -224,7 +219,7 @@ public class GlobalBusinessExceptionHandler { } } LogUtil.logExceptionStackInfo(exceptionCause, tempException); -// logService.recodeBusinessExceptionLog(tempException, httpServletRequest, exceptionCause); + recodeAdviceLog(exception, exceptionCause); //判断方法上是否有自定义注解,做特殊处理 // Method method = ReflectCommonUtil.getMethod(exception); // if (!Objects.isNull(method)){ @@ -236,18 +231,30 @@ public class GlobalBusinessExceptionHandler { } - - /** * json解析异常 * * @param jsonException json参数 */ @ExceptionHandler(JSONException.class) - public HttpResult handleIndexOutOfBoundsException(HttpServletRequest httpServletRequest, JSONException jsonException) { + public HttpResult handleIndexOutOfBoundsException(JSONException jsonException) { LogUtil.logExceptionStackInfo(CommonResponseEnum.JSON_CONVERT_EXCEPTION.getMessage(), jsonException); -// logService.recodeBusinessExceptionLog(jsonException, httpServletRequest, CommonResponseEnum.JSON_CONVERT_EXCEPTION.getMessage()); + recodeAdviceLog(jsonException, CommonResponseEnum.JSON_CONVERT_EXCEPTION.getMessage()); return HttpResultUtil.assembleCommonResponseResult(CommonResponseEnum.JSON_CONVERT_EXCEPTION, jsonException.getMessage(), ReflectCommonUtil.getMethodDescribeByException(jsonException)); } + private void recodeAdviceLog(Exception businessException, String methodDescribe) { + HttpServletRequest httpServletRequest = HttpServletUtil.getRequest(); + Future future = executor.submit(() -> { + HttpServletUtil.setRequest(httpServletRequest); + sysLogAuditService.recodeBusinessExceptionLog(businessException, methodDescribe); + }); + try { + // 抛出 ExecutionException + future.get(); + } catch (ExecutionException | InterruptedException e) { + log.error("保存审计日志异常,异常为:" + e.getMessage()); + } + } + } diff --git a/entrance/src/main/resources/model/njcn_882.docx b/entrance/src/main/resources/model/njcn_882.docx index 7596037a163b40c79638e9b15949f48faad93646..8abab757a750c58086d4513569388ce1d0cef279 100644 GIT binary patch delta 18055 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