11 Commits
2026-06 ... dev

Author SHA1 Message Date
caozehui
16ace94f8d 开始检测接口参数调整 2026-07-13 16:25:31 +08:00
caozehui
ca9885c5dc 被检设备上传图片功能 2026-07-09 16:35:32 +08:00
caozehui
78b52fa336 归档 2026-07-09 09:16:09 +08:00
caozehui
da08271f76 暂态写入写出时间 2026-07-08 15:50:38 +08:00
caozehui
75bc44172b 相角180度时特殊处理 2026-07-08 10:50:00 +08:00
caozehui
95544f1dc6 补充三相功率因数 2026-07-08 08:34:11 +08:00
7a8071c9d7 feat(report): 优化报告生成工具中的文档处理功能
- 在Docx4jUtil中添加enableUpdateFieldsOnOpen方法,确保Word打开时自动刷新页码等域
- 移除ItemAnchorAssembler中不必要的导入包,精简代码结构
- 重构谐波数据处理逻辑,添加谐波衍生值计算功能
- 实现固定占位符替换机制,支持谐波电压和功率表的动态填充
- 优化表格结构分离逻辑,使用空段落而非可见标题分隔多表
- 增强错误范围处理,移除单位后缀并优化谐波电流计算逻辑
- 添加全面的单元测试验证各种谐波检测场景的功能正确性
2026-07-07 22:49:46 +08:00
75b19eb6ab feat(report): 新增指标锚点模板支持及数据库连接配置更新
- 更新数据库连接URL从pqs910到pqs91001
- 在BaseReportKeyEnum中新增ARRIVED_DATE_C、TEST_DATE_C和YEAR_MONTH_DAY_UP枚举值
- 在ItemReportKeyEnum中新增标准电压电流功率等指标锚点相关枚举值
- 新增ItemAnchorAssembler工具类实现指标锚点模板的表格装配功能
- 实现云南检定报告的指标锚点新型模板处理流程
- 添加中文数字日期格式化功能支持
- 实现有功功率特例的数据映射处理
- 支持不平衡度类表格数据的生成处理
2026-07-07 10:10:49 +08:00
caozehui
efb379d6a4 补充三相功率因数 2026-07-06 20:05:29 +08:00
caozehui
cf2dcf8ff4 补充三相总有功功率、三相总无功功率、三相总视在功率 2026-07-06 16:28:24 +08:00
caozehui
4d52e9c820 补充功率因数 2026-07-06 08:43:07 +08:00
50 changed files with 4327 additions and 116 deletions

View File

@@ -307,7 +307,7 @@ public class SocketContrastResponseService {
FormalTestManager.numMap.clear(); FormalTestManager.numMap.clear();
FormalTestManager.numMap.putAll(numMap); FormalTestManager.numMap.putAll(numMap);
SysTestConfig oneConfig = sysTestConfigService.getOneConfig(); SysTestConfig oneConfig = sysTestConfigService.getOneConfig();
if (param.getTestItemList().get(2)) { if (param.isFormalTestSelected()) {
numMap.forEach((devMonitorId, num) -> { numMap.forEach((devMonitorId, num) -> {
if (oneConfig.getMaxTime() < num) { if (oneConfig.getMaxTime() < num) {
throw new BusinessException(DetectionResponseEnum.EXCEED_MAX_TIME); throw new BusinessException(DetectionResponseEnum.EXCEED_MAX_TIME);
@@ -1154,7 +1154,7 @@ public class SocketContrastResponseService {
private void sendFormalTest(String s, PreDetectionParam param, SourceOperateCodeEnum requestOperateCode, SourceOperateCodeEnum quitOperateCode) { private void sendFormalTest(String s, PreDetectionParam param, SourceOperateCodeEnum requestOperateCode, SourceOperateCodeEnum quitOperateCode) {
// 后续做正式检测 // 后续做正式检测
if (param.getTestItemList().get(2)) { if (param.isFormalTestSelected()) {
System.out.println("开始正式检测!"); System.out.println("开始正式检测!");
if (ObjectUtil.isNotNull(FormalTestManager.nonWaveDataSourceEnum)) { if (ObjectUtil.isNotNull(FormalTestManager.nonWaveDataSourceEnum)) {
SocketMsg<String> socketMsg = new SocketMsg<>(); SocketMsg<String> socketMsg = new SocketMsg<>();

View File

@@ -398,7 +398,7 @@ public class SocketDevResponseService {
sendWebSocket(param.getUserPageId(), SourceOperateCodeEnum.Coefficient_Check.getValue(), SourceOperateCodeEnum.small_comp_end.getValue(), XiNumberManager.devParameterList.get(1)); sendWebSocket(param.getUserPageId(), SourceOperateCodeEnum.Coefficient_Check.getValue(), SourceOperateCodeEnum.small_comp_end.getValue(), XiNumberManager.devParameterList.get(1));
System.out.println("-------------------------已经全部结束----------------------"); System.out.println("-------------------------已经全部结束----------------------");
if (param.getTestItemList().get(2)) { if (param.isFormalTestSelected()) {
//如果后续做正式检测 //如果后续做正式检测
PqScriptIssueParam issueParam = new PqScriptIssueParam(); PqScriptIssueParam issueParam = new PqScriptIssueParam();
issueParam.setSourceId(param.getSourceName()); issueParam.setSourceId(param.getSourceName());
@@ -912,13 +912,13 @@ public class SocketDevResponseService {
socketMsg.setOperateCode(SourceOperateCodeEnum.OPER_GATHER.getValue()); socketMsg.setOperateCode(SourceOperateCodeEnum.OPER_GATHER.getValue());
List<SourceIssue> sourceIssues; List<SourceIssue> sourceIssues;
// 做预检测、后续做系数校准 // 做预检测、后续做系数校准
if (param.getTestItemList().get(0) || param.getTestItemList().get(1)) { if (param.isPreTestSelected() || param.isCoefficientSelected()) {
issueParam.setIsPhaseSequence(CommonEnum.PHASE_TEST.getValue()); issueParam.setIsPhaseSequence(CommonEnum.PHASE_TEST.getValue());
sourceIssues = pqScriptDtlsService.listSourceIssue(issueParam); sourceIssues = pqScriptDtlsService.listSourceIssue(issueParam);
socketMsg.setRequestId(SourceOperateCodeEnum.YJC_XUJY.getValue()); socketMsg.setRequestId(SourceOperateCodeEnum.YJC_XUJY.getValue());
socketMsg.setData(JSON.toJSONString(sourceIssues.get(0))); socketMsg.setData(JSON.toJSONString(sourceIssues.get(0)));
SocketManager.sendMsg(param.getUserPageId() + CnSocketUtil.SOURCE_TAG, JSON.toJSONString(socketMsg)); SocketManager.sendMsg(param.getUserPageId() + CnSocketUtil.SOURCE_TAG, JSON.toJSONString(socketMsg));
} else if (param.getTestItemList().get(2)) { } else if (param.isFormalTestSelected()) {
// 后续做正式检测 // 后续做正式检测
if (param.getReCheckType().equals(SourceOperateCodeEnum.RE_ERROR_TEST.getValue())) { if (param.getReCheckType().equals(SourceOperateCodeEnum.RE_ERROR_TEST.getValue())) {
//不合格项复检 //不合格项复检
@@ -959,9 +959,10 @@ public class SocketDevResponseService {
//告诉前端当前项开始了 //告诉前端当前项开始了
WebSocketVO<Object> webSocketVO = new WebSocketVO<>(); WebSocketVO<Object> webSocketVO = new WebSocketVO<>();
String type = sourceIssues.get(0).getType(); String type = sourceIssues.get(0).getType();
if (ResultUnitEnum.P.getCode().equals(type)) { if (StrUtil.isNotBlank(sourceIssues.get(0).getOtherType())) {
sourceIssues.get(0).setType(ResultUnitEnum.V_ABSOLUTELY.getCode()); sourceIssues.get(0).setType(ResultUnitEnum.V_ABSOLUTELY.getCode());
webSocketVO.setRequestId(ResultUnitEnum.P.getCode() + CnSocketUtil.START_TAG); webSocketVO.setRequestId(sourceIssues.get(0).getOtherType() + CnSocketUtil.START_TAG);
type = sourceIssues.get(0).getOtherType();
} else { } else {
webSocketVO.setRequestId(sourceIssues.get(0).getType() + CnSocketUtil.START_TAG); webSocketVO.setRequestId(sourceIssues.get(0).getType() + CnSocketUtil.START_TAG);
} }
@@ -1146,7 +1147,7 @@ public class SocketDevResponseService {
} }
// 后续做系数校准 // 后续做系数校准
if (param.getTestItemList().get(1)) { if (param.isCoefficientSelected()) {
WebSocketVO<Object> webSocketVO = new WebSocketVO<>(); WebSocketVO<Object> webSocketVO = new WebSocketVO<>();
webSocketVO.setRequestId(SourceOperateCodeEnum.Coefficient_Check.getValue()); webSocketVO.setRequestId(SourceOperateCodeEnum.Coefficient_Check.getValue());
webSocketVO.setOperateCode(SourceOperateCodeEnum.big_start.getValue()); webSocketVO.setOperateCode(SourceOperateCodeEnum.big_start.getValue());
@@ -1170,7 +1171,7 @@ public class SocketDevResponseService {
XiNumberManager.smallDevXiNumDataMap.clear(); XiNumberManager.smallDevXiNumDataMap.clear();
System.out.println("开始系数校准》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》"); System.out.println("开始系数校准》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》");
} else if (param.getTestItemList().get(2)) { } else if (param.isFormalTestSelected()) {
// 后续做正式检测 // 后续做正式检测
PqScriptIssueParam issueParam = new PqScriptIssueParam(); PqScriptIssueParam issueParam = new PqScriptIssueParam();
issueParam.setSourceId(param.getSourceName()); issueParam.setSourceId(param.getSourceName());
@@ -1216,13 +1217,16 @@ public class SocketDevResponseService {
//告诉前端当前项开始了 //告诉前端当前项开始了
WebSocketVO<Object> webSocketVO = new WebSocketVO<>(); WebSocketVO<Object> webSocketVO = new WebSocketVO<>();
String type = sourceIssues.get(0).getType(); String type = sourceIssues.get(0).getType();
if (ResultUnitEnum.P.getCode().equals(type)) { if (StrUtil.isNotBlank(sourceIssues.get(0).getOtherType())) {
sourceIssues.get(0).setType(ResultUnitEnum.V_ABSOLUTELY.getCode()); sourceIssues.get(0).setType(ResultUnitEnum.V_ABSOLUTELY.getCode());
webSocketVO.setRequestId(ResultUnitEnum.P.getCode() + CnSocketUtil.START_TAG); webSocketVO.setRequestId(sourceIssues.get(0).getOtherType() + CnSocketUtil.START_TAG);
type = sourceIssues.get(0).getOtherType();
} else { } else {
webSocketVO.setRequestId(sourceIssues.get(0).getType() + CnSocketUtil.START_TAG); webSocketVO.setRequestId(sourceIssues.get(0).getType() + CnSocketUtil.START_TAG);
} }
FormalTestManager.currentIssue = sourceIssues.get(0); FormalTestManager.currentIssue = sourceIssues.get(0);
socketMsg.setData(JSON.toJSONString(sourceIssues.get(0))); socketMsg.setData(JSON.toJSONString(sourceIssues.get(0)));
socketMsg.setRequestId(SourceOperateCodeEnum.FORMAL_REAL.getValue() + CnSocketUtil.STEP_TAG + type); socketMsg.setRequestId(SourceOperateCodeEnum.FORMAL_REAL.getValue() + CnSocketUtil.STEP_TAG + type);
@@ -1316,9 +1320,9 @@ public class SocketDevResponseService {
//小项检测完后小项数减一并更新map //小项检测完后小项数减一并更新map
long residueCount = 0; long residueCount = 0;
if (sourceIssue.getIsP()) { if (StrUtil.isNotBlank(sourceIssue.getOtherType())) {
residueCount = SocketManager.getSourceTarget(ResultUnitEnum.P.getCode()) - 1; residueCount = SocketManager.getSourceTarget(sourceIssue.getOtherType()) - 1;
SocketManager.addTargetMap(ResultUnitEnum.P.getCode(), residueCount); SocketManager.addTargetMap(sourceIssue.getOtherType(), residueCount);
} else { } else {
residueCount = SocketManager.getSourceTarget(sourceIssue.getType()) - 1; residueCount = SocketManager.getSourceTarget(sourceIssue.getType()) - 1;
SocketManager.addTargetMap(sourceIssue.getType(), residueCount); SocketManager.addTargetMap(sourceIssue.getType(), residueCount);
@@ -1362,14 +1366,15 @@ public class SocketDevResponseService {
List<SourceIssue> sourceIssueList = SocketManager.getSourceList(); List<SourceIssue> sourceIssueList = SocketManager.getSourceList();
if (CollUtil.isNotEmpty(sourceIssueList)) { if (CollUtil.isNotEmpty(sourceIssueList)) {
SourceIssue sourceIssues = SocketManager.getSourceList().get(0); SourceIssue sourceIssues = SocketManager.getSourceList().get(0);
String type = sourceIssues.getType();
if (StrUtil.isNotBlank(sourceIssues.getOtherType())) {
sourceIssues.setType(ResultUnitEnum.V_ABSOLUTELY.getCode());
type = sourceIssues.getOtherType();
}
// 如果上一个大项检测完成,则检测下一个大项,并向前端推送消息 // 如果上一个大项检测完成,则检测下一个大项,并向前端推送消息
if (residueCount == 0) { if (residueCount == 0) {
WebServiceManager.sendDetectionMessage(param.getUserPageId(), sourceIssues.getType() + CnSocketUtil.START_TAG, null, new ArrayList<>(), null); WebServiceManager.sendDetectionMessage(param.getUserPageId(), type + CnSocketUtil.START_TAG, null, new ArrayList<>(), null);
}
String type = sourceIssues.getType();
if (sourceIssues.getIsP()) {
sourceIssues.setType(ResultUnitEnum.V_ABSOLUTELY.getCode());
type = ResultUnitEnum.P.getCode();
} }
//控源下发下一个小项脚本 //控源下发下一个小项脚本
@@ -1810,7 +1815,7 @@ public class SocketDevResponseService {
//字典树 //字典树
SocketManager.valueTypeMap = iPqScriptCheckDataService.getValueTypeMap(param.getScriptId()); SocketManager.valueTypeMap = iPqScriptCheckDataService.getValueTypeMap(param.getScriptId());
if (param.getTestItemList().get(1)) { if (param.isCoefficientSelected()) {
initXiManager(param); initXiManager(param);
} }
@@ -1967,9 +1972,6 @@ public class SocketDevResponseService {
if (nonHarmonicList.contains(sourceIssue.getType())) { if (nonHarmonicList.contains(sourceIssue.getType())) {
for (DevData.SqlDataDTO sqlDataDTO : data.getSqlData()) { for (DevData.SqlDataDTO sqlDataDTO : data.getSqlData()) {
if (sqlDataDTO.getDesc().equals("PF")) {
continue;
}
DevData.SqlDataDTO.ListDTO listDTO = sqlDataDTO.getList(); DevData.SqlDataDTO.ListDTO listDTO = sqlDataDTO.getList();
SimAndDigNonHarmonicResult adNonHarmonicResult = new SimAndDigNonHarmonicResult(); SimAndDigNonHarmonicResult adNonHarmonicResult = new SimAndDigNonHarmonicResult();
adNonHarmonicResult.setTimeId(localDateTime); adNonHarmonicResult.setTimeId(localDateTime);

View File

@@ -575,9 +575,6 @@ public class SocketSourceResponseService {
DevPhaseSequenceParam phaseSequenceParam = new DevPhaseSequenceParam(); DevPhaseSequenceParam phaseSequenceParam = new DevPhaseSequenceParam();
// 设置监测点ID列表 // 设置监测点ID列表
phaseSequenceParam.setMoniterIdList(monitorIdList); phaseSequenceParam.setMoniterIdList(monitorIdList);
if (socketDataMsg.getRequestId().equals(SourceOperateCodeEnum.FORMAL_REAL.getValue() + CnSocketUtil.STEP_TAG + "P")) {
comm.add("real$PF");
}
// 设置数据类型列表 // 设置数据类型列表
phaseSequenceParam.setDataType(comm); phaseSequenceParam.setDataType(comm);
// 设置读取次数 // 设置读取次数

View File

@@ -37,11 +37,15 @@ public enum DetectionCodeEnum {
I_UNBAN("I_UNBAN", "三相电流负序不平衡度"), I_UNBAN("I_UNBAN", "三相电流负序不平衡度"),
SeqA("SeqA", "电流序分量"), SeqA("SeqA", "电流序分量"),
PST("PST", "短时间闪变"), PST("PST", "短时间闪变"),
W("W", "有功功率"), W("W", "有功功率"),
VARW("VARW", "无功功率"), VARW("VARW", "无功功率"),
VAW("VAW", "视在功率"), VAW("VAW", "视在功率"),
// PF("PF", "功率因数"), PF("PF", "功率因数"),
// P_FUND("P_FUND", "基波有功功率"), TOTW("TotW", "三相总有功功率"),
TOTVA("TotVA", "三相总视在功率"),
TOTVAR("TotVAr", "三相总无功功率"),
TOTPF("TotPF","三相功率因数"),
// P_FUND("P_FUND", "基波有功功率"),
// P_HVAR("P_HVAR", "基波无功功率"), // P_HVAR("P_HVAR", "基波无功功率"),
// P_HVA("P_HVA", "基波视在功率"), // P_HVA("P_HVA", "基波视在功率"),
I1("I1", "基波电流"), I1("I1", "基波电流"),

View File

@@ -38,10 +38,31 @@ public class ContrastDetectionParam {
@NotEmpty(message = DetectionValidMessage.PAIRS_NOT_EMPTY) @NotEmpty(message = DetectionValidMessage.PAIRS_NOT_EMPTY)
private Map<String, String> pairs; private Map<String, String> pairs;
/** /**
* 检测项列表。第一个元素为预检测、第二个元素为系数校准、第三个元素为正式检测 * 检测项列表。第一个元素为预检测、第二个元素为系数校准、第三个元素为守时校验、第四个元素为正式检测
* 比对场景暂不支持系数校准和守时校验,第二、第三个元素固定为 false。
*/ */
private List<Boolean> testItemList; private List<Boolean> testItemList;
public boolean isPreTestSelected() {
return isTestItemSelected(0);
}
public boolean isCoefficientSelected() {
return isTestItemSelected(1);
}
public boolean isTimeCheckSelected() {
return isTestItemSelected(2);
}
public boolean isFormalTestSelected() {
return isTestItemSelected(3);
}
private boolean isTestItemSelected(int index) {
return testItemList != null && testItemList.size() > index && Boolean.TRUE.equals(testItemList.get(index));
}
private String userId; private String userId;
/** /**

View File

@@ -5,7 +5,6 @@ import lombok.Data;
import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotEmpty;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* @author wr * @author wr
@@ -76,7 +75,27 @@ public class PreDetectionParam {
private Float humidity; private Float humidity;
/** /**
* 检测项列表。第一个元素为预检测、第二个元素为系数校准、第三个元素为正式检测 * 检测项列表。第一个元素为预检测、第二个元素为系数校准、第三个元素为守时校验、第四个元素为正式检测
*/ */
private List<Boolean> testItemList; private List<Boolean> testItemList;
public boolean isPreTestSelected() {
return isTestItemSelected(0);
}
public boolean isCoefficientSelected() {
return isTestItemSelected(1);
}
public boolean isTimeCheckSelected() {
return isTestItemSelected(2);
}
public boolean isFormalTestSelected() {
return isTestItemSelected(3);
}
private boolean isTestItemSelected(int index) {
return testItemList != null && testItemList.size() > index && Boolean.TRUE.equals(testItemList.get(index));
}
} }

View File

@@ -5,6 +5,7 @@ import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.NumberUtil; import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSON;
import com.github.yulichang.wrapper.MPJLambdaWrapper; import com.github.yulichang.wrapper.MPJLambdaWrapper;
import com.njcn.gather.detection.pojo.enums.DetectionCodeEnum; import com.njcn.gather.detection.pojo.enums.DetectionCodeEnum;
@@ -28,6 +29,7 @@ import com.njcn.gather.monitor.service.IPqMonitorService;
import com.njcn.gather.plan.pojo.enums.DataSourceEnum; import com.njcn.gather.plan.pojo.enums.DataSourceEnum;
import com.njcn.gather.report.pojo.enums.PowerIndexEnum; import com.njcn.gather.report.pojo.enums.PowerIndexEnum;
import com.njcn.gather.result.pojo.enums.ResultUnitEnum; import com.njcn.gather.result.pojo.enums.ResultUnitEnum;
import com.njcn.gather.script.mapper.PqScriptMapper;
import com.njcn.gather.script.pojo.po.PqScriptCheckData; import com.njcn.gather.script.pojo.po.PqScriptCheckData;
import com.njcn.gather.script.pojo.po.SourceIssue; import com.njcn.gather.script.pojo.po.SourceIssue;
import com.njcn.gather.script.service.IPqScriptCheckDataService; import com.njcn.gather.script.service.IPqScriptCheckDataService;
@@ -72,6 +74,7 @@ public class DetectionServiceImpl {
private final IPqMonitorService pqMonitorService; private final IPqMonitorService pqMonitorService;
private final IPqDevService pqDevService; private final IPqDevService pqDevService;
private final IPqStandardDevGainRecordService pqStandardDevGainRecordService; private final IPqStandardDevGainRecordService pqStandardDevGainRecordService;
private final PqScriptMapper pqScriptMapper;
public static final String TYPE_A = "A"; public static final String TYPE_A = "A";
public static final String TYPE_B = "B"; public static final String TYPE_B = "B";
@@ -161,9 +164,10 @@ public class DetectionServiceImpl {
SysTestConfig oneConfig = sysTestConfigService.getOneConfig(); SysTestConfig oneConfig = sysTestConfigService.getOneConfig();
List<ErrDtlsCheckDataVO> errDtlsCheckData = pqErrSysDtlsService.listByPqErrSysIdAndTypes(param); List<ErrDtlsCheckDataVO> errDtlsCheckData = pqErrSysDtlsService.listByPqErrSysIdAndTypes(param);
ResultUnitEnum resultUnitEnumByCode = ResultUnitEnum.getResultUnitEnumByCode(sourceIssue.getType()); ResultUnitEnum resultUnitEnumByCode = ResultUnitEnum.getResultUnitEnumByCode(sourceIssue.getType());
if (sourceIssue.getIsP()) { if (StrUtil.isNotBlank(sourceIssue.getOtherType())) {
resultUnitEnumByCode = ResultUnitEnum.P; resultUnitEnumByCode = ResultUnitEnum.getResultUnitEnumByCode(sourceIssue.getOtherType());
} }
switch (resultUnitEnumByCode) { switch (resultUnitEnumByCode) {
/** /**
* 频率 * 频率
@@ -172,6 +176,8 @@ public class DetectionServiceImpl {
return isQualified(dev, devIdMapComm, errDtlsCheckData, F, sourceIssue, dataRule, code, oneConfig.getScale()); return isQualified(dev, devIdMapComm, errDtlsCheckData, F, sourceIssue, dataRule, code, oneConfig.getScale());
case P: case P:
return isQualified(dev, devIdMapComm, errDtlsCheckData, P, sourceIssue, dataRule, code, oneConfig.getScale()); return isQualified(dev, devIdMapComm, errDtlsCheckData, P, sourceIssue, dataRule, code, oneConfig.getScale());
case ANGLE:
return isQualified(dev, devIdMapComm, errDtlsCheckData, resultUnitEnumByCode.getCode(), sourceIssue, dataRule, code, oneConfig.getScale());
/** /**
* 电压 * 电压
*/ */
@@ -414,14 +420,12 @@ public class DetectionServiceImpl {
List<SimAndDigNonHarmonicResult> info = new ArrayList<>(); List<SimAndDigNonHarmonicResult> info = new ArrayList<>();
List<String> devValueTypeList = sourceIssue.getDevValueTypeList(); List<String> devValueTypeList = sourceIssue.getDevValueTypeList();
for (String s : devValueTypeList) { for (String s : devValueTypeList) {
if ((DetectionCodeEnum.REAL_PREFIX.getCode() + "PF").equals(s)) { String[] splitArr = s.split("\\$");
continue;
}
// 根据数据处理规则取值。key为相别value为值列表 // 根据数据处理规则取值。key为相别value为值列表
Map<String, List<Double>> map = devListMap(dev, dataRule, s.split("\\$")[1]); Map<String, List<Double>> map = devListMap(dev, dataRule, splitArr[1]);
List<ErrDtlsCheckDataVO> dtlsCheckData = null; List<ErrDtlsCheckDataVO> dtlsCheckData = null;
if (CollUtil.isNotEmpty(errDtlsCheckData)) { if (CollUtil.isNotEmpty(errDtlsCheckData)) {
dtlsCheckData = errDtlsCheckData.stream().filter(x -> x.getValueTypeCode().equals(s.split("\\$")[1])).collect(Collectors.toList()); dtlsCheckData = errDtlsCheckData.stream().filter(x -> x.getValueTypeCode().equals(splitArr[1])).collect(Collectors.toList());
} }
Double fData = 1.0; Double fData = 1.0;
if (U.equals(type)) { if (U.equals(type)) {
@@ -434,10 +438,18 @@ public class DetectionServiceImpl {
fData = sourceIssue.getFFreq(); fData = sourceIssue.getFFreq();
} }
if (P.equals(type)) { if (P.equals(type)) {
fData = sourceIssue.getFUn() * sourceIssue.getFIn() * 0.01; if (!DetectionCodeEnum.PF.getCode().equals(splitArr[1])) {
Double finalFData = fData; Boolean valueType = pqScriptMapper.selectScriptIsValueType(sourceIssue.getScriptId());
dtlsCheckData.stream().forEach(x -> x.setValue(x.getValue() * finalFData)); if (valueType) {
fData = sourceIssue.getFUn() * sourceIssue.getFIn() / 100;
}
Double finalFData = fData;
dtlsCheckData.stream().forEach(x -> x.setValue(x.getValue() * finalFData));
}
} }
// if (ResultUnitEnum.ANGLE.getCode().equals(type)) {
// fData = null;
// }
SimAndDigNonHarmonicResult result = new SimAndDigNonHarmonicResult(); SimAndDigNonHarmonicResult result = new SimAndDigNonHarmonicResult();
String[] split = dev.get(0).getId().split("_"); String[] split = dev.get(0).getId().split("_");
String devID = devIdMapComm.get(split[0]); String devID = devIdMapComm.get(split[0]);
@@ -476,6 +488,19 @@ public class DetectionServiceImpl {
.collect(Collectors.toList()); .collect(Collectors.toList());
if (CollUtil.isNotEmpty(checkData)) { if (CollUtil.isNotEmpty(checkData)) {
List<Double> phaseValue = map.get(phase); List<Double> phaseValue = map.get(phase);
if (ResultUnitEnum.ANGLE.getCode().equals(type)) {
phaseValue = phaseValue.stream().map(x -> {
// 原始误差值
double originDiff = x - checkData.get(0).getValue();
// 转换误差值
double translateDiff = Math.abs(originDiff) - 360;
if (Math.abs(translateDiff) <= 10) {
return Math.abs(x);
} else {
return x;
}
}).collect(Collectors.toList());
}
// 注意如果map中不存在该phase的键phaseValue可能为null需确保map包含该键 // 注意如果map中不存在该phase的键phaseValue可能为null需确保map包含该键
DetectionData detectionData = rangeComparisonList(phaseValue, isQualified, pqErrSysDtls, fData, checkData.get(0).getValue(), dataRule, scale); DetectionData detectionData = rangeComparisonList(phaseValue, isQualified, pqErrSysDtls, fData, checkData.get(0).getValue(), dataRule, scale);
resultFlag.add(detectionData); resultFlag.add(detectionData);

View File

@@ -3,6 +3,7 @@ package com.njcn.gather.detection.service.impl;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.njcn.common.pojo.exception.BusinessException; import com.njcn.common.pojo.exception.BusinessException;
@@ -310,9 +311,9 @@ public class PreDetectionServiceImpl implements PreDetectionService {
SourceIssue sourceIssue = sourceIssues.get(0); SourceIssue sourceIssue = sourceIssues.get(0);
String type = sourceIssue.getType(); String type = sourceIssue.getType();
if (sourceIssue.getIsP()) { if (StrUtil.isNotBlank(sourceIssue.getOtherType())) {
sourceIssue.setType(ResultUnitEnum.V_ABSOLUTELY.getCode()); sourceIssue.setType(ResultUnitEnum.V_ABSOLUTELY.getCode());
type = ResultUnitEnum.P.getCode(); type = sourceIssue.getOtherType();
} }
List<String> comm = sourceIssue.getDevValueTypeList(); List<String> comm = sourceIssue.getDevValueTypeList();

View File

@@ -67,7 +67,7 @@ public class NettyDevClientHandler extends SimpleChannelInboundHandler<String> {
/** /**
* 实时数据检测超时时间1分钟60秒 * 实时数据检测超时时间1分钟60秒
*/ */
private static final long REALTIME_TIMEOUT = 60L; private static final long REALTIME_TIMEOUT = 60 * 3L;
/** /**
* 暂停操作超时时间10分钟600秒 * 暂停操作超时时间10分钟600秒

View File

@@ -23,6 +23,7 @@ import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@@ -69,10 +70,10 @@ public class PqDevController extends BaseController {
} }
@OperateInfo(operateType = OperateType.ADD) @OperateInfo(operateType = OperateType.ADD)
@PostMapping("/add") @PostMapping(value = "/add", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@ApiOperation("新增被检设备") @ApiOperation("新增被检设备")
@ApiImplicitParam(name = "pqDevParam", value = "被检设备", required = true) @ApiImplicitParam(name = "pqDevParam", value = "被检设备", required = true)
public HttpResult<Boolean> add(@RequestBody @Validated PqDevParam pqDevParam) { public HttpResult<Boolean> add(@ModelAttribute @Validated PqDevParam pqDevParam) {
String methodDescribe = getMethodDescribe("add"); String methodDescribe = getMethodDescribe("add");
LogUtil.njcnDebug(log, "{},新增数据为:{}", methodDescribe, pqDevParam); LogUtil.njcnDebug(log, "{},新增数据为:{}", methodDescribe, pqDevParam);
boolean result = pqDevService.addPqDev(pqDevParam); boolean result = pqDevService.addPqDev(pqDevParam);
@@ -84,10 +85,10 @@ public class PqDevController extends BaseController {
} }
@OperateInfo(operateType = OperateType.UPDATE) @OperateInfo(operateType = OperateType.UPDATE)
@PostMapping("/update") @PostMapping(value = "/update", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@ApiOperation("修改被检设备") @ApiOperation("修改被检设备")
@ApiImplicitParam(name = "updateParam", value = "被检设备", required = true) @ApiImplicitParam(name = "updateParam", value = "被检设备", required = true)
public HttpResult<Boolean> update(@RequestBody @Validated PqDevParam.UpdateParam updateParam) { public HttpResult<Boolean> update(@ModelAttribute @Validated PqDevParam.UpdateParam updateParam) {
String methodDescribe = getMethodDescribe("update"); String methodDescribe = getMethodDescribe("update");
LogUtil.njcnDebug(log, "{},修改数据为:{}", methodDescribe, updateParam); LogUtil.njcnDebug(log, "{},修改数据为:{}", methodDescribe, updateParam);
boolean result = pqDevService.updatePqDev(updateParam); boolean result = pqDevService.updatePqDev(updateParam);
@@ -98,6 +99,14 @@ public class PqDevController extends BaseController {
} }
} }
@OperateInfo(info = LogEnum.BUSINESS_COMMON, operateType = OperateType.DOWNLOAD)
@GetMapping("/image/download")
@ApiOperation("下载设备图片")
@ApiImplicitParam(name = "id", value = "被检设备id", required = true)
public void downloadImage(@RequestParam("id") String id, HttpServletResponse response) {
pqDevService.downloadImage(id, response);
}
@OperateInfo(operateType = OperateType.DELETE) @OperateInfo(operateType = OperateType.DELETE)
@PostMapping("/delete") @PostMapping("/delete")
@ApiOperation("删除被检设备") @ApiOperation("删除被检设备")

View File

@@ -86,8 +86,45 @@
WHERE id = #{planId} WHERE id = #{planId}
</select> </select>
<sql id="DevColumnsNoImage">
dev.Id,
dev.Name,
dev.Pattern,
dev.Dev_Type,
dev.Manufacturer,
dev.Create_Date,
dev.Create_Id,
dev.Hardware_Version,
dev.Software_Version,
dev.Protocol,
dev.IP,
dev.Port,
dev.Encryption_Flag,
dev.Series,
dev.Dev_Key,
dev.Sample_Id,
dev.Arrived_Date,
dev.City_Name,
dev.Gd_Name,
dev.Sub_Name,
dev.Report_Path,
dev.Plan_Id,
dev.Factor_Flag,
dev.Preinvestment_Plan,
dev.Delegate,
dev.Inspect_Channel,
dev.Inspect_Date,
dev.Harm_Sys_Id,
dev.Import_Flag,
dev.State,
dev.Create_By,
dev.Create_Time,
dev.Update_By,
dev.Update_Time
</sql>
<select id="selectByQueryParam" resultType="com.njcn.gather.device.pojo.vo.PqDevVO"> <select id="selectByQueryParam" resultType="com.njcn.gather.device.pojo.vo.PqDevVO">
SELECT dev.*,dev_sub.* FROM pq_dev dev SELECT <include refid="DevColumnsNoImage"/>, dev_sub.* FROM pq_dev dev
left JOIN pq_dev_sub dev_sub ON dev.Id = dev_sub.Dev_Id left JOIN pq_dev_sub dev_sub ON dev.Id = dev_sub.Dev_Id
<where> <where>
dev.state = 1 dev.state = 1
@@ -124,7 +161,7 @@
</select> </select>
<select id="listByDevIds" resultType="com.njcn.gather.device.pojo.vo.PqDevVO"> <select id="listByDevIds" resultType="com.njcn.gather.device.pojo.vo.PqDevVO">
SELECT dev.*, dev_sub.* SELECT <include refid="DevColumnsNoImage"/>, dev_sub.*
FROM pq_dev dev FROM pq_dev dev
left JOIN pq_dev_sub dev_sub ON dev.Id = dev_sub.Dev_Id left JOIN pq_dev_sub dev_sub ON dev.Id = dev_sub.Dev_Id
WHERE dev.state = 1 WHERE dev.state = 1

View File

@@ -9,6 +9,7 @@ import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import org.hibernate.validator.constraints.Range; import org.hibernate.validator.constraints.Range;
import org.springframework.web.multipart.MultipartFile;
import javax.validation.Valid; import javax.validation.Valid;
import javax.validation.constraints.*; import javax.validation.constraints.*;
@@ -136,6 +137,9 @@ public class PqDevParam {
@ApiModelProperty("是否为导入设备") @ApiModelProperty("是否为导入设备")
private Integer importFlag; private Integer importFlag;
@ApiModelProperty("设备图片")
private MultipartFile image;
/** /**
* 更新操作实体 * 更新操作实体
*/ */

View File

@@ -3,6 +3,7 @@ package com.njcn.gather.device.pojo.po;
import com.baomidou.mybatisplus.annotation.FieldFill; import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize;
@@ -184,6 +185,9 @@ public class PqDev extends BaseEntity implements Serializable {
*/ */
private Integer importFlag; private Integer importFlag;
@JsonIgnore
private byte[] image;
/** /**
* 状态0-删除 1-正常 * 状态0-删除 1-正常
*/ */

View File

@@ -122,4 +122,10 @@ public class PqDevVO extends PqDev {
* 检测点结果 * 检测点结果
*/ */
private List<Integer> monitorResults; private List<Integer> monitorResults;
private Boolean hasImage;
private String imageBase64;
private String imageContentType;
} }

View File

@@ -96,6 +96,14 @@ public interface IPqDevService extends IService<PqDev> {
*/ */
PqDevVO getPqDevById(String id); PqDevVO getPqDevById(String id);
/**
* 下载设备图片
*
* @param id 设备id
* @param response 响应
*/
void downloadImage(String id, HttpServletResponse response);
/** /**
* 获取装置信息和装置下监测点信息 * 获取装置信息和装置下监测点信息
* *

View File

@@ -5,6 +5,7 @@ import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.ImportParams; import cn.afterturn.easypoi.excel.entity.ImportParams;
import cn.afterturn.easypoi.excel.entity.result.ExcelImportResult; import cn.afterturn.easypoi.excel.entity.result.ExcelImportResult;
import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
@@ -37,6 +38,7 @@ import com.njcn.gather.device.pojo.po.PqDevSub;
import com.njcn.gather.device.pojo.vo.*; import com.njcn.gather.device.pojo.vo.*;
import com.njcn.gather.device.service.IPqDevService; import com.njcn.gather.device.service.IPqDevService;
import com.njcn.gather.device.service.IPqDevSubService; import com.njcn.gather.device.service.IPqDevSubService;
import com.njcn.gather.device.util.PqDevImageSupport;
import com.njcn.gather.monitor.pojo.po.PqMonitor; import com.njcn.gather.monitor.pojo.po.PqMonitor;
import com.njcn.gather.monitor.pojo.vo.PqMonitorExcel; import com.njcn.gather.monitor.pojo.vo.PqMonitorExcel;
import com.njcn.gather.monitor.service.IPqMonitorService; import com.njcn.gather.monitor.service.IPqMonitorService;
@@ -71,6 +73,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.*; import java.util.*;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -138,7 +141,8 @@ public class PqDevServiceImpl extends ServiceImpl<PqDevMapper, PqDev> implements
this.checkRepeat(pqDevParam, false); this.checkRepeat(pqDevParam, false);
PqDev pqDev = new PqDev(); PqDev pqDev = new PqDev();
BeanUtil.copyProperties(pqDevParam, pqDev); BeanUtil.copyProperties(pqDevParam, pqDev, CopyOptions.create().setIgnoreProperties("image"));
fillImage(pqDev, pqDevParam);
String currrentScene = sysTestConfigService.getCurrrentScene(); String currrentScene = sysTestConfigService.getCurrrentScene();
this.checkParams(pqDev, currrentScene); this.checkParams(pqDev, currrentScene);
@@ -169,6 +173,19 @@ public class PqDevServiceImpl extends ServiceImpl<PqDevMapper, PqDev> implements
return this.save(pqDev); return this.save(pqDev);
} }
private void fillImage(PqDev pqDev, PqDevParam pqDevParam) {
try {
byte[] image = PqDevImageSupport.processUpload(pqDevParam.getImage());
if (image != null) {
pqDev.setImage(image);
}
} catch (IllegalArgumentException e) {
throw new BusinessException(CommonResponseEnum.FAIL, e.getMessage());
} catch (IOException e) {
throw new BusinessException(CommonResponseEnum.FAIL, "图片处理失败");
}
}
/** /**
* 校验参数 * 校验参数
* *
@@ -211,7 +228,8 @@ public class PqDevServiceImpl extends ServiceImpl<PqDevMapper, PqDev> implements
this.checkRepeat(updateParam, true); this.checkRepeat(updateParam, true);
PqDev pqDev = new PqDev(); PqDev pqDev = new PqDev();
BeanUtil.copyProperties(updateParam, pqDev); BeanUtil.copyProperties(updateParam, pqDev, CopyOptions.create().setIgnoreProperties("image"));
fillImage(pqDev, updateParam);
String currrentScene = sysTestConfigService.getCurrrentScene(); String currrentScene = sysTestConfigService.getCurrrentScene();
this.checkParams(pqDev, currrentScene); this.checkParams(pqDev, currrentScene);
@@ -278,6 +296,8 @@ public class PqDevServiceImpl extends ServiceImpl<PqDevMapper, PqDev> implements
@Override @Override
public List<Map<String, Object>> listUnbound(String pattern) { public List<Map<String, Object>> listUnbound(String pattern) {
List<PqDev> pqDevList = this.lambdaQuery() List<PqDev> pqDevList = this.lambdaQuery()
.select(PqDev::getId, PqDev::getName, PqDev::getDevType, PqDev::getManufacturer,
PqDev::getCityName, PqDev::getGdName, PqDev::getSubName)
.eq(PqDev::getPattern, pattern) .eq(PqDev::getPattern, pattern)
.eq(PqDev::getState, DataStateEnum.ENABLE.getCode()) .eq(PqDev::getState, DataStateEnum.ENABLE.getCode())
.isNull(PqDev::getPlanId) .isNull(PqDev::getPlanId)
@@ -392,6 +412,10 @@ public class PqDevServiceImpl extends ServiceImpl<PqDevMapper, PqDev> implements
@Override @Override
public PqDevVO getPqDevById(String id) { public PqDevVO getPqDevById(String id) {
PqDevVO pqDevVO = this.baseMapper.selectByDevId(id); PqDevVO pqDevVO = this.baseMapper.selectByDevId(id);
if (ObjectUtil.isNull(pqDevVO)) {
return null;
}
setImageEchoFields(pqDevVO);
if (StrUtil.isNotBlank(pqDevVO.getSeries())) { if (StrUtil.isNotBlank(pqDevVO.getSeries())) {
pqDevVO.setSeries(EncryptionUtil.decoderString(1, pqDevVO.getSeries())); pqDevVO.setSeries(EncryptionUtil.decoderString(1, pqDevVO.getSeries()));
} }
@@ -420,6 +444,37 @@ public class PqDevServiceImpl extends ServiceImpl<PqDevMapper, PqDev> implements
return pqDevVO; return pqDevVO;
} }
@Override
public void downloadImage(String id, HttpServletResponse response) {
PqDev pqDev = this.getById(id);
if (ObjectUtil.isNull(pqDev) || pqDev.getImage() == null || pqDev.getImage().length == 0) {
throw new BusinessException(CommonResponseEnum.FAIL, "设备图片不存在");
}
String contentType = PqDevImageSupport.detectContentType(pqDev.getImage());
String filename = "device-image-" + id + "." + PqDevImageSupport.getExtension(contentType);
response.setContentType(contentType);
response.setContentLength(pqDev.getImage().length);
response.setHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");
try {
response.getOutputStream().write(pqDev.getImage());
response.flushBuffer();
} catch (IOException e) {
throw new BusinessException(CommonResponseEnum.FAIL, "设备图片下载失败");
}
}
private void setImageEchoFields(PqDevVO pqDevVO) {
try {
PqDevImageSupport.ImageEchoPayload payload = PqDevImageSupport.buildEchoPayload(pqDevVO.getImage());
pqDevVO.setHasImage(payload.isHasImage());
pqDevVO.setImageBase64(payload.getImageBase64());
pqDevVO.setImageContentType(payload.getImageContentType());
pqDevVO.setImage(null);
} catch (IllegalArgumentException e) {
throw new BusinessException(CommonResponseEnum.FAIL, e.getMessage());
}
}
/** /**
* 获取查询条件wrapper * 获取查询条件wrapper
@@ -429,6 +484,7 @@ public class PqDevServiceImpl extends ServiceImpl<PqDevMapper, PqDev> implements
*/ */
private Wrapper getQueryWrapper(PqDevParam.QueryParam queryParam) { private Wrapper getQueryWrapper(PqDevParam.QueryParam queryParam) {
QueryWrapper<PqDev> queryWrapper = new QueryWrapper<>(); QueryWrapper<PqDev> queryWrapper = new QueryWrapper<>();
queryWrapper.select("pq_dev.Id");
if (ObjectUtil.isNotNull(queryParam)) { if (ObjectUtil.isNotNull(queryParam)) {
queryWrapper queryWrapper
.like(StrUtil.isNotBlank(queryParam.getName()), "pq_dev.name", queryParam.getName()) .like(StrUtil.isNotBlank(queryParam.getName()), "pq_dev.name", queryParam.getName())
@@ -1659,7 +1715,10 @@ public class PqDevServiceImpl extends ServiceImpl<PqDevMapper, PqDev> implements
@Override @Override
public Map<String, List<String>> listSelectOptions(String pattern) { public Map<String, List<String>> listSelectOptions(String pattern) {
List<PqDev> pqDevList = this.lambdaQuery().eq(PqDev::getPattern, pattern) List<PqDev> pqDevList = this.lambdaQuery()
.select(PqDev::getCityName, PqDev::getGdName, PqDev::getSubName,
PqDev::getHardwareVersion, PqDev::getSoftwareVersion)
.eq(PqDev::getPattern, pattern)
.eq(PqDev::getState, DataStateEnum.ENABLE.getCode()) .eq(PqDev::getState, DataStateEnum.ENABLE.getCode())
.list(); .list();
String[] devKeyArray = new String[]{"cityName", "gdName", "subName", "hardwareVersion", "softwareVersion"}; String[] devKeyArray = new String[]{"cityName", "gdName", "subName", "hardwareVersion", "softwareVersion"};

View File

@@ -0,0 +1,181 @@
package com.njcn.gather.device.util;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Base64;
import java.util.Iterator;
import java.util.Locale;
public final class PqDevImageSupport {
private static final long MAX_FILE_SIZE = 10L * 1024L * 1024L;
private static final int MAX_EDGE = 1600;
private static final float JPEG_QUALITY = 0.82f;
private PqDevImageSupport() {
}
public static ImageEchoPayload buildEchoPayload(byte[] imageBytes) {
if (imageBytes == null || imageBytes.length == 0) {
return new ImageEchoPayload(false, null, null);
}
return new ImageEchoPayload(true, Base64.getEncoder().encodeToString(imageBytes), detectContentType(imageBytes));
}
public static String detectContentType(byte[] imageBytes) {
if (imageBytes == null || imageBytes.length < 4) {
throw new IllegalArgumentException("图片内容无效");
}
if (isPng(imageBytes)) {
return "image/png";
}
if (isJpeg(imageBytes)) {
return "image/jpeg";
}
throw new IllegalArgumentException("仅支持 JPG、PNG 图片");
}
public static byte[] processUpload(MultipartFile image) throws IOException {
if (image == null || image.isEmpty()) {
return null;
}
if (image.getSize() > MAX_FILE_SIZE) {
throw new IllegalArgumentException("图片大小不能超过 10MB");
}
String normalizedName = normalize(image.getOriginalFilename());
String normalizedContentType = normalize(image.getContentType());
boolean allowedType = normalizedContentType.equals("image/jpeg")
|| normalizedContentType.equals("image/png")
|| normalizedName.endsWith(".jpg")
|| normalizedName.endsWith(".jpeg")
|| normalizedName.endsWith(".png");
if (!allowedType) {
throw new IllegalArgumentException("仅支持 JPG、PNG 图片");
}
byte[] originalBytes = image.getBytes();
String contentType = detectContentType(originalBytes);
BufferedImage source;
try (InputStream inputStream = image.getInputStream()) {
source = ImageIO.read(inputStream);
}
if (source == null) {
throw new IllegalArgumentException("图片内容无效");
}
BufferedImage target = scaleIfNeeded(source);
if ("image/png".equals(contentType)) {
return writePng(target);
}
return writeJpeg(target);
}
public static String getExtension(String contentType) {
return "image/png".equals(contentType) ? "png" : "jpg";
}
private static BufferedImage scaleIfNeeded(BufferedImage source) {
int width = source.getWidth();
int height = source.getHeight();
int max = Math.max(width, height);
if (max <= MAX_EDGE) {
return source;
}
double scale = (double) MAX_EDGE / max;
int targetWidth = Math.max(1, (int) Math.round(width * scale));
int targetHeight = Math.max(1, (int) Math.round(height * scale));
int type = source.getType() == BufferedImage.TYPE_CUSTOM ? BufferedImage.TYPE_INT_ARGB : source.getType();
BufferedImage target = new BufferedImage(targetWidth, targetHeight, type);
Graphics2D graphics = target.createGraphics();
try {
graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
graphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
graphics.drawImage(source, 0, 0, targetWidth, targetHeight, null);
} finally {
graphics.dispose();
}
return target;
}
private static byte[] writePng(BufferedImage image) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ImageIO.write(image, "png", outputStream);
return outputStream.toByteArray();
}
private static byte[] writeJpeg(BufferedImage image) throws IOException {
BufferedImage rgbImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_RGB);
Graphics2D graphics = rgbImage.createGraphics();
try {
graphics.drawImage(image, 0, 0, null);
} finally {
graphics.dispose();
}
Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpg");
if (!writers.hasNext()) {
throw new IOException("未找到 JPG 图片编码器");
}
ImageWriter writer = writers.next();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try (ImageOutputStream imageOutputStream = ImageIO.createImageOutputStream(outputStream)) {
writer.setOutput(imageOutputStream);
ImageWriteParam writeParam = writer.getDefaultWriteParam();
if (writeParam.canWriteCompressed()) {
writeParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
writeParam.setCompressionQuality(JPEG_QUALITY);
}
writer.write(null, new IIOImage(rgbImage, null, null), writeParam);
} finally {
writer.dispose();
}
return outputStream.toByteArray();
}
private static boolean isPng(byte[] imageBytes) {
return imageBytes.length >= 8
&& (imageBytes[0] & 0xFF) == 0x89
&& imageBytes[1] == 0x50
&& imageBytes[2] == 0x4E
&& imageBytes[3] == 0x47
&& imageBytes[4] == 0x0D
&& imageBytes[5] == 0x0A
&& imageBytes[6] == 0x1A
&& imageBytes[7] == 0x0A;
}
private static boolean isJpeg(byte[] imageBytes) {
return imageBytes.length >= 3
&& (imageBytes[0] & 0xFF) == 0xFF
&& (imageBytes[1] & 0xFF) == 0xD8
&& (imageBytes[2] & 0xFF) == 0xFF;
}
private static String normalize(String value) {
return value == null ? "" : value.toLowerCase(Locale.ROOT);
}
@Data
@AllArgsConstructor
public static class ImageEchoPayload {
private boolean hasImage;
private String imageBase64;
private String imageContentType;
}
}

View File

@@ -0,0 +1,68 @@
package com.njcn.gather.report.pojo.constant;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* "指标锚点"新型模板的书签命名空间常量。
* 设计文档docs/superpowers/specs/2026-07-02-item-anchor-report-template-design.md §3.1
*
* @author hongawen
*/
public interface ItemAnchorConstant {
/** 数据锚点书签前缀item_{scriptCode},锚点处按回路插入检测数据表 */
String ITEM_PREFIX = "item_";
/** 区段书签前缀sect_{scriptCode} / sect_{GRP_XXX},指标无数据时整段删除 */
String SECT_PREFIX = "sect_";
/**
* 父节 → 子节 scriptCode 映射;一个父节的全部子节都隐藏时,父节(组标题+组内公共文字)一并隐藏。
* key 与模板中 sect_GRP_* 书签名的 GRP_* 部分一致。
*/
Map<String, List<String>> GRP_CHILDREN = buildGrpChildren();
/**
* 已知指标 scriptCode 白名单(规范大写,即云南模板 11 个锚点)。
* 分流与锚点解析以此为准:白名单外的 item_* 书签一律视为无效,防止老模板中
* 作者手工遗留的同前缀书签(如 item_1把整份报告误路由进新路径。
*/
Set<String> KNOWN_SCRIPT_CODES = buildKnownScriptCodes();
static Map<String, List<String>> buildGrpChildren() {
Map<String, List<String>> map = new LinkedHashMap<>();
map.put("GRP_FUND", Arrays.asList("V", "I", "P", "ANGLE"));
map.put("GRP_UNB", Arrays.asList("IMBV", "IMBA"));
map.put("GRP_HARM", Arrays.asList("HV", "HI", "HP"));
return Collections.unmodifiableMap(map);
}
static Set<String> buildKnownScriptCodes() {
return Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList(
"V", "I", "P", "ANGLE", "FREQ", "F", "IMBV", "IMBA", "HV", "HI", "HP")));
}
/**
* 从书签名解析指标锚点的 scriptCode。
*
* @param bookmarkName 书签名
* @return 规范大写 scriptCodescriptMap / 模板 H5 分组 key 均为大写);
* 非 item_ 前缀、空 code 或不在 {@link #KNOWN_SCRIPT_CODES} 白名单内返回 null
*/
static String resolveItemScriptCode(String bookmarkName) {
if (bookmarkName == null || bookmarkName.length() <= ITEM_PREFIX.length()) {
return null;
}
if (!bookmarkName.substring(0, ITEM_PREFIX.length()).equalsIgnoreCase(ITEM_PREFIX)) {
return null;
}
String code = bookmarkName.substring(ITEM_PREFIX.length()).toUpperCase();
return KNOWN_SCRIPT_CODES.contains(code) ? code : null;
}
}

View File

@@ -26,12 +26,15 @@ public enum BaseReportKeyEnum {
MANUFACTURER("manufacturer","设备厂家、制造厂商"), MANUFACTURER("manufacturer","设备厂家、制造厂商"),
SAMPLE_ID("sampleId","样品编号"), SAMPLE_ID("sampleId","样品编号"),
ARRIVED_DATE("arrivedDate","收样日期"), ARRIVED_DATE("arrivedDate","收样日期"),
ARRIVED_DATE_C("arrivedDateC","收样日期"),
TEST_DATE("testDate","检测日期"), TEST_DATE("testDate","检测日期"),
TEST_DATE_C("testDateC","检测日期"),
INSPECTOR("inspector","检测员"), INSPECTOR("inspector","检测员"),
YEAR("year","年份"), YEAR("year","年份"),
MONTH("month","月份"), MONTH("month","月份"),
DAY("day",""), DAY("day",""),
YEAR_MONTH_DAY("year-month-day","年-月-日"), YEAR_MONTH_DAY("year-month-day","年-月-日"),
YEAR_MONTH_DAY_UP("year-month-day-up","中文数字 年-月-日"),
REPORT_DATE("reportDate","年-月-日"), REPORT_DATE("reportDate","年-月-日"),
GD_NAME("gdName","供电部门"), GD_NAME("gdName","供电部门"),
SUB_NAME("subName","变电站"), SUB_NAME("subName","变电站"),

View File

@@ -45,7 +45,34 @@ public enum ItemReportKeyEnum {
RESULT_B("resultB", "结论 比如:合格/不合格"), RESULT_B("resultB", "结论 比如:合格/不合格"),
RESULT_C("resultC", "结论 比如:合格/不合格"), RESULT_C("resultC", "结论 比如:合格/不合格"),
RESULT_MAG("resultMag", "特征幅值结论 比如:合格/不合格"), RESULT_MAG("resultMag", "特征幅值结论 比如:合格/不合格"),
RESULT_DUR("resultDur", "持续时间结论 比如:合格/不合格"); RESULT_DUR("resultDur", "持续时间结论 比如:合格/不合格"),
// ===== 以下为"指标锚点"新型模板(云南检定报告)新增 key见 docs/superpowers/specs/2026-07-02-item-anchor-report-template-design.md §5 =====
STANDARD_V("standardV", "P表基波电压设置值"),
STANDARD_I("standardI", "P表基波电流设置值"),
STANDARD_U_ANGLE("standardUAngle", "P表电压相角设置值"),
STANDARD_I_ANGLE("standardIAngle", "P表电流相角设置值"),
ACTIVE_POWER("activePower", "P表有功功率实测值三相之和"),
REACTIVE_POWER("reactivePower", "P表无功功率实测值数据侧暂无留空"),
APPARENT_POWER("apparentPower", "P表视在功率实测值数据侧暂无留空"),
POWER_FACTOR("powerFactor", "P表功率因数实测值数据侧暂无留空"),
POS_SEQ("posSeq", "不平衡表:基波正序(数据侧暂无,留空)"),
NEG_SEQ("negSeq", "不平衡表:基波负序(数据侧暂无,留空)"),
ZERO_SEQ("zeroSeq", "不平衡表:基波零序(数据侧暂无,留空)"),
BASE_CURRENT_A("baseCurrentA", "HI表A相基波电流幅值用于计算谐波电流含有率"),
BASE_CURRENT_B("baseCurrentB", "HI表B相基波电流幅值用于计算谐波电流含有率"),
BASE_CURRENT_C("baseCurrentC", "HI表C相基波电流幅值用于计算谐波电流含有率"),
STANDARD_RATE("standardRate", "HI表标准源输出含有率"),
TEST_RATE_A("testRateA", "HI表A相谐波电流含有率"),
TEST_RATE_B("testRateB", "HI表B相谐波电流含有率"),
TEST_RATE_C("testRateC", "HI表C相谐波电流含有率"),
ERROR_RATE_A("errorRateA", "HI表A相谐波电流含有率误差"),
ERROR_RATE_B("errorRateB", "HI表B相谐波电流含有率误差"),
ERROR_RATE_C("errorRateC", "HI表C相谐波电流含有率误差"),
STANDARD_THD("standardThd", "HV/HI表标准源总谐波畸变率"),
THD_A("thdA", "HV/HI表A相总谐波畸变率"),
THD_B("thdB", "HV/HI表B相总谐波畸变率"),
THD_C("thdC", "HV/HI表C相总谐波畸变率");
private String key; private String key;

View File

@@ -18,6 +18,7 @@ public enum PowerIndexEnum {
V("V", "电压"), V("V", "电压"),
I("I", "电流"), I("I", "电流"),
P("P", "功率"), P("P", "功率"),
ANGLE("Angle", "基波相位"),
IMBV("IMBV", "负序电压不平衡度"), IMBV("IMBV", "负序电压不平衡度"),
IMBA("IMBA", "负序电流不平衡度"), IMBA("IMBA", "负序电流不平衡度"),
F("F", "短时电压闪变"), F("F", "短时电压闪变"),

View File

@@ -52,6 +52,7 @@ import com.njcn.gather.plan.service.IAdPlanTestConfigService;
import com.njcn.gather.pojo.enums.DetectionResponseEnum; import com.njcn.gather.pojo.enums.DetectionResponseEnum;
import com.njcn.gather.report.mapper.PqReportMapper; import com.njcn.gather.report.mapper.PqReportMapper;
import com.njcn.gather.report.pojo.DevReportParam; import com.njcn.gather.report.pojo.DevReportParam;
import com.njcn.gather.report.pojo.constant.ItemAnchorConstant;
import com.njcn.gather.report.pojo.constant.PowerConstant; import com.njcn.gather.report.pojo.constant.PowerConstant;
import com.njcn.gather.report.pojo.enums.*; import com.njcn.gather.report.pojo.enums.*;
import com.njcn.gather.report.pojo.param.ReportParam; import com.njcn.gather.report.pojo.param.ReportParam;
@@ -60,6 +61,8 @@ import com.njcn.gather.report.pojo.result.ContrastTestResult;
import com.njcn.gather.report.pojo.result.SingleTestResult; import com.njcn.gather.report.pojo.result.SingleTestResult;
import com.njcn.gather.report.pojo.vo.PqReportVO; import com.njcn.gather.report.pojo.vo.PqReportVO;
import com.njcn.gather.report.service.IPqReportService; import com.njcn.gather.report.service.IPqReportService;
import com.njcn.gather.report.util.ItemAnchorAssembler;
import com.njcn.gather.report.util.ItemAnchorSectionUtil;
import com.njcn.gather.result.service.IResultService; import com.njcn.gather.result.service.IResultService;
import com.njcn.gather.script.pojo.vo.PqScriptDtlDataVO; import com.njcn.gather.script.pojo.vo.PqScriptDtlDataVO;
import com.njcn.gather.script.service.IPqScriptDtlsService; import com.njcn.gather.script.service.IPqScriptDtlsService;
@@ -117,6 +120,7 @@ import java.nio.file.Files;
import java.nio.file.NoSuchFileException; import java.nio.file.NoSuchFileException;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.List; import java.util.List;
@@ -144,6 +148,7 @@ public class PqReportServiceImpl extends ServiceImpl<PqReportMapper, PqReport> i
* 的真实业务取值撞车。 * 的真实业务取值撞车。
*/ */
private static final String RESULT_MAP_NO_DATA_FLAG = "__internal_no_data__"; private static final String RESULT_MAP_NO_DATA_FLAG = "__internal_no_data__";
private static final String[] CHINESE_DIGITS = {"O", "", "", "", "", "", "", "", "", ""};
// @Value("${report.template:D:\\template}") // @Value("${report.template:D:\\template}")
// private String templatePath; // private String templatePath;
@@ -860,6 +865,7 @@ public class PqReportServiceImpl extends ServiceImpl<PqReportMapper, PqReport> i
Docx4jUtil.addWatermarkToDocument(baseModelDocument, "非正式"); Docx4jUtil.addWatermarkToDocument(baseModelDocument, "非正式");
} }
Docx4jUtil.cleanBlankPagesAndRedundantPageBreaks(baseModelDocument); Docx4jUtil.cleanBlankPagesAndRedundantPageBreaks(baseModelDocument);
Docx4jUtil.enableUpdateFieldsOnOpen(baseModelDocument);
baseModelDocument.save(new File(dirPath.concat(File.separator).concat(fileName))); baseModelDocument.save(new File(dirPath.concat(File.separator).concat(fileName)));
this.updateDevAndPlanState(devId, devReportParam.getPlanId()); this.updateDevAndPlanState(devId, devReportParam.getPlanId());
} catch (NoSuchFileException e) { } catch (NoSuchFileException e) {
@@ -926,12 +932,21 @@ public class PqReportServiceImpl extends ServiceImpl<PqReportMapper, PqReport> i
// 获取数据模版页内容,根据脚本动态组装数据页内容 // 获取数据模版页内容,根据脚本动态组装数据页内容
MainDocumentPart detailDocumentPart = detailModelDocument.getMainDocumentPart(); MainDocumentPart detailDocumentPart = detailModelDocument.getMainDocumentPart();
dealDataModelScatteredByBookmark(baseDocumentPart, detailDocumentPart, devReportParam, pqDevVO); // 新旧模板分流base 含白名单内 item_ 前缀书签 → 指标锚点新型模板(云南检定报告类),走独立旁路;
// 否则走原路径,行为与改动前完全一致。设计文档 specs/2026-07-02-item-anchor-report-template-design.md §4.1
// 书签全文档扫描只做一次,分流判断与新路径共用;老路径方法签名不动,内部自扫
List<BookmarkUtil.BookmarkInfo> baseBookmarks = BookmarkUtil.findAllBookmarks(baseDocumentPart);
if (hasItemAnchor(baseBookmarks)) {
dealDataModelByItemAnchor(baseDocumentPart, detailDocumentPart, devReportParam, pqDevVO, baseBookmarks);
} else {
dealDataModelScatteredByBookmark(baseDocumentPart, detailDocumentPart, devReportParam, pqDevVO);
}
// 保存新的文档 // 保存新的文档
String dirPath = pathConfig.getReportPath().concat(File.separator).concat(FilePathSanitizer.toSafeFileName(devType.getName())); String dirPath = pathConfig.getReportPath().concat(File.separator).concat(FilePathSanitizer.toSafeFileName(devType.getName()));
// 确保目录存在 // 确保目录存在
ensureDirectoryExists(dirPath); ensureDirectoryExists(dirPath);
Docx4jUtil.enableUpdateFieldsOnOpen(baseModelDocument);
baseModelDocument.save(new File(dirPath.concat(File.separator).concat(pqDevVO.getCreateId()).concat(ReportConstant.DOCX))); baseModelDocument.save(new File(dirPath.concat(File.separator).concat(pqDevVO.getCreateId()).concat(ReportConstant.DOCX)));
this.updateDevAndPlanState(devId, devReportParam.getPlanId()); this.updateDevAndPlanState(devId, devReportParam.getPlanId());
} catch (NoSuchFileException e) { } catch (NoSuchFileException e) {
@@ -1229,6 +1244,131 @@ public class PqReportServiceImpl extends ServiceImpl<PqReportMapper, PqReport> i
} }
/**
* 判断 base 文档是否为"指标锚点"新型模板(含任一白名单内 item_ 前缀书签)。
* 白名单校验在 resolveItemScriptCode 内:老模板中手工遗留的同前缀书签(如 item_1
* 不会触发新路径,避免整份报告错路由导致 DATA_LINE / TEST_RESULT_* 全不填。
*
* @param bookmarks base 文档书签集合(调用方已扫,避免全文档重复扫描)
*/
private boolean hasItemAnchor(List<BookmarkUtil.BookmarkInfo> bookmarks) {
for (BookmarkUtil.BookmarkInfo info : bookmarks) {
if (ItemAnchorConstant.resolveItemScriptCode(info.bookmark.getName()) != null) {
return true;
}
}
return false;
}
/**
* "指标锚点"新型模板装配(云南检定报告类)。
* 流程:逐 item_* 锚点 → detail 池取 H5 模板表 → 按回路构造成品表插入锚点;
* 无数据/无模板的指标记入隐藏集合 → 区段删除 → 第 7 节编号重排 → 残留书签清理。
* 设计文档docs/superpowers/specs/2026-07-02-item-anchor-report-template-design.md §4.2
*/
private void dealDataModelByItemAnchor(MainDocumentPart baseDocumentPart, MainDocumentPart detailDocumentPart,
DevReportParam devReportParam, PqDevVO pqDevVO,
List<BookmarkUtil.BookmarkInfo> bookmarks) {
ObjectFactory factory = new ObjectFactory();
// 检测脚本明细按 scriptCode 分组
List<PqScriptDtlDataVO> pqScriptDtlsList = pqScriptDtlsService.getScriptDtlsDataList(devReportParam.getScriptId());
Map<String, List<PqScriptDtlDataVO>> scriptMap = pqScriptDtlsList.stream()
.collect(Collectors.groupingBy(item -> itemAnchorGroupKey(item.getScriptCode()), LinkedHashMap::new, Collectors.toList()));
// detail 模板池按 H5 分组
List<Docx4jUtil.HeadingContent> headingContents = Docx4jUtil.extractHeading5Contents(detailDocumentPart.getContent(), detailDocumentPart);
Map<String, List<Docx4jUtil.HeadingContent>> contentMap = headingContents.stream()
.collect(Collectors.groupingBy(item -> itemAnchorGroupKey(item.getHeadingText()), LinkedHashMap::new, Collectors.toList()));
int devChns = pqDevVO.getDevChns() == null ? 1 : pqDevVO.getDevChns();
Set<String> hiddenCodes = new LinkedHashSet<>();
for (BookmarkUtil.BookmarkInfo info : bookmarks) {
String scriptCode = ItemAnchorConstant.resolveItemScriptCode(info.bookmark.getName());
if (scriptCode == null) {
// 新路径按设计忽略老锚点互斥分流spec §4.1);发现 BookmarkEnum 残留提示模板可能混用
if (Objects.nonNull(BookmarkEnum.getByKey(info.bookmark.getName()))) {
log.warn("指标锚点模板中发现老模板锚点书签 {},新路径不处理该书签", info.bookmark.getName());
}
continue;
}
List<PqScriptDtlDataVO> itemDtls = scriptMap.get(scriptCode);
List<Docx4jUtil.HeadingContent> tpl = contentMap.get(scriptCode);
if (CollUtil.isEmpty(itemDtls) || CollUtil.isEmpty(tpl)) {
log.warn("指标锚点 {} 无脚本数据或无模板分组,整节隐藏", info.bookmark.getName());
hiddenCodes.add(scriptCode);
continue;
}
List<String> tableKeys = ItemAnchorAssembler.extractTableKeys(tpl);
// 有脚本明细且模板分组在keys 仍取不到属模板错误 → 显性中断,不得静默走"未测→整节隐藏"
ItemAnchorAssembler.requireTableKeys(scriptCode, tableKeys);
List<Object> todoInsertList = new ArrayList<>();
for (int line = 0; line < devChns; line++) {
List<SingleTestResult> results = new ArrayList<>();
if (PowerConstant.TIME.contains(scriptCode)) {
// 谐波类:一个 scriptIndex 一张表(与老路径语义一致)
Map<Integer, List<PqScriptDtlDataVO>> scriptIndexMap = itemDtls.stream()
.collect(Collectors.groupingBy(PqScriptDtlDataVO::getScriptIndex, TreeMap::new, Collectors.toList()));
for (List<PqScriptDtlDataVO> indexItem : scriptIndexMap.values()) {
SingleTestResult single = resultService.getFinalContent(indexItem, devReportParam.getPlanCode(), pqDevVO.getId(), line + 1, tableKeys);
if (Objects.nonNull(single)) {
results.add(single);
}
}
} else {
SingleTestResult single = resultService.getFinalContent(itemDtls, devReportParam.getPlanCode(), pqDevVO.getId(), line + 1, tableKeys);
if (Objects.nonNull(single)) {
results.add(single);
}
}
List<Object> lineTables = ItemAnchorAssembler.buildTables(scriptCode, results, tpl.get(0), tableKeys, factory);
if (lineTables.isEmpty()) {
// 该回路无数据,跳过;是否整节隐藏由所有回路汇总后判断
continue;
}
// 回路标题:单回路省略;多回路"测量回路{n}"从 1 起spec §4.2
if (devChns > 1) {
todoInsertList.add(buildItemLineTitle(contentMap, line, factory));
}
todoInsertList.addAll(lineTables);
}
if (todoInsertList.isEmpty()) {
log.warn("指标 {} 所有回路均无检测数据,整节隐藏", scriptCode);
hiddenCodes.add(scriptCode);
continue;
}
BookmarkUtil.insertElement(info, todoInsertList);
// 已填充节不再需要 item/sect 标记:空锚点段直接删除,避免表格前残留空行;
// 若未来模板把锚点放进非空段,则只清书签保留段落正文。
if (!ItemAnchorSectionUtil.removeEmptyItemAnchorParagraph(info)) {
BookmarkUtil.removeBookmark(info);
}
}
ItemAnchorSectionUtil.removeSectRanges(baseDocumentPart, hiddenCodes);
ItemAnchorSectionUtil.renumberChapter7(baseDocumentPart);
ItemAnchorSectionUtil.stripItemAnchorBookmarks(baseDocumentPart);
}
private static String itemAnchorGroupKey(String value) {
return value == null ? "" : value.trim().toUpperCase(Locale.ROOT);
}
/**
* 新型模板的回路标题(仅多回路时被调用):"测量回路{n}"n 从 1 起。
* 与老路径 getLineTitle 的差异(有意):不带 "n." 前缀、单回路由调用方直接省略。
*/
private P buildItemLineTitle(Map<String, List<Docx4jUtil.HeadingContent>> contentMap, int lineIndex, ObjectFactory factory) {
int lineNo = lineIndex + 1;
P titleParagraph = factory.createP();
PPr pPr = factory.createPPr();
PPrBase.Spacing spacing = factory.createPPrBaseSpacing();
spacing.setBefore(BigInteger.valueOf(60));
spacing.setAfter(BigInteger.valueOf(20));
spacing.setLine(BigInteger.valueOf(240));
spacing.setLineRule(STLineSpacingRule.AUTO);
pPr.setSpacing(spacing);
titleParagraph.setPPr(pPr);
Docx4jUtil.createTitle(factory, titleParagraph, "测量回路" + lineNo, 24, true);
return titleParagraph;
}
/** /**
* 通过提前在模板文档里埋下书签 * 通过提前在模板文档里埋下书签
* 1、目录信息 * 1、目录信息
@@ -2168,14 +2308,17 @@ public class PqReportServiceImpl extends ServiceImpl<PqReportMapper, PqReport> i
baseModelMap.put(BaseReportKeyEnum.SAMPLE_ID.getKey(), StrUtil.isEmpty(pqDevVO.getSampleId()) ? StrPool.TAB : pqDevVO.getSampleId()); baseModelMap.put(BaseReportKeyEnum.SAMPLE_ID.getKey(), StrUtil.isEmpty(pqDevVO.getSampleId()) ? StrPool.TAB : pqDevVO.getSampleId());
// 收样日期 // 收样日期
baseModelMap.put(BaseReportKeyEnum.ARRIVED_DATE.getKey(), Objects.isNull(pqDevVO.getArrivedDate()) ? StrPool.TAB : String.valueOf(pqDevVO.getArrivedDate())); baseModelMap.put(BaseReportKeyEnum.ARRIVED_DATE.getKey(), Objects.isNull(pqDevVO.getArrivedDate()) ? StrPool.TAB : String.valueOf(pqDevVO.getArrivedDate()));
baseModelMap.put(BaseReportKeyEnum.ARRIVED_DATE_C.getKey(), Objects.isNull(pqDevVO.getArrivedDate()) ? StrPool.TAB : formatNumberChineseDate(pqDevVO.getArrivedDate()));
// 检测日期 // 检测日期
baseModelMap.put(BaseReportKeyEnum.TEST_DATE.getKey(), Objects.isNull(pqDevVO.getCheckEndTime()) ? StrPool.TAB : String.valueOf(pqDevVO.getCheckEndTime()).substring(0, 10)); baseModelMap.put(BaseReportKeyEnum.TEST_DATE.getKey(), Objects.isNull(pqDevVO.getCheckEndTime()) ? StrPool.TAB : String.valueOf(pqDevVO.getCheckEndTime()).substring(0, 10));
baseModelMap.put(BaseReportKeyEnum.TEST_DATE_C.getKey(), Objects.isNull(pqDevVO.getCheckEndTime()) ? StrPool.TAB : formatNumberChineseDate(pqDevVO.getCheckEndTime().toLocalDate()));
baseModelMap.put(BaseReportKeyEnum.TEMPERATURE.getKey(), Objects.isNull(pqDevVO.getTemperature()) ? StrPool.TAB : pqDevVO.getTemperature().toString()); baseModelMap.put(BaseReportKeyEnum.TEMPERATURE.getKey(), Objects.isNull(pqDevVO.getTemperature()) ? StrPool.TAB : pqDevVO.getTemperature().toString());
baseModelMap.put(BaseReportKeyEnum.HUMIDITY.getKey(), Objects.isNull(pqDevVO.getHumidity()) ? StrPool.TAB : pqDevVO.getHumidity().toString()); baseModelMap.put(BaseReportKeyEnum.HUMIDITY.getKey(), Objects.isNull(pqDevVO.getHumidity()) ? StrPool.TAB : pqDevVO.getHumidity().toString());
baseModelMap.put(BaseReportKeyEnum.YEAR.getKey(), DateUtil.format(new Date(), DatePattern.NORM_YEAR_PATTERN)); baseModelMap.put(BaseReportKeyEnum.YEAR.getKey(), DateUtil.format(new Date(), DatePattern.NORM_YEAR_PATTERN));
baseModelMap.put(BaseReportKeyEnum.MONTH.getKey(), DateUtil.format(new Date(), DatePattern.SIMPLE_MONTH_PATTERN).substring(4)); baseModelMap.put(BaseReportKeyEnum.MONTH.getKey(), DateUtil.format(new Date(), DatePattern.SIMPLE_MONTH_PATTERN).substring(4));
baseModelMap.put(BaseReportKeyEnum.DAY.getKey(), DateUtil.format(new Date(), DatePattern.PURE_DATE_PATTERN).substring(6)); baseModelMap.put(BaseReportKeyEnum.DAY.getKey(), DateUtil.format(new Date(), DatePattern.PURE_DATE_PATTERN).substring(6));
baseModelMap.put(BaseReportKeyEnum.YEAR_MONTH_DAY.getKey(), DateUtil.format(new Date(), DatePattern.NORM_DATE_PATTERN)); baseModelMap.put(BaseReportKeyEnum.YEAR_MONTH_DAY.getKey(), DateUtil.format(new Date(), DatePattern.NORM_DATE_PATTERN));
baseModelMap.put(BaseReportKeyEnum.YEAR_MONTH_DAY_UP.getKey(), formatChineseReportDate(LocalDate.now()));
baseModelMap.put(BaseReportKeyEnum.REPORT_DATE.getKey(), DateUtil.format(new Date(), DatePattern.CHINESE_DATE_PATTERN)); baseModelMap.put(BaseReportKeyEnum.REPORT_DATE.getKey(), DateUtil.format(new Date(), DatePattern.CHINESE_DATE_PATTERN));
return baseModelMap; return baseModelMap;
} }
@@ -2480,6 +2623,40 @@ public class PqReportServiceImpl extends ServiceImpl<PqReportMapper, PqReport> i
return String.format("%.4f", bp.setScale(i, RoundingMode.HALF_UP).doubleValue()); return String.format("%.4f", bp.setScale(i, RoundingMode.HALF_UP).doubleValue());
} }
static String formatChineseReportDate(LocalDate date) {
Objects.requireNonNull(date, "date");
return formatChineseYear(date.getYear()) + ""
+ formatChineseMonthDayNumber(date.getMonthValue()) + ""
+ formatChineseMonthDayNumber(date.getDayOfMonth()) + "";
}
static String formatNumberChineseDate(LocalDate date) {
Objects.requireNonNull(date, "date");
return date.getYear() + "" + date.getMonthValue() + "" + date.getDayOfMonth() + "";
}
private static String formatChineseYear(int year) {
String yearValue = String.valueOf(year);
StringBuilder builder = new StringBuilder(yearValue.length());
for (int i = 0; i < yearValue.length(); i++) {
builder.append(CHINESE_DIGITS[yearValue.charAt(i) - '0']);
}
return builder.toString();
}
private static String formatChineseMonthDayNumber(int value) {
if (value < 10) {
return CHINESE_DIGITS[value];
}
if (value == 10) {
return "";
}
if (value < 20) {
return "" + CHINESE_DIGITS[value % 10];
}
return CHINESE_DIGITS[value / 10] + "" + (value % 10 == 0 ? "" : CHINESE_DIGITS[value % 10]);
}
private void ensureDirectoryExists(String directoryPath) { private void ensureDirectoryExists(String directoryPath) {
File directory = new File(directoryPath); File directory = new File(directoryPath);

View File

@@ -0,0 +1,532 @@
package com.njcn.gather.report.util;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.report.pojo.enums.ItemReportKeyEnum;
import com.njcn.gather.report.pojo.result.SingleTestResult;
import com.njcn.gather.tools.report.util.Docx4jUtil;
import org.docx4j.wml.ObjectFactory;
import org.docx4j.wml.P;
import org.docx4j.wml.R;
import org.docx4j.wml.RPr;
import org.docx4j.wml.Tbl;
import org.docx4j.wml.Tc;
import org.docx4j.wml.TcPr;
import org.docx4j.wml.Text;
import org.docx4j.wml.Tr;
import org.docx4j.wml.TrPr;
import javax.xml.bind.JAXBElement;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/**
* "指标锚点"模板的表格装配器:模板表(末行为 key 行)+ SingleTestResult → 成品表列表。
* 只服务新路径,不被老路径引用;对 Docx4jUtil 只调用不修改。
* 设计文档docs/superpowers/specs/2026-07-02-item-anchor-report-template-design.md §4.2 / §6
*
* @author hongawen
*/
public final class ItemAnchorAssembler {
private ItemAnchorAssembler() {
}
/**
* 构造成品表。results 每个有效元素产出一张表(谐波类调用方按 scriptIndex 拆分后传入)。
*
* @param scriptCode 指标编码P 走有功功率特例映射)
* @param results 检测结果列表null/空 detail 的元素跳过
* @param template detail 模板池中该指标的 H5 分组(取其中第一张表为模板)
* @param tableKeys key 行的占位符列表(调用方经 Docx4jUtil.getTableFillKeys 读出)
* @param factory docx4j 工厂
* @return 待插入锚点的表格元素列表JAXBElement&lt;Tbl&gt;
*/
@SuppressWarnings("unchecked")
public static List<Object> buildTables(String scriptCode, List<SingleTestResult> results,
Docx4jUtil.HeadingContent template, List<String> tableKeys,
ObjectFactory factory) {
List<Object> out = new ArrayList<>();
JAXBElement<Tbl> templateTbl = findFirstTable(template);
if (templateTbl == null || CollUtil.isEmpty(results) || CollUtil.isEmpty(tableKeys)) {
return out;
}
int tableNo = 0;
for (SingleTestResult result : results) {
List<Map<String, String>> rows = flattenRows(result, scriptCode);
if (rows.isEmpty()) {
continue;
}
tableNo++;
applyHarmonicDerivedValues(rows, scriptCode);
Map<String, String> fixedPlaceholders = buildHarmonicFixedPlaceholders(rows, scriptCode);
JAXBElement<Tbl> copied;
try {
copied = Docx4jUtil.deepCopyTbl(templateTbl);
} catch (Exception e) {
throw new RuntimeException("指标锚点模板表深拷贝失败: " + scriptCode, e);
}
Tbl tbl = copied.getValue();
replaceFixedPlaceholders(tbl, fixedPlaceholders);
applyHarmonicPowerHeader(tbl, scriptCode, tableNo, rows);
List<Object> trs = tbl.getContent();
// 模板规范约定:最后一行是 key 行(不走 judgeTableCross新路径自带定位依据
// 从尾部找最后一个真正的 Tr——Word 重存可能把 bookmarkStart/End 漏成 w:tbl 直接子节点(本仓 _GoBack 先例)
Tr keyRow = findLastRow(trs);
if (keyRow == null) {
throw new BusinessException("指标 " + scriptCode + " 的模板表无有效行,请检查 detail 模板该分组表格");
}
TrPr trPr = keyRow.getTrPr();
List<TcPr> tcPrList = new ArrayList<>();
RPr templateRPr = null;
for (Object cellObj : keyRow.getContent()) {
if (cellObj instanceof JAXBElement) {
Tc cell = ((JAXBElement<Tc>) cellObj).getValue();
TcPr tcPr = cell.getTcPr() != null ? cell.getTcPr() : factory.createTcPr();
// 注意:不覆写 tcW老 fill 的 5000/n 均分会破坏客户模板列宽),沿用模板原宽
tcPrList.add(tcPr);
if (templateRPr == null && !cell.getContent().isEmpty() && cell.getContent().get(0) instanceof P) {
templateRPr = Docx4jUtil.getTcPrFromParagraph((P) cell.getContent().get(0));
}
}
}
trs.remove(keyRow);
for (Map<String, String> row : rows) {
// 缺失 key 一律补空串:"拿不到的先空着"spec §6 #2
for (String key : tableKeys) {
row.putIfAbsent(key, "");
}
Tr newRow = Docx4jUtil.createCustomRow(factory, row, tableKeys, trPr, tcPrList, templateRPr, true);
// Docx4jUtil.createCustomRow 产出的单元格是裸 Tc未包 JAXBElement
// 与模板行(真实 docx4j 解析结果 / deepCopy 而来)里统一的 JAXBElement<Tc> 不一致;
// 这里做一次归一化包装,避免下游按 JAXBElement 读取该行时 ClassCastException不改 createCustomRow 本身)
List<Object> normalized = new ArrayList<>();
for (Object cellObj : newRow.getContent()) {
if (cellObj instanceof Tc) {
normalized.add(factory.createTrTc((Tc) cellObj));
} else {
normalized.add(cellObj);
}
}
newRow.getContent().clear();
newRow.getContent().addAll(normalized);
trs.add(newRow);
}
out.add(copied);
}
// 多表(谐波类多 scriptIndex背靠背插入时OOXML 相邻 w:tbl 会被 Word 合并渲染成一张大表;
// 只插入空段落做结构分隔。测点号属于 HP 表内标题单元格,不能作为表外可见段落。
if (out.size() > 1) {
List<Object> withSeparators = new ArrayList<>(out.size() * 2 - 1);
for (int i = 0; i < out.size(); i++) {
withSeparators.add(out.get(i));
if (i < out.size() - 1) {
withSeparators.add(factory.createP());
}
}
return withSeparators;
}
return out;
}
/**
* 校验模板 key 行占位符已成功取出。调用方仅在"该指标有脚本明细且模板分组存在"时调用:
* 此时 keys 为空属模板错误key 行缺失或表结构不符合 getTableFillKeys 的识别规则),
* 必须显性中断报告生成,不得静默走"未测→整节隐藏"(否则有数据的节凭空消失,违反红线 2
*/
public static void requireTableKeys(String scriptCode, List<String> tableKeys) {
if (CollUtil.isEmpty(tableKeys)) {
throw new BusinessException("指标 " + scriptCode + " 的模板表 key 行占位符解析为空,请检查 detail 模板该分组的表格(表末行应为占位符 key 行),已中断报告生成");
}
}
/**
* 指标锚点模板专用 key 读取:按 spec 直接取 H5 分组第一张表的最后一个真实 Tr。
* 不复用旧 getTableFillKeys 的横/纵表启发式,避免 HV/HI/HP 标题区末格为数值时被误判。
*/
public static List<String> extractTableKeys(List<Docx4jUtil.HeadingContent> tempContent) {
List<String> keys = new ArrayList<>();
if (CollUtil.isEmpty(tempContent)) {
return keys;
}
JAXBElement<Tbl> table = findFirstTable(tempContent.get(0));
if (table == null) {
return keys;
}
Tr keyRow = findLastRow(table.getValue().getContent());
if (keyRow == null) {
return keys;
}
for (Object cellObj : keyRow.getContent()) {
Object value = cellObj instanceof JAXBElement ? ((JAXBElement<?>) cellObj).getValue() : cellObj;
if (value instanceof Tc) {
keys.add(Docx4jUtil.getTextFromCell((Tc) value));
}
}
return keys;
}
/** 从尾部向前找最后一个真正的 Tr含 JAXBElement 解包);无 Tr 返回 null */
private static Tr findLastRow(List<Object> content) {
for (int i = content.size() - 1; i >= 0; i--) {
Object v = content.get(i) instanceof JAXBElement
? ((JAXBElement<?>) content.get(i)).getValue() : content.get(i);
if (v instanceof Tr) {
return (Tr) v;
}
}
return null;
}
/**
* 把 SingleTestResult 的三层嵌套 detail 摊平成行列表,并做新模板专属的行级补充:
* errorScope 注入HV/HI/HP 的标准限值列、time 缺失时"测点{n}"兜底F/IMBV/IMBA、P 有功特例。
*/
static List<Map<String, String>> flattenRows(SingleTestResult result, String scriptCode) {
List<Map<String, String>> out = new ArrayList<>();
if (result == null || CollUtil.isEmpty(result.getDetail())) {
return out;
}
int seq = 0;
for (Map.Entry<String, List<Map<String, List<Map<String, String>>>>> influenceEntry : result.getDetail().entrySet()) {
if (influenceEntry.getValue() == null) {
continue;
}
for (Map<String, List<Map<String, String>>> byError : influenceEntry.getValue()) {
if (byError == null) {
continue;
}
for (Map.Entry<String, List<Map<String, String>>> errorEntry : byError.entrySet()) {
String errorScope = errorEntry.getKey();
if (errorEntry.getValue() == null) {
continue;
}
for (Map<String, String> raw : errorEntry.getValue()) {
seq++;
Map<String, String> row = new LinkedHashMap<>(raw);
applyErrorScope(row, scriptCode, errorScope);
if (StrUtil.isBlank(row.get(ItemReportKeyEnum.TIME.getKey()))) {
row.put(ItemReportKeyEnum.TIME.getKey(), "测点" + seq);
}
if ("P".equalsIgnoreCase(scriptCode)) {
applyPowerMapping(row);
}
out.add(row);
}
}
}
}
return out;
}
private static void applyErrorScope(Map<String, String> row, String scriptCode, String errorScope) {
String key = ItemReportKeyEnum.ERROR_SCOPE.getKey();
if ("HV".equalsIgnoreCase(scriptCode) || "HP".equalsIgnoreCase(scriptCode)) {
String value = StrUtil.isBlank(row.get(key)) ? errorScope : row.get(key);
row.put(key, stripTrailingUnit(value));
return;
}
row.putIfAbsent(key, errorScope);
}
private static String stripTrailingUnit(String value) {
if (StrUtil.isBlank(value)) {
return value;
}
return value.trim().replaceFirst("\\s*(?:(?:[%%‰°℃]|[a-zA-Z]+|[\\u4e00-\\u9fa5]+)\\s*)+$", "");
}
/**
* P有功功率特例activePower = 三相实测之和;设置类与无功/视在/功率因数数据侧暂无,留空。
* spec §6 #2 处置。
*/
static void applyPowerMapping(Map<String, String> row) {
BigDecimal sum = BigDecimal.ZERO;
boolean hasValue = false;
for (String key : Arrays.asList(ItemReportKeyEnum.TEST_A.getKey(),
ItemReportKeyEnum.TEST_B.getKey(), ItemReportKeyEnum.TEST_C.getKey())) {
String value = row.get(key);
if (StrUtil.isNotBlank(value)) {
try {
sum = sum.add(new BigDecimal(value.trim()));
hasValue = true;
} catch (NumberFormatException ignored) {
// 非数值(如"--")不参与求和
}
}
}
String activePower = row.get(ItemReportKeyEnum.ACTIVE_POWER.getKey());
if (hasValue && (StrUtil.isBlank(activePower) || "/".equals(activePower))) {
row.put(ItemReportKeyEnum.ACTIVE_POWER.getKey(), sum.toPlainString());
}
}
private static void applyHarmonicDerivedValues(List<Map<String, String>> rows, String scriptCode) {
if (!"HI".equalsIgnoreCase(scriptCode)) {
return;
}
for (Map<String, String> row : rows) {
String scriptStandardRate = row.get(ItemReportKeyEnum.STANDARD_RATE.getKey());
String standardRate = parseNumber(scriptStandardRate) == null ? "" : scriptStandardRate;
String testRateA = rate(row.get(ItemReportKeyEnum.TEST_A.getKey()), row.get(ItemReportKeyEnum.BASE_CURRENT_A.getKey()));
String testRateB = rate(row.get(ItemReportKeyEnum.TEST_B.getKey()), row.get(ItemReportKeyEnum.BASE_CURRENT_B.getKey()));
String testRateC = rate(row.get(ItemReportKeyEnum.TEST_C.getKey()), row.get(ItemReportKeyEnum.BASE_CURRENT_C.getKey()));
String errorRateA = difference(testRateA, standardRate);
String errorRateB = difference(testRateB, standardRate);
String errorRateC = difference(testRateC, standardRate);
row.put(ItemReportKeyEnum.STANDARD_RATE.getKey(), standardRate);
row.put(ItemReportKeyEnum.TEST_RATE_A.getKey(), testRateA);
row.put(ItemReportKeyEnum.TEST_RATE_B.getKey(), testRateB);
row.put(ItemReportKeyEnum.TEST_RATE_C.getKey(), testRateC);
row.put(ItemReportKeyEnum.ERROR_RATE_A.getKey(), errorRateA);
row.put(ItemReportKeyEnum.ERROR_RATE_B.getKey(), errorRateB);
row.put(ItemReportKeyEnum.ERROR_RATE_C.getKey(), errorRateC);
row.put(ItemReportKeyEnum.ERROR_SCOPE.getKey(), harmonicCurrentErrorScope(standardRate));
}
}
private static String harmonicCurrentErrorScope(String standardRateText) {
Double standardRate = parseNumber(standardRateText);
if (standardRate == null) {
return "";
}
double limit = standardRate >= 3D ? standardRate * 0.05D : 0.15D;
return "±" + format4(limit);
}
private static Map<String, String> buildHarmonicFixedPlaceholders(List<Map<String, String>> rows, String scriptCode) {
Map<String, String> placeholders = new LinkedHashMap<>();
if ("HV".equalsIgnoreCase(scriptCode)) {
placeholders.put(ItemReportKeyEnum.STANDARD_THD.getKey(), thd(rows, ItemReportKeyEnum.STANDARD.getKey()));
placeholders.put(ItemReportKeyEnum.THD_A.getKey(), thd(rows, ItemReportKeyEnum.TEST_A.getKey()));
placeholders.put(ItemReportKeyEnum.THD_B.getKey(), thd(rows, ItemReportKeyEnum.TEST_B.getKey()));
placeholders.put(ItemReportKeyEnum.THD_C.getKey(), thd(rows, ItemReportKeyEnum.TEST_C.getKey()));
} else if ("HI".equalsIgnoreCase(scriptCode)) {
placeholders.put(ItemReportKeyEnum.STANDARD_THD.getKey(), thd(rows, ItemReportKeyEnum.STANDARD_RATE.getKey()));
placeholders.put(ItemReportKeyEnum.THD_A.getKey(), thd(rows, ItemReportKeyEnum.TEST_RATE_A.getKey()));
placeholders.put(ItemReportKeyEnum.THD_B.getKey(), thd(rows, ItemReportKeyEnum.TEST_RATE_B.getKey()));
placeholders.put(ItemReportKeyEnum.THD_C.getKey(), thd(rows, ItemReportKeyEnum.TEST_RATE_C.getKey()));
}
return placeholders;
}
private static void applyHarmonicPowerHeader(Tbl tbl, String scriptCode, int pointNo, List<Map<String, String>> rows) {
if (!"HP".equalsIgnoreCase(scriptCode)) {
return;
}
Tr firstRow = findFirstRow(tbl.getContent());
if (firstRow == null) {
return;
}
Tc titleCell = cellAt(firstRow, 0);
if (titleCell == null) {
return;
}
String title = Docx4jUtil.getTextFromCell(titleCell);
String baseTitle = StrUtil.blankToDefault(title, StrUtil.EMPTY)
.replaceFirst("(测点\\s*\\d+$", "");
setCellText(titleCell, baseTitle + "(测点" + pointNo + "");
fillNextCellByLabel(tbl, "基波电压", firstDataValue(rows, ItemReportKeyEnum.STANDARD_V.getKey()));
fillNextCellByLabel(tbl, "基波电流", firstDataValue(rows, ItemReportKeyEnum.STANDARD_I.getKey()));
}
private static String firstDataValue(List<Map<String, String>> rows, String key) {
if (CollUtil.isEmpty(rows)) {
return "";
}
for (Map<String, String> row : rows) {
String value = row.get(key);
if (StrUtil.isNotBlank(value) && !"/".equals(value.trim())) {
return value;
}
}
return "";
}
private static void fillNextCellByLabel(Tbl tbl, String label, String value) {
if (StrUtil.isBlank(value) || tbl == null || tbl.getContent() == null) {
return;
}
for (Object o : tbl.getContent()) {
Object v = o instanceof JAXBElement ? ((JAXBElement<?>) o).getValue() : o;
if (v instanceof Tr && fillNextCellByLabel((Tr) v, label, value)) {
return;
}
}
}
private static boolean fillNextCellByLabel(Tr row, String label, String value) {
if (StrUtil.isBlank(value) || row == null || row.getContent() == null) {
return false;
}
List<Object> cells = row.getContent();
for (int i = 0; i < cells.size() - 1; i++) {
Tc labelCell = cellAt(row, i);
if (labelCell != null && Docx4jUtil.getTextFromCell(labelCell).contains(label)) {
Tc valueCell = cellAt(row, i + 1);
if (valueCell != null) {
setCellText(valueCell, value);
}
return true;
}
}
return false;
}
private static Tr findFirstRow(List<Object> content) {
for (Object o : content) {
Object v = o instanceof JAXBElement ? ((JAXBElement<?>) o).getValue() : o;
if (v instanceof Tr) {
return (Tr) v;
}
}
return null;
}
private static Tc cellAt(Tr row, int index) {
if (row == null || row.getContent() == null || row.getContent().size() <= index) {
return null;
}
Object cellObj = row.getContent().get(index);
Object value = cellObj instanceof JAXBElement ? ((JAXBElement<?>) cellObj).getValue() : cellObj;
return value instanceof Tc ? (Tc) value : null;
}
private static String rate(String numeratorText, String denominatorText) {
Double numerator = parseNumber(numeratorText);
Double denominator = parseNumber(denominatorText);
if (numerator == null || denominator == null || denominator == 0D) {
return "";
}
return format4(numerator / denominator * 100D);
}
private static String difference(String minuendText, String subtrahendText) {
Double minuend = parseNumber(minuendText);
Double subtrahend = parseNumber(subtrahendText);
if (minuend == null || subtrahend == null) {
return "";
}
return format4(minuend - subtrahend);
}
private static String thd(List<Map<String, String>> rows, String key) {
double sum = 0D;
boolean hasValue = false;
for (Map<String, String> row : rows) {
Double value = parseNumber(row.get(key));
if (value != null) {
sum += value * value;
hasValue = true;
}
}
return hasValue ? format4(Math.sqrt(sum)) : "";
}
private static Double parseNumber(String text) {
if (StrUtil.isBlank(text) || "/".equals(text.trim()) || "--".equals(text.trim())) {
return null;
}
try {
return Double.parseDouble(text.trim());
} catch (NumberFormatException ignored) {
return null;
}
}
private static String format4(double value) {
return String.format(Locale.ROOT, "%.4f", value);
}
private static void replaceFixedPlaceholders(Tbl tbl, Map<String, String> values) {
if (values.isEmpty()) {
return;
}
for (Object rowObj : tbl.getContent()) {
Object rowValue = rowObj instanceof JAXBElement ? ((JAXBElement<?>) rowObj).getValue() : rowObj;
if (!(rowValue instanceof Tr)) {
continue;
}
for (Object cellObj : ((Tr) rowValue).getContent()) {
Object cellValue = cellObj instanceof JAXBElement ? ((JAXBElement<?>) cellObj).getValue() : cellObj;
if (cellValue instanceof Tc) {
Tc cell = (Tc) cellValue;
String replacement = values.get(Docx4jUtil.getTextFromCell(cell));
if (replacement != null) {
setCellText(cell, replacement);
}
}
}
}
}
private static void setCellText(Tc cell, String value) {
P firstParagraph = null;
for (Object content : cell.getContent()) {
if (content instanceof P) {
firstParagraph = (P) content;
break;
}
}
if (firstParagraph == null) {
firstParagraph = new P();
cell.getContent().add(firstParagraph);
}
boolean written = false;
for (Object paragraphObj : cell.getContent()) {
if (!(paragraphObj instanceof P)) {
continue;
}
P paragraph = (P) paragraphObj;
for (Object runObj : paragraph.getContent()) {
if (!(runObj instanceof R)) {
continue;
}
R run = (R) runObj;
for (Object textObj : run.getContent()) {
Text text = unwrapText(textObj);
if (text != null) {
text.setValue(written ? "" : value);
written = true;
}
}
}
}
if (!written) {
R run = new R();
Text text = new Text();
text.setValue(value);
run.getContent().add(text);
firstParagraph.getContent().add(run);
}
}
private static Text unwrapText(Object textObj) {
if (textObj instanceof Text) {
return (Text) textObj;
}
if (textObj instanceof JAXBElement && ((JAXBElement<?>) textObj).getValue() instanceof Text) {
return (Text) ((JAXBElement<?>) textObj).getValue();
}
return null;
}
@SuppressWarnings("unchecked")
private static JAXBElement<Tbl> findFirstTable(Docx4jUtil.HeadingContent template) {
if (template == null || CollUtil.isEmpty(template.getSubContent())) {
return null;
}
for (Object o : template.getSubContent()) {
if (o instanceof JAXBElement && ((JAXBElement<?>) o).getValue() instanceof Tbl) {
return (JAXBElement<Tbl>) o;
}
}
return null;
}
}

View File

@@ -0,0 +1,276 @@
package com.njcn.gather.report.util;
import com.njcn.gather.report.pojo.constant.ItemAnchorConstant;
import com.njcn.gather.tools.report.util.BookmarkUtil;
import com.njcn.gather.tools.report.util.Docx4jUtil;
import lombok.extern.slf4j.Slf4j;
import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart;
import org.docx4j.wml.CTBookmark;
import org.docx4j.wml.CTMarkupRange;
import org.docx4j.wml.P;
import org.docx4j.wml.R;
import org.docx4j.wml.Text;
import javax.xml.bind.JAXBElement;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* "指标锚点"模板的区段隐藏 / 编号重排 / 书签清理工具。
* 只服务新路径 dealDataModelByItemAnchor不被老路径引用。
* 设计文档docs/superpowers/specs/2026-07-02-item-anchor-report-template-design.md §4.2
*
* @author hongawen
*/
@Slf4j
public final class ItemAnchorSectionUtil {
private ItemAnchorSectionUtil() {
}
/** 三级编号7.x.y半角点 */
private static final Pattern LEVEL3 = Pattern.compile("^7\\.(\\d+)\\.(\\d+)");
/** 二级编号7.x半角点排除 7.x.y */
private static final Pattern LEVEL2 = Pattern.compile("^7\\.(\\d+)(?![\\.\\d])");
/**
* 删除已填充 item_* 的空锚点段,避免插入表格前残留一行空段落。
* 只处理指标锚点新路径的白名单 item_*;段落内存在正文时保留,防止模板内容被误删。
*/
public static boolean removeEmptyItemAnchorParagraph(BookmarkUtil.BookmarkInfo info) {
if (info == null || info.bookmark == null || info.parentParagraph == null || info.parentContainer == null) {
return false;
}
if (ItemAnchorConstant.resolveItemScriptCode(info.bookmark.getName()) == null) {
return false;
}
String text = Docx4jUtil.getTextFromP(info.parentParagraph);
if (text != null && !text.trim().isEmpty()) {
return false;
}
return info.parentContainer.getContent().remove(info.parentParagraph);
}
/**
* 删除隐藏指标对应的 sect_{code} 区段body 块级整段删除)。
* GRP 折叠规则GRP_CHILDREN 中某组的子节全部在 hiddenCodes 中时,删父节区段(含组标题与组内公共文字),
* 不再单删子节;父区段定位失败时回退按子节各自删除,避免组和子节一个都删不掉(静默违反红线 2
*/
public static void removeSectRanges(MainDocumentPart part, Set<String> hiddenCodes) {
if (part == null || hiddenCodes == null || hiddenCodes.isEmpty()) {
return;
}
List<Object> body = part.getContent();
// 单遍扫描收集全部书签区段,后续按名 O(1) 取用
Map<String, int[]> allRanges = locateRanges(body);
List<int[]> ranges = new ArrayList<>();
Set<String> coveredByGrp = new HashSet<>();
for (Map.Entry<String, List<String>> entry : ItemAnchorConstant.GRP_CHILDREN.entrySet()) {
if (!hiddenCodes.containsAll(entry.getValue())) {
continue;
}
int[] grpRange = allRanges.get((ItemAnchorConstant.SECT_PREFIX + entry.getKey()).toLowerCase());
if (grpRange != null) {
ranges.add(grpRange);
coveredByGrp.addAll(entry.getValue());
} else {
log.warn("父区段书签 {}{} 定位失败,回退按子节区段各自删除,请检查模板书签是否完整",
ItemAnchorConstant.SECT_PREFIX, entry.getKey());
}
}
for (String code : hiddenCodes) {
if (coveredByGrp.contains(code)) {
continue;
}
int[] range = allRanges.get((ItemAnchorConstant.SECT_PREFIX + code).toLowerCase());
if (range != null) {
ranges.add(range);
} else {
log.warn("区段书签 {}{} 定位失败,该节无法隐藏,请检查模板书签是否完整",
ItemAnchorConstant.SECT_PREFIX, code);
}
}
// 起点降序删除,避免前面的删除让后面的索引失效
ranges.sort((a, b) -> Integer.compare(b[0], a[0]));
for (int[] range : ranges) {
for (int i = range[1]; i >= range[0]; i--) {
if (i < body.size()) {
body.remove(i);
}
}
}
}
/**
* 清理全文残留的 item_ / sect_ 书签start 与配对的 end其他书签不动。
* 已填充节的 sect end 会被 BookmarkUtil.removeBookmark 顺带清掉,留下无 end 的 start本方法兜底清理。
*/
public static void stripItemAnchorBookmarks(MainDocumentPart part) {
if (part == null) {
return;
}
Set<BigInteger> ourIds = new HashSet<>();
// 第一遍:删 start 并记 id
for (Object block : part.getContent()) {
P p = asP(block);
if (p == null) {
continue;
}
Iterator<Object> it = p.getContent().iterator();
while (it.hasNext()) {
Object v = unwrap(it.next());
if (v instanceof CTBookmark) {
String name = ((CTBookmark) v).getName();
if (isOurNamespace(name)) {
ourIds.add(((CTBookmark) v).getId());
it.remove();
}
}
}
}
// 第二遍:删配对 end
for (Object block : part.getContent()) {
P p = asP(block);
if (p == null) {
continue;
}
Iterator<Object> it = p.getContent().iterator();
while (it.hasNext()) {
Object v = unwrap(it.next());
if (v instanceof CTMarkupRange && !(v instanceof CTBookmark)
&& ourIds.contains(((CTMarkupRange) v).getId())) {
it.remove();
}
}
}
}
/**
* 第 7 节编号重排:区段删除后,把剩余的 7.x / 7.x.y 标题按出现顺序重写为连续编号。
* 顶级"7准确度检查"用全角点,正则天然不匹配,保持不动。
*/
public static void renumberChapter7(MainDocumentPart part) {
if (part == null) {
return;
}
int l2 = 0;
int l3 = 0;
for (Object block : part.getContent()) {
P p = asP(block);
if (p == null) {
continue;
}
String text = Docx4jUtil.getTextFromP(p);
if (text == null) {
continue;
}
Matcher m3 = LEVEL3.matcher(text);
if (m3.find()) {
l3++;
String newPrefix = "7." + l2 + "." + l3;
if (!m3.group().equals(newPrefix)) {
replacePrefix(p, m3.group().length(), newPrefix);
}
continue;
}
Matcher m2 = LEVEL2.matcher(text);
if (m2.find()) {
l2++;
l3 = 0;
String newPrefix = "7." + l2;
if (!m2.group().equals(newPrefix)) {
replacePrefix(p, m2.group().length(), newPrefix);
}
}
}
}
/**
* 单遍扫描 body收集全部段落内书签区段书签名小写→ [start块下标, end块下标]。
* 同名书签只记首个(与逐名定位"取第一处命中"语义一致);无配对 end 的 start 不产出条目。
*/
private static Map<String, int[]> locateRanges(List<Object> body) {
Map<String, int[]> out = new HashMap<>();
Map<BigInteger, String> openNames = new HashMap<>();
Map<BigInteger, Integer> openStarts = new HashMap<>();
for (int i = 0; i < body.size(); i++) {
P p = asP(body.get(i));
if (p == null) {
continue;
}
for (Object o : p.getContent()) {
Object v = unwrap(o);
// CTBookmark 继承 CTMarkupRange先判 startelse 分支天然排除 CTBookmark即 bookmarkEnd
if (v instanceof CTBookmark) {
CTBookmark start = (CTBookmark) v;
if (start.getName() != null && start.getId() != null) {
openNames.put(start.getId(), start.getName().toLowerCase());
openStarts.put(start.getId(), i);
}
} else if (v instanceof CTMarkupRange) {
BigInteger id = ((CTMarkupRange) v).getId();
String name = openNames.remove(id);
if (name != null) {
out.putIfAbsent(name, new int[]{openStarts.remove(id), i});
}
}
}
}
return out;
}
private static boolean isOurNamespace(String name) {
if (name == null) {
return false;
}
String lower = name.toLowerCase();
return lower.startsWith(ItemAnchorConstant.ITEM_PREFIX) || lower.startsWith(ItemAnchorConstant.SECT_PREFIX);
}
private static Object unwrap(Object o) {
return (o instanceof JAXBElement) ? ((JAXBElement<?>) o).getValue() : o;
}
private static P asP(Object block) {
Object v = unwrap(block);
return (v instanceof P) ? (P) v : null;
}
/** 跨 run 替换段首前 oldLen 个字符为 newPrefix编号可能被 Word 拆进多个 text 节点) */
private static void replacePrefix(P p, int oldLen, String newPrefix) {
List<Text> texts = new ArrayList<>();
collectTexts(p.getContent(), texts);
int remaining = oldLen;
boolean inserted = false;
for (Text t : texts) {
if (remaining <= 0) {
break;
}
String value = t.getValue() == null ? "" : t.getValue();
int cut = Math.min(remaining, value.length());
remaining -= cut;
String rest = value.substring(cut);
t.setValue(inserted ? rest : newPrefix + rest);
inserted = true;
}
}
private static void collectTexts(List<Object> content, List<Text> out) {
for (Object o : content) {
Object v = unwrap(o);
if (v instanceof R) {
collectTexts(((R) v).getContent(), out);
} else if (v instanceof Text) {
out.add((Text) v);
}
}
}
}

View File

@@ -26,6 +26,7 @@ public enum ResultUnitEnum {
IMBA("%", "电流不平衡度","IMBA"), IMBA("%", "电流不平衡度","IMBA"),
F("","闪变","F"), F("","闪变","F"),
P("W", "功率","P"), P("W", "功率","P"),
ANGLE("°", "相角","Angle"),
UNKNOWN("","未知","UNKNOWN"), UNKNOWN("","未知","UNKNOWN"),
; ;

View File

@@ -64,6 +64,9 @@ import com.njcn.gather.result.pojo.param.ResultParam;
import com.njcn.gather.result.pojo.vo.*; import com.njcn.gather.result.pojo.vo.*;
import com.njcn.gather.result.service.DataCheckAsyncNotifier; import com.njcn.gather.result.service.DataCheckAsyncNotifier;
import com.njcn.gather.result.service.IResultService; import com.njcn.gather.result.service.IResultService;
import com.njcn.gather.result.util.AngleResultAssembler;
import com.njcn.gather.result.util.PowerResultAssembler;
import com.njcn.gather.result.util.UnbalanceResultAssembler;
import com.njcn.gather.script.mapper.PqScriptMapper; import com.njcn.gather.script.mapper.PqScriptMapper;
import com.njcn.gather.script.pojo.param.PqScriptCheckDataParam; import com.njcn.gather.script.pojo.param.PqScriptCheckDataParam;
import com.njcn.gather.script.pojo.param.PqScriptIssueParam; import com.njcn.gather.script.pojo.param.PqScriptIssueParam;
@@ -536,6 +539,15 @@ public class ResultServiceImpl implements IResultService {
private void ratedScriptTypeName(List<PqScriptDtls> scriptDtlIndexList, Boolean isValueType, TreeDataVO dtlType, DictTree dictTree) { private void ratedScriptTypeName(List<PqScriptDtls> scriptDtlIndexList, Boolean isValueType, TreeDataVO dtlType, DictTree dictTree) {
ResultUnitEnum resultUnitEnumByCode = ResultUnitEnum.getResultUnitEnumByCode(dictTree.getCode()); ResultUnitEnum resultUnitEnumByCode = ResultUnitEnum.getResultUnitEnumByCode(dictTree.getCode());
String unitV;
String unitI;
if (isValueType) {
unitV = ResultUnitEnum.V_RELATIVE.getUnit();
unitI = ResultUnitEnum.I_RELATIVE.getUnit();
} else {
unitV = ResultUnitEnum.V_ABSOLUTELY.getUnit();
unitI = ResultUnitEnum.I_ABSOLUTELY.getUnit();
}
switch (resultUnitEnumByCode) { switch (resultUnitEnumByCode) {
/** /**
* 频率 * 频率
@@ -545,15 +557,6 @@ public class ResultServiceImpl implements IResultService {
dtlType.setScriptTypeName(dictTree.getName() + "=" + f.get(0).getValue() + ResultUnitEnum.FREQ.getUnit()); dtlType.setScriptTypeName(dictTree.getName() + "=" + f.get(0).getValue() + ResultUnitEnum.FREQ.getUnit());
break; break;
case P: { case P: {
String unitV;
String unitI;
if (isValueType) {
unitV = ResultUnitEnum.V_RELATIVE.getUnit();
unitI = ResultUnitEnum.I_RELATIVE.getUnit();
} else {
unitV = ResultUnitEnum.V_ABSOLUTELY.getUnit();
unitI = ResultUnitEnum.I_ABSOLUTELY.getUnit();
}
List<PqScriptDtls> v = scriptDtlIndexList.stream().filter(x -> "VOL".equals(x.getValueType())).collect(Collectors.toList()); List<PqScriptDtls> v = scriptDtlIndexList.stream().filter(x -> "VOL".equals(x.getValueType())).collect(Collectors.toList());
List<PqScriptDtls> i = scriptDtlIndexList.stream().filter(x -> "CUR".equals(x.getValueType())).collect(Collectors.toList()); List<PqScriptDtls> i = scriptDtlIndexList.stream().filter(x -> "CUR".equals(x.getValueType())).collect(Collectors.toList());
List<PqScriptDtls> aDtl1 = scriptDtlIndexList.stream().filter(x -> "A".equals(x.getPhase()) && "VOL".equals(x.getValueType())).collect(Collectors.toList()); List<PqScriptDtls> aDtl1 = scriptDtlIndexList.stream().filter(x -> "A".equals(x.getPhase()) && "VOL".equals(x.getValueType())).collect(Collectors.toList());
@@ -572,26 +575,14 @@ public class ResultServiceImpl implements IResultService {
*/ */
case V_RELATIVE: case V_RELATIVE:
case V_ABSOLUTELY: case V_ABSOLUTELY:
String unit;
if (isValueType) {
unit = ResultUnitEnum.V_RELATIVE.getUnit();
} else {
unit = ResultUnitEnum.V_ABSOLUTELY.getUnit();
}
List<PqScriptDtls> v = scriptDtlIndexList.stream().filter(x -> "VOL".equals(x.getValueType())).collect(Collectors.toList()); List<PqScriptDtls> v = scriptDtlIndexList.stream().filter(x -> "VOL".equals(x.getValueType())).collect(Collectors.toList());
dtlType.setScriptTypeName(dictTree.getName() + "=" + v.get(0).getValue() + unit); dtlType.setScriptTypeName(dictTree.getName() + "=" + v.get(0).getValue() + unitV);
break; break;
/** /**
* 电流 * 电流
*/ */
case I_RELATIVE: case I_RELATIVE:
case I_ABSOLUTELY: case I_ABSOLUTELY:
String unitI;
if (isValueType) {
unitI = ResultUnitEnum.I_RELATIVE.getUnit();
} else {
unitI = ResultUnitEnum.I_ABSOLUTELY.getUnit();
}
List<PqScriptDtls> I = scriptDtlIndexList.stream().filter(x -> "CUR".equals(x.getValueType())).collect(Collectors.toList()); List<PqScriptDtls> I = scriptDtlIndexList.stream().filter(x -> "CUR".equals(x.getValueType())).collect(Collectors.toList());
dtlType.setScriptTypeName(dictTree.getName() + "=" + I.get(0).getValue() + unitI); dtlType.setScriptTypeName(dictTree.getName() + "=" + I.get(0).getValue() + unitI);
break; break;
@@ -745,7 +736,36 @@ public class ResultServiceImpl implements IResultService {
*/ */
case VOLTAGE_MAG: case VOLTAGE_MAG:
case VOLTAGE_DUR: case VOLTAGE_DUR:
break; case ANGLE: {
List<PqScriptDtls> aDtlV = scriptDtlIndexList.stream().filter(x -> "A".equals(x.getPhase()) && "VOL".equals(x.getValueType())).collect(Collectors.toList());
List<PqScriptDtls> aDtlI = scriptDtlIndexList.stream().filter(x -> "A".equals(x.getPhase()) && "CUR".equals(x.getValueType())).collect(Collectors.toList());
List<PqScriptDtls> bDtlV = scriptDtlIndexList.stream().filter(x -> "B".equals(x.getPhase()) && "VOL".equals(x.getValueType())).collect(Collectors.toList());
List<PqScriptDtls> bDtlI = scriptDtlIndexList.stream().filter(x -> "B".equals(x.getPhase()) && "CUR".equals(x.getValueType())).collect(Collectors.toList());
List<PqScriptDtls> cDtlV = scriptDtlIndexList.stream().filter(x -> "C".equals(x.getPhase()) && "VOL".equals(x.getValueType())).collect(Collectors.toList());
List<PqScriptDtls> cDtlI = scriptDtlIndexList.stream().filter(x -> "C".equals(x.getPhase()) && "CUR".equals(x.getValueType())).collect(Collectors.toList());
StringBuilder scriptTypeName = new StringBuilder();
if (CollUtil.isNotEmpty(aDtlV)) {
scriptTypeName.append("A相电压相角=" + aDtlV.get(0).getAngle() + ResultUnitEnum.ANGLE.getUnit());
}
if (CollUtil.isNotEmpty(aDtlI)) {
scriptTypeName.append(",电流相角=" + aDtlI.get(0).getAngle() + ResultUnitEnum.ANGLE.getUnit());
}
if (CollUtil.isNotEmpty(bDtlV)) {
scriptTypeName.append(";B相电压相角=" + bDtlV.get(0).getAngle() + ResultUnitEnum.ANGLE.getUnit());
}
if (CollUtil.isNotEmpty(bDtlI)) {
scriptTypeName.append(",电流相角=" + bDtlI.get(0).getAngle() + ResultUnitEnum.ANGLE.getUnit());
}
if (CollUtil.isNotEmpty(cDtlV)) {
scriptTypeName.append(";C相电压相角=" + cDtlV.get(0).getAngle() + ResultUnitEnum.ANGLE.getUnit());
}
if (CollUtil.isNotEmpty(cDtlI)) {
scriptTypeName.append(",电流相角=" + cDtlI.get(0).getAngle() + ResultUnitEnum.ANGLE.getUnit());
}
dtlType.setScriptTypeName(scriptTypeName.toString());
}
break;
default: default:
break; break;
} }
@@ -1451,6 +1471,55 @@ public class ResultServiceImpl implements IResultService {
List<Integer> indexList = scriptDtlDataVOList.stream().map(PqScriptDtlDataVO::getScriptIndex).distinct().collect(Collectors.toList()); List<Integer> indexList = scriptDtlDataVOList.stream().map(PqScriptDtlDataVO::getScriptIndex).distinct().collect(Collectors.toList());
List<PqScriptCheckData> scriptCheckDataList = pqScriptCheckDataService.listCheckData(scriptId, indexList); List<PqScriptCheckData> scriptCheckDataList = pqScriptCheckDataService.listCheckData(scriptId, indexList);
List<String> valueTypeList = scriptCheckDataList.stream().map(PqScriptCheckData::getValueType).distinct().collect(Collectors.toList()); List<String> valueTypeList = scriptCheckDataList.stream().map(PqScriptCheckData::getValueType).distinct().collect(Collectors.toList());
if (PowerIndexEnum.P.getKey().equalsIgnoreCase(scriptCode)) {
SingleNonHarmParam param = new SingleNonHarmParam(planCode, devId, lineNo, valueTypeList, indexList);
List<SimAndDigNonHarmonicResult> powerResults = simAndDigNonHarmonicService.queryBySortListWithAdTypeCode(param);
List<Map<String, String>> keyFillMapList = PowerResultAssembler.assembleRows(powerResults, scriptDtlDataVOList);
if (CollUtil.isEmpty(keyFillMapList)) {
log.warn("生成功率类表格数据跳过结果表数据为空planCode={}, devId={}, lineNo={}, scriptCode={}, scriptIndexList={}",
planCode, devId, lineNo, scriptCode, indexList);
return;
}
Map<String, List<Map<String, String>>> errorScoperMap = keyFillMapList.stream()
.collect(Collectors.groupingBy(map -> map.get(ItemReportKeyEnum.ERROR_SCOPE.getKey()), LinkedHashMap::new, Collectors.toList()));
List<Map<String, List<Map<String, String>>>> errorList = new ArrayList<>();
errorList.add(errorScoperMap);
finalContent.put(affectName, errorList);
return;
}
if (PowerIndexEnum.ANGLE.getKey().equalsIgnoreCase(scriptCode)) {
SingleNonHarmParam param = new SingleNonHarmParam(planCode, devId, lineNo, valueTypeList, indexList);
List<SimAndDigNonHarmonicResult> angleResults = simAndDigNonHarmonicService.queryBySortListWithAdTypeCode(param);
List<Map<String, String>> keyFillMapList = AngleResultAssembler.assembleRows(angleResults);
if (CollUtil.isEmpty(keyFillMapList)) {
log.warn("生成基波相位类表格数据跳过结果表数据为空planCode={}, devId={}, lineNo={}, scriptCode={}, scriptIndexList={}",
planCode, devId, lineNo, scriptCode, indexList);
return;
}
Map<String, List<Map<String, String>>> errorScoperMap = keyFillMapList.stream()
.collect(Collectors.groupingBy(map -> map.get(ItemReportKeyEnum.ERROR_SCOPE.getKey()), LinkedHashMap::new, Collectors.toList()));
List<Map<String, List<Map<String, String>>>> errorList = new ArrayList<>();
errorList.add(errorScoperMap);
finalContent.put(affectName, errorList);
return;
}
if (PowerIndexEnum.IMBV.getKey().equalsIgnoreCase(scriptCode)
|| PowerIndexEnum.IMBA.getKey().equalsIgnoreCase(scriptCode)) {
SingleNonHarmParam param = new SingleNonHarmParam(planCode, devId, lineNo, valueTypeList, indexList);
List<SimAndDigNonHarmonicResult> unbalanceResults = simAndDigNonHarmonicService.queryBySortListWithAdTypeCode(param);
List<Map<String, String>> keyFillMapList = UnbalanceResultAssembler.assembleRows(unbalanceResults);
if (CollUtil.isEmpty(keyFillMapList)) {
log.warn("生成不平衡度类表格数据跳过结果表数据为空planCode={}, devId={}, lineNo={}, scriptCode={}, scriptIndexList={}",
planCode, devId, lineNo, scriptCode, indexList);
return;
}
Map<String, List<Map<String, String>>> errorScoperMap = keyFillMapList.stream()
.collect(Collectors.groupingBy(map -> map.get(ItemReportKeyEnum.ERROR_SCOPE.getKey()), LinkedHashMap::new, Collectors.toList()));
List<Map<String, List<Map<String, String>>>> errorList = new ArrayList<>();
errorList.add(errorScoperMap);
finalContent.put(affectName, errorList);
return;
}
// 稍作区分暂态的脚本同一个scriptIndex可以有两个指标其余脚本每个checkData的valueType对应一个指标结果 // 稍作区分暂态的脚本同一个scriptIndex可以有两个指标其余脚本每个checkData的valueType对应一个指标结果
if (PowerConstant.VOLTAGE.equalsIgnoreCase(scriptCode)) { if (PowerConstant.VOLTAGE.equalsIgnoreCase(scriptCode)) {
// 暂态的valueType通常只有2个一个特征幅值一个持续时间 // 暂态的valueType通常只有2个一个特征幅值一个持续时间
@@ -1517,6 +1586,8 @@ public class ResultServiceImpl implements IResultService {
int timeInt = (int) timeDouble; int timeInt = (int) timeDouble;
// 填充结果数据 // 填充结果数据
if (fillThreePhaseData(singleResult, timeInt, keyFillMap, scriptCode)) { if (fillThreePhaseData(singleResult, timeInt, keyFillMap, scriptCode)) {
fillHarmonicScriptStandard(keyFillMap, scriptCode, dtlsList);
fillHarmonicPowerSourceSettings(keyFillMap, scriptCode, scriptDtlDataVOList);
keyFillMapList.add(keyFillMap); keyFillMapList.add(keyFillMap);
} }
}); });
@@ -1719,9 +1790,100 @@ public class ResultServiceImpl implements IResultService {
keyFillMap.put(ItemReportKeyEnum.RESULT.getKey(), result); keyFillMap.put(ItemReportKeyEnum.RESULT.getKey(), result);
errorScope = dealErrorScope(errorScope).concat(unit); errorScope = dealErrorScope(errorScope).concat(unit);
keyFillMap.put(ItemReportKeyEnum.ERROR_SCOPE.getKey(), errorScope); keyFillMap.put(ItemReportKeyEnum.ERROR_SCOPE.getKey(), errorScope);
if (PowerIndexEnum.HI.getKey().equalsIgnoreCase(scriptCode)) {
fillBaseCurrentData(singleResult, keyFillMap);
}
return true; return true;
} }
/**
* HI 新模板明细显示谐波电流含有率,装配期需要同一条谐波结果中的三相基波电流作为分母。
*/
private void fillBaseCurrentData(Object singleResult, Map<String, String> keyFillMap) {
putBaseCurrent(singleResult, PowerConstant.PHASE_A, ItemReportKeyEnum.BASE_CURRENT_A.getKey(), keyFillMap);
putBaseCurrent(singleResult, PowerConstant.PHASE_B, ItemReportKeyEnum.BASE_CURRENT_B.getKey(), keyFillMap);
putBaseCurrent(singleResult, PowerConstant.PHASE_C, ItemReportKeyEnum.BASE_CURRENT_C.getKey(), keyFillMap);
}
private void putBaseCurrent(Object singleResult, String phase, String testKey, Map<String, String> keyFillMap) {
DetectionData base = getResultData(singleResult, 1, phase);
keyFillMap.put(testKey, formatBaseCurrent(base == null ? null : base.getData()));
}
private String formatBaseCurrent(Double value) {
String formatted = value == null ? "" : PubUtils.doubleRoundStr(4, value);
return formatted == null ? "" : formatted;
}
/**
* HI 的标准源输出值在云南模板中展示含有率,优先沿用脚本下发的百分比值。
*/
private void fillHarmonicScriptStandard(Map<String, String> keyFillMap, String scriptCode, List<PqScriptCheckData> checkDataList) {
if (!PowerIndexEnum.HI.getKey().equalsIgnoreCase(scriptCode) || CollUtil.isEmpty(checkDataList)) {
return;
}
if (StrUtil.isNotBlank(keyFillMap.get(ItemReportKeyEnum.STANDARD_RATE.getKey()))) {
return;
}
PqScriptCheckData standard = firstCheckValue(checkDataList);
if (standard == null || standard.getValue() == null) {
return;
}
String value = PubUtils.doubleRoundStr(4, standard.getValue());
if (value != null) {
keyFillMap.put(ItemReportKeyEnum.STANDARD_RATE.getKey(), value);
}
}
private PqScriptCheckData firstCheckValue(List<PqScriptCheckData> checkDataList) {
for (String phase : Arrays.asList(PowerConstant.PHASE_A, PowerConstant.PHASE_B, PowerConstant.PHASE_C)) {
for (PqScriptCheckData data : checkDataList) {
if (phase.equalsIgnoreCase(data.getPhase()) && data.getValue() != null) {
return data;
}
}
}
return checkDataList.stream().filter(data -> data.getValue() != null).findFirst().orElse(null);
}
/**
* HP 新模板表头需要基波电压/基波电流设置值,来源是同一 scriptIndex 的 VOL/CUR 脚本明细。
*/
private void fillHarmonicPowerSourceSettings(Map<String, String> keyFillMap, String scriptCode,
List<? extends PqScriptDtls> scriptDetails) {
if (!PowerIndexEnum.HP.getKey().equalsIgnoreCase(scriptCode) || CollUtil.isEmpty(scriptDetails)) {
return;
}
putScriptSetting(keyFillMap, ItemReportKeyEnum.STANDARD_V.getKey(), firstScriptSetting(scriptDetails, "VOL"));
putScriptSetting(keyFillMap, ItemReportKeyEnum.STANDARD_I.getKey(), firstScriptSetting(scriptDetails, "CUR"));
}
private void putScriptSetting(Map<String, String> keyFillMap, String key, PqScriptDtls detail) {
if (detail == null || detail.getValue() == null || StrUtil.isNotBlank(keyFillMap.get(key))) {
return;
}
String value = PubUtils.doubleRoundStr(4, detail.getValue());
if (StrUtil.isNotBlank(value)) {
keyFillMap.put(key, value);
}
}
private PqScriptDtls firstScriptSetting(List<? extends PqScriptDtls> scriptDetails, String valueType) {
for (String phase : Arrays.asList(PowerConstant.PHASE_A, PowerConstant.PHASE_B, PowerConstant.PHASE_C)) {
for (PqScriptDtls detail : scriptDetails) {
if (valueType.equalsIgnoreCase(detail.getValueType())
&& phase.equalsIgnoreCase(detail.getPhase())
&& detail.getValue() != null) {
return detail;
}
}
}
return scriptDetails.stream()
.filter(detail -> valueType.equalsIgnoreCase(detail.getValueType()) && detail.getValue() != null)
.findFirst()
.orElse(null);
}
/** /**
* 针对浙江杭州处理脚本输出细节todo... 需要更改更合理的方式,现在算是写死的了部分 * 针对浙江杭州处理脚本输出细节todo... 需要更改更合理的方式,现在算是写死的了部分
* 目前浙江这边就是(间)谐波电压/电流 * 目前浙江这边就是(间)谐波电压/电流

View File

@@ -0,0 +1,191 @@
package com.njcn.gather.result.util;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.njcn.gather.detection.pojo.enums.DetectionCodeEnum;
import com.njcn.gather.detection.pojo.vo.DetectionData;
import com.njcn.gather.report.pojo.enums.ItemReportKeyEnum;
import com.njcn.gather.storage.pojo.po.SimAndDigNonHarmonicResult;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
/**
* 基波相位报告结果行装配:云南模板以电压相角为 0 度基准,取 I1A 的 A/B/C 三相电流相角填表。
*/
public final class AngleResultAssembler {
private AngleResultAssembler() {
}
public static List<Map<String, String>> assembleRows(List<SimAndDigNonHarmonicResult> results) {
List<Map<String, String>> rows = new ArrayList<>();
if (CollUtil.isEmpty(results)) {
return rows;
}
Map<Integer, List<SimAndDigNonHarmonicResult>> bySort = results.stream()
.filter(result -> result.getSort() != null)
.collect(Collectors.groupingBy(SimAndDigNonHarmonicResult::getSort, TreeMap::new, Collectors.toList()));
for (List<SimAndDigNonHarmonicResult> sameSortResults : bySort.values()) {
SimAndDigNonHarmonicResult currentAngle = firstCurrentAngle(sameSortResults);
if (currentAngle == null) {
continue;
}
DetectionData[] phases = phases(currentAngle);
DetectionData primary = firstPresent(phases);
if (primary == null) {
continue;
}
Map<String, String> row = defaultAngleRow();
row.put(ItemReportKeyEnum.STANDARD.getKey(), valueOrSlash(primary.getResultData()));
putPhaseDetails(row, phases);
row.put(ItemReportKeyEnum.ERROR_SCOPE.getKey(), errorScope(primary));
row.put(ItemReportKeyEnum.RESULT.getKey(), overallResult(phases));
rows.add(row);
}
return rows;
}
private static SimAndDigNonHarmonicResult firstCurrentAngle(List<SimAndDigNonHarmonicResult> results) {
if (CollUtil.isEmpty(results)) {
return null;
}
for (SimAndDigNonHarmonicResult result : results) {
if (DetectionCodeEnum.I1A.getCode().equalsIgnoreCase(result.getAdType())) {
return result;
}
}
return null;
}
private static Map<String, String> defaultAngleRow() {
Map<String, String> row = new LinkedHashMap<>();
for (ItemReportKeyEnum key : Arrays.asList(
ItemReportKeyEnum.STANDARD,
ItemReportKeyEnum.TEST_A,
ItemReportKeyEnum.TEST_B,
ItemReportKeyEnum.TEST_C,
ItemReportKeyEnum.ERROR_A,
ItemReportKeyEnum.ERROR_B,
ItemReportKeyEnum.ERROR_C,
ItemReportKeyEnum.ERROR_SCOPE,
ItemReportKeyEnum.RESULT,
ItemReportKeyEnum.RESULT_A,
ItemReportKeyEnum.RESULT_B,
ItemReportKeyEnum.RESULT_C)) {
row.put(key.getKey(), "/");
}
return row;
}
private static DetectionData[] phases(SimAndDigNonHarmonicResult result) {
return new DetectionData[]{
parse(result.getAValue()),
parse(result.getBValue()),
parse(result.getCValue())
};
}
private static void putPhaseDetails(Map<String, String> row, DetectionData[] phases) {
List<String> tests = Arrays.asList(ItemReportKeyEnum.TEST_A.getKey(),
ItemReportKeyEnum.TEST_B.getKey(), ItemReportKeyEnum.TEST_C.getKey());
List<String> errors = Arrays.asList(ItemReportKeyEnum.ERROR_A.getKey(),
ItemReportKeyEnum.ERROR_B.getKey(), ItemReportKeyEnum.ERROR_C.getKey());
List<String> results = Arrays.asList(ItemReportKeyEnum.RESULT_A.getKey(),
ItemReportKeyEnum.RESULT_B.getKey(), ItemReportKeyEnum.RESULT_C.getKey());
for (int i = 0; i < phases.length; i++) {
DetectionData phase = phases[i];
row.put(tests.get(i), phase == null ? "/" : valueOrSlash(phase.getData()));
row.put(errors.get(i), phase == null || phase.getErrorData() == null ? "/" : format(phase.getErrorData()));
row.put(results.get(i), phase == null ? "/" : resultText(phase.getIsData()));
}
}
private static DetectionData firstPresent(DetectionData[] values) {
for (DetectionData value : values) {
if (value != null) {
return value;
}
}
return null;
}
private static DetectionData parse(String value) {
if (StrUtil.isBlank(value)) {
return null;
}
return JSONUtil.toBean(value, DetectionData.class);
}
private static String overallResult(DetectionData[] phases) {
boolean hasResult = false;
for (DetectionData phase : phases) {
if (phase == null || phase.getIsData() == null) {
continue;
}
hasResult = true;
if (phase.getIsData() == 2) {
return "不合格";
}
}
return hasResult ? "合格" : "/";
}
private static String resultText(Integer isData) {
if (isData == null) {
return "/";
}
if (isData == 1) {
return "合格";
}
if (isData == 2) {
return "不合格";
}
return "/";
}
private static String errorScope(DetectionData primary) {
String scope = dealErrorScope(primary.getRadius());
if (StrUtil.isNotBlank(primary.getUnit()) && !"/".equals(scope)) {
return scope + primary.getUnit();
}
return scope;
}
private static String dealErrorScope(String errorScope) {
if (StrUtil.isBlank(errorScope)) {
return "/";
}
if (errorScope.contains("~")) {
String[] split = errorScope.split("~");
String begin = split[0];
if (begin.startsWith("-")) {
begin = begin.substring(1);
}
if (split.length > 1 && split[1].equalsIgnoreCase(begin)) {
return "±" + begin;
}
}
return errorScope;
}
private static String valueOrSlash(Double value) {
return value == null ? "/" : format(value);
}
private static String format(Double value) {
return value == null ? "/" : BigDecimal.valueOf(value).setScale(4, RoundingMode.HALF_UP).toPlainString();
}
private static String format(BigDecimal value) {
return value == null ? "/" : value.setScale(4, RoundingMode.HALF_UP).toPlainString();
}
}

View File

@@ -0,0 +1,342 @@
package com.njcn.gather.result.util;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.njcn.gather.detection.pojo.vo.DetectionData;
import com.njcn.gather.report.pojo.enums.ItemReportKeyEnum;
import com.njcn.gather.script.pojo.po.PqScriptDtls;
import com.njcn.gather.storage.pojo.po.SimAndDigNonHarmonicResult;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
/**
* 功率 P 报告结果行装配:同一 sort 下可能同时存在总有功、总无功、总视在等结果。
*/
public final class PowerResultAssembler {
private PowerResultAssembler() {
}
public static List<Map<String, String>> assembleRows(List<SimAndDigNonHarmonicResult> results) {
return assembleRows(results, Collections.emptyList());
}
public static List<Map<String, String>> assembleRows(List<SimAndDigNonHarmonicResult> results,
List<? extends PqScriptDtls> scriptDetails) {
if (CollUtil.isEmpty(results)) {
return new ArrayList<>();
}
Map<Integer, List<PqScriptDtls>> scriptDetailsBySort = groupScriptDetailsBySort(scriptDetails);
Map<Integer, List<SimAndDigNonHarmonicResult>> bySort = results.stream()
.filter(result -> result.getSort() != null)
.collect(Collectors.groupingBy(SimAndDigNonHarmonicResult::getSort, TreeMap::new, Collectors.toList()));
List<Map<String, String>> rows = new ArrayList<>();
for (Map.Entry<Integer, List<SimAndDigNonHarmonicResult>> sortEntry : bySort.entrySet()) {
List<SimAndDigNonHarmonicResult> sameSortResults = sortEntry.getValue();
Map<String, String> row = defaultPowerRow();
DetectionData primary = null;
boolean hasPowerValue = false;
for (SimAndDigNonHarmonicResult result : sameSortResults) {
String code = result.getAdType();
if (equalsCode(code, "TotW")) {
DetectionData data = parse(result.getTValue());
hasPowerValue |= putData(row, ItemReportKeyEnum.ACTIVE_POWER.getKey(), data);
primary = firstPresent(data, primary);
} else if (equalsCode(code, "TotVAr")) {
hasPowerValue |= putData(row, ItemReportKeyEnum.REACTIVE_POWER.getKey(), parse(result.getTValue()));
} else if (equalsCode(code, "TotVA")) {
hasPowerValue |= putData(row, ItemReportKeyEnum.APPARENT_POWER.getKey(), parse(result.getTValue()));
} else if (equalsCode(code, "TotPF")) {
DetectionData data = parse(result.getTValue());
hasPowerValue |= putData(row, ItemReportKeyEnum.POWER_FACTOR.getKey(), data);
} else if (equalsCode(code, "PF")) {
if (isBlankPowerValue(row, ItemReportKeyEnum.POWER_FACTOR.getKey())) {
DetectionData data = firstPresent(parse(result.getTValue()), phaseAverage(result));
hasPowerValue |= putData(row, ItemReportKeyEnum.POWER_FACTOR.getKey(), data);
}
} else if (equalsCode(code, "W")) {
DetectionData[] phases = phases(result);
if (isBlankPowerValue(row, ItemReportKeyEnum.ACTIVE_POWER.getKey())) {
hasPowerValue |= putSum(row, ItemReportKeyEnum.ACTIVE_POWER.getKey(), phases);
}
putPhaseDetails(row, phases);
primary = preferPrimary(primary, firstPresent(phases));
} else if (equalsCode(code, "VARW")) {
if (isBlankPowerValue(row, ItemReportKeyEnum.REACTIVE_POWER.getKey())) {
hasPowerValue |= putSum(row, ItemReportKeyEnum.REACTIVE_POWER.getKey(), phases(result));
}
} else if (equalsCode(code, "VAW")) {
if (isBlankPowerValue(row, ItemReportKeyEnum.APPARENT_POWER.getKey())) {
hasPowerValue |= putSum(row, ItemReportKeyEnum.APPARENT_POWER.getKey(), phases(result));
}
}
}
if (primary != null) {
putPrimary(row, primary);
}
if (hasPowerValue) {
putStandardSourceValues(row, sortEntry.getKey(), scriptDetailsBySort);
rows.add(row);
}
}
return rows;
}
private static Map<String, String> defaultPowerRow() {
Map<String, String> row = new LinkedHashMap<>();
List<ItemReportKeyEnum> defaultKeys = Arrays.asList(
ItemReportKeyEnum.SCRIPT_DETAIL,
ItemReportKeyEnum.STANDARD,
ItemReportKeyEnum.STANDARD_A,
ItemReportKeyEnum.STANDARD_B,
ItemReportKeyEnum.STANDARD_C,
ItemReportKeyEnum.STANDARD_V,
ItemReportKeyEnum.STANDARD_I,
ItemReportKeyEnum.STANDARD_U_ANGLE,
ItemReportKeyEnum.STANDARD_I_ANGLE,
ItemReportKeyEnum.TEST,
ItemReportKeyEnum.TEST_A,
ItemReportKeyEnum.TEST_B,
ItemReportKeyEnum.TEST_C,
ItemReportKeyEnum.ERROR,
ItemReportKeyEnum.ERROR_A,
ItemReportKeyEnum.ERROR_B,
ItemReportKeyEnum.ERROR_C,
ItemReportKeyEnum.ERROR_SCOPE,
ItemReportKeyEnum.RESULT,
ItemReportKeyEnum.RESULT_A,
ItemReportKeyEnum.RESULT_B,
ItemReportKeyEnum.RESULT_C,
ItemReportKeyEnum.ACTIVE_POWER,
ItemReportKeyEnum.REACTIVE_POWER,
ItemReportKeyEnum.APPARENT_POWER,
ItemReportKeyEnum.POWER_FACTOR
);
for (ItemReportKeyEnum key : defaultKeys) {
row.put(key.getKey(), "/");
}
return row;
}
private static Map<Integer, List<PqScriptDtls>> groupScriptDetailsBySort(List<? extends PqScriptDtls> scriptDetails) {
Map<Integer, List<PqScriptDtls>> bySort = new TreeMap<>();
if (CollUtil.isEmpty(scriptDetails)) {
return bySort;
}
for (PqScriptDtls detail : scriptDetails) {
if (detail == null || detail.getScriptIndex() == null) {
continue;
}
bySort.computeIfAbsent(detail.getScriptIndex(), key -> new ArrayList<>()).add(detail);
}
return bySort;
}
private static void putStandardSourceValues(Map<String, String> row, Integer sort,
Map<Integer, List<PqScriptDtls>> scriptDetailsBySort) {
List<PqScriptDtls> sameSortDetails = scriptDetailsBySort.get(sort);
if (CollUtil.isEmpty(sameSortDetails)) {
return;
}
PqScriptDtls voltage = pickPhaseDetail(sameSortDetails, "VOL");
PqScriptDtls current = pickPhaseDetail(sameSortDetails, "CUR");
putDouble(row, ItemReportKeyEnum.STANDARD_V.getKey(), voltage == null ? null : voltage.getValue());
putDouble(row, ItemReportKeyEnum.STANDARD_I.getKey(), current == null ? null : current.getValue());
putDouble(row, ItemReportKeyEnum.STANDARD_U_ANGLE.getKey(), voltage == null ? null : voltage.getAngle());
putDouble(row, ItemReportKeyEnum.STANDARD_I_ANGLE.getKey(), current == null ? null : current.getAngle());
}
private static PqScriptDtls pickPhaseDetail(List<PqScriptDtls> details, String valueType) {
for (String phase : Arrays.asList("A", "B", "C")) {
PqScriptDtls detail = firstMatchingDetail(details, valueType, phase);
if (detail != null) {
return detail;
}
}
return firstMatchingDetail(details, valueType, null);
}
private static PqScriptDtls firstMatchingDetail(List<PqScriptDtls> details, String valueType, String phase) {
for (PqScriptDtls detail : details) {
if (!equalsCode(detail.getValueType(), valueType)) {
continue;
}
if (phase != null && !equalsCode(detail.getPhase(), phase)) {
continue;
}
if (detail.getValue() != null || detail.getAngle() != null) {
return detail;
}
}
return null;
}
private static void putDouble(Map<String, String> row, String key, Double value) {
if (value != null) {
row.put(key, format(value));
}
}
private static boolean putData(Map<String, String> row, String key, DetectionData data) {
if (data == null || data.getData() == null) {
return false;
}
row.put(key, format(data.getData()));
return true;
}
private static boolean putSum(Map<String, String> row, String key, DetectionData[] phases) {
BigDecimal sum = BigDecimal.ZERO;
boolean hasValue = false;
for (DetectionData phase : phases) {
if (phase != null && phase.getData() != null) {
sum = sum.add(BigDecimal.valueOf(phase.getData()));
hasValue = true;
}
}
if (hasValue) {
row.put(key, format(sum));
}
return hasValue;
}
private static boolean isBlankPowerValue(Map<String, String> row, String key) {
String value = row.get(key);
return StrUtil.isBlank(value) || "/".equals(value);
}
private static void putPrimary(Map<String, String> row, DetectionData primary) {
row.put(ItemReportKeyEnum.STANDARD.getKey(), valueOrSlash(primary.getResultData()));
row.put(ItemReportKeyEnum.TEST.getKey(), valueOrSlash(primary.getData()));
row.put(ItemReportKeyEnum.ERROR.getKey(), primary.getErrorData() == null ? "/" : format(primary.getErrorData()));
row.put(ItemReportKeyEnum.RESULT.getKey(), resultText(primary.getIsData()));
String errorScope = dealErrorScope(primary.getRadius());
if (StrUtil.isNotBlank(primary.getUnit()) && !"/".equals(errorScope)) {
errorScope = errorScope.concat(primary.getUnit());
}
row.put(ItemReportKeyEnum.ERROR_SCOPE.getKey(), errorScope);
}
private static void putPhaseDetails(Map<String, String> row, DetectionData[] phases) {
List<String> standards = Arrays.asList(ItemReportKeyEnum.STANDARD_A.getKey(),
ItemReportKeyEnum.STANDARD_B.getKey(), ItemReportKeyEnum.STANDARD_C.getKey());
List<String> tests = Arrays.asList(ItemReportKeyEnum.TEST_A.getKey(),
ItemReportKeyEnum.TEST_B.getKey(), ItemReportKeyEnum.TEST_C.getKey());
List<String> errors = Arrays.asList(ItemReportKeyEnum.ERROR_A.getKey(),
ItemReportKeyEnum.ERROR_B.getKey(), ItemReportKeyEnum.ERROR_C.getKey());
List<String> results = Arrays.asList(ItemReportKeyEnum.RESULT_A.getKey(),
ItemReportKeyEnum.RESULT_B.getKey(), ItemReportKeyEnum.RESULT_C.getKey());
for (int i = 0; i < phases.length; i++) {
DetectionData phase = phases[i];
row.put(standards.get(i), phase == null ? "/" : valueOrSlash(phase.getResultData()));
row.put(tests.get(i), phase == null ? "/" : valueOrSlash(phase.getData()));
row.put(errors.get(i), phase == null || phase.getErrorData() == null ? "/" : format(phase.getErrorData()));
row.put(results.get(i), phase == null ? "/" : resultText(phase.getIsData()));
}
}
private static DetectionData phaseAverage(SimAndDigNonHarmonicResult result) {
DetectionData[] phases = phases(result);
BigDecimal sum = BigDecimal.ZERO;
int count = 0;
for (DetectionData phase : phases) {
if (phase != null && phase.getData() != null) {
sum = sum.add(BigDecimal.valueOf(phase.getData()));
count++;
}
}
if (count == 0) {
return null;
}
DetectionData data = new DetectionData();
data.setData(sum.divide(BigDecimal.valueOf(count), 8, RoundingMode.HALF_UP).doubleValue());
return data;
}
private static DetectionData[] phases(SimAndDigNonHarmonicResult result) {
return new DetectionData[]{
parse(result.getAValue()),
parse(result.getBValue()),
parse(result.getCValue())
};
}
private static DetectionData firstPresent(DetectionData... values) {
if (values == null) {
return null;
}
for (DetectionData value : values) {
if (value != null) {
return value;
}
}
return null;
}
private static DetectionData preferPrimary(DetectionData current, DetectionData candidate) {
return current != null ? current : candidate;
}
private static DetectionData parse(String value) {
if (StrUtil.isBlank(value)) {
return null;
}
return JSONUtil.toBean(value, DetectionData.class);
}
private static boolean equalsCode(String actual, String expected) {
return expected.equalsIgnoreCase(actual);
}
private static String resultText(Integer isData) {
if (isData == null) {
return "/";
}
if (isData == 1) {
return "合格";
}
if (isData == 2) {
return "不合格";
}
return "/";
}
private static String valueOrSlash(Double value) {
return value == null ? "/" : format(value);
}
private static String format(Double value) {
return value == null ? "/" : format(BigDecimal.valueOf(value));
}
private static String format(BigDecimal value) {
return value.setScale(4, RoundingMode.HALF_UP).toPlainString();
}
private static String dealErrorScope(String errorScope) {
if (StrUtil.isBlank(errorScope)) {
return "/";
}
if (errorScope.contains("~")) {
String[] split = errorScope.split("~");
String begin = split[0];
if (begin.startsWith("-")) {
begin = begin.substring(1);
}
if (split.length > 1 && split[1].equalsIgnoreCase(begin)) {
return "±" + begin;
}
}
return errorScope;
}
}

View File

@@ -0,0 +1,165 @@
package com.njcn.gather.result.util;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.njcn.gather.detection.pojo.vo.DetectionData;
import com.njcn.gather.report.pojo.enums.ItemReportKeyEnum;
import com.njcn.gather.storage.pojo.po.SimAndDigNonHarmonicResult;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
/**
* IMBV/IMBA 报告结果行装配:同一 sort 下的不平衡度 T 值与序分量 ABC 值合并为一行。
*/
public final class UnbalanceResultAssembler {
private UnbalanceResultAssembler() {
}
public static List<Map<String, String>> assembleRows(List<SimAndDigNonHarmonicResult> results) {
if (CollUtil.isEmpty(results)) {
return java.util.Collections.emptyList();
}
Map<Integer, List<SimAndDigNonHarmonicResult>> bySort = results.stream()
.filter(result -> result.getSort() != null)
.collect(Collectors.groupingBy(SimAndDigNonHarmonicResult::getSort, TreeMap::new, Collectors.toList()));
return bySort.values().stream()
.map(UnbalanceResultAssembler::assembleRow)
.filter(row -> row != null)
.collect(Collectors.toList());
}
private static Map<String, String> assembleRow(List<SimAndDigNonHarmonicResult> sameSortResults) {
Map<String, String> row = defaultRow();
boolean hasData = false;
for (SimAndDigNonHarmonicResult result : sameSortResults) {
String code = result.getAdType();
if (equalsAny(code, "V_UNBAN", "I_UNBAN")) {
DetectionData data = parse(result.getTValue());
if (data != null) {
putPrimary(row, data);
hasData = true;
}
} else if (equalsAny(code, "SeqV", "SeqA")) {
hasData |= putSequence(row, result);
}
}
return hasData ? row : null;
}
private static Map<String, String> defaultRow() {
Map<String, String> row = new LinkedHashMap<>();
for (ItemReportKeyEnum key : Arrays.asList(
ItemReportKeyEnum.STANDARD,
ItemReportKeyEnum.TEST,
ItemReportKeyEnum.ERROR,
ItemReportKeyEnum.ERROR_SCOPE,
ItemReportKeyEnum.POS_SEQ,
ItemReportKeyEnum.NEG_SEQ,
ItemReportKeyEnum.ZERO_SEQ,
ItemReportKeyEnum.RESULT)) {
row.put(key.getKey(), "/");
}
return row;
}
private static void putPrimary(Map<String, String> row, DetectionData data) {
row.put(ItemReportKeyEnum.STANDARD.getKey(), valueOrSlash(data.getResultData()));
row.put(ItemReportKeyEnum.TEST.getKey(), valueOrSlash(data.getData()));
row.put(ItemReportKeyEnum.ERROR.getKey(), data.getErrorData() == null ? "/" : format(data.getErrorData()));
String errorScope = dealErrorScope(data.getRadius());
if (StrUtil.isNotBlank(data.getUnit()) && !"/".equals(errorScope)) {
errorScope = errorScope.concat(data.getUnit());
}
row.put(ItemReportKeyEnum.ERROR_SCOPE.getKey(), errorScope);
row.put(ItemReportKeyEnum.RESULT.getKey(), resultText(data.getIsData()));
}
private static boolean putSequence(Map<String, String> row, SimAndDigNonHarmonicResult result) {
boolean hasData = false;
hasData |= putData(row, ItemReportKeyEnum.POS_SEQ.getKey(), parse(result.getAValue()));
hasData |= putData(row, ItemReportKeyEnum.NEG_SEQ.getKey(), parse(result.getBValue()));
hasData |= putData(row, ItemReportKeyEnum.ZERO_SEQ.getKey(), parse(result.getCValue()));
return hasData;
}
private static boolean putData(Map<String, String> row, String key, DetectionData data) {
if (data == null || data.getData() == null) {
return false;
}
row.put(key, format(data.getData()));
return true;
}
private static DetectionData parse(String value) {
if (StrUtil.isBlank(value)) {
return null;
}
return JSONUtil.toBean(value, DetectionData.class);
}
private static String resultText(Integer isData) {
if (isData == null) {
return "/";
}
if (isData == 1) {
return "合格";
}
if (isData == 2) {
return "不合格";
}
if (isData == 4) {
return "无法比较";
}
return "/";
}
private static String valueOrSlash(Double value) {
return value == null ? "/" : format(value);
}
private static String format(Double value) {
return value == null ? "/" : format(BigDecimal.valueOf(value));
}
private static String format(BigDecimal value) {
return value.setScale(4, RoundingMode.HALF_UP).toPlainString();
}
private static String dealErrorScope(String errorScope) {
if (StrUtil.isBlank(errorScope)) {
return "/";
}
if (errorScope.contains("~")) {
String[] split = errorScope.split("~");
String begin = split[0];
if (begin.startsWith("-")) {
begin = begin.substring(1);
}
if (split.length > 1 && split[1].equalsIgnoreCase(begin)) {
return "±" + begin;
}
}
return errorScope;
}
private static boolean equalsAny(String actual, String... expectedValues) {
if (actual == null) {
return false;
}
for (String expected : expectedValues) {
if (expected.equalsIgnoreCase(actual)) {
return true;
}
}
return false;
}
}

View File

@@ -104,6 +104,18 @@ public class PqScriptDtls implements Serializable {
@TableField("FAfterTime") @TableField("FAfterTime")
private Double fAfterTime; private Double fAfterTime;
/**
* 暂态写入时间S
*/
@TableField("FRampIn")
private Double fRampIn;
/**
* 暂态写出时间S
*/
@TableField("FRampOut")
private Double fRampOut;
/** /**
* 变动频度(次/min * 变动频度(次/min
*/ */

View File

@@ -65,10 +65,10 @@ public class SourceIssue {
private String type; private String type;
/** /**
* 是否是功率 * 是否按电压执行
*/ */
@JSONField(serialize = false) @JSONField(serialize = false)
private Boolean isP; private String otherType;
/** /**
* 多量响量 * 多量响量

View File

@@ -69,13 +69,13 @@ public class PqScriptDtlsServiceImpl extends ServiceImpl<PqScriptDtlsMapper, PqS
private final static String INHARM_I = "InHarm_I"; private final static String INHARM_I = "InHarm_I";
private final static String DIP = "Dip"; private final static String DIP = "Dip";
private final static String FLICKER = "Flicker"; private final static String FLICKER = "Flicker";
// @Value("${Dip.fPreTime}") // @Value("${Dip.fPreTime}")
// private Double fPreTime; // private Double fPreTime;
@Value("${Dip.fRampIn}") // @Value("${Dip.fRampIn}")
private Double fRampIn; // private Double fRampIn;
@Value("${Dip.fRampOut}") // @Value("${Dip.fRampOut}")
private Double fRampOut; // private Double fRampOut;
// @Value("${Dip.fAfterTime}") // @Value("${Dip.fAfterTime}")
// private Double fAfterTime; // private Double fAfterTime;
private static final String waveFluType = "CPM"; private static final String waveFluType = "CPM";
private static final String waveType = "SQU"; private static final String waveType = "SQU";
@@ -247,10 +247,10 @@ public class PqScriptDtlsServiceImpl extends ServiceImpl<PqScriptDtlsMapper, PqS
issueAdd(curList, dtlsIList, HARM_I, INHARM_I, freqDtls.getValue(), "I", channelListDTOS, checkDataParam, devFly, isValueType); issueAdd(curList, dtlsIList, HARM_I, INHARM_I, freqDtls.getValue(), "I", channelListDTOS, checkDataParam, devFly, isValueType);
issue.setChannelList(channelListDTOS); issue.setChannelList(channelListDTOS);
if (ResultUnitEnum.P.getCode().equals(issue.getType())) { if (ResultUnitEnum.P.getCode().equals(issue.getType()) || ResultUnitEnum.ANGLE.getCode().equals(issue.getType())) {
issue.setIsP(true); issue.setOtherType(issue.getType());
} else { } else {
issue.setIsP(false); issue.setOtherType(null);
} }
sourceIssues.add(issue); sourceIssues.add(issue);
} }
@@ -386,6 +386,8 @@ public class PqScriptDtlsServiceImpl extends ServiceImpl<PqScriptDtlsMapper, PqS
dip.setRetainTime(dipData.getRetainTime()); dip.setRetainTime(dipData.getRetainTime());
dip.setFPreTime(dipData.getFPreTime()); dip.setFPreTime(dipData.getFPreTime());
dip.setFAfterTime(dipData.getFAfterTime()); dip.setFAfterTime(dipData.getFAfterTime());
dip.setFRampIn(dipData.getFRampIn());
dip.setFRampOut(dipData.getFRampOut());
info.add(dip); info.add(dip);
} }
} }
@@ -421,7 +423,11 @@ public class PqScriptDtlsServiceImpl extends ServiceImpl<PqScriptDtlsMapper, PqS
@Override @Override
public List<PqScriptDtlsParam.CheckData> scriptDtlsCheckDataList(PqScriptDtlsParam sourceIssue) { public List<PqScriptDtlsParam.CheckData> scriptDtlsCheckDataList(PqScriptDtlsParam sourceIssue) {
// Boolean valueType = pqScriptMapper.selectScriptIsValueType(sourceIssue.getScriptId()); Boolean valueType = pqScriptMapper.selectScriptIsValueType(sourceIssue.getScriptId());
double radio = 1;
if (valueType) {
radio = 100;
}
List<PqScriptDtlsParam.CheckData> info = new ArrayList<>(); List<PqScriptDtlsParam.CheckData> info = new ArrayList<>();
//获取所有下拉值情况 //获取所有下拉值情况
@@ -482,6 +488,7 @@ public class PqScriptDtlsServiceImpl extends ServiceImpl<PqScriptDtlsMapper, PqS
rmsCheck(info, channelListDTO, channelU, checkArchive, true); rmsCheck(info, channelListDTO, channelU, checkArchive, true);
} }
break; break;
case U1A:
case VA: case VA:
if (CollUtil.isNotEmpty(channelList)) { if (CollUtil.isNotEmpty(channelList)) {
channelU = channelList.stream().filter(x -> x.getChannelType().contains("U")).collect(Collectors.toList()); channelU = channelList.stream().filter(x -> x.getChannelType().contains("U")).collect(Collectors.toList());
@@ -583,6 +590,23 @@ public class PqScriptDtlsServiceImpl extends ServiceImpl<PqScriptDtlsMapper, PqS
} }
break; break;
case IRMS: case IRMS:
case I1A:
if (CollUtil.isNotEmpty(channelList)) {
channelI = channelList.stream().filter(x -> x.getChannelType().contains("I")).collect(Collectors.toList());
for (PqScriptDtlsParam.ChannelListDTO listDTO : channelI) {
if (listDTO.getChannelFlag()) {
checkData = new PqScriptDtlsParam.CheckData();
checkData.setErrorFlag(channelListDTO.getErrorFlag());
checkData.setEnable(channelListDTO.getEnable());
checkData.setValue(listDTO.getFPhase());
checkData.setValueType(channelListDTO.getValueType());
checkData.setPid(channelListDTO.getPid());
setCheckValue(checkArchive, checkData, listDTO);
info.add(checkData);
}
}
}
break;
case IA: case IA:
if (CollUtil.isNotEmpty(channelList)) { if (CollUtil.isNotEmpty(channelList)) {
channelI = channelList.stream().filter(x -> x.getChannelType().contains("I")).collect(Collectors.toList()); channelI = channelList.stream().filter(x -> x.getChannelType().contains("I")).collect(Collectors.toList());
@@ -637,10 +661,12 @@ public class PqScriptDtlsServiceImpl extends ServiceImpl<PqScriptDtlsMapper, PqS
checkData.setErrorFlag(channelListDTO.getErrorFlag()); checkData.setErrorFlag(channelListDTO.getErrorFlag());
checkData.setEnable(channelListDTO.getEnable()); checkData.setEnable(channelListDTO.getEnable());
//电压*电流*cos电压角度-电流角度) //电压*电流*cos电压角度-电流角度)
checkData.setValue(channelI.get(0).getFAmp() * listDTO.getFAmp() / 100 * Math.cos((listDTO.getFPhase() - channelI.get(0).getFPhase()) * Math.PI / 180)); double v = listDTO.getFPhase() - channelI.get(0).getFPhase();
// if (valueType) { if (v == 90 || v == -90 || v == 270 || v == -270) {
// checkData.setValue(checkData.getValue() * 57.74 * 5); checkData.setValue(0.0);
// } } else {
checkData.setValue(channelI.get(0).getFAmp() * listDTO.getFAmp() * Math.cos(v * Math.PI / 180));
}
setCheck(info, checkData, channelListDTO, checkArchive, listDTO); setCheck(info, checkData, channelListDTO, checkArchive, listDTO);
} }
} }
@@ -659,8 +685,13 @@ public class PqScriptDtlsServiceImpl extends ServiceImpl<PqScriptDtlsMapper, PqS
checkData = new PqScriptDtlsParam.CheckData(); checkData = new PqScriptDtlsParam.CheckData();
checkData.setErrorFlag(channelListDTO.getErrorFlag()); checkData.setErrorFlag(channelListDTO.getErrorFlag());
checkData.setEnable(channelListDTO.getEnable()); checkData.setEnable(channelListDTO.getEnable());
//电压*电流*cos(电压角度-电流角度) //电压*电流*sin(电压角度-电流角度)
checkData.setValue(channelI.get(0).getFAmp() * listDTO.getFAmp() / 100 * Math.sin((listDTO.getFPhase() - channelI.get(0).getFPhase()) * Math.PI / 180)); double v = listDTO.getFPhase() - channelI.get(0).getFPhase();
if (v == 0 || v == 360 || v == 180 || v == -180) {
checkData.setValue(0.0);
} else {
checkData.setValue(channelI.get(0).getFAmp() * listDTO.getFAmp() * Math.sin(v * Math.PI / 180));
}
// if (valueType) { // if (valueType) {
// checkData.setValue(checkData.getValue() * 57.74 * 5); // checkData.setValue(checkData.getValue() * 57.74 * 5);
// } // }
@@ -682,16 +713,151 @@ public class PqScriptDtlsServiceImpl extends ServiceImpl<PqScriptDtlsMapper, PqS
checkData = new PqScriptDtlsParam.CheckData(); checkData = new PqScriptDtlsParam.CheckData();
checkData.setErrorFlag(channelListDTO.getErrorFlag()); checkData.setErrorFlag(channelListDTO.getErrorFlag());
checkData.setEnable(channelListDTO.getEnable()); checkData.setEnable(channelListDTO.getEnable());
//电压*电流*cos电压角度-电流角度) checkData.setValue(channelI.get(0).getFAmp() * listDTO.getFAmp() / radio);
checkData.setValue(channelI.get(0).getFAmp() * listDTO.getFAmp() / 100);
// if (valueType) {
// checkData.setValue(checkData.getValue() * 57.74 * 5);
// }
setCheck(info, checkData, channelListDTO, checkArchive, listDTO); setCheck(info, checkData, channelListDTO, checkArchive, listDTO);
} }
} }
} }
break; break;
case PF:
channelU = channelList.stream().filter(x -> x.getChannelType().contains("U")).collect(Collectors.toList());
if (CollUtil.isNotEmpty(channelU)) {
for (PqScriptDtlsParam.ChannelListDTO listDTO : channelU) {
//获取电流通道
channelI = channelList.stream()
// .filter(PqScriptDtlsParam.ChannelListDTO::getHarmFlag)
.filter(x -> x.getChannelType().contains("I" + listDTO.getChannelType().substring(1, 2)))
.collect(Collectors.toList());
if (CollUtil.isNotEmpty(channelI)) {
checkData = new PqScriptDtlsParam.CheckData();
checkData.setErrorFlag(channelListDTO.getErrorFlag());
checkData.setEnable(channelListDTO.getEnable());
//cos电压角度-电流角度)
double v = listDTO.getFPhase() - channelI.get(0).getFPhase();
if (v == 90 || v == -90 || v == 270 || v == -270) {
checkData.setValue(0.0);
} else {
checkData.setValue(Math.cos(v * Math.PI / 180));
}
setCheck(info, checkData, channelListDTO, checkArchive, listDTO);
}
}
}
break;
case TOTW:
channelU = channelList.stream().filter(x -> x.getChannelType().contains("U")).collect(Collectors.toList());
if (CollUtil.isNotEmpty(channelU)) {
Double value = 0.0;
for (PqScriptDtlsParam.ChannelListDTO listDTO : channelU) {
//获取电流通道
channelI = channelList.stream()
// .filter(PqScriptDtlsParam.ChannelListDTO::getHarmFlag)
.filter(x -> x.getChannelType().contains("I" + listDTO.getChannelType().substring(1, 2)))
.collect(Collectors.toList());
if (CollUtil.isNotEmpty(channelI)) {
//cos电压角度-电流角度)
double v = listDTO.getFPhase() - channelI.get(0).getFPhase();
if (v == 90 || v == -90 || v == 270 || v == -270) {
value += 0.0;
} else {
value += channelI.get(0).getFAmp() * listDTO.getFAmp() * Math.cos(v * Math.PI / 180);
}
}
}
checkData = new PqScriptDtlsParam.CheckData();
checkData.setErrorFlag(channelListDTO.getErrorFlag());
checkData.setEnable(channelListDTO.getEnable());
checkData.setValue(value);
checkData.setPhase("T");
checkData.setPid(channelListDTO.getPid());
checkData.setValueType(channelListDTO.getValueType());
info.add(checkData);
}
break;
case TOTVAR:
channelU = channelList.stream().filter(x -> x.getChannelType().contains("U")).collect(Collectors.toList());
if (CollUtil.isNotEmpty(channelU)) {
Double value = 0.0;
for (PqScriptDtlsParam.ChannelListDTO listDTO : channelU) {
//获取电流通道
channelI = channelList.stream()
.filter(x -> x.getChannelType().contains("I" + listDTO.getChannelType().substring(1, 2)))
.collect(Collectors.toList());
if (CollUtil.isNotEmpty(channelI)) {
//电压*电流*sin电压角度-电流角度)
double v = listDTO.getFPhase() - channelI.get(0).getFPhase();
if (v == 0 || v == 360 || v == 180 || v == -180) {
value += 0.0;
} else {
value += channelI.get(0).getFAmp() * listDTO.getFAmp() * Math.sin(v * Math.PI / 180);
}
}
}
checkData = new PqScriptDtlsParam.CheckData();
checkData.setErrorFlag(channelListDTO.getErrorFlag());
checkData.setEnable(channelListDTO.getEnable());
checkData.setValue(value);
checkData.setPhase("T");
checkData.setPid(channelListDTO.getPid());
checkData.setValueType(channelListDTO.getValueType());
info.add(checkData);
}
break;
case TOTVA:
channelU = channelList.stream().filter(x -> x.getChannelType().contains("U")).collect(Collectors.toList());
if (CollUtil.isNotEmpty(channelU)) {
Double value = 0.0;
for (PqScriptDtlsParam.ChannelListDTO listDTO : channelU) {
//获取电流通道
channelI = channelList.stream()
.filter(x -> x.getChannelType().contains("I" + listDTO.getChannelType().substring(1, 2)))
.collect(Collectors.toList());
if (CollUtil.isNotEmpty(channelI)) {
value += channelI.get(0).getFAmp() * listDTO.getFAmp();
}
}
checkData = new PqScriptDtlsParam.CheckData();
checkData.setErrorFlag(channelListDTO.getErrorFlag());
checkData.setEnable(channelListDTO.getEnable());
checkData.setValue(value);
checkData.setPhase("T");
checkData.setPid(channelListDTO.getPid());
checkData.setValueType(channelListDTO.getValueType());
info.add(checkData);
}
break;
case TOTPF:
channelU = channelList.stream().filter(x -> x.getChannelType().contains("U")).collect(Collectors.toList());
if (CollUtil.isNotEmpty(channelU)) {
Double valueTotW = 0.0;
Double valueTotVA = 0.0;
for (PqScriptDtlsParam.ChannelListDTO listDTO : channelU) {
//获取电流通道
channelI = channelList.stream()
// .filter(PqScriptDtlsParam.ChannelListDTO::getHarmFlag)
.filter(x -> x.getChannelType().contains("I" + listDTO.getChannelType().substring(1, 2)))
.collect(Collectors.toList());
if (CollUtil.isNotEmpty(channelI)) {
//cos电压角度-电流角度)
double v = listDTO.getFPhase() - channelI.get(0).getFPhase();
if (v == 90 || v == -90 || v == 270 || v == -270) {
valueTotW += 0.0;
} else {
valueTotW += channelI.get(0).getFAmp() * listDTO.getFAmp() * Math.cos(v * Math.PI / 180);
}
valueTotVA += channelI.get(0).getFAmp() * listDTO.getFAmp();
}
}
checkData = new PqScriptDtlsParam.CheckData();
checkData.setErrorFlag(channelListDTO.getErrorFlag());
checkData.setEnable(channelListDTO.getEnable());
checkData.setValue(valueTotW / valueTotVA);
checkData.setPhase("T");
checkData.setPid(channelListDTO.getPid());
checkData.setValueType(channelListDTO.getValueType());
info.add(checkData);
}
break;
case UNKNOWN_ERROR: case UNKNOWN_ERROR:
break; break;
default: default:
@@ -1151,8 +1317,8 @@ public class PqScriptDtlsServiceImpl extends ServiceImpl<PqScriptDtlsMapper, PqS
dipDataDTO.setFRetainTime(0.0); dipDataDTO.setFRetainTime(0.0);
// dipDataDTO.setFPreTime(fPreTime); // dipDataDTO.setFPreTime(fPreTime);
dipDataDTO.setFRampIn(fRampIn); // dipDataDTO.setFRampIn(fRampIn);
dipDataDTO.setFRampOut(fRampOut); // dipDataDTO.setFRampOut(fRampOut);
// dipDataDTO.setFAfterTime(fAfterTime); // dipDataDTO.setFAfterTime(fAfterTime);
@@ -1209,6 +1375,8 @@ public class PqScriptDtlsServiceImpl extends ServiceImpl<PqScriptDtlsMapper, PqS
dipDataDTO.setFTransValue(dip.getTransValue()); dipDataDTO.setFTransValue(dip.getTransValue());
dipDataDTO.setFPreTime(dip.getFPreTime()); dipDataDTO.setFPreTime(dip.getFPreTime());
dipDataDTO.setFAfterTime(dip.getFAfterTime()); dipDataDTO.setFAfterTime(dip.getFAfterTime());
dipDataDTO.setFRampIn(dip.getFRampIn());
dipDataDTO.setFRampOut(dip.getFRampOut());
if (devFly) { if (devFly) {
// if (isValueType) { // if (isValueType) {
dipDataDTO.setFTransValue(dip.getTransValue()); dipDataDTO.setFTransValue(dip.getTransValue());

View File

@@ -0,0 +1,55 @@
package com.njcn.gather.device.util;
import org.junit.Test;
import javax.imageio.ImageIO;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class PqDevImageSupportTest {
@Test
public void buildEchoPayloadReturnsEmptyFieldsWhenImageMissing() {
PqDevImageSupport.ImageEchoPayload payload = PqDevImageSupport.buildEchoPayload(null);
assertFalse(payload.isHasImage());
assertNull(payload.getImageBase64());
assertNull(payload.getImageContentType());
}
@Test
public void buildEchoPayloadDetectsPngAndCreatesBase64() throws Exception {
byte[] imageBytes = createImageBytes("png");
PqDevImageSupport.ImageEchoPayload payload = PqDevImageSupport.buildEchoPayload(imageBytes);
assertTrue(payload.isHasImage());
assertEquals("image/png", payload.getImageContentType());
assertNotNull(payload.getImageBase64());
assertFalse(payload.getImageBase64().isEmpty());
}
@Test(expected = IllegalArgumentException.class)
public void detectContentTypeRejectsInvalidImageBytes() {
PqDevImageSupport.detectContentType(new byte[]{1, 2, 3, 4});
}
private byte[] createImageBytes(String format) throws Exception {
BufferedImage image = new BufferedImage(2, 2, BufferedImage.TYPE_INT_RGB);
image.setRGB(0, 0, Color.RED.getRGB());
image.setRGB(1, 0, Color.GREEN.getRGB());
image.setRGB(0, 1, Color.BLUE.getRGB());
image.setRGB(1, 1, Color.WHITE.getRGB());
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ImageIO.write(image, format, outputStream);
return outputStream.toByteArray();
}
}

View File

@@ -0,0 +1,45 @@
package com.njcn.gather.report.pojo.constant;
import org.junit.Test;
import static org.junit.Assert.*;
public class ItemAnchorConstantTest {
@Test
public void resolveItemScriptCode_normal() {
assertEquals("V", ItemAnchorConstant.resolveItemScriptCode("item_V"));
assertEquals("FREQ", ItemAnchorConstant.resolveItemScriptCode("ITEM_FREQ")); // 前缀大小写不敏感
}
@Test
public void resolveItemScriptCode_nonAnchor() {
assertNull(ItemAnchorConstant.resolveItemScriptCode("_GoBack"));
assertNull(ItemAnchorConstant.resolveItemScriptCode("data_line"));
assertNull(ItemAnchorConstant.resolveItemScriptCode("sect_V"));
assertNull(ItemAnchorConstant.resolveItemScriptCode(null));
assertNull(ItemAnchorConstant.resolveItemScriptCode("item_")); // 空 code 视为非法
}
/** F3白名单外的 item_ 书签(如作者手工遗留的 item_1不得解析成功否则老模板会整体误入新路径 */
@Test
public void resolveItemScriptCode_unknownCode_rejected() {
assertNull(ItemAnchorConstant.resolveItemScriptCode("item_1"));
assertNull(ItemAnchorConstant.resolveItemScriptCode("item_XYZ"));
assertNull(ItemAnchorConstant.resolveItemScriptCode("item_HSV")); // TIME 里有但云南模板白名单没有
}
/** F3白名单命中时大小写归一为规范大写scriptMap / 模板 H5 分组 key 均为大写) */
@Test
public void resolveItemScriptCode_caseNormalized() {
assertEquals("HV", ItemAnchorConstant.resolveItemScriptCode("item_hv"));
assertEquals("IMBV", ItemAnchorConstant.resolveItemScriptCode("Item_imbv"));
}
@Test
public void grpChildren_mapping() {
assertEquals(java.util.Arrays.asList("V", "I", "P", "ANGLE"), ItemAnchorConstant.GRP_CHILDREN.get("GRP_FUND"));
assertEquals(java.util.Arrays.asList("IMBV", "IMBA"), ItemAnchorConstant.GRP_CHILDREN.get("GRP_UNB"));
assertEquals(java.util.Arrays.asList("HV", "HI", "HP"), ItemAnchorConstant.GRP_CHILDREN.get("GRP_HARM"));
}
}

View File

@@ -0,0 +1,74 @@
package com.njcn.gather.report.service.impl;
import com.njcn.gather.report.pojo.enums.PowerIndexEnum;
import com.njcn.gather.tools.report.util.Docx4jUtil;
import org.docx4j.wml.ObjectFactory;
import org.docx4j.wml.P;
import org.docx4j.wml.R;
import org.junit.Test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.time.LocalDate;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
public class PqReportServiceImplTest {
@Test
public void formatChineseReportDateUsesChineseNumerals() {
assertEquals("二O二六年一月七日", PqReportServiceImpl.formatChineseReportDate(LocalDate.of(2026, 1, 7)));
}
@Test
public void formatChineseReportDateHandlesTwoDigitMonthAndDay() {
assertEquals("二O二六年十二月三十一日", PqReportServiceImpl.formatChineseReportDate(LocalDate.of(2026, 12, 31)));
}
@Test
public void formatNumberChineseDateUsesChineseDateSeparators() {
assertEquals("2021年1月1日", PqReportServiceImpl.formatNumberChineseDate(LocalDate.of(2021, 1, 1)));
}
@Test
public void buildItemLineTitleCreatesNormalSmallParagraph() throws Exception {
PqReportServiceImpl service = newReportService();
ObjectFactory factory = new ObjectFactory();
Docx4jUtil.HeadingContent lineTitleTemplate = new Docx4jUtil.HeadingContent();
lineTitleTemplate.setHeadingText(PowerIndexEnum.LINE_TITLE.getKey());
Map<String, List<Docx4jUtil.HeadingContent>> contentMap = new HashMap<>();
contentMap.put(PowerIndexEnum.LINE_TITLE.getKey(), Collections.singletonList(lineTitleTemplate));
Method method = PqReportServiceImpl.class.getDeclaredMethod("buildItemLineTitle", Map.class, int.class, ObjectFactory.class);
method.setAccessible(true);
P paragraph = (P) method.invoke(service, contentMap, 0, factory);
assertEquals("测量回路1", Docx4jUtil.getTextFromP(paragraph));
assertNull(paragraph.getPPr().getOutlineLvl());
R run = (R) paragraph.getContent().get(0);
assertNotNull(run.getRPr());
assertEquals(BigInteger.valueOf(24), run.getRPr().getSz().getVal());
}
@Test
public void itemAnchorGroupKeyNormalizesAngleScriptCode() throws Exception {
Method method = PqReportServiceImpl.class.getDeclaredMethod("itemAnchorGroupKey", String.class);
method.setAccessible(true);
assertEquals("ANGLE", method.invoke(null, "Angle"));
assertEquals("ANGLE", method.invoke(null, "ANGLE"));
}
private PqReportServiceImpl newReportService() throws Exception {
Constructor<?> constructor = PqReportServiceImpl.class.getDeclaredConstructors()[0];
constructor.setAccessible(true);
return (PqReportServiceImpl) constructor.newInstance(new Object[constructor.getParameterCount()]);
}
}

View File

@@ -0,0 +1,32 @@
package com.njcn.gather.report.util;
import com.njcn.gather.tools.report.util.BookmarkUtil;
import com.njcn.gather.tools.report.util.Docx4jUtil;
import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart;
import org.docx4j.wml.P;
import javax.xml.bind.JAXBElement;
import java.util.ArrayList;
import java.util.List;
/** 单测辅助:读块文本 / 收集书签名 */
final class Docx4jUtilTextHelper {
private Docx4jUtilTextHelper() {
}
static String textOf(MainDocumentPart mdp, int blockIndex) {
Object o = mdp.getContent().get(blockIndex);
Object v = (o instanceof JAXBElement) ? ((JAXBElement<?>) o).getValue() : o;
return (v instanceof P) ? Docx4jUtil.getTextFromP((P) v) : "";
}
/** 直接复用生产扫描器 BookmarkUtil.findAllBookmarks保证与生产"可见书签"语义一致(含表格内书签) */
static List<String> allBookmarkNames(MainDocumentPart mdp) {
List<String> names = new ArrayList<>();
for (BookmarkUtil.BookmarkInfo info : BookmarkUtil.findAllBookmarks(mdp)) {
names.add(info.bookmark.getName());
}
return names;
}
}

View File

@@ -0,0 +1,26 @@
package com.njcn.gather.report.util;
import com.njcn.gather.tools.report.util.Docx4jUtil;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.docx4j.openpackaging.parts.WordprocessingML.DocumentSettingsPart;
import org.docx4j.wml.CTSettings;
import org.junit.Test;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class Docx4jUtilUpdateFieldsTest {
@Test
public void enableUpdateFieldsOnOpenSetsWordSettingsFlag() throws Exception {
WordprocessingMLPackage wordPackage = WordprocessingMLPackage.createPackage();
Docx4jUtil.enableUpdateFieldsOnOpen(wordPackage);
DocumentSettingsPart settingsPart = wordPackage.getMainDocumentPart().getDocumentSettingsPart();
assertNotNull(settingsPart);
CTSettings settings = settingsPart.getContents();
assertNotNull(settings.getUpdateFields());
assertTrue(settings.getUpdateFields().isVal());
}
}

View File

@@ -0,0 +1,601 @@
package com.njcn.gather.report.util;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.report.pojo.result.SingleTestResult;
import com.njcn.gather.tools.report.util.Docx4jUtil;
import org.docx4j.wml.CTMarkupRange;
import org.docx4j.wml.ObjectFactory;
import org.docx4j.wml.P;
import org.docx4j.wml.R;
import org.docx4j.wml.Tbl;
import org.docx4j.wml.Tc;
import org.docx4j.wml.Text;
import org.docx4j.wml.Tr;
import org.junit.Test;
import javax.xml.bind.JAXBElement;
import javax.xml.namespace.QName;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class ItemAnchorAssemblerTest {
private static final String W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
private final ObjectFactory f = new ObjectFactory();
private P para(String text) {
P p = f.createP();
R r = f.createR();
Text t = f.createText();
t.setValue(text);
r.getContent().add(t);
p.getContent().add(r);
return p;
}
private Tc cell(String text) {
Tc tc = f.createTc();
tc.getContent().add(para(text));
return tc;
}
private Tr row(String... cellTexts) {
Tr tr = f.createTr();
for (String t : cellTexts) {
tr.getContent().add(f.createTrTc(cell(t)));
}
return tr;
}
/** 模板1 行表头 + 1 行 key 行standard/test/error/result */
private Docx4jUtil.HeadingContent template(String... keys) {
Tbl tbl = f.createTbl();
String[] headers = new String[keys.length];
Arrays.fill(headers, "表头");
tbl.getContent().add(row(headers));
tbl.getContent().add(row(keys));
JAXBElement<Tbl> el = new JAXBElement<>(new QName(W_NS, "tbl"), Tbl.class, tbl);
Docx4jUtil.HeadingContent hc = new Docx4jUtil.HeadingContent();
hc.setHeadingText("FREQ");
hc.addSubContent(el);
return hc;
}
private Docx4jUtil.HeadingContent hvTemplate(String... keys) {
Tbl tbl = f.createTbl();
tbl.getContent().add(row("谐波电压检验结果", "基波电压V", "57.7000"));
tbl.getContent().add(row("总谐波畸变率(%", "", "standardThd", "", ""));
tbl.getContent().add(row("测点", "标准源输出值", "被检仪器测量值", "误差", "标准限值", "检验结论"));
tbl.getContent().add(row("", "", "A", "B", "C", "A", "B", "C", "", "A", "B", "C"));
tbl.getContent().add(row("THD", "standardThd", "thdA", "thdB", "thdC", "", "", "", "", "", "", ""));
tbl.getContent().add(row(keys));
JAXBElement<Tbl> el = new JAXBElement<>(new QName(W_NS, "tbl"), Tbl.class, tbl);
Docx4jUtil.HeadingContent hc = new Docx4jUtil.HeadingContent();
hc.setHeadingText("HV");
hc.addSubContent(el);
return hc;
}
private Docx4jUtil.HeadingContent hpTemplate(String... keys) {
Tbl tbl = f.createTbl();
tbl.getContent().add(row("谐波功率检验结果", "基波电压V", "", "基波电流A", ""));
tbl.getContent().add(row("谐波次数", "标准源输出值", "被检仪器测量值", "误差", "允许误差", "检定结论"));
tbl.getContent().add(row("", "", "L1", "L2", "L3", "L1", "L2", "L3", "", ""));
tbl.getContent().add(row(keys));
JAXBElement<Tbl> el = new JAXBElement<>(new QName(W_NS, "tbl"), Tbl.class, tbl);
Docx4jUtil.HeadingContent hc = new Docx4jUtil.HeadingContent();
hc.setHeadingText("HP");
hc.addSubContent(el);
return hc;
}
private Docx4jUtil.HeadingContent hpTemplateWithSplitHeader(String... keys) {
Tbl tbl = f.createTbl();
tbl.getContent().add(row("谐波功率检验结果", "基波电压V", ""));
tbl.getContent().add(row("", "基波电流A", ""));
tbl.getContent().add(row("谐波次数", "标准源输出值", "被检仪器测量值", "误差", "允许误差", "检定结论"));
tbl.getContent().add(row("", "", "L1", "L2", "L3", "L1", "L2", "L3", "", ""));
tbl.getContent().add(row(keys));
JAXBElement<Tbl> el = new JAXBElement<>(new QName(W_NS, "tbl"), Tbl.class, tbl);
Docx4jUtil.HeadingContent hc = new Docx4jUtil.HeadingContent();
hc.setHeadingText("HP");
hc.addSubContent(el);
return hc;
}
private SingleTestResult resultWithRows(String errorScope, List<Map<String, String>> rows) {
Map<String, List<Map<String, String>>> byError = new LinkedHashMap<>();
byError.put(errorScope, rows);
Map<String, List<Map<String, List<Map<String, String>>>>> detail = new LinkedHashMap<>();
detail.put("额定工作条件", Collections.singletonList(byError));
SingleTestResult r = new SingleTestResult();
r.setDetail(detail);
return r;
}
private String cellText(Tbl tbl, int rowIdx, int colIdx) {
Tr tr = (Tr) tbl.getContent().get(rowIdx);
@SuppressWarnings("unchecked")
JAXBElement<Tc> tc = (JAXBElement<Tc>) tr.getContent().get(colIdx);
return Docx4jUtil.getTextFromCell(tc.getValue());
}
@Test
public void buildTables_fillsRowsAndRemovesKeyRow() {
Docx4jUtil.HeadingContent tpl = template("standard", "test", "error", "result");
Map<String, String> row1 = new HashMap<>();
row1.put("standard", "50.0000");
row1.put("test", "50.0001");
row1.put("error", "0.0001");
row1.put("result", "合格");
Map<String, String> row2 = new HashMap<>(row1);
row2.put("standard", "48.0000");
SingleTestResult r = resultWithRows("0.01", Arrays.asList(row1, row2));
List<Object> out = ItemAnchorAssembler.buildTables("FREQ", Collections.singletonList(r), tpl,
Arrays.asList("standard", "test", "error", "result"), f);
assertEquals(1, out.size());
@SuppressWarnings("unchecked")
Tbl tbl = ((JAXBElement<Tbl>) out.get(0)).getValue();
// 表头 + 2 行数据key 行已删)
assertEquals(3, tbl.getContent().size());
assertEquals("50.0000", cellText(tbl, 1, 0));
assertEquals("48.0000", cellText(tbl, 2, 0));
assertEquals("合格", cellText(tbl, 1, 3));
// 原模板未被改动(深拷贝)
@SuppressWarnings("unchecked")
Tbl original = ((JAXBElement<Tbl>) tpl.getSubContent().get(0)).getValue();
assertEquals(2, original.getContent().size());
}
@Test
public void buildTables_missingKeysBlank_andErrorScopeInjected() {
Docx4jUtil.HeadingContent tpl = template("time", "standard", "errorScope", "posSeq");
Map<String, String> row1 = new HashMap<>();
row1.put("standard", "0.0000"); // 无 time/posSeq/errorScope
SingleTestResult r = resultWithRows("0.15%", Collections.singletonList(row1));
List<Object> out = ItemAnchorAssembler.buildTables("IMBV", Collections.singletonList(r), tpl,
Arrays.asList("time", "standard", "errorScope", "posSeq"), f);
@SuppressWarnings("unchecked")
Tbl tbl = ((JAXBElement<Tbl>) out.get(0)).getValue();
assertEquals("测点1", cellText(tbl, 1, 0)); // time 缺失 → 测点{n} 兜底
assertEquals("0.15%", cellText(tbl, 1, 2)); // errorScope ← 分组 key
assertEquals("", cellText(tbl, 1, 3)); // 数据侧没有的列留空
}
@Test
public void buildTables_harmonicVoltageAndPowerStripErrorScopeUnit() {
Docx4jUtil.HeadingContent tpl = template("time", "errorScope");
Map<String, String> row = new HashMap<>();
row.put("time", "2");
SingleTestResult hv = resultWithRows("0.15%", Collections.singletonList(row));
SingleTestResult hp = resultWithRows("-0.1~0.1W", Collections.singletonList(row));
List<Object> hvOut = ItemAnchorAssembler.buildTables("HV", Collections.singletonList(hv), tpl,
Arrays.asList("time", "errorScope"), f);
List<Object> hpOut = ItemAnchorAssembler.buildTables("HP", Collections.singletonList(hp), tpl,
Arrays.asList("time", "errorScope"), f);
@SuppressWarnings("unchecked")
Tbl hvTbl = ((JAXBElement<Tbl>) hvOut.get(0)).getValue();
@SuppressWarnings("unchecked")
Tbl hpTbl = ((JAXBElement<Tbl>) hpOut.get(0)).getValue();
assertEquals("0.15", cellText(hvTbl, 1, 1));
assertEquals("-0.1~0.1", cellText(hpTbl, 1, 1));
}
@Test
public void buildTables_powerMapping_sumsThreePhases() {
Docx4jUtil.HeadingContent tpl = template("standardV", "activePower", "reactivePower");
Map<String, String> row1 = new HashMap<>();
row1.put("testA", "288.4515");
row1.put("testB", "288.4515");
row1.put("testC", "288.4515");
SingleTestResult r = resultWithRows("1%", Collections.singletonList(row1));
List<Object> out = ItemAnchorAssembler.buildTables("P", Collections.singletonList(r), tpl,
Arrays.asList("standardV", "activePower", "reactivePower"), f);
@SuppressWarnings("unchecked")
Tbl tbl = ((JAXBElement<Tbl>) out.get(0)).getValue();
assertEquals("", cellText(tbl, 1, 0)); // 设置值数据侧暂无 → 留空
assertEquals("865.3545", cellText(tbl, 1, 1)); // 有功 = 三相实测之和
assertEquals("", cellText(tbl, 1, 2)); // 无功暂无 → 留空
}
@Test
public void buildTables_powerMapping_keepsExistingActivePower() {
Docx4jUtil.HeadingContent tpl = template("activePower");
Map<String, String> row1 = new HashMap<>();
row1.put("activePower", "1000.0000");
row1.put("testA", "1.0");
row1.put("testB", "2.0");
row1.put("testC", "3.0");
SingleTestResult r = resultWithRows("1%", Collections.singletonList(row1));
List<Object> out = ItemAnchorAssembler.buildTables("P", Collections.singletonList(r), tpl,
Collections.singletonList("activePower"), f);
@SuppressWarnings("unchecked")
Tbl tbl = ((JAXBElement<Tbl>) out.get(0)).getValue();
assertEquals("1000.0000", cellText(tbl, 1, 0));
}
@Test
public void buildTables_multipleResults_multipleTables() {
Docx4jUtil.HeadingContent tpl = template("time", "standard");
Map<String, String> row = new HashMap<>();
row.put("time", "2");
row.put("standard", "0.5000");
SingleTestResult r1 = resultWithRows("0.05", Collections.singletonList(row));
SingleTestResult r2 = resultWithRows("0.05", Collections.singletonList(row));
SingleTestResult empty = new SingleTestResult(); // detail 为 null → 跳过
List<Object> out = ItemAnchorAssembler.buildTables("HV", Arrays.asList(r1, empty, r2), tpl,
Arrays.asList("time", "standard"), f);
// 空结果跳过 → 2 张表;多表之间允许有空段落防止 Word 合并,但不得出现可见的表外"测点"标题
assertEquals(2, countTables(out));
assertTrue(nonBlankParagraphTexts(out).isEmpty());
}
/** HP 多测点:测点号必须写在表格首行标题单元格内,不能生成表外可见标题段 */
@Test
public void buildTables_harmonicPowerMultipleTables_putsPointCaptionInTitleCell() {
List<String> keys = Arrays.asList("time", "standard");
Docx4jUtil.HeadingContent tpl = hpTemplate(keys.toArray(new String[0]));
Map<String, String> row = new HashMap<>();
row.put("time", "3");
row.put("standard", "0.5000");
SingleTestResult r1 = resultWithRows("0.05", Collections.singletonList(row));
SingleTestResult r2 = resultWithRows("0.05", Collections.singletonList(row));
List<Object> out = ItemAnchorAssembler.buildTables("HP", Arrays.asList(r1, r2), tpl, keys, f);
assertEquals(2, countTables(out));
assertTrue(nonBlankParagraphTexts(out).isEmpty());
List<Tbl> tables = collectTables(out);
assertEquals("谐波功率检验结果测点1", cellText(tables.get(0), 0, 0));
assertEquals("谐波功率检验结果测点2", cellText(tables.get(1), 0, 0));
}
@Test
public void buildTables_harmonicPowerFillsBaseVoltageAndCurrentHeaderCells() {
List<String> keys = Arrays.asList("time", "standard");
Docx4jUtil.HeadingContent tpl = hpTemplate(keys.toArray(new String[0]));
Map<String, String> row = new HashMap<>();
row.put("time", "3");
row.put("standard", "23.0800");
row.put("standardV", "57.7000");
row.put("standardI", "5.0000");
SingleTestResult result = resultWithRows("1.5W", Collections.singletonList(row));
List<Object> out = ItemAnchorAssembler.buildTables("HP", Collections.singletonList(result), tpl, keys, f);
@SuppressWarnings("unchecked")
Tbl tbl = ((JAXBElement<Tbl>) out.get(0)).getValue();
assertEquals("57.7000", cellText(tbl, 0, 2));
assertEquals("5.0000", cellText(tbl, 0, 4));
}
@Test
public void buildTables_harmonicPowerFillsCurrentWhenHeaderIsOnSecondRow() {
List<String> keys = Arrays.asList("time", "standard");
Docx4jUtil.HeadingContent tpl = hpTemplateWithSplitHeader(keys.toArray(new String[0]));
Map<String, String> row = new HashMap<>();
row.put("time", "3");
row.put("standard", "23.0800");
row.put("standardV", "57.7000");
row.put("standardI", "5.0000");
SingleTestResult result = resultWithRows("1.5W", Collections.singletonList(row));
List<Object> out = ItemAnchorAssembler.buildTables("HP", Collections.singletonList(result), tpl, keys, f);
@SuppressWarnings("unchecked")
Tbl tbl = ((JAXBElement<Tbl>) out.get(0)).getValue();
assertEquals("57.7000", cellText(tbl, 0, 2));
assertEquals("5.0000", cellText(tbl, 1, 2));
}
/** F1 边界:单表不加标题段,既有单表输出不变 */
@Test
public void buildTables_singleTable_noCaption() {
Docx4jUtil.HeadingContent tpl = template("time", "standard");
Map<String, String> row = new HashMap<>();
row.put("time", "2");
row.put("standard", "0.5000");
SingleTestResult r1 = resultWithRows("0.05", Collections.singletonList(row));
List<Object> out = ItemAnchorAssembler.buildTables("HV", Collections.singletonList(r1), tpl,
Arrays.asList("time", "standard"), f);
assertEquals(1, out.size());
assertTrue(unwrap(out.get(0)) instanceof Tbl);
}
/** F4bookmarkEnd 漏成 w:tbl 直接子节点(本仓 _GoBack 崩溃同类key 行定位不得 ClassCastException */
@Test
public void buildTables_bookmarkEndAsTblChild_noClassCast() {
Docx4jUtil.HeadingContent tpl = template("standard", "test");
@SuppressWarnings("unchecked")
Tbl tplTbl = ((JAXBElement<Tbl>) tpl.getSubContent().get(0)).getValue();
CTMarkupRange end = f.createCTMarkupRange();
end.setId(BigInteger.ONE);
tplTbl.getContent().add(f.createPBookmarkEnd(end));
Map<String, String> row1 = new HashMap<>();
row1.put("standard", "50.0000");
row1.put("test", "50.0001");
SingleTestResult r = resultWithRows("0.01", Collections.singletonList(row1));
List<Object> out = ItemAnchorAssembler.buildTables("FREQ", Collections.singletonList(r), tpl,
Arrays.asList("standard", "test"), f);
assertEquals(1, out.size());
@SuppressWarnings("unchecked")
Tbl tbl = ((JAXBElement<Tbl>) out.get(0)).getValue();
// 表头 + 数据行key 行已删bookmarkEnd 保留不参与行计数)
List<Tr> trs = collectTrs(tbl);
assertEquals(2, trs.size());
assertEquals("50.0000", Docx4jUtil.getTextFromCell(cellOf(trs.get(1), 0)));
}
/** F2模板 key 行取不到占位符属模板错误,必须显性报错而不是静默走"未测→整节隐藏" */
@Test
public void requireTableKeys_empty_throwsWithScriptCode() {
try {
ItemAnchorAssembler.requireTableKeys("FREQ", Collections.<String>emptyList());
fail("空 tableKeys 应抛 BusinessException");
} catch (BusinessException e) {
assertTrue(String.valueOf(e.getMessage()), String.valueOf(e.getMessage()).contains("FREQ"));
}
}
@Test
public void requireTableKeys_nonEmpty_passes() {
ItemAnchorAssembler.requireTableKeys("FREQ", Collections.singletonList("standard"));
}
@Test
public void extractTableKeys_itemAnchorReadsLastRowEvenWhenOldCrossHeuristicRejectsFirstRow() {
List<String> expected = Arrays.asList("time", "standard", "testA", "testB", "testC",
"errorA", "errorB", "errorC", "errorScope", "resultA", "resultB", "resultC");
Docx4jUtil.HeadingContent tpl = hvTemplate(expected.toArray(new String[0]));
assertTrue("old getTableFillKeys should reject this HV title row",
Docx4jUtil.getTableFillKeys(Collections.singletonList(tpl)).isEmpty());
assertEquals(expected, ItemAnchorAssembler.extractTableKeys(Collections.singletonList(tpl)));
}
@Test
public void buildTables_harmonicVoltageCalculatesThdFixedRowFromPhaseRates() {
List<String> keys = Arrays.asList("time", "standard", "testA", "testB", "testC",
"errorA", "errorB", "errorC", "errorScope", "resultA", "resultB", "resultC");
Docx4jUtil.HeadingContent tpl = hvTemplate(keys.toArray(new String[0]));
Map<String, String> row1 = harmonicRow("2", "3.0000", "6.0000", "8.0000");
row1.put("standard", "3.0000");
Map<String, String> row2 = harmonicRow("3", "4.0000", "8.0000", "15.0000");
row2.put("standard", "4.0000");
SingleTestResult r = resultWithRows("0.05", Arrays.asList(row1, row2));
List<Object> out = ItemAnchorAssembler.buildTables("HV", Collections.singletonList(r), tpl, keys, f);
@SuppressWarnings("unchecked")
Tbl tbl = ((JAXBElement<Tbl>) out.get(0)).getValue();
assertEquals("5.0000", cellText(tbl, 1, 2));
assertEquals("5.0000", cellText(tbl, 4, 1));
assertEquals("5.0000", cellText(tbl, 4, 2));
assertEquals("10.0000", cellText(tbl, 4, 3));
assertEquals("17.0000", cellText(tbl, 4, 4));
}
@Test
public void buildTables_harmonicCurrentCalculatesRateRowsAndThdFromBaseCurrent() {
List<String> keys = Arrays.asList("time", "standardRate", "testRateA", "testRateB", "testRateC",
"errorRateA", "errorRateB", "errorRateC", "errorScope", "resultA", "resultB", "resultC");
Docx4jUtil.HeadingContent tpl = hvTemplate(keys.toArray(new String[0]));
Map<String, String> row1 = harmonicRow("2", "1.2000", "1.6000", "2.0000");
row1.put("standard", "1.0000");
row1.put("standardRate", "25.0000");
row1.put("errorA", "0.2000");
row1.put("errorB", "0.6000");
row1.put("errorC", "1.0000");
row1.put("baseCurrentA", "4.0000");
row1.put("baseCurrentB", "4.0000");
row1.put("baseCurrentC", "4.0000");
Map<String, String> row2 = harmonicRow("3", "0.9000", "1.2000", "1.5000");
row2.put("standard", "1.0000");
row2.put("standardRate", "25.0000");
row2.put("errorA", "-0.1000");
row2.put("errorB", "0.2000");
row2.put("errorC", "0.5000");
row2.put("baseCurrentA", "4.0000");
row2.put("baseCurrentB", "4.0000");
row2.put("baseCurrentC", "4.0000");
SingleTestResult r = resultWithRows("0.05", Arrays.asList(row1, row2));
List<Object> out = ItemAnchorAssembler.buildTables("HI", Collections.singletonList(r), tpl, keys, f);
@SuppressWarnings("unchecked")
Tbl tbl = ((JAXBElement<Tbl>) out.get(0)).getValue();
assertEquals("35.3553", cellText(tbl, 1, 2));
assertEquals("35.3553", cellText(tbl, 4, 1));
assertEquals("37.5000", cellText(tbl, 4, 2));
assertEquals("50.0000", cellText(tbl, 4, 3));
assertEquals("62.5000", cellText(tbl, 4, 4));
assertEquals("25.0000", cellText(tbl, 5, 1));
assertEquals("30.0000", cellText(tbl, 5, 2));
assertEquals("40.0000", cellText(tbl, 5, 3));
assertEquals("50.0000", cellText(tbl, 5, 4));
assertEquals("5.0000", cellText(tbl, 5, 5));
assertEquals("15.0000", cellText(tbl, 5, 6));
assertEquals("25.0000", cellText(tbl, 5, 7));
assertEquals("±1.2500", cellText(tbl, 5, 8));
assertEquals("±1.2500", cellText(tbl, 6, 8));
}
@Test
public void buildTables_harmonicCurrentLeavesStandardRateBlankWhenScriptRateMissing() {
List<String> keys = Arrays.asList("time", "standardRate", "testRateA", "testRateB", "testRateC",
"errorRateA", "errorRateB", "errorRateC", "errorScope", "resultA", "resultB", "resultC");
Docx4jUtil.HeadingContent tpl = hvTemplate(keys.toArray(new String[0]));
Map<String, String> row = harmonicRow("2", "1.0000", "1.0000", "1.0000");
row.put("standard", "1.0000");
row.put("baseCurrentA", "5.0000");
row.put("baseCurrentB", "5.0000");
row.put("baseCurrentC", "5.0000");
SingleTestResult r = resultWithRows("0.05", Collections.singletonList(row));
List<Object> out = ItemAnchorAssembler.buildTables("HI", Collections.singletonList(r), tpl, keys, f);
@SuppressWarnings("unchecked")
Tbl tbl = ((JAXBElement<Tbl>) out.get(0)).getValue();
assertEquals("", cellText(tbl, 1, 2));
assertEquals("", cellText(tbl, 4, 1));
assertEquals("", cellText(tbl, 5, 1));
assertEquals("20.0000", cellText(tbl, 5, 2));
assertEquals("", cellText(tbl, 5, 5));
assertEquals("", cellText(tbl, 5, 8));
}
@Test
public void buildTables_harmonicCurrentKeepsScriptStandardRateWhenAmplitudeAvailable() {
List<String> keys = Arrays.asList("time", "standardRate", "testRateA", "testRateB", "testRateC",
"errorRateA", "errorRateB", "errorRateC", "errorScope", "resultA", "resultB", "resultC");
Docx4jUtil.HeadingContent tpl = hvTemplate(keys.toArray(new String[0]));
Map<String, String> row = harmonicRow("2", "1.0000", "1.0000", "1.0000");
row.put("standard", "2.0000");
row.put("standardRate", "1.0000");
row.put("baseCurrentA", "10.0000");
row.put("baseCurrentB", "10.0000");
row.put("baseCurrentC", "10.0000");
SingleTestResult r = resultWithRows("0.05", Collections.singletonList(row));
List<Object> out = ItemAnchorAssembler.buildTables("HI", Collections.singletonList(r), tpl, keys, f);
@SuppressWarnings("unchecked")
Tbl tbl = ((JAXBElement<Tbl>) out.get(0)).getValue();
assertEquals("1.0000", cellText(tbl, 5, 1));
assertEquals("9.0000", cellText(tbl, 5, 5));
assertEquals("±0.1500", cellText(tbl, 5, 8));
}
@Test
public void buildTables_harmonicCurrentErrorRateUsesRateDifference() {
List<String> keys = Arrays.asList("time", "standardRate", "testRateA", "testRateB", "testRateC",
"errorRateA", "errorRateB", "errorRateC", "errorScope", "resultA", "resultB", "resultC");
Docx4jUtil.HeadingContent tpl = hvTemplate(keys.toArray(new String[0]));
Map<String, String> row = harmonicRow("2", "2.0000", "1.0000", "1.0000");
row.put("standard", "1.0000");
row.put("standardRate", "10.0000");
row.put("errorA", "0.0100");
row.put("baseCurrentA", "10.0000");
row.put("baseCurrentB", "10.0000");
row.put("baseCurrentC", "10.0000");
SingleTestResult r = resultWithRows("0.05", Collections.singletonList(row));
List<Object> out = ItemAnchorAssembler.buildTables("HI", Collections.singletonList(r), tpl, keys, f);
@SuppressWarnings("unchecked")
Tbl tbl = ((JAXBElement<Tbl>) out.get(0)).getValue();
assertEquals("10.0000", cellText(tbl, 5, 1));
assertEquals("20.0000", cellText(tbl, 5, 2));
assertEquals("10.0000", cellText(tbl, 5, 5));
}
@Test
public void buildTables_harmonicCurrentKeepsScriptStandardRateWhenAmplitudeMissing() {
List<String> keys = Arrays.asList("time", "standardRate", "testRateA", "testRateB", "testRateC",
"errorRateA", "errorRateB", "errorRateC", "errorScope", "resultA", "resultB", "resultC");
Docx4jUtil.HeadingContent tpl = hvTemplate(keys.toArray(new String[0]));
Map<String, String> row = harmonicRow("2", "0.040144", "0.040248", "0.040116");
row.put("standard", "/");
row.put("standardRate", "1.0000");
row.put("baseCurrentA", "4.0000");
row.put("baseCurrentB", "4.0000");
row.put("baseCurrentC", "4.0000");
SingleTestResult r = resultWithRows("0.0075A", Collections.singletonList(row));
List<Object> out = ItemAnchorAssembler.buildTables("HI", Collections.singletonList(r), tpl, keys, f);
@SuppressWarnings("unchecked")
Tbl tbl = ((JAXBElement<Tbl>) out.get(0)).getValue();
assertEquals("1.0000", cellText(tbl, 1, 2));
assertEquals("1.0000", cellText(tbl, 4, 1));
assertEquals("1.0000", cellText(tbl, 5, 1));
assertEquals("1.0036", cellText(tbl, 5, 2));
assertEquals("0.0036", cellText(tbl, 5, 5));
}
private Map<String, String> harmonicRow(String time, String testA, String testB, String testC) {
Map<String, String> row = new HashMap<>();
row.put("time", time);
row.put("standard", "0.0000");
row.put("testA", testA);
row.put("testB", testB);
row.put("testC", testC);
row.put("errorA", "0.0000");
row.put("errorB", "0.0000");
row.put("errorC", "0.0000");
row.put("resultA", "合格");
row.put("resultB", "合格");
row.put("resultC", "合格");
return row;
}
private Object unwrap(Object o) {
return (o instanceof JAXBElement) ? ((JAXBElement<?>) o).getValue() : o;
}
private int countTables(List<Object> out) {
int n = 0;
for (Object o : out) {
if (unwrap(o) instanceof Tbl) {
n++;
}
}
return n;
}
private List<Tbl> collectTables(List<Object> out) {
List<Tbl> tables = new ArrayList<>();
for (Object o : out) {
Object v = unwrap(o);
if (v instanceof Tbl) {
tables.add((Tbl) v);
}
}
return tables;
}
private List<String> nonBlankParagraphTexts(List<Object> out) {
List<String> texts = new ArrayList<>();
for (Object o : out) {
Object v = unwrap(o);
if (v instanceof P) {
String text = Docx4jUtil.getTextFromP((P) v);
if (text != null && !text.trim().isEmpty()) {
texts.add(text);
}
}
}
return texts;
}
private List<Tr> collectTrs(Tbl tbl) {
List<Tr> trs = new ArrayList<>();
for (Object o : tbl.getContent()) {
Object v = unwrap(o);
if (v instanceof Tr) {
trs.add((Tr) v);
}
}
return trs;
}
private Tc cellOf(Tr tr, int colIdx) {
@SuppressWarnings("unchecked")
JAXBElement<Tc> tc = (JAXBElement<Tc>) tr.getContent().get(colIdx);
return tc.getValue();
}
}

View File

@@ -0,0 +1,254 @@
package com.njcn.gather.report.util;
import com.njcn.gather.tools.report.util.BookmarkUtil;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart;
import org.docx4j.wml.CTBookmark;
import org.docx4j.wml.CTMarkupRange;
import org.docx4j.wml.ObjectFactory;
import org.docx4j.wml.P;
import org.docx4j.wml.R;
import org.docx4j.wml.Text;
import org.junit.Test;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class ItemAnchorSectionUtilTest {
private final ObjectFactory f = new ObjectFactory();
private P para(String text) {
P p = f.createP();
R r = f.createR();
Text t = f.createText();
t.setValue(text);
r.getContent().add(t);
p.getContent().add(r);
return p;
}
private void addStart(P p, long id, String name) {
CTBookmark b = f.createCTBookmark();
b.setId(BigInteger.valueOf(id));
b.setName(name);
p.getContent().add(0, f.createPBookmarkStart(b));
}
private void addEnd(P p, long id) {
CTMarkupRange e = f.createCTMarkupRange();
e.setId(BigInteger.valueOf(id));
p.getContent().add(f.createPBookmarkEnd(e));
}
private BookmarkUtil.BookmarkInfo addBookmarkPair(P p, long id, String name, MainDocumentPart mdp) {
CTBookmark b = f.createCTBookmark();
b.setId(BigInteger.valueOf(id));
b.setName(name);
p.getContent().add(0, f.createPBookmarkStart(b));
addEnd(p, id);
BookmarkUtil.BookmarkInfo info = new BookmarkUtil.BookmarkInfo();
info.bookmark = b;
info.parentParagraph = p;
info.parentContainer = mdp;
return info;
}
/**
* 构造文档骨架(模拟 base 第 7 节):
* [0] 7.1 标题 {start sect_GRP_FUND}
* [1] 7.1.1 标题 {start sect_V}
* [2] item_V 锚点段 {end sect_V}
* [3] 7.1.2 标题 {start sect_I}
* [4] item_I 锚点段 {end sect_I, end sect_GRP_FUND}
* [5] 7.2 标题 {start sect_FREQ}
* [6] item_FREQ 锚点 {end sect_FREQ}
*/
private MainDocumentPart buildDoc() throws Exception {
WordprocessingMLPackage pkg = WordprocessingMLPackage.createPackage();
MainDocumentPart mdp = pkg.getMainDocumentPart();
P p71 = para("7.1 基波测试");
addStart(p71, 100, "sect_GRP_FUND");
P p711 = para("7.1.1 电压");
addStart(p711, 101, "sect_V");
P aV = para("");
addEnd(aV, 101);
P p712 = para("7.1.2 电流");
addStart(p712, 102, "sect_I");
P aI = para("");
addEnd(aI, 102);
addEnd(aI, 100);
P p72 = para("7.2 频率");
addStart(p72, 103, "sect_FREQ");
P aF = para("");
addEnd(aF, 103);
mdp.getContent().addAll(Arrays.asList(p71, p711, aV, p712, aI, p72, aF));
return mdp;
}
private MainDocumentPart docWithTexts(String... texts) throws Exception {
WordprocessingMLPackage pkg = WordprocessingMLPackage.createPackage();
MainDocumentPart mdp = pkg.getMainDocumentPart();
for (String t : texts) {
mdp.getContent().add(para(t));
}
return mdp;
}
@Test
public void removeSingleChildSect() throws Exception {
MainDocumentPart mdp = buildDoc();
ItemAnchorSectionUtil.removeSectRanges(mdp, new LinkedHashSet<>(Collections.singletonList("V")));
// 删除 [1][2] 两块,剩 5 块
assertEquals(5, mdp.getContent().size());
assertTrue(Docx4jUtilTextHelper.textOf(mdp, 0).startsWith("7.1 "));
assertTrue(Docx4jUtilTextHelper.textOf(mdp, 1).startsWith("7.1.2"));
}
@Test
public void removeWholeGroupWhenAllChildrenHidden() throws Exception {
MainDocumentPart mdp = buildDoc();
// GRP_FUND 子节全集是 V/I/P/ANGLE文档里只有 V、I但隐藏集合包含全部四个 → 整组删除
ItemAnchorSectionUtil.removeSectRanges(mdp, new LinkedHashSet<>(Arrays.asList("V", "I", "P", "ANGLE")));
// [0]~[4] 全删,剩 7.2 两块
assertEquals(2, mdp.getContent().size());
assertTrue(Docx4jUtilTextHelper.textOf(mdp, 0).startsWith("7.2"));
}
/** F6父 sect_GRP_* 书签缺失(如 Word 重存丢书签)时,必须回退按子节各自删除,不能一个都删不掉 */
@Test
public void removeGroup_parentRangeMissing_fallsBackToChildren() throws Exception {
WordprocessingMLPackage pkg = WordprocessingMLPackage.createPackage();
MainDocumentPart mdp = pkg.getMainDocumentPart();
P p71 = para("7.1 基波测试"); // 故意不挂 sect_GRP_FUND
P p711 = para("7.1.1 电压");
addStart(p711, 101, "sect_V");
P aV = para("");
addEnd(aV, 101);
P p712 = para("7.1.2 电流");
addStart(p712, 102, "sect_I");
P aI = para("");
addEnd(aI, 102);
P p72 = para("7.2 频率");
addStart(p72, 103, "sect_FREQ");
P aF = para("");
addEnd(aF, 103);
mdp.getContent().addAll(Arrays.asList(p71, p711, aV, p712, aI, p72, aF));
ItemAnchorSectionUtil.removeSectRanges(mdp, new LinkedHashSet<>(Arrays.asList("V", "I", "P", "ANGLE")));
// 父范围定位失败 → 回退删 sect_V/sect_I 各自范围;组标题 7.1(无书签定位不了)与 7.2 保留
assertEquals(3, mdp.getContent().size());
assertTrue(Docx4jUtilTextHelper.textOf(mdp, 0).startsWith("7.1 "));
assertTrue(Docx4jUtilTextHelper.textOf(mdp, 1).startsWith("7.2"));
}
@Test
public void stripBookmarks_removesOnlyOurNamespace() throws Exception {
MainDocumentPart mdp = buildDoc();
// 额外放一个第三方书签,应保留
P other = (P) mdp.getContent().get(0);
CTBookmark foreign = f.createCTBookmark();
foreign.setId(BigInteger.valueOf(999));
foreign.setName("bgbh1");
other.getContent().add(f.createPBookmarkStart(foreign));
ItemAnchorSectionUtil.stripItemAnchorBookmarks(mdp);
// 全文不再有 sect_/item_ 书签,但 bgbh1 保留
java.util.List<String> names = Docx4jUtilTextHelper.allBookmarkNames(mdp);
assertEquals(Collections.singletonList("bgbh1"), names);
}
@Test
public void removeEmptyItemAnchorParagraph_removesStandaloneItemAnchorParagraph() throws Exception {
MainDocumentPart mdp = docWithTexts("7.1.1 电压");
P anchor = para("");
BookmarkUtil.BookmarkInfo info = addBookmarkPair(anchor, 9000, "item_V", mdp);
addEnd(anchor, 9011);
mdp.getContent().add(anchor);
assertTrue(ItemAnchorSectionUtil.removeEmptyItemAnchorParagraph(info));
assertEquals(1, mdp.getContent().size());
assertEquals("7.1.1 电压", Docx4jUtilTextHelper.textOf(mdp, 0));
}
@Test
public void removeEmptyItemAnchorParagraph_keepsItemAnchorParagraphWithText() throws Exception {
MainDocumentPart mdp = docWithTexts("7.1.1 电压");
P anchor = para("保留文本");
BookmarkUtil.BookmarkInfo info = addBookmarkPair(anchor, 9000, "item_V", mdp);
mdp.getContent().add(anchor);
assertTrue(!ItemAnchorSectionUtil.removeEmptyItemAnchorParagraph(info));
assertEquals(2, mdp.getContent().size());
assertEquals("保留文本", Docx4jUtilTextHelper.textOf(mdp, 1));
}
@Test
public void removeEmptyItemAnchorParagraph_ignoresNonItemAnchor() throws Exception {
MainDocumentPart mdp = docWithTexts("7.1.1 电压");
P anchor = para("");
BookmarkUtil.BookmarkInfo info = addBookmarkPair(anchor, 1, "data_line", mdp);
mdp.getContent().add(anchor);
assertTrue(!ItemAnchorSectionUtil.removeEmptyItemAnchorParagraph(info));
assertEquals(2, mdp.getContent().size());
}
@Test
public void renumber_afterMiddleRemoval() throws Exception {
// 模拟 7.1.1 已被删除7.1.2→7.1.17.1.4→…按序补位7.2 及以后不变
MainDocumentPart mdp = docWithTexts(
"7准确度检查",
"7.1 基波测试",
"7.1.2 电流",
"7.1.4 相位",
"7.2 频率",
"7.6.1 频率变化影响");
ItemAnchorSectionUtil.renumberChapter7(mdp);
assertEquals("7准确度检查", Docx4jUtilTextHelper.textOf(mdp, 0)); // 全角点不动
assertEquals("7.1 基波测试", Docx4jUtilTextHelper.textOf(mdp, 1));
assertEquals("7.1.1 电流", Docx4jUtilTextHelper.textOf(mdp, 2));
assertEquals("7.1.2 相位", Docx4jUtilTextHelper.textOf(mdp, 3));
assertEquals("7.2 频率", Docx4jUtilTextHelper.textOf(mdp, 4));
assertEquals("7.2.1 频率变化影响", Docx4jUtilTextHelper.textOf(mdp, 5));
}
@Test
public void renumber_afterLevel2Removal() throws Exception {
// 模拟整个 7.1 组已删除7.2→7.17.5.1→7.4.1 之类整体前移
MainDocumentPart mdp = docWithTexts("7.2 频率", "7.3 闪变", "7.5 谐波", "7.5.1 谐波电压");
ItemAnchorSectionUtil.renumberChapter7(mdp);
assertEquals("7.1 频率", Docx4jUtilTextHelper.textOf(mdp, 0));
assertEquals("7.2 闪变", Docx4jUtilTextHelper.textOf(mdp, 1));
assertEquals("7.3 谐波", Docx4jUtilTextHelper.textOf(mdp, 2));
assertEquals("7.3.1 谐波电压", Docx4jUtilTextHelper.textOf(mdp, 3));
}
@Test
public void renumber_prefixSplitAcrossRuns() throws Exception {
// 编号被拆在两个 run 里:"7." + "2 频率"
WordprocessingMLPackage pkg = WordprocessingMLPackage.createPackage();
MainDocumentPart mdp = pkg.getMainDocumentPart();
P p = f.createP();
R r1 = f.createR();
Text t1 = f.createText();
t1.setValue("7.");
r1.getContent().add(t1);
R r2 = f.createR();
Text t2 = f.createText();
t2.setValue("2 频率");
r2.getContent().add(t2);
p.getContent().add(r1);
p.getContent().add(r2);
mdp.getContent().add(p);
ItemAnchorSectionUtil.renumberChapter7(mdp);
assertEquals("7.1 频率", Docx4jUtilTextHelper.textOf(mdp, 0));
}
}

View File

@@ -0,0 +1,232 @@
package com.njcn.gather.result.service.impl;
import com.njcn.gather.report.pojo.enums.ItemReportKeyEnum;
import com.njcn.gather.report.pojo.result.SingleTestResult;
import com.njcn.gather.script.pojo.po.PqScriptCheckData;
import com.njcn.gather.script.pojo.vo.PqScriptDtlDataVO;
import com.njcn.gather.script.service.IPqScriptCheckDataService;
import com.njcn.gather.storage.pojo.po.SimAndDigHarmonicResult;
import com.njcn.gather.storage.pojo.po.SimAndDigNonHarmonicResult;
import com.njcn.gather.storage.pojo.param.SingleNonHarmParam;
import com.njcn.gather.storage.service.SimAndDigHarmonicService;
import com.njcn.gather.storage.service.SimAndDigNonHarmonicService;
import org.junit.Test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class ResultServiceImplTest {
@Test
public void fillThreePhaseData_harmonicCurrentCarriesBaseCurrentForRateCalculation() throws Exception {
SimAndDigHarmonicResult result = new SimAndDigHarmonicResult();
result.setAValue1(detectionData("4.0000", "10.0000"));
result.setBValue1(detectionData("5.0000", "11.0000"));
result.setCValue1(detectionData("6.0000", "12.0000"));
result.setAValue2(detectionData("1.0000"));
result.setBValue2(detectionData("1.5000"));
result.setCValue2(detectionData("2.0000"));
Map<String, String> row = new HashMap<>();
Method method = ResultServiceImpl.class.getDeclaredMethod(
"fillThreePhaseData", Object.class, Integer.class, Map.class, String.class);
method.setAccessible(true);
boolean filled = (Boolean) method.invoke(newResultService(), result, 2, row, "HI");
assertTrue(filled);
assertEquals("4.0000", row.get(ItemReportKeyEnum.BASE_CURRENT_A.getKey()));
assertEquals("5.0000", row.get(ItemReportKeyEnum.BASE_CURRENT_B.getKey()));
assertEquals("6.0000", row.get(ItemReportKeyEnum.BASE_CURRENT_C.getKey()));
}
@Test
public void fillThreePhaseData_harmonicCurrentUsesBaseCurrentDataWhenResultDataMissing() throws Exception {
SimAndDigHarmonicResult result = new SimAndDigHarmonicResult();
result.setAValue1(detectionDataOnlyData("1.0000"));
result.setBValue1(detectionDataOnlyData("2.0000"));
result.setCValue1(detectionDataOnlyData("3.0000"));
result.setAValue2(detectionData("0.1000"));
result.setBValue2(detectionData("0.2000"));
result.setCValue2(detectionData("0.3000"));
Map<String, String> row = new HashMap<>();
Method method = ResultServiceImpl.class.getDeclaredMethod(
"fillThreePhaseData", Object.class, Integer.class, Map.class, String.class);
method.setAccessible(true);
boolean filled = (Boolean) method.invoke(newResultService(), result, 2, row, "HI");
assertTrue(filled);
assertEquals("1.0000", row.get(ItemReportKeyEnum.BASE_CURRENT_A.getKey()));
assertEquals("2.0000", row.get(ItemReportKeyEnum.BASE_CURRENT_B.getKey()));
assertEquals("3.0000", row.get(ItemReportKeyEnum.BASE_CURRENT_C.getKey()));
}
@Test
public void fillHarmonicScriptStandard_harmonicCurrentCopiesScriptPercentToStandardRate() throws Exception {
Map<String, String> row = new HashMap<>();
List<PqScriptCheckData> checkData = Arrays.asList(
checkData("A", 2D, 1D),
checkData("B", 2D, 1D),
checkData("C", 2D, 1D)
);
Method method = ResultServiceImpl.class.getDeclaredMethod(
"fillHarmonicScriptStandard", Map.class, String.class, List.class);
method.setAccessible(true);
method.invoke(newResultService(), row, "HI", checkData);
assertEquals("1.0000", row.get(ItemReportKeyEnum.STANDARD_RATE.getKey()));
}
@Test
public void getFinalContent_harmonicPowerCarriesBaseVoltageAndCurrentFromScriptDetails() throws Exception {
IPqScriptCheckDataService checkDataService = mock(IPqScriptCheckDataService.class);
SimAndDigHarmonicService harmonicService = mock(SimAndDigHarmonicService.class);
when(checkDataService.listCheckData(eq("script"), anyList()))
.thenReturn(Collections.singletonList(checkData("A", 3D, 23.08D)));
SimAndDigHarmonicResult harmonicResult = new SimAndDigHarmonicResult();
harmonicResult.setAValue3(detectionData("23.0745", "23.0800"));
harmonicResult.setBValue3(detectionData("23.0809", "23.0800"));
harmonicResult.setCValue3(detectionData("23.0814", "23.0800"));
when(harmonicService.getSingleResult(any())).thenReturn(harmonicResult);
List<PqScriptDtlDataVO> scriptDetails = Arrays.asList(
scriptDetail("VOL", "A", 57.7D),
scriptDetail("CUR", "A", 5.0D)
);
SingleTestResult result = newResultService(harmonicService, checkDataService)
.getFinalContent(scriptDetails, "plan", "dev", 1,
Arrays.asList("time", "standardV", "standardI"));
Map<String, String> row = firstRow(result);
assertEquals("57.7000", row.get(ItemReportKeyEnum.STANDARD_V.getKey()));
assertEquals("5.0000", row.get(ItemReportKeyEnum.STANDARD_I.getKey()));
}
@Test
public void getFinalContent_angleUsesCurrentAngleResultRows() throws Exception {
IPqScriptCheckDataService checkDataService = mock(IPqScriptCheckDataService.class);
SimAndDigNonHarmonicService nonHarmonicService = mock(SimAndDigNonHarmonicService.class);
when(checkDataService.listCheckData(eq("script"), anyList())).thenReturn(Collections.emptyList());
SimAndDigNonHarmonicResult result = new SimAndDigNonHarmonicResult();
result.setSort(21);
result.setAdType("I1A");
result.setAValue(detectionAngleData("61.4", "60", "1.4", 2));
result.setBValue(detectionAngleData("61", "60", "1", 1));
result.setCValue(detectionAngleData("61", "60", "1", 1));
when(nonHarmonicService.queryBySortListWithAdTypeCode(any(SingleNonHarmParam.class)))
.thenReturn(Collections.singletonList(result));
SingleTestResult reportResult = newResultService(null, checkDataService, nonHarmonicService)
.getFinalContent(Collections.singletonList(scriptDetail("Angle", "Base", 21)),
"plan", "dev", 1, Arrays.asList("standard", "testA", "testB", "testC"));
Map<String, String> row = firstRow(reportResult);
assertEquals("60.0000", row.get(ItemReportKeyEnum.STANDARD.getKey()));
assertEquals("61.4000", row.get(ItemReportKeyEnum.TEST_A.getKey()));
assertEquals("61.0000", row.get(ItemReportKeyEnum.TEST_B.getKey()));
assertEquals("61.0000", row.get(ItemReportKeyEnum.TEST_C.getKey()));
assertEquals("不合格", row.get(ItemReportKeyEnum.RESULT_A.getKey()));
assertEquals("合格", row.get(ItemReportKeyEnum.RESULT_B.getKey()));
assertEquals("合格", row.get(ItemReportKeyEnum.RESULT_C.getKey()));
}
private ResultServiceImpl newResultService() throws Exception {
return newResultService(null, null);
}
private ResultServiceImpl newResultService(SimAndDigHarmonicService harmonicService,
IPqScriptCheckDataService checkDataService) throws Exception {
return newResultService(harmonicService, checkDataService, null);
}
private ResultServiceImpl newResultService(SimAndDigHarmonicService harmonicService,
IPqScriptCheckDataService checkDataService,
SimAndDigNonHarmonicService nonHarmonicService) throws Exception {
Constructor<?> constructor = ResultServiceImpl.class.getDeclaredConstructors()[0];
constructor.setAccessible(true);
Class<?>[] parameterTypes = constructor.getParameterTypes();
Object[] args = new Object[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) {
if (nonHarmonicService != null && parameterTypes[i].equals(SimAndDigNonHarmonicService.class)) {
args[i] = nonHarmonicService;
} else if (harmonicService != null && parameterTypes[i].equals(SimAndDigHarmonicService.class)) {
args[i] = harmonicService;
} else if (checkDataService != null && parameterTypes[i].equals(IPqScriptCheckDataService.class)) {
args[i] = checkDataService;
}
}
return (ResultServiceImpl) constructor.newInstance(args);
}
private Map<String, String> firstRow(SingleTestResult result) {
return result.getDetail().values().iterator().next()
.get(0).values().iterator().next()
.get(0);
}
private String detectionData(String value) {
return detectionData(value, value);
}
private String detectionData(String data, String resultData) {
return "{\"isData\":1,\"data\":" + data + ",\"resultData\":" + resultData
+ ",\"radius\":\"0.05\",\"unit\":\"A\"}";
}
private String detectionDataOnlyData(String data) {
return "{\"num\":1,\"isData\":4,\"data\":" + data + ",\"radius\":\"0.05\",\"unit\":\"A\"}";
}
private String detectionAngleData(String data, String resultData, String errorData, int isData) {
return "{\"isData\":" + isData + ",\"data\":" + data + ",\"resultData\":" + resultData
+ ",\"errorData\":" + errorData + ",\"radius\":\"-1.0~1.0\",\"unit\":\"\\u00b0\"}";
}
private PqScriptCheckData checkData(String phase, Double harmNum, Double value) {
PqScriptCheckData data = new PqScriptCheckData();
data.setScriptIndex(3);
data.setValueType("HP");
data.setPhase(phase);
data.setHarmNum(harmNum);
data.setValue(value);
return data;
}
private PqScriptDtlDataVO scriptDetail(String valueType, String phase, Double value) {
PqScriptDtlDataVO detail = new PqScriptDtlDataVO();
detail.setScriptId("script");
detail.setScriptCode("HP");
detail.setScriptSubType("Base");
detail.setScriptIndex(3);
detail.setValueType(valueType);
detail.setPhase(phase);
detail.setValue(value);
return detail;
}
private PqScriptDtlDataVO scriptDetail(String scriptCode, String scriptSubType, Integer scriptIndex) {
PqScriptDtlDataVO detail = new PqScriptDtlDataVO();
detail.setScriptId("script");
detail.setScriptCode(scriptCode);
detail.setScriptSubType(scriptSubType);
detail.setScriptIndex(scriptIndex);
return detail;
}
}

View File

@@ -0,0 +1,64 @@
package com.njcn.gather.result.util;
import com.njcn.gather.report.pojo.enums.ItemReportKeyEnum;
import com.njcn.gather.storage.pojo.po.SimAndDigNonHarmonicResult;
import org.junit.Test;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class AngleResultAssemblerTest {
@Test
public void assembleRows_usesCurrentAngleI1AAndIgnoresVoltageAngleU1A() {
SimAndDigNonHarmonicResult voltageAngle = result("U1A",
data(0D, 0D, 0D, 1), data(0D, 0D, 0D, 1), data(0D, 0D, 0D, 1));
SimAndDigNonHarmonicResult currentAngle = result("I1A",
data(61.4D, 60D, 1.4D, 2), data(61D, 60D, 1D, 1), data(61D, 60D, 1D, 1));
List<Map<String, String>> rows = AngleResultAssembler.assembleRows(Arrays.asList(voltageAngle, currentAngle));
assertEquals(1, rows.size());
Map<String, String> row = rows.get(0);
assertEquals("60.0000", row.get(ItemReportKeyEnum.STANDARD.getKey()));
assertEquals("61.4000", row.get(ItemReportKeyEnum.TEST_A.getKey()));
assertEquals("61.0000", row.get(ItemReportKeyEnum.TEST_B.getKey()));
assertEquals("61.0000", row.get(ItemReportKeyEnum.TEST_C.getKey()));
assertEquals("1.4000", row.get(ItemReportKeyEnum.ERROR_A.getKey()));
assertEquals("1.0000", row.get(ItemReportKeyEnum.ERROR_B.getKey()));
assertEquals("1.0000", row.get(ItemReportKeyEnum.ERROR_C.getKey()));
assertEquals("不合格", row.get(ItemReportKeyEnum.RESULT_A.getKey()));
assertEquals("合格", row.get(ItemReportKeyEnum.RESULT_B.getKey()));
assertEquals("合格", row.get(ItemReportKeyEnum.RESULT_C.getKey()));
assertEquals("\u00b11.0\u00b0", row.get(ItemReportKeyEnum.ERROR_SCOPE.getKey()));
}
@Test
public void assembleRows_voltageAngleOnlyReturnsNoRows() {
SimAndDigNonHarmonicResult voltageAngle = result("U1A",
data(0D, 0D, 0D, 1), data(0D, 0D, 0D, 1), data(0D, 0D, 0D, 1));
assertTrue(AngleResultAssembler.assembleRows(Collections.singletonList(voltageAngle)).isEmpty());
}
private SimAndDigNonHarmonicResult result(String adType, String a, String b, String c) {
SimAndDigNonHarmonicResult result = new SimAndDigNonHarmonicResult();
result.setSort(21);
result.setAdType(adType);
result.setAValue(a);
result.setBValue(b);
result.setCValue(c);
return result;
}
private String data(Double data, Double resultData, Double errorData, Integer isData) {
return "{\"data\":" + data + ",\"unit\":\"\\u00b0\",\"isData\":" + isData
+ ",\"radius\":\"-1.0~1.0\",\"errorData\":" + errorData
+ ",\"resultData\":" + resultData + "}";
}
}

View File

@@ -0,0 +1,173 @@
package com.njcn.gather.result.util;
import com.njcn.gather.report.pojo.enums.ItemReportKeyEnum;
import com.njcn.gather.script.pojo.vo.PqScriptDtlDataVO;
import com.njcn.gather.storage.pojo.po.SimAndDigNonHarmonicResult;
import org.junit.Test;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
public class PowerResultAssemblerTest {
@Test
public void assembleRows_mergesTotalPowerResultsBySort() {
List<SimAndDigNonHarmonicResult> results = Arrays.asList(
total("TotW", 14, "{\"data\": -1079.364014, \"unit\": \"%\", \"isData\": 1, \"radius\": \"-0.5~0.5\", \"errorData\": -0.058888, \"resultData\": -1080}"),
total("TotVAr", 14, "{\"data\": 12.16591, \"isData\": 5, \"resultData\": 0}"),
total("TotVA", 14, "{\"data\": 1079.463013, \"isData\": 5, \"resultData\": 1080}")
);
List<Map<String, String>> rows = PowerResultAssembler.assembleRows(results);
assertEquals(1, rows.size());
Map<String, String> row = rows.get(0);
assertEquals("-1079.3640", row.get(ItemReportKeyEnum.ACTIVE_POWER.getKey()));
assertEquals("12.1659", row.get(ItemReportKeyEnum.REACTIVE_POWER.getKey()));
assertEquals("1079.4630", row.get(ItemReportKeyEnum.APPARENT_POWER.getKey()));
assertEquals("-1080.0000", row.get(ItemReportKeyEnum.STANDARD.getKey()));
assertEquals("合格", row.get(ItemReportKeyEnum.RESULT.getKey()));
}
@Test
public void assembleRows_sumsPhasePowerWhenTotalPowerIsAbsent() {
SimAndDigNonHarmonicResult result = new SimAndDigNonHarmonicResult();
result.setAdType("W");
result.setSort(15);
result.setAValue("{\"data\": 100.1, \"isData\": 1, \"resultData\": 100}");
result.setBValue("{\"data\": 200.2, \"isData\": 1, \"resultData\": 200}");
result.setCValue("{\"data\": 300.3, \"isData\": 1, \"resultData\": 300}");
List<Map<String, String>> rows = PowerResultAssembler.assembleRows(Arrays.asList(result));
assertEquals(1, rows.size());
assertEquals("600.6000", rows.get(0).get(ItemReportKeyEnum.ACTIVE_POWER.getKey()));
}
@Test
public void assembleRows_prefersTotalPowerWhenTotalAndPhaseBothExist() {
SimAndDigNonHarmonicResult total = total("TotW", 16,
"{\"data\": 1000.0, \"isData\": 1, \"resultData\": 1000}");
SimAndDigNonHarmonicResult phases = new SimAndDigNonHarmonicResult();
phases.setAdType("W");
phases.setSort(16);
phases.setAValue("{\"data\": 1.0, \"isData\": 1, \"resultData\": 1}");
phases.setBValue("{\"data\": 2.0, \"isData\": 1, \"resultData\": 2}");
phases.setCValue("{\"data\": 3.0, \"isData\": 1, \"resultData\": 3}");
List<Map<String, String>> rows = PowerResultAssembler.assembleRows(Arrays.asList(total, phases));
assertEquals(1, rows.size());
assertEquals("1000.0000", rows.get(0).get(ItemReportKeyEnum.ACTIVE_POWER.getKey()));
}
@Test
public void assembleRows_usesTotalPowerAsPrimaryWhenPhaseResultComesFirst() {
SimAndDigNonHarmonicResult phases = new SimAndDigNonHarmonicResult();
phases.setAdType("W");
phases.setSort(17);
phases.setAValue("{\"data\": 1.0, \"isData\": 1, \"resultData\": 1}");
phases.setBValue("{\"data\": 2.0, \"isData\": 1, \"resultData\": 2}");
phases.setCValue("{\"data\": 3.0, \"isData\": 1, \"resultData\": 3}");
SimAndDigNonHarmonicResult total = total("TotW", 17,
"{\"data\": 1000.0, \"isData\": 1, \"resultData\": 2000}");
List<Map<String, String>> rows = PowerResultAssembler.assembleRows(Arrays.asList(phases, total));
assertEquals(1, rows.size());
assertEquals("1000.0000", rows.get(0).get(ItemReportKeyEnum.ACTIVE_POWER.getKey()));
assertEquals("2000.0000", rows.get(0).get(ItemReportKeyEnum.STANDARD.getKey()));
assertEquals("1000.0000", rows.get(0).get(ItemReportKeyEnum.TEST.getKey()));
}
@Test
public void assembleRows_prefersTotalPowerFactorWhenTotalAndPhaseBothExist() {
SimAndDigNonHarmonicResult total = total("TotPF", 18,
"{\"data\": 0.9900, \"isData\": 1, \"resultData\": 1.0}");
SimAndDigNonHarmonicResult phases = new SimAndDigNonHarmonicResult();
phases.setAdType("PF");
phases.setSort(18);
phases.setAValue("{\"data\": 0.7, \"isData\": 1, \"resultData\": 1.0}");
phases.setBValue("{\"data\": 0.8, \"isData\": 1, \"resultData\": 1.0}");
phases.setCValue("{\"data\": 0.9, \"isData\": 1, \"resultData\": 1.0}");
List<Map<String, String>> rows = PowerResultAssembler.assembleRows(Arrays.asList(total, phases));
assertEquals(1, rows.size());
assertEquals("0.9900", rows.get(0).get(ItemReportKeyEnum.POWER_FACTOR.getKey()));
}
@Test
public void assembleRows_defaultsMissingPowerKeysToSlash() {
SimAndDigNonHarmonicResult total = total("TotW", 18,
"{\"data\": 1000.0, \"isData\": 1, \"resultData\": 1000}");
List<Map<String, String>> rows = PowerResultAssembler.assembleRows(Arrays.asList(total));
assertEquals(1, rows.size());
assertEquals("/", rows.get(0).get(ItemReportKeyEnum.STANDARD_V.getKey()));
assertEquals("/", rows.get(0).get(ItemReportKeyEnum.STANDARD_A.getKey()));
assertEquals("/", rows.get(0).get(ItemReportKeyEnum.POWER_FACTOR.getKey()));
}
@Test
public void assembleRows_fillsStandardSourceValuesFromScriptDetailsPreferringPhaseA() {
SimAndDigNonHarmonicResult total = total("TotW", 19,
"{\"data\": 1000.0, \"isData\": 1, \"resultData\": 1000}");
List<PqScriptDtlDataVO> scriptDetails = Arrays.asList(
scriptDetail(19, "VOL", "B", 58.0, 120.0),
scriptDetail(19, "CUR", "B", 6.0, 90.0),
scriptDetail(19, "VOL", "A", 57.74, 0.0),
scriptDetail(19, "CUR", "A", 5.0, -30.0)
);
List<Map<String, String>> rows = PowerResultAssembler.assembleRows(Arrays.asList(total), scriptDetails);
assertEquals(1, rows.size());
assertEquals("57.7400", rows.get(0).get(ItemReportKeyEnum.STANDARD_V.getKey()));
assertEquals("5.0000", rows.get(0).get(ItemReportKeyEnum.STANDARD_I.getKey()));
assertEquals("0.0000", rows.get(0).get(ItemReportKeyEnum.STANDARD_U_ANGLE.getKey()));
assertEquals("-30.0000", rows.get(0).get(ItemReportKeyEnum.STANDARD_I_ANGLE.getKey()));
}
@Test
public void assembleRows_fallsBackToPhaseBForStandardSourceValuesWhenPhaseAIsAbsent() {
SimAndDigNonHarmonicResult total = total("TotW", 20,
"{\"data\": 1000.0, \"isData\": 1, \"resultData\": 1000}");
List<PqScriptDtlDataVO> scriptDetails = Arrays.asList(
scriptDetail(20, "VOL", "C", 59.0, 240.0),
scriptDetail(20, "CUR", "C", 7.0, 210.0),
scriptDetail(20, "VOL", "B", 58.0, 120.0),
scriptDetail(20, "CUR", "B", 6.0, 90.0)
);
List<Map<String, String>> rows = PowerResultAssembler.assembleRows(Arrays.asList(total), scriptDetails);
assertEquals(1, rows.size());
assertEquals("58.0000", rows.get(0).get(ItemReportKeyEnum.STANDARD_V.getKey()));
assertEquals("6.0000", rows.get(0).get(ItemReportKeyEnum.STANDARD_I.getKey()));
assertEquals("120.0000", rows.get(0).get(ItemReportKeyEnum.STANDARD_U_ANGLE.getKey()));
assertEquals("90.0000", rows.get(0).get(ItemReportKeyEnum.STANDARD_I_ANGLE.getKey()));
}
private SimAndDigNonHarmonicResult total(String code, Integer sort, String tValue) {
SimAndDigNonHarmonicResult result = new SimAndDigNonHarmonicResult();
result.setAdType(code);
result.setSort(sort);
result.setTValue(tValue);
return result;
}
private PqScriptDtlDataVO scriptDetail(Integer sort, String valueType, String phase, Double value, Double angle) {
PqScriptDtlDataVO detail = new PqScriptDtlDataVO();
detail.setScriptIndex(sort);
detail.setValueType(valueType);
detail.setPhase(phase);
detail.setValue(value);
detail.setAngle(angle);
return detail;
}
}

View File

@@ -0,0 +1,104 @@
package com.njcn.gather.result.util;
import com.njcn.gather.report.pojo.enums.ItemReportKeyEnum;
import com.njcn.gather.storage.pojo.po.SimAndDigNonHarmonicResult;
import org.junit.Test;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
public class UnbalanceResultAssemblerTest {
@Test
public void assembleRows_mergesVoltageUnbalanceAndSequenceBySort() {
SimAndDigNonHarmonicResult unbalance = total("V_UNBAN", 31,
"{\"data\": 0.002506, \"unit\": \"%\", \"isData\": 4, \"radius\": \"-0.2~0.2\", \"errorData\": 0.002506, \"resultData\": 0}");
SimAndDigNonHarmonicResult sequence = sequence("SeqV", 31,
"{\"data\": 57.728199, \"isData\": 5, \"resultData\": 33.31598}",
"{\"data\": 0.004355, \"isData\": 5, \"resultData\": 0}",
"{\"data\": 0.00516, \"isData\": 5, \"resultData\": 0}");
List<Map<String, String>> rows = UnbalanceResultAssembler.assembleRows(Arrays.asList(unbalance, sequence));
assertEquals(1, rows.size());
Map<String, String> row = rows.get(0);
assertEquals("0.0000", row.get(ItemReportKeyEnum.STANDARD.getKey()));
assertEquals("0.0025", row.get(ItemReportKeyEnum.TEST.getKey()));
assertEquals("0.0025", row.get(ItemReportKeyEnum.ERROR.getKey()));
assertEquals("±0.2%", row.get(ItemReportKeyEnum.ERROR_SCOPE.getKey()));
assertEquals("无法比较", row.get(ItemReportKeyEnum.RESULT.getKey()));
assertEquals("57.7282", row.get(ItemReportKeyEnum.POS_SEQ.getKey()));
assertEquals("0.0044", row.get(ItemReportKeyEnum.NEG_SEQ.getKey()));
assertEquals("0.0052", row.get(ItemReportKeyEnum.ZERO_SEQ.getKey()));
}
@Test
public void assembleRows_mergesCurrentUnbalanceAndSequenceBySort() {
SimAndDigNonHarmonicResult unbalance = total("I_UNBAN", 12,
"{\"data\": 0.31, \"unit\": \"%\", \"isData\": 2, \"radius\": \"-0.1~0.1\", \"errorData\": 0.21, \"resultData\": 0.1}");
SimAndDigNonHarmonicResult sequence = sequence("SeqA", 12,
"{\"data\": 4.9999, \"isData\": 5, \"resultData\": 5}",
"{\"data\": 0.0021, \"isData\": 5, \"resultData\": 0}",
"{\"data\": 0.0008, \"isData\": 5, \"resultData\": 0}");
List<Map<String, String>> rows = UnbalanceResultAssembler.assembleRows(Arrays.asList(sequence, unbalance));
assertEquals(1, rows.size());
Map<String, String> row = rows.get(0);
assertEquals("0.1000", row.get(ItemReportKeyEnum.STANDARD.getKey()));
assertEquals("0.3100", row.get(ItemReportKeyEnum.TEST.getKey()));
assertEquals("0.2100", row.get(ItemReportKeyEnum.ERROR.getKey()));
assertEquals("不合格", row.get(ItemReportKeyEnum.RESULT.getKey()));
assertEquals("4.9999", row.get(ItemReportKeyEnum.POS_SEQ.getKey()));
assertEquals("0.0021", row.get(ItemReportKeyEnum.NEG_SEQ.getKey()));
assertEquals("0.0008", row.get(ItemReportKeyEnum.ZERO_SEQ.getKey()));
}
@Test
public void assembleRows_defaultsMissingSequenceValuesToSlash() {
SimAndDigNonHarmonicResult unbalance = total("V_UNBAN", 7,
"{\"data\": 0.05, \"unit\": \"%\", \"isData\": 1, \"radius\": \"-0.2~0.2\", \"errorData\": 0.01, \"resultData\": 0.04}");
List<Map<String, String>> rows = UnbalanceResultAssembler.assembleRows(Arrays.asList(unbalance));
assertEquals(1, rows.size());
Map<String, String> row = rows.get(0);
assertEquals("/", row.get(ItemReportKeyEnum.POS_SEQ.getKey()));
assertEquals("/", row.get(ItemReportKeyEnum.NEG_SEQ.getKey()));
assertEquals("/", row.get(ItemReportKeyEnum.ZERO_SEQ.getKey()));
assertEquals("合格", row.get(ItemReportKeyEnum.RESULT.getKey()));
}
@Test
public void assembleRows_leavesTimeUnsetForItemAnchorPointFallback() {
SimAndDigNonHarmonicResult unbalance = total("V_UNBAN", 7,
"{\"data\": 0.05, \"unit\": \"%\", \"isData\": 1, \"radius\": \"-0.2~0.2\", \"errorData\": 0.01, \"resultData\": 0.04}");
List<Map<String, String>> rows = UnbalanceResultAssembler.assembleRows(Arrays.asList(unbalance));
assertEquals(1, rows.size());
assertNull(rows.get(0).get(ItemReportKeyEnum.TIME.getKey()));
}
private SimAndDigNonHarmonicResult total(String code, Integer sort, String tValue) {
SimAndDigNonHarmonicResult result = new SimAndDigNonHarmonicResult();
result.setAdType(code);
result.setSort(sort);
result.setTValue(tValue);
return result;
}
private SimAndDigNonHarmonicResult sequence(String code, Integer sort, String aValue, String bValue, String cValue) {
SimAndDigNonHarmonicResult result = new SimAndDigNonHarmonicResult();
result.setAdType(code);
result.setSort(sort);
result.setAValue(aValue);
result.setBValue(bValue);
result.setCValue(cValue);
return result;
}
}

View File

@@ -66,13 +66,13 @@ webSocket:
sntp: sntp:
port: 123 port: 123
Dip: #Dip:
# 暂态前时间s # 暂态前时间s
# fPreTime: 2f # fPreTime: 2f
#写入时间s #写入时间s
fRampIn: 0.001f # fRampIn: 0.001f
#写出时间s #写出时间s
fRampOut: 0.001f # fRampOut: 0.001f
# 暂态后时间s # 暂态后时间s
# fAfterTime: 3f # fAfterTime: 3f

View File

@@ -62,6 +62,12 @@ public interface SimAndDigNonHarmonicService extends IService<SimAndDigNonHarmon
*/ */
List<SimAndDigNonHarmonicResult> queryByCondition(SingleNonHarmParam param); List<SimAndDigNonHarmonicResult> queryByCondition(SingleNonHarmParam param);
/**
* 按设备回路和 sort 查询所有非谐波结果,并把 adType 映射为字典 CODE。
* 用于功率 P同一 sort 下可能同时存在 TotW/TotVAr/TotVA 等总量结果。
*/
List<SimAndDigNonHarmonicResult> queryBySortListWithAdTypeCode(SingleNonHarmParam param);
/** /**
* *

View File

@@ -104,7 +104,6 @@ public class SimAndDigNonHarmonicServiceImpl extends ServiceImpl<SimAndDigNonHar
Map<String, List<RawResultDataVO>> info = new LinkedHashMap<>(2); Map<String, List<RawResultDataVO>> info = new LinkedHashMap<>(2);
Map<String, SimAndDigNonHarmonicResult> adTypeMap = SimAndDigHarmonicResults.stream() Map<String, SimAndDigNonHarmonicResult> adTypeMap = SimAndDigHarmonicResults.stream()
.sorted(Comparator.comparing(SimAndDigNonHarmonicResult::getResultFlag))
.collect(Collectors.toMap(SimAndDigNonHarmonicResult::getAdType, Function.identity(), (a, b) -> a, LinkedHashMap::new)); .collect(Collectors.toMap(SimAndDigNonHarmonicResult::getAdType, Function.identity(), (a, b) -> a, LinkedHashMap::new));
List<DictTree> dictTreeById = dictTreeService.getDictTreeById(new ArrayList<>(adTypeMap.keySet())); List<DictTree> dictTreeById = dictTreeService.getDictTreeById(new ArrayList<>(adTypeMap.keySet()));
Map<String, DictTree> dictTreeByName = dictTreeById.stream().collect(Collectors.toMap(DictTree::getId, Function.identity())); Map<String, DictTree> dictTreeByName = dictTreeById.stream().collect(Collectors.toMap(DictTree::getId, Function.identity()));
@@ -181,6 +180,21 @@ public class SimAndDigNonHarmonicServiceImpl extends ServiceImpl<SimAndDigNonHar
return adNonHarmonicResults; return adNonHarmonicResults;
} }
@Override
public List<SimAndDigNonHarmonicResult> queryBySortListWithAdTypeCode(SingleNonHarmParam param) {
String prefix = "ad_non_harmonic_result_";
DynamicTableNameHandler.setTableName(prefix + param.getPlanCode());
MPJLambdaWrapper<SimAndDigNonHarmonicResult> wrapper = new MPJLambdaWrapper<>();
wrapper.selectAll(SimAndDigNonHarmonicResult.class)
.selectAs(DictTree::getCode, SimAndDigNonHarmonicResult::getAdType)
.leftJoin(DictTree.class, DictTree::getId, SimAndDigNonHarmonicResult::getAdType)
.like(SimAndDigNonHarmonicResult::getDevMonitorId, param.getDevId() + "_" + param.getChannelNo())
.in(SimAndDigNonHarmonicResult::getSort, param.getSortList());
List<SimAndDigNonHarmonicResult> adNonHarmonicResults = this.getBaseMapper().selectJoinList(SimAndDigNonHarmonicResult.class, wrapper);
DynamicTableNameHandler.remove();
return adNonHarmonicResults;
}
@Override @Override
public List<SimAndDigNonHarmonicResult> listAllSimAndDigRawData(String scriptId, String code, String devId) { public List<SimAndDigNonHarmonicResult> listAllSimAndDigRawData(String scriptId, String code, String devId) {

View File

@@ -90,10 +90,14 @@ public class StorageUtil {
*/ */
case "P": case "P":
case "W": case "W":
case "VARW":
case "VAW":
unit = "W"; unit = "W";
break; break;
case "VARW":
unit = "Var";
break;
case "VAW":
unit = "VA";
break;
/** /**
* 闪变 * 闪变
*/ */
@@ -116,6 +120,8 @@ public class StorageUtil {
*/ */
case "VA": case "VA":
case "IA": case "IA":
case "U1A":
case "I1A":
unit = "°"; unit = "°";
break; break;
/** /**

View File

@@ -83,9 +83,9 @@ public class DictTreeServiceImpl extends ServiceImpl<DictTreeMapper, DictTree> i
public boolean updateDictTree(DictTreeParam.UpdateParam param) { public boolean updateDictTree(DictTreeParam.UpdateParam param) {
param.setName(param.getName().trim()); param.setName(param.getName().trim());
DictTree dictTree = this.getById(param.getId()); DictTree dictTree = this.getById(param.getId());
if ("975f63baeb6f653c54fca226a9ae36ca".equals(param.getId()) || dictTree.getPids().contains("975f63baeb6f653c54fca226a9ae36ca")) { // if ("975f63baeb6f653c54fca226a9ae36ca".equals(param.getId()) || dictTree.getPids().contains("975f63baeb6f653c54fca226a9ae36ca")) {
throw new BusinessException(SystemResponseEnum.CAN_NOT_UPDATE_USED_DICT); // throw new BusinessException(SystemResponseEnum.CAN_NOT_UPDATE_USED_DICT);
} // }
checkRepeat(param, true); checkRepeat(param, true);
DictTree copyDictTree = new DictTree(); DictTree copyDictTree = new DictTree();
BeanUtils.copyProperties(param, copyDictTree); BeanUtils.copyProperties(param, copyDictTree);

View File

@@ -23,6 +23,7 @@ public enum DicDataEnum {
HSI("HSI","间谐波电流"), HSI("HSI","间谐波电流"),
VOLTAGE("VOLTAGE","暂态"), VOLTAGE("VOLTAGE","暂态"),
F("F","闪变"), F("F","闪变"),
P("P","功率")

View File

@@ -8,6 +8,7 @@ import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.docx4j.XmlUtils; import org.docx4j.XmlUtils;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage; import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.docx4j.openpackaging.parts.WordprocessingML.DocumentSettingsPart;
import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart; import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart;
import org.docx4j.wml.*; import org.docx4j.wml.*;
import org.docx4j.wml.TcPrInner.GridSpan; import org.docx4j.wml.TcPrInner.GridSpan;
@@ -31,6 +32,27 @@ import java.util.Map;
@Slf4j @Slf4j
public class Docx4jUtil { public class Docx4jUtil {
/**
* 让 Word 打开文档时自动刷新 PAGE / NUMPAGES 等域,避免动态插表后页码显示旧值。
*/
public static void enableUpdateFieldsOnOpen(WordprocessingMLPackage wordPackage) throws Exception {
if (wordPackage == null || wordPackage.getMainDocumentPart() == null) {
return;
}
DocumentSettingsPart settingsPart = wordPackage.getMainDocumentPart().getDocumentSettingsPart(true);
CTSettings settings = settingsPart.getContents();
if (settings == null) {
settings = new CTSettings();
settingsPart.setJaxbElement(settings);
}
BooleanDefaultTrue updateFields = settings.getUpdateFields();
if (updateFields == null) {
updateFields = new BooleanDefaultTrue();
settings.setUpdateFields(updateFields);
}
updateFields.setVal(Boolean.TRUE);
}
/** /**
* 创建标题 * 创建标题
* *